From 6ead99e15c56ed91157e55b815d568dc27a11372 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A0=95=EB=B0=B0?= Date: Tue, 28 Oct 2025 16:58:10 +0900 Subject: [PATCH 01/85] =?UTF-8?q?round1:=201=EC=A3=BC=EC=B0=A8=20=EB=AC=B8?= =?UTF-8?q?=EC=84=9C,=20=EA=B3=BC=EC=A0=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/week01.md | 289 ++++++++++++++++++++++++++++++++++++++++++ docs/week01_quests.md | 131 +++++++++++++++++++ 2 files changed, 420 insertions(+) create mode 100644 docs/week01.md create mode 100644 docs/week01_quests.md diff --git a/docs/week01.md b/docs/week01.md new file mode 100644 index 000000000..decb1ea16 --- /dev/null +++ b/docs/week01.md @@ -0,0 +1,289 @@ +# ๐Ÿงญ ๋ฃจํ”„ํŒฉ BE L2 - Round 1 + +> ๋‹จ์ˆœํžˆ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜๋Š” ๊ฒŒ ์•„๋‹ˆ๋ผ, ์˜๋„๋ฅผ ์„ค๊ณ„ํ•œ๋‹ค. +> + + + +- ๊ธฐ๋Šฅ ๊ตฌํ˜„๋ณด๋‹ค ๋จผ์ € ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด๋ณธ๋‹ค. +- ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅํ•œ ๊ตฌ์กฐ๋ž€ ๋ฌด์—‡์ธ์ง€ ์ฒด๊ฐํ•ด๋ณธ๋‹ค. +- ์œ ์ € ๋“ฑ๋ก/์กฐํšŒ, ํฌ์ธํŠธ ์ถฉ์ „ ๊ธฐ๋Šฅ์„ ํ…Œ์ŠคํŠธ ์ฃผ๋„๋กœ ๊ตฌํ˜„ํ•ด๋ณธ๋‹ค. + + + +- ๋‹จ์œ„ ํ…Œ์ŠคํŠธ vs ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ +- ํ…Œ์ŠคํŠธ ๋”๋ธ”(Mock, Stub, Fake ๋“ฑ) +- ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅํ•œ ์ฝ”๋“œ ๊ตฌ์กฐ +- ํ…Œ์ŠคํŠธ ์ฃผ๋„ ๊ฐœ๋ฐœ (TDD) + + + +## ๐Ÿงช ํ…Œ์ŠคํŠธ ํ”ผ๋ผ๋ฏธ๋“œ + +> ํ…Œ์ŠคํŠธ๋Š” ์•„๋ž˜์ฒ˜๋Ÿผ **๋ฒ”์œ„์— ๋”ฐ๋ผ ์—ญํ• ๊ณผ ์ฑ…์ž„์ด ๋‚˜๋‰˜๋ฉฐ**, +ํ•˜๋‹จ์ผ์ˆ˜๋ก ๋น ๋ฅด๊ณ  ๋งŽ์ด, ์ƒ๋‹จ์ผ์ˆ˜๋ก ๋А๋ฆฌ์ง€๋งŒ ์‹ ์ค‘ํ•˜๊ฒŒ ๊ตฌ์„ฑ๋ฉ๋‹ˆ๋‹ค. +> + +![Untitled](attachment:54f631d6-538a-44fa-8358-026c73efed68:Untitled.png) + +### ๐Ÿงฑ 1. **๋‹จ์œ„ ํ…Œ์ŠคํŠธ (Unit Test)** + +- **๋Œ€์ƒ:** ๋„๋ฉ”์ธ ๋ชจ๋ธ (Entity, VO, Domain Service) +- **๋ชฉ์ :** ์ˆœ์ˆ˜ ๋กœ์ง์˜ ์ •ํ•ฉ์„ฑ๊ณผ ๊ทœ์น™ ๊ฒ€์ฆ +- **ํ™˜๊ฒฝ:** Spring ์—†์ด ์ˆœ์ˆ˜ JVM์—์„œ ์‹คํ–‰ (JVM ๋‹จ์œ„ ํ…Œ์ŠคํŠธ) / **ํ…Œ์ŠคํŠธ ๋Œ€์—ญ** ์„ ํ™œ์šฉํ•ด ๋ชจ๋“  ์˜์กด์„ฑ์„ ๋Œ€์ฒด +- **๊ธฐ์ˆ :** JUnit5, Kotest, AssertJ ๋“ฑ + +> ๐Ÿ’ฌ ์˜ˆ: ํฌ์ธํŠธ ์ถฉ์ „ ์‹œ ์ตœ๋Œ€ ํ•œ๋„ ์ดˆ๊ณผ ์—ฌ๋ถ€๋ฅผ ๊ฒ€์ฆํ•˜๋Š” ํ…Œ์ŠคํŠธ +> + +### ๐Ÿ” 2. **ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ (Integration Test)** + +- **๋Œ€์ƒ:** ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ Service, Facade ๋“ฑ ๊ณ„์ธต ๋กœ์ง +- **๋ชฉ์ :** ์—ฌ๋Ÿฌ ์ปดํฌ๋„ŒํŠธ(Repo, Domain, ์™ธ๋ถ€ API Stub)๊ฐ€ ์—ฐ๊ฒฐ๋œ ์ƒํƒœ์—์„œ **๋น„์ฆˆ๋‹ˆ์Šค ํ๋ฆ„ ์ „์ฒด๋ฅผ ๊ฒ€์ฆ** +- **ํ™˜๊ฒฝ:** `@SpringBootTest`, ์‹ค์ œ Bean ๊ตฌ์„ฑ, Test DB +- **๊ธฐ์ˆ :** SpringBootTest + H2 + TestContainers ๋“ฑ + +> ๐Ÿ’ฌ ์˜ˆ: ์‹ค์ œ ํฌ์ธํŠธ๊ฐ€ ์ถฉ์ „๋˜๊ณ , DB์— ๋ฐ˜์˜๋˜๋ฉฐ, ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœํ–‰๋˜๋Š” ์ „ ๊ณผ์ •์„ ๊ฒ€์ฆ +> + +### ๐ŸŒ 3. **E2E ํ…Œ์ŠคํŠธ (End-to-End Test)** + +- **๋Œ€์ƒ:** ์ „์ฒด ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ (Controller โ†’ Service โ†’ DB) +- **๋ชฉ์ :** ์‹ค์ œ HTTP ์š”์ฒญ ๋‹จ์œ„ ์‹œ๋‚˜๋ฆฌ์˜ค ํ…Œ์ŠคํŠธ +- **ํ™˜๊ฒฝ:** `MockMvc` ๋˜๋Š” `TestRestTemplate`์„ ํ†ตํ•ด ์‹ค์ œ API ์š”์ฒญ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ +- **๊ธฐ์ˆ :** SpringBootTest + `@AutoConfigureMockMvc`, `WebTestClient` ๋“ฑ + +> ๐Ÿ’ฌ ์˜ˆ: ์‚ฌ์šฉ์ž๊ฐ€ ํšŒ์›๊ฐ€์ž… โ†’ ํฌ์ธํŠธ ์ถฉ์ „ โ†’ ์ฃผ๋ฌธ ํ๋ฆ„์„ HTTP ์š”์ฒญ์œผ๋กœ ์ˆ˜ํ–‰ํ–ˆ์„ ๋•Œ์˜ ๊ฒฐ๊ณผ ํ™•์ธ +> + +--- + +## ๐Ÿ”ง ํ…Œ์ŠคํŠธ ๋”๋ธ”(Test Doubles) + +> ํ…Œ์ŠคํŠธ ๋Œ€์ƒ์ด ์˜์กดํ•˜๋Š” ์™ธ๋ถ€ ๊ฐ์ฒด์˜ ๋™์ž‘์„ **๋น ๋ฅด๊ณ  ์•ˆ์ „ํ•˜๊ฒŒ ํ‰๋‚ด ๋‚ด๋Š” ๋Œ€์—ญ ๊ฐ์ฒด** ์ž…๋‹ˆ๋‹ค. +๋А๋ฆฌ๊ณ  ๋ถˆ์•ˆ์ •ํ•œ ์‹ค์ œ ๊ตฌํ˜„ ๋Œ€์‹ , ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์— ๋งž๋Š” **โ€˜์กฐ์šฉํ•œ ๋Œ€์—ญโ€™** ์„ ์„ธ์›Œ์ค๋‹ˆ๋‹ค. +> + +### ๐Ÿงฉ ํ…Œ์ŠคํŠธ ๋”๋ธ”์€ ์—ญํ• , `mock()`๊ณผ `spy()`๋Š” ๋„๊ตฌ + +- `Stub`, `Mock`, `Spy`, `Fake` ๋Š” **ํ…Œ์ŠคํŠธ ๋ชฉ์  (์—ญํ• )** +- `mock()`, `spy()`๋Š” **๊ฐ์ฒด ์ƒ์„ฑ ๋ฐฉ์‹ (๋„๊ตฌ)** + +e.g. + +```kotlin +val repo = mock() // ๋„๊ตฌ: mock() +whenever(repo.findById(1L)).thenReturn(User(...)) // ์—ญํ• : Stub +verify(repo).findById(1L) // ์—ญํ• : Mock +``` + +> โœ… mock ๊ฐ์ฒด์— stub + mock ์—ญํ• ์„ ๋™์‹œ์— ๋ถ€์—ฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. +> + +### ๐Ÿ“š TestDouble ์—ญํ• ๋ณ„ ์ •๋ฆฌ + +| ์—ญํ•  | ๋ชฉ์  | ์‚ฌ์šฉ ๋ฐฉ์‹ | ์˜ˆ์‹œ | +| --- | --- | --- | --- | +| **Dummy** | ์ž๋ฆฌ๋งŒ ์ฑ„์›€ (์‚ฌ์šฉ๋˜์ง€ ์•Š์Œ) | ์ƒ์„ฑ์ž ๋“ฑ์—์„œ ์ „๋‹ฌ | `User(null, null)` | +| **Stub** | ๊ณ ์ •๋œ ์‘๋‹ต ์ œ๊ณต (์ƒํƒœ ๊ธฐ๋ฐ˜) | `when().thenReturn()` | `repo.find()` โ†’ ํ•ญ์ƒ ํŠน์ • ์œ ์ € ๋ฐ˜ํ™˜ | +| **Mock** | ํ˜ธ์ถœ ์—ฌ๋ถ€/ํšŸ์ˆ˜ ๊ฒ€์ฆ (ํ–‰์œ„ ๊ธฐ๋ฐ˜) | `verify(...)` | ํ•จ์ˆ˜๊ฐ€ ์‹คํ–‰๋˜์—ˆ๋Š”์ง€ ๊ฒ€์ฆ | +| **Spy** | ์ง„์งœ ๊ฐ์ฒด ๊ฐ์‹ธ๊ธฐ + ์ผ๋ถ€ ์กฐ์ž‘ | `spy()` + `doReturn()` | ์ง„์งœ ์„œ๋น„์Šค ๊ฐ์‹ธ๊ณ  ์ผ๋ถ€๋งŒ stub | +| **Fake** | ์‹ค์ œ์ฒ˜๋Ÿผ ๋™์ž‘ํ•˜๋Š” ๊ฐ€์งœ ๊ตฌํ˜„์ฒด | ์ง์ ‘ ํด๋ž˜์Šค ๊ตฌํ˜„ | **InMemoryUserRepository** | + +### ๐Ÿ” TestDouble ์‹ค์ „ ์˜ˆ์ œ + +### ๐Ÿ“ฆ Stub ์˜ˆ์ œ + +```kotlin +val userRepo = mock() +whenever(userRepo.findById(1L)).thenReturn(User("alen")) +``` + +- ํ๋ฆ„๋งŒ ํ†ต์ œํ•˜๊ณ  ์‹ถ์€ ๊ฒฝ์šฐ +- โ€œ์ด๋ ‡๊ฒŒ ํ˜ธ์ถœํ•˜๋ฉด, ์ด๋ ‡๊ฒŒ ์‘๋‹ตํ•ด์ค˜โ€ + +### ๐Ÿ“ฌ Mock ์˜ˆ์ œ + +```kotlin +val speaker = mock() +speaker.say("hello") +verify(speaker, times(1)).say("hello") +``` + +- ํ˜ธ์ถœ ์—ฌ๋ถ€๊ฐ€ ๊ฒ€์ฆ ๋Œ€์ƒ +- โ€œ๋„ˆ ์ด๋ ‡๊ฒŒ ๋™์ž‘ํ–ˆ๋‹ˆ?โ€ + +### ๐Ÿ•ต๏ธ Spy ์˜ˆ์ œ + +```kotlin +val friend = Friend() +val spyFriend = spy(friend) +spyFriend.hangout() +verify(spyFriend).hangout() +``` + +- ์ง„์งœ ๊ฐ์ฒด์ฒ˜๋Ÿผ ๋™์ž‘ํ•˜๋ฉด์„œ ์ผ๋ถ€๋งŒ ์กฐ์ž‘ +- "๋กœ์ง์€ ๊ทธ๋Œ€๋กœ ์“ฐ๊ณ , ํŠน์ • ๋™์ž‘๋งŒ ๋ฎ์–ด์”Œ์šฐ๊ณ  / ๊ฒ€์ฆํ•˜๊ณ  ์‹ถ๋‹ค" + +### ๐Ÿงช Fake ์˜ˆ์ œ + +```kotlin +class InMemoryUserRepository : UserRepository { + private val data = mutableMapOf() + + override fun save(user: User) { data[user.id] = user } + override fun findById(id: Long): User? = data[user.id] +} +``` + +- ์‹ค์ œ DB ์—†์ด ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅํ•œ ์ €์žฅ์†Œ ๊ตฌํ˜„ +- "์™„์ „ํžˆ ๋…๋ฆฝ์ ์ธ ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์ด ํ•„์š”ํ•  ๋•Œโ€ + +--- + +## ๐Ÿงฑ ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅํ•œ ๊ตฌ์กฐ + +> **๊ฒ€์ฆํ•˜๊ณ  ์‹ถ์€ ๋กœ์ง์„, ์™ธ๋ถ€ ์˜์กด์„ฑ๊ณผ ๊ฒฉ๋ฆฌ๋œ ์ƒํƒœ์—์„œ ๋‹จ๋…์œผ๋กœ ๊ฒ€์ฆํ•  ์ˆ˜ ์žˆ๋Š” ๊ตฌ์กฐ**์ž…๋‹ˆ๋‹ค. +> +> +> ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅํ•œ ๊ตฌ์กฐ๋ž€, ๊ฒ€์ฆํ•˜๊ณ  ์‹ถ์€ ์ฝ”๋“œ๋งŒ ์ •ํ™•ํžˆ ๊บผ๋‚ด์„œ **์กฐ์šฉํ•˜๊ณ  ๋‹จ๋‹จํ•˜๊ฒŒ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋Š” ๊ตฌ์กฐ**๋‹ค. +> + +### โŒ ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์–ด๋ ค์šด ๊ตฌ์กฐ์˜ ํŠน์ง• + +| ๋ฌธ์ œ | ์„ค๋ช… | +| --- | --- | +| **๋‚ด๋ถ€์—์„œ ์˜์กด ๊ฐ์ฒด ์ง์ ‘ ์ƒ์„ฑ (`new`)** | ํ…Œ์ŠคํŠธ ๋Œ€์—ญ์œผ๋กœ ๋Œ€์ฒด ๋ถˆ๊ฐ€ โ†’ ํ…Œ์ŠคํŠธ ๊ฒฉ๋ฆฌ ๋ถˆ๊ฐ€๋Šฅ | +| **ํ•˜๋‚˜์˜ ํ•จ์ˆ˜๊ฐ€ ๋„ˆ๋ฌด ๋งŽ์€ ์ฑ…์ž„** | ํ…Œ์ŠคํŠธ ๋Œ€์ƒ์ด ๋ชจํ˜ธํ•ด์ง โ†’ ์‹คํŒจ ์›์ธ ์ถ”์  ์–ด๋ ค์›€ | +| **์™ธ๋ถ€ API ํ˜ธ์ถœ, DB ์ ‘๊ทผ ๋“ฑ์ด ํ•˜๋“œ์ฝ”๋”ฉ** | ์‹ค์ œ ํ™˜๊ฒฝ ์—†์ด ํ…Œ์ŠคํŠธ ๋ถˆ๊ฐ€๋Šฅ โ†’ ๋А๋ฆฌ๊ณ  ๋ถˆ์•ˆ์ • | +| **private ๋กœ์ง, static ๋ฉ”์„œ๋“œ ๋‚จ์šฉ** | ์™ธ๋ถ€์—์„œ ๋กœ์ง ๋ถ„๋ฆฌ ๋ถˆ๊ฐ€ โ†’ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ๋ถˆ๊ฐ€ | + +### โœ… ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅํ•œ ๊ตฌ์กฐ๋กœ ๋ณ€๊ฒฝ + +| ํฌ์ธํŠธ | ์„ค๋ช… | +| --- | --- | +| **์™ธ๋ถ€ ์˜์กด์„ฑ ๋ถ„๋ฆฌ** | ์ธํ„ฐํŽ˜์ด์Šคํ™” + ์ƒ์„ฑ์ž ์ฃผ์ž…(DI) | +| **๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ๋ถ„๋ฆฌ** | ๋„๋ฉ”์ธ ์—”ํ‹ฐํ‹ฐ or ์ „์šฉ Service์—์„œ ์ฑ…์ž„ ๋ถ„์‚ฐ | +| **์ฑ…์ž„ ๋‹จ์ผํ™”** | ํ•œ ํ•จ์ˆ˜๋Š” ํ•œ ์—ญํ• ๋งŒ (e.g. ๊ฒฐ์ œ๋งŒ, ์žฌ๊ณ ๋งŒ ๋“ฑ) | +| **์ƒํƒœ ์ค‘์‹ฌ ์„ค๊ณ„** | โ€œ์ž…๋ ฅ โ†’ ์ƒํƒœ ๋ณ€ํ™” โ†’ ๊ฒฐ๊ณผโ€ ๊ตฌ์กฐ๋กœ ์ •๋ฆฌ | + +### ๐Ÿ” ์‚ฌ๋ก€๋กœ ์‚ดํŽด๋ณด๊ธฐ + +```kotlin +class OrderService { + fun completeOrder(userId: Long, productId: Long) { + val user = UserJpaRepository().findById(userId) + val product = ProductJpaRepository().findById(productId) + + if (product.stock <= 0) throw IllegalStateException() + product.stock-- + + if (user.point < product.price) throw IllegalStateException() + user.point -= product.price + + OrderRepository().save(Order(user, product)) + } +} +``` + +- ์™ธ๋ถ€ ์˜์กด์„ฑ ์ง์ ‘ ์ƒ์„ฑ โ†’ Mock/Fake ๋ถˆ๊ฐ€ +- ๋„๋ฉ”์ธ ๋กœ์ง, ์ƒํƒœ๋ณ€๊ฒฝ, ์™ธ๋ถ€ ํ˜ธ์ถœ์ด ํ•œ ๊ณณ์— ๋ชฐ๋ ค ์žˆ์Œ +- `OrderServiceTest` ํ•˜๋‚˜๋กœ ๋ชจ๋“  ์ผ€์ด์Šค ์ปค๋ฒ„ํ•ด์•ผ ํ•จ โ†’ ์‹คํŒจ ์‹œ ์–ด๋””์„œ ์ž˜๋ชป๋๋Š”์ง€ ์ถ”์  ๋ถˆ๊ฐ€ + +--- + +```kotlin +class OrderService( + private val userReader: UserReader, + private val productReader: ProductReader, + private val orderRepository: OrderRepository, +) { + fun completeOrder(command: OrderCommand) { + val user = userReader.get(command.userId) + val product = productReader.get(command.productId) + + product.decreaseStock() + user.pay(product.price) + + orderRepository.save(Order(user, product)) + } +} +``` + +- ์™ธ๋ถ€๋Š” ์ธํ„ฐํŽ˜์ด์Šค๋กœ ์ฃผ์ž… โ†’ Fake/Mock ๊ฐ€๋Šฅ +- ๋กœ์ง์€ `user.pay()`, `product.decreaseStock()` ์ฒ˜๋Ÿผ ๋„๋ฉ”์ธ์œผ๋กœ ์œ„์ž„ +- ํ…Œ์ŠคํŠธ ๋‹จ์œ„๋ณ„๋กœ ๋‚˜๋ˆŒ ์ˆ˜ ์žˆ์Œ โ†’ `UserTest`, `ProductTest`, `OrderServiceTest` + +--- + +## ๐Ÿ” TDD (Test-Driven Development) + +> TDD๋Š” ํ…Œ์ŠคํŠธ์˜ ์ˆœ์„œ๋ณด๋‹ค +**โ€์„ค๊ณ„ ๋‹จ์œ„๋ฅผ ์ž˜๊ฒŒ ์ชผ๊ฐœ๊ณ , ๊ทธ๊ฒƒ์ด ๊ฒ€์ฆ ๊ฐ€๋Šฅํ•˜๊ฒŒ ๊ตฌํ˜„๋˜์—ˆ๋Š”๊ฐ€โ€**๊ฐ€ ํ•ต์‹ฌ์ด๋‹ค. +> + +### ๐Ÿ”„ 3๋‹จ๊ณ„ ๋ฃจํ”„: Red โ†’ Green โ†’ Refactor + +``` +< ๋ฐ˜๋ณต > +1. ์‹คํŒจํ•˜๋Š” ํ…Œ์ŠคํŠธ ์ž‘์„ฑ (Red) +2. ํ†ต๊ณผํ•  ์ตœ์†Œํ•œ์˜ ์ฝ”๋“œ ์ž‘์„ฑ (Green) +3. ๊ตฌ์กฐ ๊ฐœ์„  ๋ฐ ๋ฆฌํŒฉํ† ๋ง (Refactor) +``` + +### ๐Ÿง  ๊ทธ๋Ÿฐ๋ฐ ๊ผญ ํ…Œ์ŠคํŠธ๋ฅผ ๋จผ์ € ์จ์•ผ ํ• ๊นŒ? + +| **์ „๋žต** | **์ด๋ฆ„** | **์„ค๋ช…** | +| --- | --- | --- | +| ๐Ÿงช TFD (Test First Development) | ํ…Œ์ŠคํŠธ ๋จผ์ € ์ž‘์„ฑ โ†’ ์ฝ”๋“œ๋ฅผ ๋งž์ถฐ ๊ตฌํ˜„ | ๋„๋ฉ”์ธ/๋กœ์ง ์ค‘์‹ฌ์— ์ ํ•ฉ | +| ๐Ÿ— TLD (Test Last Development) | ์ฝ”๋“œ๋ฅผ ๋จผ์ € ์ž‘์„ฑ โ†’ ํ…Œ์ŠคํŠธ๋Š” ๋‚˜์ค‘์— ์ž‘์„ฑ | API/๊ณ„์ธต ์„ค๊ณ„๊ฐ€ ๋จผ์ € ํ•„์š”ํ•œ ์ƒํ™ฉ์— ์ ํ•ฉ | + +### ๐ŸŸข TDD๊ฐ€ ํ•„์š”ํ•œ ์ด์œ  + +- **์š”๊ตฌ์‚ฌํ•ญ์„ ๋จผ์ € ์ •๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค** +- **์ž‘๊ฒŒ ์ชผ๊ฐœ๊ณ  ์ ์ง„์ ์œผ๋กœ ์„ค๊ณ„ํ•˜๊ฒŒ ๋œ๋‹ค** +- **์ธํ„ฐํŽ˜์ด์Šค ์„ค๊ณ„๊ฐ€ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ๋‚˜์˜จ๋‹ค** +- **๋ฆฌํŒฉํ† ๋ง์ด ๊ฐ€๋Šฅํ•ด์ง„๋‹ค** + + + +| ๊ตฌ๋ถ„ | ๋งํฌ | +| --- | --- | +| ๐Ÿ”ข ํ…Œ์ŠคํŠธ ํ”ผ๋ผ๋ฏธ๋“œ | [Testing Pyramid - Martin Fowler](https://martinfowler.com/bliki/TestPyramid.html) | +| ๐Ÿงช JUnit5 | [JUnit5 ๊ณต์‹ ๋ฌธ์„œ](https://junit.org/junit5/docs/current/user-guide/) | +| โš™๏ธ Mockito | [Mockito ๊ณต์‹ ๋ฌธ์„œ](https://site.mockito.org/) | +| ๐Ÿงฐ Mockito-Kotlin | [GitHub: mockito-kotlin](https://github.com/mockito/mockito-kotlin) | +| ๐Ÿงต Spring ํ…Œ์ŠคํŠธ | [Spring Boot Testing Guide](https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.testing) | + +> ๋ณธ ๊ณผ์ •์—์„œ๋Š” ์›ํ™œํ•œ ๋ฉ˜ํ† ๋ง์„ ์œ„ํ•ด `JUnit5 + Mockito` ๊ธฐ๋ฐ˜์œผ๋กœ ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค. +> + + + +> ๋‹ค์Œ ์ฃผ์—๋Š” ๋ณธ๊ฒฉ์ ์œผ๋กœ ์šฐ๋ฆฌ๋งŒ์˜ e-commerce ์‹œ์Šคํ…œ์„ **์„ค๊ณ„** ํ•ด๋ด…๋‹ˆ๋‹ค. +> \ No newline at end of file diff --git a/docs/week01_quests.md b/docs/week01_quests.md new file mode 100644 index 000000000..9d6ae8170 --- /dev/null +++ b/docs/week01_quests.md @@ -0,0 +1,131 @@ + + +# ๐Ÿ“ Round 1 Quests + +--- + +## ๐Ÿงช Implementation Quest + +> ์ง€์ •๋œ **๋‹จ์œ„ ํ…Œ์ŠคํŠธ / ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ / E2E ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค**๋ฅผ ํ•„์ˆ˜๋กœ ๊ตฌํ˜„ํ•˜๊ณ , ๋ชจ๋“  ํ…Œ์ŠคํŠธ๋ฅผ ํ†ต๊ณผ์‹œํ‚ค๋Š” ๊ฒƒ์„ ๋ชฉํ‘œ๋กœ ํ•ฉ๋‹ˆ๋‹ค. +> + +### ํšŒ์› ๊ฐ€์ž… + +**๐Ÿงฑ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ** + +- [ ] ID ๊ฐ€ `์˜๋ฌธ ๋ฐ ์ˆซ์ž 10์ž ์ด๋‚ด` ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. +- [ ] ์ด๋ฉ”์ผ์ด `xx@yy.zz` ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. +- [ ] ์ƒ๋…„์›”์ผ์ด `yyyy-MM-dd` ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. + +**๐Ÿ”— ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ** + +- [ ] ํšŒ์› ๊ฐ€์ž…์‹œ User ์ €์žฅ์ด ์ˆ˜ํ–‰๋œ๋‹ค. ( spy ๊ฒ€์ฆ ) +- [ ] ์ด๋ฏธ ๊ฐ€์ž…๋œ ID ๋กœ ํšŒ์›๊ฐ€์ž… ์‹œ๋„ ์‹œ, ์‹คํŒจํ•œ๋‹ค. + +**๐ŸŒ E2E ํ…Œ์ŠคํŠธ** + +- [ ] ํšŒ์› ๊ฐ€์ž…์ด ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ์ƒ์„ฑ๋œ ์œ ์ € ์ •๋ณด๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค. +- [ ] ํšŒ์› ๊ฐ€์ž… ์‹œ์— ์„ฑ๋ณ„์ด ์—†์„ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. + +### ๋‚ด ์ •๋ณด ์กฐํšŒ + +**๐Ÿ”— ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ** + +- [ ] ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•  ๊ฒฝ์šฐ, ํšŒ์› ์ •๋ณด๊ฐ€ ๋ฐ˜ํ™˜๋œ๋‹ค. +- [ ] ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ, null ์ด ๋ฐ˜ํ™˜๋œ๋‹ค. + +**๐ŸŒ E2E ํ…Œ์ŠคํŠธ** + +- [ ] ๋‚ด ์ •๋ณด ์กฐํšŒ์— ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ํ•ด๋‹นํ•˜๋Š” ์œ ์ € ์ •๋ณด๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค. +- [ ] ์กด์žฌํ•˜์ง€ ์•Š๋Š” ID ๋กœ ์กฐํšŒํ•  ๊ฒฝ์šฐ, `404 Not Found` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. + +### ํฌ์ธํŠธ ์กฐํšŒ + +**๐Ÿ”— ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ** + +- [ ] ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•  ๊ฒฝ์šฐ, ๋ณด์œ  ํฌ์ธํŠธ๊ฐ€ ๋ฐ˜ํ™˜๋œ๋‹ค. +- [ ] ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ, null ์ด ๋ฐ˜ํ™˜๋œ๋‹ค. + +**๐ŸŒ E2E ํ…Œ์ŠคํŠธ** + +- [ ] ํฌ์ธํŠธ ์กฐํšŒ์— ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ๋ณด์œ  ํฌ์ธํŠธ๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค. +- [ ] `X-USER-ID` ํ—ค๋”๊ฐ€ ์—†์„ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. + +### ํฌ์ธํŠธ ์ถฉ์ „ + +**๐Ÿงฑ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ** + +- [ ] 0 ์ดํ•˜์˜ ์ •์ˆ˜๋กœ ํฌ์ธํŠธ๋ฅผ ์ถฉ์ „ ์‹œ ์‹คํŒจํ•œ๋‹ค. + +**๐Ÿ”— ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ** + +- [ ] ์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ์ € ID ๋กœ ์ถฉ์ „์„ ์‹œ๋„ํ•œ ๊ฒฝ์šฐ, ์‹คํŒจํ•œ๋‹ค. + +**๐ŸŒ E2E ํ…Œ์ŠคํŠธ** + +- [ ] ์กด์žฌํ•˜๋Š” ์œ ์ €๊ฐ€ 1000์›์„ ์ถฉ์ „ํ•  ๊ฒฝ์šฐ, ์ถฉ์ „๋œ ๋ณด์œ  ์ด๋Ÿ‰์„ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค. +- [ ] ์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ์ €๋กœ ์š”์ฒญํ•  ๊ฒฝ์šฐ, `404 Not Found` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. + +## โœ… Checklist + +- [ ] ํ…Œ์ŠคํŠธ๋Š” ๋ชจ๋‘ ํ†ต๊ณผํ•ด์•ผ ํ•˜๋ฉฐ, `@Test` ๊ธฐ๋ฐ˜์œผ๋กœ ๋ช…์‹œ์ ์œผ๋กœ ์ž‘์„ฑ +- [ ] ๊ฐ ํ…Œ์ŠคํŠธ๋Š” ํ…Œ์ŠคํŠธ ๋ช…/์„ค๋ช…/์ž…๋ ฅ/์˜ˆ์ƒ ๊ฒฐ๊ณผ๊ฐ€ ๋ถ„๋ช…ํ•ด์•ผ ํ•จ +- [ ] E2E ํ…Œ์ŠคํŠธ๋Š” ์‹ค์ œ HTTP ์š”์ฒญ์„ ํ†ตํ•ด ํ๋ฆ„์„ ๊ฒ€์ฆํ•  ๊ฒƒ + +--- + +## โœ๏ธ Technical Writing Quest + +> ์ด๋ฒˆ ์ฃผ์— ํ•™์Šตํ•œ ๋‚ด์šฉ, ๊ณผ์ œ ์ง„ํ–‰์„ ๋˜๋Œ์•„๋ณด๋ฉฐ +**"๋‚ด๊ฐ€ ์–ด๋–ค ํŒ๋‹จ์„ ํ•˜๊ณ  ์™œ ๊ทธ๋ ‡๊ฒŒ ๊ตฌํ˜„ํ–ˆ๋Š”์ง€"** ๋ฅผ ๊ธ€๋กœ ์ •๋ฆฌํ•ด๋ด…๋‹ˆ๋‹ค. +> +> +> **์ข‹์€ ๋ธ”๋กœ๊ทธ ๊ธ€์€ ๋‚ด๊ฐ€ ๊ฒช์€ ๋ฌธ์ œ๋ฅผ, ํƒ€์ธ๋„ ๊ณต๊ฐํ•  ์ˆ˜ ์žˆ๊ฒŒ ์ •๋ฆฌํ•œ ๊ธ€์ž…๋‹ˆ๋‹ค.** +> +> ์ด ๊ธ€์€ ๋‹จ์ˆœ ๊ณผ์ œ๊ฐ€ ์•„๋‹ˆ๋ผ, **ํ–ฅํ›„ ์ด์ง์— ๋„์›€์ด ๋  ์ˆ˜ ์žˆ๋Š” ํฌํŠธํด๋ฆฌ์˜ค** ๊ฐ€ ๋  ์ˆ˜ ์žˆ์–ด์š”. +> + +### ๐Ÿ“š Technical Writing Guide + +### โœ… ์ž‘์„ฑ ๊ธฐ์ค€ + +| ํ•ญ๋ชฉ | ์„ค๋ช… | +| --- | --- | +| **ํ˜•์‹** | ๋ธ”๋กœ๊ทธ | +| **๊ธธ์ด** | ์ œํ•œ ์—†์Œ, ๋‹จ ๊ผญ **1์ค„ ์š”์•ฝ (TL;DR)** ์„ ํฌํ•จํ•ด ์ฃผ์„ธ์š” | +| **ํฌ์ธํŠธ** | โ€œ๋ฌด์—‡์„ ํ–ˆ๋‹คโ€ ๋ณด๋‹ค **โ€œ์™œ ๊ทธ๋ ‡๊ฒŒ ํŒ๋‹จํ–ˆ๋Š”๊ฐ€โ€** ์ค‘์‹ฌ | +| **์˜ˆ์‹œ ํฌํ•จ** | ์ฝ”๋“œ ๋น„๊ต, ํ๋ฆ„๋„, ๋ฆฌํŒฉํ† ๋ง ์ „ํ›„ ์˜ˆ์‹œ ๋“ฑ ์ž์œ ๋กญ๊ฒŒ | +| **ํ†ค** | ์‹ค๋ ฅ์€ ๋ณด์ด์ง€๋งŒ, ์ž๋งŒํ•˜์ง€ ์•Š๊ณ , **๊ณ ๋ฏผ์ด ์ฝํžˆ๋Š” ๊ธ€**์˜ˆ: โ€œ์ฒ˜์Œ์—” mock์œผ๋กœ ์ถฉ๋ถ„ํ•˜๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ์ง€๋งŒ, ๋‚˜์ค‘์— fake๋กœ ๊ต์ฒดํ•˜๊ฒŒ ๋œ ์ด์œ ๋Š”โ€ฆโ€ | + +--- + +### โœจ ์ข‹์€ ํ†ค์€ ์ด๋Ÿฐ ๋А๋‚Œ์ด์—์š” + +> ๋‚ด๊ฐ€ ๊ฒช์€ ์‹ค์ „์  ๊ณ ๋ฏผ์„ ๋‹ค๋ฅธ ๊ฐœ๋ฐœ์ž๋„ ๊ณต๊ฐํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ’€์–ด๋‚ด์ž +> + +| ํŠน์ง• | ์˜ˆ์‹œ | +| --- | --- | +| ๐Ÿค” ๋‚ด ์–ธ์–ด๋กœ ์„ค๋ช…ํ•œ ๊ฐœ๋… | Stub๊ณผ Mock์˜ ์ฐจ์ด๋ฅผ ์ด๋ฒˆ ์ฃผ๋ฌธ ํ…Œ์ŠคํŠธ์—์„œ ์ฒ˜์Œ ์‹ค๊ฐํ–ˆ๋‹ค | +| ๐Ÿ’ญ ํŒ๋‹จ ํ๋ฆ„์ด ๋“œ๋Ÿฌ๋‚˜๋Š” ๊ธ€ | ์ฒ˜์Œ์—” ๋„๋ฉ”์ธ์„ ๋‚˜๋ˆ„์ง€ ์•Š์•˜๋Š”๋ฐ, ํ…Œ์ŠคํŠธ๊ฐ€ ์–ด๋ ค์›Œ์ง€๋ฉฐ ๋ถ„๋ฆฌํ–ˆ๋‹ค | +| ๐Ÿ“ ์ •๋ณด ๋‚˜์—ด๋ณด๋‹ค ์ธ์‚ฌ์ดํŠธ ์ค‘์‹ฌ | ํ…Œ์ŠคํŠธ๋Š” ์ž‘์„ฑํ–ˆ์ง€๋งŒ, ๊ตฌ์กฐ๋Š” ๋งŒ์กฑ์Šค๋Ÿฝ์ง€ ์•Š๋‹ค. ๋‹ค์Œ์—”โ€ฆ | + +### โŒ ํ”ผํ•ด์•ผ ํ•  ์Šคํƒ€์ผ + +| ์˜ˆ์‹œ | ์ด์œ  | +| --- | --- | +| ๋งŽ์ด ๋ถ€์กฑํ–ˆ๊ณ , ๋ฐ˜์„ฑํ•ฉ๋‹ˆ๋‹คโ€ฆ | ํšŒ๊ณ ๊ฐ€ ์•„๋‹ˆ๋ผ ์ผ๊ธฐ์ฒ˜๋Ÿผ ๋ณด์ž…๋‹ˆ๋‹ค | +| Stub์€ ์‘๋‹ต์„ ์ง€์ •ํ•˜๊ณ โ€ฆ | ๋‚ด ์ƒ๊ฐ์ด ์•„๋‹Œ ์š”์•ฝ๋ฌธ์ฒ˜๋Ÿผ ๋ณด์ž…๋‹ˆ๋‹ค | +| ํ…Œ์ŠคํŠธ๊ฐ€ ์ง„๋ฆฌ๋‹ค | ๋„ˆ๋ฌด ๋‹จ์ •์ ์ด๊ฑฐ๋‚˜ ์˜ค๋งŒํ•ด ๋ณด์ž…๋‹ˆ๋‹ค | + +### ๐ŸŽฏ Feature Suggestions + +- ๋‚ด๊ฐ€ ์ž‘์„ฑํ•œ ํ…Œ์ŠคํŠธ ์ค‘ ๊ฐ€์žฅ ์˜๋ฏธ ์žˆ์—ˆ๋˜ ๊ฒƒ 1๊ฐ€์ง€ +- ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅํ•œ ๊ตฌ์กฐ๋ฅผ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด ํ•œ ๋ฆฌํŒฉํ† ๋ง +- Mock, Stub, Fake ์ค‘ ์‹ค์ œ ํ™œ์šฉ ๊ฒฝํ—˜๊ณผ ๋‚˜๋งŒ์˜ ๊ตฌ๋ถ„ ๊ธฐ์ค€ +- TDD ๋ฐฉ์‹์œผ๋กœ ์ ‘๊ทผํ•˜๊ฑฐ๋‚˜ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•ด๋ณด๋ฉฐ ์–ด๋ ค์› ๋˜ ์  \ No newline at end of file From 8e06929200b0d3b6ef4380c5ad54c911870ee97a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A0=95=EB=B0=B0?= Date: Tue, 28 Oct 2025 17:04:18 +0900 Subject: [PATCH 02/85] =?UTF-8?q?round1:=20redis,=20kafka=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EC=A3=BC=EC=84=9D=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/commerce-api/build.gradle.kts | 4 +- .../src/main/resources/application.yml | 2 +- docker/infra-compose.yml | 174 +++++++++--------- settings.gradle.kts | 6 +- 4 files changed, 93 insertions(+), 93 deletions(-) diff --git a/apps/commerce-api/build.gradle.kts b/apps/commerce-api/build.gradle.kts index 03ce68f02..e6d28d4ed 100644 --- a/apps/commerce-api/build.gradle.kts +++ b/apps/commerce-api/build.gradle.kts @@ -1,7 +1,7 @@ dependencies { // add-ons implementation(project(":modules:jpa")) - implementation(project(":modules:redis")) +// implementation(project(":modules:redis")) implementation(project(":supports:jackson")) implementation(project(":supports:logging")) implementation(project(":supports:monitoring")) @@ -18,5 +18,5 @@ dependencies { // test-fixtures testImplementation(testFixtures(project(":modules:jpa"))) - testImplementation(testFixtures(project(":modules:redis"))) +// testImplementation(testFixtures(project(":modules:redis"))) } diff --git a/apps/commerce-api/src/main/resources/application.yml b/apps/commerce-api/src/main/resources/application.yml index 484c070d0..a8b0b72e3 100644 --- a/apps/commerce-api/src/main/resources/application.yml +++ b/apps/commerce-api/src/main/resources/application.yml @@ -20,7 +20,7 @@ spring: config: import: - jpa.yml - - redis.yml +# - redis.yml - logging.yml - monitoring.yml diff --git a/docker/infra-compose.yml b/docker/infra-compose.yml index 18e5fcf5f..d2607d47a 100644 --- a/docker/infra-compose.yml +++ b/docker/infra-compose.yml @@ -14,97 +14,97 @@ services: volumes: - mysql-8-data:/var/lib/mysql - redis-master: - image: redis:7.0 - container_name: redis-master - ports: - - "6379:6379" - volumes: - - redis_master_data:/data - command: - [ - "redis-server", # redis ์„œ๋ฒ„ ์‹คํ–‰ ๋ช…๋ น์–ด - "--appendonly", "yes", # AOF (AppendOnlyFile) ์˜์†์„ฑ ๊ธฐ๋Šฅ ์ผœ๊ธฐ - "--save", "", - "--latency-monitor-threshold", "100", # ํŠน์ • command ๊ฐ€ ์ง€์ • ์‹œ๊ฐ„(ms) ์ด์ƒ ๊ฑธ๋ฆฌ๋ฉด monitor ๊ธฐ๋ก - ] - healthcheck: - test: ["CMD", "redis-cli", "-p", "6379", "PING"] - interval: 5s - timeout: 2s - retries: 10 - - redis-readonly: - image: redis:7.0 - container_name: redis-readonly - depends_on: - redis-master: - condition: service_healthy - ports: - - "6380:6379" - volumes: - - redis_readonly_data:/data - command: - [ - "redis-server", - "--appendonly", "yes", - "--appendfsync", "everysec", - "--replicaof", "redis-master", "6379", # replica ๋ชจ๋“œ๋กœ ์‹คํ–‰ + ์„œ๋น„์Šค ๋ช…, ์„œ๋น„์Šค ํฌํŠธ - "--replica-read-only", "yes", # ์ฝ๊ธฐ ์ „์šฉ์œผ๋กœ ์„ค์ • - "--latency-monitor-threshold", "100", - ] - healthcheck: - test: ["CMD", "redis-cli", "-p", "6379", "PING"] - interval: 5s - timeout: 2s - retries: 10 - - kafka: - image: bitnamilegacy/kafka:3.5.1 - container_name: kafka - ports: - - "9092:9092" # ์นดํ”„์นด ๋ธŒ๋กœ์ปค PORT - - "19092:19092" # ํ˜ธ์ŠคํŠธ ๋ฆฌ์Šค๋„ˆ ์–˜ ๋–„๋ฌธ์ธ๊ฐ€ - environment: - - KAFKA_CFG_NODE_ID=1 # ๋ธŒ๋กœ์ปค ๊ณ ์œ  ID - - KAFKA_CFG_PROCESS_ROLES=broker,controller # KRaft ๋ชจ๋“œ์—ฌ์„œ, broker / controller ์—ญํ•  ๋ชจ๋‘ ๋ถ€์—ฌ - - KAFKA_CFG_LISTENERS=PLAINTEXT://:9092,PLAINTEXT_HOST://:19092,CONTROLLER://:9093 - # ๋ธŒ๋กœ์ปค ํด๋ผ์ด์–ธํŠธ (PLAINTEXT), ๋ธŒ๋กœ์ปค ํ˜ธ์ŠคํŠธ (PLAINTEXT) ๋‚ด๋ถ€ ์ปจํŠธ๋กค๋Ÿฌ (CONTROLLER) - - KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://kafka:9092,PLAINTEXT_HOST://localhost:19092 - # ์™ธ๋ถ€ ํด๋ผ์ด์–ธํŠธ ์ ‘์† ํ˜ธ์ŠคํŠธ (localhost:9092), ๋ธŒ๋กœ์ปค ์ ‘์† ํ˜ธ์ŠคํŠธ (localhost:19092) - - KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT,CONTROLLER:PLAINTEXT - # ๊ฐ ๋ฆฌ์Šค๋„ˆ๋ณ„ ๋ณด์•ˆ ํ”„๋กœํ† ์ฝœ ์„ค์ • - - KAFKA_CFG_INTER_BROKER_LISTENER_NAME=PLAINTEXT - - KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER # ์ปจํŠธ๋กค๋Ÿฌ ๋‹ด๋‹น ๋ฆฌ์Šค๋„ˆ ์ง€์ • - - KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=1@kafka:9093 # ์ปจํŠธ๋กค๋Ÿฌ ํ›„๋ณด ๋…ธ๋“œ ์ •์˜ (๋‹จ์ผ ๋ธŒ๋กœ์ปค๋ผ ์ž๊ธฐ ์ž์‹ ๋งŒ ์žˆ์Œ) - - KAFKA_CFG_OFFSETS_TOPIC_REPLICATION_FACTOR=1 # consumer offset ๋ณต์ œ ๊ฐฏ์ˆ˜ (ํ˜„์žฌ๋Š” 1 - ๋กœ์ปฌ์šฉ์ด๋ผ์„œ) - - KAFKA_CFG_TRANSACTION_STATE_LOG_REPLICATION_FACTOR=1 # transaction log ํ† ํ”ฝ ๋ณต์ œ ๊ฐฏ์ˆ˜ (ํ˜„์žฌ๋Š” 1 - ๋กœ์ปฌ์šฉ์ด๋ผ์„œ) - - KAFKA_CFG_TRANSACTION_STATE_LOG_MIN_ISR=1 # In-Sync-Replica ์ตœ์†Œ ์ˆ˜ (ํ˜„์žฌ๋Š” 1 - ๋กœ์ปฌ์šฉ์ด๋ผ์„œ) - volumes: - - kafka-data:/bitnami/kafka - healthcheck: - test: ["CMD", "bash", "-c", "kafka-topics.sh --bootstrap-server localhost:9092 --list || exit 1"] - interval: 10s - timeout: 5s - retries: 10 - - kafka-ui: - image: provectuslabs/kafka-ui:latest - container_name: kafka-ui - depends_on: - kafka: - condition: service_healthy - ports: - - "9099:8080" - environment: - KAFKA_CLUSTERS_0_NAME: local # kafka-ui ์—์„œ ๋ณด์ด๋Š” ํด๋Ÿฌ์Šคํ„ฐ๋ช… - KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:9092 # kafka-ui ๊ฐ€ ์—ฐ๊ฒทํ•  ๋ธŒ๋กœ์ปค ์ฃผ์†Œ +# redis-master: +# image: redis:7.0 +# container_name: redis-master +# ports: +# - "6379:6379" +# volumes: +# - redis_master_data:/data +# command: +# [ +# "redis-server", # redis ์„œ๋ฒ„ ์‹คํ–‰ ๋ช…๋ น์–ด +# "--appendonly", "yes", # AOF (AppendOnlyFile) ์˜์†์„ฑ ๊ธฐ๋Šฅ ์ผœ๊ธฐ +# "--save", "", +# "--latency-monitor-threshold", "100", # ํŠน์ • command ๊ฐ€ ์ง€์ • ์‹œ๊ฐ„(ms) ์ด์ƒ ๊ฑธ๋ฆฌ๋ฉด monitor ๊ธฐ๋ก +# ] +# healthcheck: +# test: ["CMD", "redis-cli", "-p", "6379", "PING"] +# interval: 5s +# timeout: 2s +# retries: 10 +# +# redis-readonly: +# image: redis:7.0 +# container_name: redis-readonly +# depends_on: +# redis-master: +# condition: service_healthy +# ports: +# - "6380:6379" +# volumes: +# - redis_readonly_data:/data +# command: +# [ +# "redis-server", +# "--appendonly", "yes", +# "--appendfsync", "everysec", +# "--replicaof", "redis-master", "6379", # replica ๋ชจ๋“œ๋กœ ์‹คํ–‰ + ์„œ๋น„์Šค ๋ช…, ์„œ๋น„์Šค ํฌํŠธ +# "--replica-read-only", "yes", # ์ฝ๊ธฐ ์ „์šฉ์œผ๋กœ ์„ค์ • +# "--latency-monitor-threshold", "100", +# ] +# healthcheck: +# test: ["CMD", "redis-cli", "-p", "6379", "PING"] +# interval: 5s +# timeout: 2s +# retries: 10 +# +# kafka: +# image: bitnamilegacy/kafka:3.5.1 +# container_name: kafka +# ports: +# - "9092:9092" # ์นดํ”„์นด ๋ธŒ๋กœ์ปค PORT +# - "19092:19092" # ํ˜ธ์ŠคํŠธ ๋ฆฌ์Šค๋„ˆ ์–˜ ๋–„๋ฌธ์ธ๊ฐ€ +# environment: +# - KAFKA_CFG_NODE_ID=1 # ๋ธŒ๋กœ์ปค ๊ณ ์œ  ID +# - KAFKA_CFG_PROCESS_ROLES=broker,controller # KRaft ๋ชจ๋“œ์—ฌ์„œ, broker / controller ์—ญํ•  ๋ชจ๋‘ ๋ถ€์—ฌ +# - KAFKA_CFG_LISTENERS=PLAINTEXT://:9092,PLAINTEXT_HOST://:19092,CONTROLLER://:9093 +# # ๋ธŒ๋กœ์ปค ํด๋ผ์ด์–ธํŠธ (PLAINTEXT), ๋ธŒ๋กœ์ปค ํ˜ธ์ŠคํŠธ (PLAINTEXT) ๋‚ด๋ถ€ ์ปจํŠธ๋กค๋Ÿฌ (CONTROLLER) +# - KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://kafka:9092,PLAINTEXT_HOST://localhost:19092 +# # ์™ธ๋ถ€ ํด๋ผ์ด์–ธํŠธ ์ ‘์† ํ˜ธ์ŠคํŠธ (localhost:9092), ๋ธŒ๋กœ์ปค ์ ‘์† ํ˜ธ์ŠคํŠธ (localhost:19092) +# - KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT,CONTROLLER:PLAINTEXT +# # ๊ฐ ๋ฆฌ์Šค๋„ˆ๋ณ„ ๋ณด์•ˆ ํ”„๋กœํ† ์ฝœ ์„ค์ • +# - KAFKA_CFG_INTER_BROKER_LISTENER_NAME=PLAINTEXT +# - KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER # ์ปจํŠธ๋กค๋Ÿฌ ๋‹ด๋‹น ๋ฆฌ์Šค๋„ˆ ์ง€์ • +# - KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=1@kafka:9093 # ์ปจํŠธ๋กค๋Ÿฌ ํ›„๋ณด ๋…ธ๋“œ ์ •์˜ (๋‹จ์ผ ๋ธŒ๋กœ์ปค๋ผ ์ž๊ธฐ ์ž์‹ ๋งŒ ์žˆ์Œ) +# - KAFKA_CFG_OFFSETS_TOPIC_REPLICATION_FACTOR=1 # consumer offset ๋ณต์ œ ๊ฐฏ์ˆ˜ (ํ˜„์žฌ๋Š” 1 - ๋กœ์ปฌ์šฉ์ด๋ผ์„œ) +# - KAFKA_CFG_TRANSACTION_STATE_LOG_REPLICATION_FACTOR=1 # transaction log ํ† ํ”ฝ ๋ณต์ œ ๊ฐฏ์ˆ˜ (ํ˜„์žฌ๋Š” 1 - ๋กœ์ปฌ์šฉ์ด๋ผ์„œ) +# - KAFKA_CFG_TRANSACTION_STATE_LOG_MIN_ISR=1 # In-Sync-Replica ์ตœ์†Œ ์ˆ˜ (ํ˜„์žฌ๋Š” 1 - ๋กœ์ปฌ์šฉ์ด๋ผ์„œ) +# volumes: +# - kafka-data:/bitnami/kafka +# healthcheck: +# test: ["CMD", "bash", "-c", "kafka-topics.sh --bootstrap-server localhost:9092 --list || exit 1"] +# interval: 10s +# timeout: 5s +# retries: 10 +# +# kafka-ui: +# image: provectuslabs/kafka-ui:latest +# container_name: kafka-ui +# depends_on: +# kafka: +# condition: service_healthy +# ports: +# - "9099:8080" +# environment: +# KAFKA_CLUSTERS_0_NAME: local # kafka-ui ์—์„œ ๋ณด์ด๋Š” ํด๋Ÿฌ์Šคํ„ฐ๋ช… +# KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:9092 # kafka-ui ๊ฐ€ ์—ฐ๊ฒทํ•  ๋ธŒ๋กœ์ปค ์ฃผ์†Œ volumes: mysql-8-data: - redis_master_data: +# redis_master_data: redis_readonly_data: - kafka-data: +# kafka-data: networks: default: diff --git a/settings.gradle.kts b/settings.gradle.kts index c99fb6360..83ff00abc 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -2,10 +2,10 @@ rootProject.name = "loopers-java-spring-template" include( ":apps:commerce-api", - ":apps:commerce-streamer", +// ":apps:commerce-streamer", ":modules:jpa", - ":modules:redis", - ":modules:kafka", +// ":modules:redis", +// ":modules:kafka", ":supports:jackson", ":supports:logging", ":supports:monitoring", From 5569bf6a83a3ad3a4884c7d6305db2d6d422d3f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A0=95=EB=B0=B0?= Date: Tue, 28 Oct 2025 17:46:19 +0900 Subject: [PATCH 03/85] =?UTF-8?q?round1:=20User=20=EA=B0=9D=EC=B2=B4=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84,=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20(ID)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/loopers/domain/user/User.java | 19 +++++++ .../loopers/domain/user/UserModelTest.java | 51 +++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/user/User.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/user/UserModelTest.java diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java new file mode 100644 index 000000000..4c889f4ab --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java @@ -0,0 +1,19 @@ +package com.loopers.domain.user; + +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; + +public class User { + private final String id; + + private User(String id) { + this.id = id; + } + + public static User create(String id) { + if (!id.matches("^(?=.*[a-zA-Z])(?=.*\\d)[a-zA-Z\\d]{1,10}$")) { + throw new CoreException(ErrorType.BAD_REQUEST, "ID๋Š” ์˜๋ฌธ ๋ฐ ์ˆซ์ž 10์ž ์ด๋‚ด์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + } + return new User(id); + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserModelTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserModelTest.java new file mode 100644 index 000000000..06b884e8d --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserModelTest.java @@ -0,0 +1,51 @@ +package com.loopers.domain.user; + +import com.loopers.support.error.CoreException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class UserModelTest { + @DisplayName("ํšŒ์› ๊ฐ€์ž…์„ ํ•  ๋•Œ, ") + @Nested + class Create { + // ID ๊ฐ€ ์˜๋ฌธ ๋ฐ ์ˆซ์ž 10์ž ์ด๋‚ด ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. + + @DisplayName("ID ๊ฐ€ ์˜๋ฌธ ๋ฐ ์ˆซ์ž 10์ž ์ด๋‚ด ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. - ์˜๋ฌธ ๋ฐ ์ˆซ์ž๊ฐ€ ์•„๋‹Œ ๊ฒฝ์šฐ") + @Test + void throwsException_whenIdIsInvalidFormat_NotAlphanumeric() { + // arrange + String invalidId = "user!@#"; + + // act + CoreException result = assertThrows(CoreException.class, () -> { + User.create(invalidId); + }); + + // assert + assertThat(result.getMessage()).isEqualTo("ID๋Š” ์˜๋ฌธ ๋ฐ ์ˆซ์ž 10์ž ์ด๋‚ด์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + } + + @DisplayName("ID ๊ฐ€ ์˜๋ฌธ ๋ฐ ์ˆซ์ž 10์ž ์ด๋‚ด ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. - ์˜๋ฌธ ๋ฐ ์ˆซ์ž 10์ž ์ดˆ๊ณผ์ธ ๊ฒฝ์šฐ") + @Test + void throwsException_whenIdIsInvalidFormat_TooLong() { + // arrange + String invalidId = "user1234567"; + + // act + CoreException result = assertThrows(CoreException.class, () -> { + User.create(invalidId); + }); + + // assert + assertThat(result.getMessage()).isEqualTo("ID๋Š” ์˜๋ฌธ ๋ฐ ์ˆซ์ž 10์ž ์ด๋‚ด์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + } + + // extra case + // 0์ž ์ดํ•˜์ธ ๊ฒฝ์šฐ + // ์ˆซ์ž๋งŒ ์žˆ๋Š” ๊ฒฝ์šฐ + } +} From ee02ce7e19e62fa36175020bc813558734368cb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A0=95=EB=B0=B0?= Date: Tue, 28 Oct 2025 17:58:51 +0900 Subject: [PATCH 04/85] =?UTF-8?q?round1:=20User=20email=20=ED=95=84?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80,=20email=20=EC=A0=9C=EC=95=BD=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/loopers/domain/user/User.java | 14 +++- .../loopers/domain/user/UserModelTest.java | 71 ++++++++++++++++++- 2 files changed, 80 insertions(+), 5 deletions(-) diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java index 4c889f4ab..355421dd1 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java @@ -5,15 +5,23 @@ public class User { private final String id; + private final String email; - private User(String id) { + private User(String id, String email) { this.id = id; + this.email = email; } - public static User create(String id) { + public static User create(String id, String email) { + // ID ๊ฐ€ ์˜๋ฌธ ๋ฐ ์ˆซ์ž 10์ž ์ด๋‚ด ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. if (!id.matches("^(?=.*[a-zA-Z])(?=.*\\d)[a-zA-Z\\d]{1,10}$")) { throw new CoreException(ErrorType.BAD_REQUEST, "ID๋Š” ์˜๋ฌธ ๋ฐ ์ˆซ์ž 10์ž ์ด๋‚ด์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค."); } - return new User(id); + // ์ด๋ฉ”์ผ์ด xx@yy.zz ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. + if (!email.matches("^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$")) { + throw new CoreException(ErrorType.BAD_REQUEST, "์ด๋ฉ”์ผ ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); + } + + return new User(id, email); } } diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserModelTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserModelTest.java index 06b884e8d..7ef97f12f 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserModelTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserModelTest.java @@ -12,6 +12,9 @@ public class UserModelTest { @DisplayName("ํšŒ์› ๊ฐ€์ž…์„ ํ•  ๋•Œ, ") @Nested class Create { + private final String validId = "user123"; + private final String validEmail = "xx@yy.zz"; + // ID ๊ฐ€ ์˜๋ฌธ ๋ฐ ์ˆซ์ž 10์ž ์ด๋‚ด ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. @DisplayName("ID ๊ฐ€ ์˜๋ฌธ ๋ฐ ์ˆซ์ž 10์ž ์ด๋‚ด ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. - ์˜๋ฌธ ๋ฐ ์ˆซ์ž๊ฐ€ ์•„๋‹Œ ๊ฒฝ์šฐ") @@ -22,7 +25,7 @@ void throwsException_whenIdIsInvalidFormat_NotAlphanumeric() { // act CoreException result = assertThrows(CoreException.class, () -> { - User.create(invalidId); + User.create(invalidId, validEmail); }); // assert @@ -37,7 +40,7 @@ void throwsException_whenIdIsInvalidFormat_TooLong() { // act CoreException result = assertThrows(CoreException.class, () -> { - User.create(invalidId); + User.create(invalidId, validEmail); }); // assert @@ -47,5 +50,69 @@ void throwsException_whenIdIsInvalidFormat_TooLong() { // extra case // 0์ž ์ดํ•˜์ธ ๊ฒฝ์šฐ // ์ˆซ์ž๋งŒ ์žˆ๋Š” ๊ฒฝ์šฐ + + // ์ด๋ฉ”์ผ์ด xx@yy.zz ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. + @DisplayName("์ด๋ฉ”์ผ์ด xx@yy.zz ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. - @๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ") + @Test + void throwsException_whenEmailIsInvalidFormat_MissingAtSymbol() { + // arrange + String invalidEmail = "userexample.com"; + + // act + CoreException result = assertThrows(CoreException.class, () -> { + User.create(validId, invalidEmail); + }); + + // assert + assertThat(result.getMessage()).isEqualTo("์ด๋ฉ”์ผ ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); + } + + @DisplayName("์ด๋ฉ”์ผ์ด xx@yy.zz ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. - ๋„๋ฉ”์ธ ๋ถ€๋ถ„์ด ์—†๋Š” ๊ฒฝ์šฐ") + @Test + void throwsException_whenEmailIsInvalidFormat_MissingDomain() { + // arrange + String invalidEmail = "user@.com"; + + // act + CoreException result = assertThrows(CoreException.class, () -> { + User.create(validId, invalidEmail); + }); + + // assert + assertThat(result.getMessage()).isEqualTo("์ด๋ฉ”์ผ ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); + } + + @DisplayName("์ด๋ฉ”์ผ์ด xx@yy.zz ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. - ์ตœ์ƒ์œ„ ๋„๋ฉ”์ธ์ด ์—†๋Š” ๊ฒฝ์šฐ") + @Test + void throwsException_whenEmailIsInvalidFormat_MissingTopLevelDomain() { + // arrange + String invalidEmail = "user@example"; + + // act + CoreException result = assertThrows(CoreException.class, () -> { + User.create(validId, invalidEmail); + }); + + // assert + assertThat(result.getMessage()).isEqualTo("์ด๋ฉ”์ผ ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); + } + + @DisplayName("์ด๋ฉ”์ผ์ด xx@yy.zz ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. - @.๋งŒ ์žˆ๋Š” ๊ฒฝ์šฐ") + @Test + void throwsException_whenEmailIsInvalidFormat_OnlyAtAndDot() { + // arrange + String invalidEmail = "@."; + + // act + CoreException result = assertThrows(CoreException.class, () -> { + User.create(validId, invalidEmail); + }); + + // assert + assertThat(result.getMessage()).isEqualTo("์ด๋ฉ”์ผ ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); + } + + // extra case + // ๊ณต๋ฐฑ์ด ํฌํ•จ๋œ ๊ฒฝ์šฐ } } From e1d3f05cf7bbf4bc8ac8c41a1084907d779f7338 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A0=95=EB=B0=B0?= Date: Tue, 28 Oct 2025 18:10:32 +0900 Subject: [PATCH 05/85] =?UTF-8?q?round1:=20User=20birthday=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80,=20birthday=20=EC=A0=9C=EC=95=BD=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/loopers/domain/user/User.java | 12 ++- .../loopers/domain/user/UserModelTest.java | 89 +++++++++++++++++-- 2 files changed, 92 insertions(+), 9 deletions(-) diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java index 355421dd1..6cf45fe57 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java @@ -6,13 +6,15 @@ public class User { private final String id; private final String email; + private final String birthday; - private User(String id, String email) { + private User(String id, String email, String birthday) { this.id = id; this.email = email; + this.birthday = birthday; } - public static User create(String id, String email) { + public static User create(String id, String email, String birthday) { // ID ๊ฐ€ ์˜๋ฌธ ๋ฐ ์ˆซ์ž 10์ž ์ด๋‚ด ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. if (!id.matches("^(?=.*[a-zA-Z])(?=.*\\d)[a-zA-Z\\d]{1,10}$")) { throw new CoreException(ErrorType.BAD_REQUEST, "ID๋Š” ์˜๋ฌธ ๋ฐ ์ˆซ์ž 10์ž ์ด๋‚ด์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค."); @@ -21,7 +23,11 @@ public static User create(String id, String email) { if (!email.matches("^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$")) { throw new CoreException(ErrorType.BAD_REQUEST, "์ด๋ฉ”์ผ ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); } + // ์ƒ๋…„์›”์ผ์ด YYYY-MM-DD ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. + if (!birthday.matches("^\\d{4}-\\d{2}-\\d{2}$")) { + throw new CoreException(ErrorType.BAD_REQUEST, "์ƒ๋…„์›”์ผ ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); + } - return new User(id, email); + return new User(id, email, birthday); } } diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserModelTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserModelTest.java index 7ef97f12f..4c29aa8db 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserModelTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserModelTest.java @@ -14,6 +14,7 @@ public class UserModelTest { class Create { private final String validId = "user123"; private final String validEmail = "xx@yy.zz"; + private final String validBirthday = "1993-03-13"; // ID ๊ฐ€ ์˜๋ฌธ ๋ฐ ์ˆซ์ž 10์ž ์ด๋‚ด ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. @@ -25,7 +26,7 @@ void throwsException_whenIdIsInvalidFormat_NotAlphanumeric() { // act CoreException result = assertThrows(CoreException.class, () -> { - User.create(invalidId, validEmail); + User.create(invalidId, validEmail, validBirthday); }); // assert @@ -40,7 +41,7 @@ void throwsException_whenIdIsInvalidFormat_TooLong() { // act CoreException result = assertThrows(CoreException.class, () -> { - User.create(invalidId, validEmail); + User.create(invalidId, validEmail, validBirthday); }); // assert @@ -60,7 +61,7 @@ void throwsException_whenEmailIsInvalidFormat_MissingAtSymbol() { // act CoreException result = assertThrows(CoreException.class, () -> { - User.create(validId, invalidEmail); + User.create(validId, invalidEmail, validBirthday); }); // assert @@ -75,7 +76,7 @@ void throwsException_whenEmailIsInvalidFormat_MissingDomain() { // act CoreException result = assertThrows(CoreException.class, () -> { - User.create(validId, invalidEmail); + User.create(validId, invalidEmail, validBirthday); }); // assert @@ -90,7 +91,7 @@ void throwsException_whenEmailIsInvalidFormat_MissingTopLevelDomain() { // act CoreException result = assertThrows(CoreException.class, () -> { - User.create(validId, invalidEmail); + User.create(validId, invalidEmail, validBirthday); }); // assert @@ -105,7 +106,7 @@ void throwsException_whenEmailIsInvalidFormat_OnlyAtAndDot() { // act CoreException result = assertThrows(CoreException.class, () -> { - User.create(validId, invalidEmail); + User.create(validId, invalidEmail, validBirthday); }); // assert @@ -114,5 +115,81 @@ void throwsException_whenEmailIsInvalidFormat_OnlyAtAndDot() { // extra case // ๊ณต๋ฐฑ์ด ํฌํ•จ๋œ ๊ฒฝ์šฐ + + // ์ƒ๋…„์›”์ผ์ด yyyy-MM-dd ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. + @DisplayName("์ƒ๋…„์›”์ผ์ด yyyy-MM-dd ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. - ์ž˜๋ชป๋œ ํ˜•์‹์ธ ๊ฒฝ์šฐ 13-03-1993") + @Test + void throwsException_whenBirthdayIsInvalidFormat() { + // arrange + String invalidBirthday = "13-03-1993"; + + // act + CoreException result = assertThrows(CoreException.class, () -> { + User.create(validId, validEmail, invalidBirthday); + }); + + // assert + assertThat(result.getMessage()).isEqualTo("์ƒ๋…„์›”์ผ ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); + } + + @DisplayName("์ƒ๋…„์›”์ผ์ด yyyy-MM-dd ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. - ์ž˜๋ชป๋œ ํ˜•์‹์ธ ๊ฒฝ์šฐ 1993/03/13") + @Test + void throwsException_whenBirthdayIsInvalidFormat_Slashes() { + // arrange + String invalidBirthday = "1993/03/13"; + + // act + CoreException result = assertThrows(CoreException.class, () -> { + User.create(validId, validEmail, invalidBirthday); + }); + + // assert + assertThat(result.getMessage()).isEqualTo("์ƒ๋…„์›”์ผ ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); + } + + @DisplayName("์ƒ๋…„์›”์ผ์ด yyyy-MM-dd ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. - ์ž˜๋ชป๋œ ํ˜•์‹์ธ ๊ฒฝ์šฐ 19930313") + @Test + void throwsException_whenBirthdayIsInvalidFormat_NoSeparators() { + // arrange + String invalidBirthday = "19930313"; + + // act + CoreException result = assertThrows(CoreException.class, () -> { + User.create(validId, validEmail, invalidBirthday); + }); + + // assert + assertThat(result.getMessage()).isEqualTo("์ƒ๋…„์›”์ผ ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); + } + + @DisplayName("์ƒ๋…„์›”์ผ์ด yyyy-MM-dd ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. - 930313") + @Test + void throwsException_whenBirthdayIsInvalidFormat_ShortDate() { + // arrange + String invalidBirthday = "930313"; + + // act + CoreException result = assertThrows(CoreException.class, () -> { + User.create(validId, validEmail, invalidBirthday); + }); + + // assert + assertThat(result.getMessage()).isEqualTo("์ƒ๋…„์›”์ผ ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); + } + + @DisplayName("์ƒ๋…„์›”์ผ์ด yyyy-MM-dd ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. - ๋นˆ ๋ฌธ์ž์—ด") + @Test + void throwsException_whenBirthdayIsInvalidFormat_EmptyString() { + // arrange + String invalidBirthday = ""; + + // act + CoreException result = assertThrows(CoreException.class, () -> { + User.create(validId, validEmail, invalidBirthday); + }); + + // assert + assertThat(result.getMessage()).isEqualTo("์ƒ๋…„์›”์ผ ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); + } } } From b399fde2537b176e1950ebcc118f365d2063cf8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A0=95=EB=B0=B0?= Date: Tue, 28 Oct 2025 18:15:16 +0900 Subject: [PATCH 06/85] =?UTF-8?q?round1:=20User=20userId,=20email,=20birth?= =?UTF-8?q?day=20null=20=EC=95=88=EB=90=98=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95,=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/loopers/domain/user/User.java | 7 +-- .../loopers/domain/user/UserModelTest.java | 47 +++++++++++++++++++ 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java index 6cf45fe57..64d0e6852 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java @@ -2,6 +2,7 @@ import com.loopers.support.error.CoreException; import com.loopers.support.error.ErrorType; +import org.apache.commons.lang3.StringUtils; public class User { private final String id; @@ -16,15 +17,15 @@ private User(String id, String email, String birthday) { public static User create(String id, String email, String birthday) { // ID ๊ฐ€ ์˜๋ฌธ ๋ฐ ์ˆซ์ž 10์ž ์ด๋‚ด ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. - if (!id.matches("^(?=.*[a-zA-Z])(?=.*\\d)[a-zA-Z\\d]{1,10}$")) { + if (StringUtils.isBlank(id) || !id.matches("^(?=.*[a-zA-Z])(?=.*\\d)[a-zA-Z\\d]{1,10}$")) { throw new CoreException(ErrorType.BAD_REQUEST, "ID๋Š” ์˜๋ฌธ ๋ฐ ์ˆซ์ž 10์ž ์ด๋‚ด์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค."); } // ์ด๋ฉ”์ผ์ด xx@yy.zz ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. - if (!email.matches("^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$")) { + if (StringUtils.isBlank(email) || !email.matches("^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$")) { throw new CoreException(ErrorType.BAD_REQUEST, "์ด๋ฉ”์ผ ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); } // ์ƒ๋…„์›”์ผ์ด YYYY-MM-DD ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. - if (!birthday.matches("^\\d{4}-\\d{2}-\\d{2}$")) { + if (StringUtils.isBlank(birthday) || !birthday.matches("^\\d{4}-\\d{2}-\\d{2}$")) { throw new CoreException(ErrorType.BAD_REQUEST, "์ƒ๋…„์›”์ผ ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); } diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserModelTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserModelTest.java index 4c29aa8db..58764ef7c 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserModelTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserModelTest.java @@ -18,6 +18,21 @@ class Create { // ID ๊ฐ€ ์˜๋ฌธ ๋ฐ ์ˆซ์ž 10์ž ์ด๋‚ด ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. + @DisplayName("ID ๊ฐ€ ์˜๋ฌธ ๋ฐ ์ˆซ์ž 10์ž ์ด๋‚ด ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. - null ์ธ ๊ฒฝ์šฐ") + @Test + void throwsException_whenIdIsInvalidFormat_Null() { + // arrange + String invalidId = null; + + // act + CoreException result = assertThrows(CoreException.class, () -> { + User.create(invalidId, validEmail, validBirthday); + }); + + // assert + assertThat(result.getMessage()).isEqualTo("ID๋Š” ์˜๋ฌธ ๋ฐ ์ˆซ์ž 10์ž ์ด๋‚ด์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + } + @DisplayName("ID ๊ฐ€ ์˜๋ฌธ ๋ฐ ์ˆซ์ž 10์ž ์ด๋‚ด ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. - ์˜๋ฌธ ๋ฐ ์ˆซ์ž๊ฐ€ ์•„๋‹Œ ๊ฒฝ์šฐ") @Test void throwsException_whenIdIsInvalidFormat_NotAlphanumeric() { @@ -53,6 +68,22 @@ void throwsException_whenIdIsInvalidFormat_TooLong() { // ์ˆซ์ž๋งŒ ์žˆ๋Š” ๊ฒฝ์šฐ // ์ด๋ฉ”์ผ์ด xx@yy.zz ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. + + @DisplayName("์ด๋ฉ”์ผ์ด xx@yy.zz ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. - null ์ธ ๊ฒฝ์šฐ") + @Test + void throwsException_whenEmailIsInvalidFormat_Null() { + // arrange + String invalidEmail = null; + + // act + CoreException result = assertThrows(CoreException.class, () -> { + User.create(validId, invalidEmail, validBirthday); + }); + + // assert + assertThat(result.getMessage()).isEqualTo("์ด๋ฉ”์ผ ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); + } + @DisplayName("์ด๋ฉ”์ผ์ด xx@yy.zz ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. - @๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ") @Test void throwsException_whenEmailIsInvalidFormat_MissingAtSymbol() { @@ -117,6 +148,22 @@ void throwsException_whenEmailIsInvalidFormat_OnlyAtAndDot() { // ๊ณต๋ฐฑ์ด ํฌํ•จ๋œ ๊ฒฝ์šฐ // ์ƒ๋…„์›”์ผ์ด yyyy-MM-dd ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. + + @DisplayName("์ƒ๋…„์›”์ผ์ด yyyy-MM-dd ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. - null ์ธ ๊ฒฝ์šฐ") + @Test + void throwsException_whenBirthdayIsInvalidFormat_Null() { + // arrange + String invalidBirthday = null; + + // act + CoreException result = assertThrows(CoreException.class, () -> { + User.create(validId, validEmail, invalidBirthday); + }); + + // assert + assertThat(result.getMessage()).isEqualTo("์ƒ๋…„์›”์ผ ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); + } + @DisplayName("์ƒ๋…„์›”์ผ์ด yyyy-MM-dd ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. - ์ž˜๋ชป๋œ ํ˜•์‹์ธ ๊ฒฝ์šฐ 13-03-1993") @Test void throwsException_whenBirthdayIsInvalidFormat() { From a143f91054fee4ac1e6d58fac3e3efd9b221640b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A0=95=EB=B0=B0?= Date: Wed, 29 Oct 2025 15:19:41 +0900 Subject: [PATCH 07/85] =?UTF-8?q?round1:=20User=EA=B0=80=20BaseEntity=20?= =?UTF-8?q?=EC=83=81=EC=86=8D=EB=B0=9B=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95,User.id=20->=20User.userId=EB=A1=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 --- .../java/com/loopers/domain/user/User.java | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java index 64d0e6852..c4df777ee 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java @@ -1,23 +1,33 @@ package com.loopers.domain.user; +import com.loopers.domain.BaseEntity; import com.loopers.support.error.CoreException; import com.loopers.support.error.ErrorType; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import lombok.Getter; import org.apache.commons.lang3.StringUtils; -public class User { - private final String id; - private final String email; - private final String birthday; +@Entity +@Table(name = "user") +@Getter +public class User extends BaseEntity { + protected User() { + } + + private String userId; + private String email; + private String birthday; - private User(String id, String email, String birthday) { - this.id = id; + private User(String userId, String email, String birthday) { + this.userId = userId; this.email = email; this.birthday = birthday; } - public static User create(String id, String email, String birthday) { + public static User create(String userId, String email, String birthday) { // ID ๊ฐ€ ์˜๋ฌธ ๋ฐ ์ˆซ์ž 10์ž ์ด๋‚ด ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. - if (StringUtils.isBlank(id) || !id.matches("^(?=.*[a-zA-Z])(?=.*\\d)[a-zA-Z\\d]{1,10}$")) { + if (StringUtils.isBlank(userId) || !userId.matches("^(?=.*[a-zA-Z])(?=.*\\d)[a-zA-Z\\d]{1,10}$")) { throw new CoreException(ErrorType.BAD_REQUEST, "ID๋Š” ์˜๋ฌธ ๋ฐ ์ˆซ์ž 10์ž ์ด๋‚ด์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค."); } // ์ด๋ฉ”์ผ์ด xx@yy.zz ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. @@ -29,6 +39,6 @@ public static User create(String id, String email, String birthday) { throw new CoreException(ErrorType.BAD_REQUEST, "์ƒ๋…„์›”์ผ ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); } - return new User(id, email, birthday); + return new User(userId, email, birthday); } } From 0096dae9ecb6bbe5345dbf6972287758049c7cb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A0=95=EB=B0=B0?= Date: Wed, 29 Oct 2025 15:20:12 +0900 Subject: [PATCH 08/85] =?UTF-8?q?round1:=20=EC=9C=A0=EC=A0=80=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ํšŒ์› ๊ฐ€์ž…์‹œ User ์ €์žฅ์ด ์ˆ˜ํ–‰๋œ๋‹ค. ( spy ๊ฒ€์ฆ ) ์ด๋ฏธ ๊ฐ€์ž…๋œ ID ๋กœ ํšŒ์›๊ฐ€์ž… ์‹œ๋„ ์‹œ, ์‹คํŒจํ•œ๋‹ค. --- .../com/loopers/CommerceApiApplication.java | 3 + .../domain/user/UserJpaRepository.java | 13 ++++ .../com/loopers/domain/user/UserService.java | 30 ++++++++ .../user/UserServiceIntegrationTest.java | 70 +++++++++++++++++++ 4 files changed, 116 insertions(+) create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/user/UserJpaRepository.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java diff --git a/apps/commerce-api/src/main/java/com/loopers/CommerceApiApplication.java b/apps/commerce-api/src/main/java/com/loopers/CommerceApiApplication.java index 9027b51bf..a32927655 100644 --- a/apps/commerce-api/src/main/java/com/loopers/CommerceApiApplication.java +++ b/apps/commerce-api/src/main/java/com/loopers/CommerceApiApplication.java @@ -4,10 +4,13 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.ConfigurationPropertiesScan; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; + import java.util.TimeZone; @ConfigurationPropertiesScan @SpringBootApplication +@EnableJpaRepositories(basePackages = "com.loopers.domain") public class CommerceApiApplication { @PostConstruct diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserJpaRepository.java new file mode 100644 index 000000000..7f6fb8222 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserJpaRepository.java @@ -0,0 +1,13 @@ +package com.loopers.domain.user; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface UserJpaRepository extends JpaRepository { + Optional findByUserId(String userId); + + boolean existsUserByUserId(String userId); +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java new file mode 100644 index 000000000..f525d3e51 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java @@ -0,0 +1,30 @@ +package com.loopers.domain.user; + +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +@RequiredArgsConstructor +@Service +public class UserService { + private final UserJpaRepository userJpaRepository; + + @Transactional + public User registerUser(String userId, String email, String birthday) { + // ์ด๋ฏธ ๋“ฑ๋ก๋œ userId ์ธ ๊ฒฝ์šฐ, ์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚จ๋‹ค. + if (userJpaRepository.existsUserByUserId(userId)) { + throw new CoreException(ErrorType.CONFLICT, "์ด๋ฏธ ์กด์žฌํ•˜๋Š” ์‚ฌ์šฉ์ž ID ์ž…๋‹ˆ๋‹ค."); + } + User user = User.create(userId, email, birthday); + return userJpaRepository.save(user); + } + + @Transactional(readOnly = true) + public Optional findByUserId(String userId) { + return userJpaRepository.findByUserId(userId); + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java new file mode 100644 index 000000000..f5dd776ae --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java @@ -0,0 +1,70 @@ +package com.loopers.domain.user; + +import com.loopers.support.error.CoreException; +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; + +@SpringBootTest +@Transactional +public class UserServiceIntegrationTest { + @Autowired + private UserService userService; + + @MockitoSpyBean + private UserJpaRepository spyUserRepository; + + @DisplayName("ํšŒ์› ๊ฐ€์ž…์‹œ User ์ €์žฅ์ด ์ˆ˜ํ–‰๋œ๋‹ค.") + @Test + void saveUserWhenRegister() { + // arrange + String validId = "user123"; + String validEmail = "xx@yy.zz"; + String validBirthday = "1993-03-13"; + + // act + // ์œ ์ € ๋“ฑ๋ก + userService.registerUser(validId, validEmail, validBirthday); + // ์ €์žฅ๋œ ์œ ์ € ์กฐํšŒ + Optional foundUser = userService.findByUserId(validId); + + // assert + verify(spyUserRepository).save(any(User.class)); + verify(spyUserRepository).findByUserId("user123"); + assertThat(foundUser).isPresent(); + assertThat(foundUser.get().getUserId()).isEqualTo("user123"); + } + + @DisplayName("์ด๋ฏธ ๊ฐ€์ž…๋œ ID ๋กœ ํšŒ์›๊ฐ€์ž… ์‹œ๋„ ์‹œ, ์‹คํŒจํ•œ๋‹ค.") + @Test + void throwsExceptionWhenRegisterWithExistingUserId() { + // arrange + String validId = "user123"; + String validEmail = "xx@yy.zz"; + String validBirthday = "1993-03-13"; + String validOtherEmail = "zz@cc.xx"; + String validOtherBirthday = "1992-06-07"; + + // act + // ๊ธฐ์กด ์œ ์ € ๋“ฑ๋ก + userService.registerUser(validId, validEmail, validBirthday); + // ๋™์ผ ID ๋กœ ์œ ์ € ๋“ฑ๋ก ์‹œ๋„ + CoreException result = assertThrows(CoreException.class, () -> { + userService.registerUser(validId, validOtherEmail, validOtherBirthday); + }); + + // assert + assertThat(result.getMessage()).isEqualTo("์ด๋ฏธ ์กด์žฌํ•˜๋Š” ์‚ฌ์šฉ์ž ID ์ž…๋‹ˆ๋‹ค."); + } + +} From 7f05b54b0d5c6f9f9e7b082e148b8808dedfe0c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A0=95=EB=B0=B0?= Date: Wed, 29 Oct 2025 17:19:18 +0900 Subject: [PATCH 09/85] =?UTF-8?q?round1:=20=ED=9A=8C=EC=9B=90=20=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20API=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ํšŒ์› ๊ฐ€์ž…์ด ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ์ƒ์„ฑ๋œ ์œ ์ € ์ •๋ณด๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค. --- .../loopers/application/user/UserFacade.java | 17 +++++ .../loopers/application/user/UserInfo.java | 13 ++++ .../interfaces/api/user/UserV1ApiSpec.java | 23 ++++++ .../interfaces/api/user/UserV1Controller.java | 29 ++++++++ .../interfaces/api/user/UserV1Dto.java | 24 +++++++ .../interfaces/api/UserV1ApiE2ETest.java | 71 +++++++++++++++++++ 6 files changed, 177 insertions(+) create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/user/UserInfo.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1ApiSpec.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java diff --git a/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java new file mode 100644 index 000000000..bb5083996 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java @@ -0,0 +1,17 @@ +package com.loopers.application.user; + +import com.loopers.domain.user.User; +import com.loopers.domain.user.UserService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class UserFacade { + private final UserService userService; + + public UserInfo registerUser(String userId, String email, String birthday) { + User registeredUser = userService.registerUser(userId, email, birthday); + return UserInfo.from(registeredUser); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/user/UserInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/user/UserInfo.java new file mode 100644 index 000000000..86cd5edf9 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/user/UserInfo.java @@ -0,0 +1,13 @@ +package com.loopers.application.user; + +import com.loopers.domain.user.User; + +public record UserInfo(String id, String email, String birthday) { + public static UserInfo from(User user) { + return new UserInfo( + user.getUserId(), + user.getEmail(), + user.getBirthday() + ); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1ApiSpec.java new file mode 100644 index 000000000..f76f204d5 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1ApiSpec.java @@ -0,0 +1,23 @@ +package com.loopers.interfaces.api.user; + +import com.loopers.interfaces.api.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.tags.Tag; + +@Tag(name = "User V1 API", description = "์‚ฌ์šฉ์ž API ์ž…๋‹ˆ๋‹ค.") +public interface UserV1ApiSpec { + + @Operation( + method = "POST", + summary = "ํšŒ์› ๊ฐ€์ž…", + description = "ํšŒ์›๊ฐ€์ž…์„ ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค." + ) + ApiResponse registerUser( + @Schema( + name = "ํšŒ์› ๊ฐ€์ž… ์š”์ฒญ", + description = "ํšŒ์› ๊ฐ€์ž…์— ํ•„์š”ํ•œ ์ •๋ณด๋ฅผ ์ž…๋ ฅํ•ฉ๋‹ˆ๋‹ค." + ) + UserV1Dto.UserRegisterRequest request + ); +} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java new file mode 100644 index 000000000..4b44127e2 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java @@ -0,0 +1,29 @@ +package com.loopers.interfaces.api.user; + +import com.loopers.application.user.UserFacade; +import com.loopers.application.user.UserInfo; +import com.loopers.interfaces.api.ApiResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/v1/users") +public class UserV1Controller implements UserV1ApiSpec { + private final UserFacade userFacade; + + @RequestMapping(method = RequestMethod.POST) + @Override + public ApiResponse registerUser(@RequestBody UserV1Dto.UserRegisterRequest request) { + UserInfo info = userFacade.registerUser( + request.id(), + request.email(), + request.birthday() + ); + UserV1Dto.UserResponse response = UserV1Dto.UserResponse.from(info); + return ApiResponse.success(response); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java new file mode 100644 index 000000000..9679a5483 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java @@ -0,0 +1,24 @@ +package com.loopers.interfaces.api.user; + +import com.loopers.application.user.UserInfo; + +public class UserV1Dto { + public record UserResponse( + String id, + String email, + String birthday) { + public static UserResponse from(UserInfo info) { + return new UserResponse( + info.id(), + info.email(), + info.birthday() + ); + } + } + + public record UserRegisterRequest( + String id, + String email, + String birthday) { + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java new file mode 100644 index 000000000..2531560ee --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java @@ -0,0 +1,71 @@ +package com.loopers.interfaces.api; + +import com.loopers.interfaces.api.user.UserV1Dto; +import com.loopers.utils.DatabaseCleanUp; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class UserV1ApiE2ETest { + + private final String ENDPOINT_USER = "/api/v1/users"; + + private final TestRestTemplate testRestTemplate; + private final DatabaseCleanUp databaseCleanUp; + + @Autowired + public UserV1ApiE2ETest( + TestRestTemplate testRestTemplate, + DatabaseCleanUp databaseCleanUp + ) { + this.testRestTemplate = testRestTemplate; + this.databaseCleanUp = databaseCleanUp; + } + + @AfterEach + void tearDown() { + databaseCleanUp.truncateAllTables(); + } + + @DisplayName("POST /api/v1/users") + @Nested + class Post { + @DisplayName("ํšŒ์› ๊ฐ€์ž…์ด ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ์ƒ์„ฑ๋œ ์œ ์ € ์ •๋ณด๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnUserInfo_whenRegisterSuccess() { + // arrange + UserV1Dto.UserRegisterRequest request = new UserV1Dto.UserRegisterRequest( + "user123", + "xx@yy.zz", + "1993-03-13" + ); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + ResponseEntity> response = testRestTemplate.exchange(ENDPOINT_USER, HttpMethod.POST, new HttpEntity<>(request), responseType); + + // assert + assertAll( + () -> assertTrue(response.getStatusCode().is2xxSuccessful()), + () -> assertThat(response.getBody().data().id()).isEqualTo(request.id()), + () -> assertThat(response.getBody().data().email()).isEqualTo(request.email()), + () -> assertThat(response.getBody().data().birthday()).isEqualTo(request.birthday()) + ); + } + + } +} From ca1fed1b1937888d84ea8736b8072303627330dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A0=95=EB=B0=B0?= Date: Fri, 31 Oct 2025 07:06:31 +0900 Subject: [PATCH 10/85] =?UTF-8?q?round1:=20=EC=9C=A0=EC=A0=80=20=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EC=84=B1=EB=B3=84=EC=9A=94=EA=B1=B4=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 ํšŒ์› ๊ฐ€์ž… ์‹œ์— ์„ฑ๋ณ„์ด ์—†์„ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. --- .../loopers/application/user/UserFacade.java | 4 +-- .../loopers/application/user/UserInfo.java | 9 +++--- .../java/com/loopers/domain/user/User.java | 12 ++++++-- .../com/loopers/domain/user/UserService.java | 4 +-- .../interfaces/api/user/UserV1Controller.java | 8 ++--- .../interfaces/api/user/UserV1Dto.java | 9 ++++-- .../loopers/domain/user/UserModelTest.java | 29 ++++++++++--------- .../user/UserServiceIntegrationTest.java | 10 +++---- .../interfaces/api/UserV1ApiE2ETest.java | 27 +++++++++++++++-- 9 files changed, 72 insertions(+), 40 deletions(-) diff --git a/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java index bb5083996..0037eeca5 100644 --- a/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java @@ -10,8 +10,8 @@ public class UserFacade { private final UserService userService; - public UserInfo registerUser(String userId, String email, String birthday) { - User registeredUser = userService.registerUser(userId, email, birthday); + public UserInfo registerUser(String userId, String email, String birthday, String gender) { + User registeredUser = userService.registerUser(userId, email, birthday, gender); return UserInfo.from(registeredUser); } } diff --git a/apps/commerce-api/src/main/java/com/loopers/application/user/UserInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/user/UserInfo.java index 86cd5edf9..84cda840f 100644 --- a/apps/commerce-api/src/main/java/com/loopers/application/user/UserInfo.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/user/UserInfo.java @@ -2,12 +2,13 @@ import com.loopers.domain.user.User; -public record UserInfo(String id, String email, String birthday) { +public record UserInfo(String id, String email, String birthday, String gender) { public static UserInfo from(User user) { return new UserInfo( - user.getUserId(), - user.getEmail(), - user.getBirthday() + user.getUserId(), + user.getEmail(), + user.getBirthday(), + user.getGender() ); } } diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java index c4df777ee..a1074a66f 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java @@ -18,14 +18,16 @@ protected User() { private String userId; private String email; private String birthday; + private String gender; - private User(String userId, String email, String birthday) { + private User(String userId, String email, String birthday, String gender) { this.userId = userId; this.email = email; this.birthday = birthday; + this.gender = gender; } - public static User create(String userId, String email, String birthday) { + public static User create(String userId, String email, String birthday, String gender) { // ID ๊ฐ€ ์˜๋ฌธ ๋ฐ ์ˆซ์ž 10์ž ์ด๋‚ด ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. if (StringUtils.isBlank(userId) || !userId.matches("^(?=.*[a-zA-Z])(?=.*\\d)[a-zA-Z\\d]{1,10}$")) { throw new CoreException(ErrorType.BAD_REQUEST, "ID๋Š” ์˜๋ฌธ ๋ฐ ์ˆซ์ž 10์ž ์ด๋‚ด์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค."); @@ -38,7 +40,11 @@ public static User create(String userId, String email, String birthday) { if (StringUtils.isBlank(birthday) || !birthday.matches("^\\d{4}-\\d{2}-\\d{2}$")) { throw new CoreException(ErrorType.BAD_REQUEST, "์ƒ๋…„์›”์ผ ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); } + // ์„ฑ๋ณ„์ด ์—†์„ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. + if (StringUtils.isBlank(gender)) { + throw new CoreException(ErrorType.BAD_REQUEST, "์„ฑ๋ณ„์€ ํ•„์ˆ˜ ์ž…๋ ฅ ํ•ญ๋ชฉ์ž…๋‹ˆ๋‹ค."); + } - return new User(userId, email, birthday); + return new User(userId, email, birthday, gender); } } diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java index f525d3e51..8f45495e1 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java @@ -14,12 +14,12 @@ public class UserService { private final UserJpaRepository userJpaRepository; @Transactional - public User registerUser(String userId, String email, String birthday) { + public User registerUser(String userId, String email, String birthday, String gender) { // ์ด๋ฏธ ๋“ฑ๋ก๋œ userId ์ธ ๊ฒฝ์šฐ, ์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚จ๋‹ค. if (userJpaRepository.existsUserByUserId(userId)) { throw new CoreException(ErrorType.CONFLICT, "์ด๋ฏธ ์กด์žฌํ•˜๋Š” ์‚ฌ์šฉ์ž ID ์ž…๋‹ˆ๋‹ค."); } - User user = User.create(userId, email, birthday); + User user = User.create(userId, email, birthday, gender); return userJpaRepository.save(user); } diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java index 4b44127e2..5a349a998 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java @@ -4,10 +4,7 @@ import com.loopers.application.user.UserInfo; import com.loopers.interfaces.api.ApiResponse; import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; @RequiredArgsConstructor @RestController @@ -21,7 +18,8 @@ public ApiResponse registerUser(@RequestBody UserV1Dto.U UserInfo info = userFacade.registerUser( request.id(), request.email(), - request.birthday() + request.birthday(), + request.gender() ); UserV1Dto.UserResponse response = UserV1Dto.UserResponse.from(info); return ApiResponse.success(response); diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java index 9679a5483..a6500f737 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java @@ -6,12 +6,14 @@ public class UserV1Dto { public record UserResponse( String id, String email, - String birthday) { + String birthday, + String gender) { public static UserResponse from(UserInfo info) { return new UserResponse( info.id(), info.email(), - info.birthday() + info.birthday(), + info.gender() ); } } @@ -19,6 +21,7 @@ public static UserResponse from(UserInfo info) { public record UserRegisterRequest( String id, String email, - String birthday) { + String birthday, + String gender) { } } diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserModelTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserModelTest.java index 58764ef7c..45adade80 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserModelTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserModelTest.java @@ -15,6 +15,7 @@ class Create { private final String validId = "user123"; private final String validEmail = "xx@yy.zz"; private final String validBirthday = "1993-03-13"; + private final String validGender = "male"; // ID ๊ฐ€ ์˜๋ฌธ ๋ฐ ์ˆซ์ž 10์ž ์ด๋‚ด ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. @@ -26,7 +27,7 @@ void throwsException_whenIdIsInvalidFormat_Null() { // act CoreException result = assertThrows(CoreException.class, () -> { - User.create(invalidId, validEmail, validBirthday); + User.create(invalidId, validEmail, validBirthday, validGender); }); // assert @@ -41,7 +42,7 @@ void throwsException_whenIdIsInvalidFormat_NotAlphanumeric() { // act CoreException result = assertThrows(CoreException.class, () -> { - User.create(invalidId, validEmail, validBirthday); + User.create(invalidId, validEmail, validBirthday, validGender); }); // assert @@ -56,7 +57,7 @@ void throwsException_whenIdIsInvalidFormat_TooLong() { // act CoreException result = assertThrows(CoreException.class, () -> { - User.create(invalidId, validEmail, validBirthday); + User.create(invalidId, validEmail, validBirthday, validGender); }); // assert @@ -77,7 +78,7 @@ void throwsException_whenEmailIsInvalidFormat_Null() { // act CoreException result = assertThrows(CoreException.class, () -> { - User.create(validId, invalidEmail, validBirthday); + User.create(validId, invalidEmail, validBirthday, validGender); }); // assert @@ -92,7 +93,7 @@ void throwsException_whenEmailIsInvalidFormat_MissingAtSymbol() { // act CoreException result = assertThrows(CoreException.class, () -> { - User.create(validId, invalidEmail, validBirthday); + User.create(validId, invalidEmail, validBirthday, validGender); }); // assert @@ -107,7 +108,7 @@ void throwsException_whenEmailIsInvalidFormat_MissingDomain() { // act CoreException result = assertThrows(CoreException.class, () -> { - User.create(validId, invalidEmail, validBirthday); + User.create(validId, invalidEmail, validBirthday, validGender); }); // assert @@ -122,7 +123,7 @@ void throwsException_whenEmailIsInvalidFormat_MissingTopLevelDomain() { // act CoreException result = assertThrows(CoreException.class, () -> { - User.create(validId, invalidEmail, validBirthday); + User.create(validId, invalidEmail, validBirthday, validGender); }); // assert @@ -137,7 +138,7 @@ void throwsException_whenEmailIsInvalidFormat_OnlyAtAndDot() { // act CoreException result = assertThrows(CoreException.class, () -> { - User.create(validId, invalidEmail, validBirthday); + User.create(validId, invalidEmail, validBirthday, validGender); }); // assert @@ -157,7 +158,7 @@ void throwsException_whenBirthdayIsInvalidFormat_Null() { // act CoreException result = assertThrows(CoreException.class, () -> { - User.create(validId, validEmail, invalidBirthday); + User.create(validId, validEmail, invalidBirthday, validGender); }); // assert @@ -172,7 +173,7 @@ void throwsException_whenBirthdayIsInvalidFormat() { // act CoreException result = assertThrows(CoreException.class, () -> { - User.create(validId, validEmail, invalidBirthday); + User.create(validId, validEmail, invalidBirthday, validGender); }); // assert @@ -187,7 +188,7 @@ void throwsException_whenBirthdayIsInvalidFormat_Slashes() { // act CoreException result = assertThrows(CoreException.class, () -> { - User.create(validId, validEmail, invalidBirthday); + User.create(validId, validEmail, invalidBirthday, validGender); }); // assert @@ -202,7 +203,7 @@ void throwsException_whenBirthdayIsInvalidFormat_NoSeparators() { // act CoreException result = assertThrows(CoreException.class, () -> { - User.create(validId, validEmail, invalidBirthday); + User.create(validId, validEmail, invalidBirthday, validGender); }); // assert @@ -217,7 +218,7 @@ void throwsException_whenBirthdayIsInvalidFormat_ShortDate() { // act CoreException result = assertThrows(CoreException.class, () -> { - User.create(validId, validEmail, invalidBirthday); + User.create(validId, validEmail, invalidBirthday, validGender); }); // assert @@ -232,7 +233,7 @@ void throwsException_whenBirthdayIsInvalidFormat_EmptyString() { // act CoreException result = assertThrows(CoreException.class, () -> { - User.create(validId, validEmail, invalidBirthday); + User.create(validId, validEmail, invalidBirthday, validGender); }); // assert diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java index f5dd776ae..d12e12a4f 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java @@ -31,10 +31,11 @@ void saveUserWhenRegister() { String validId = "user123"; String validEmail = "xx@yy.zz"; String validBirthday = "1993-03-13"; + String validGender = "male"; // act // ์œ ์ € ๋“ฑ๋ก - userService.registerUser(validId, validEmail, validBirthday); + userService.registerUser(validId, validEmail, validBirthday, validGender); // ์ €์žฅ๋œ ์œ ์ € ์กฐํšŒ Optional foundUser = userService.findByUserId(validId); @@ -52,15 +53,14 @@ void throwsExceptionWhenRegisterWithExistingUserId() { String validId = "user123"; String validEmail = "xx@yy.zz"; String validBirthday = "1993-03-13"; - String validOtherEmail = "zz@cc.xx"; - String validOtherBirthday = "1992-06-07"; + String validGender = "male"; // act // ๊ธฐ์กด ์œ ์ € ๋“ฑ๋ก - userService.registerUser(validId, validEmail, validBirthday); + userService.registerUser(validId, validEmail, validBirthday, validGender); // ๋™์ผ ID ๋กœ ์œ ์ € ๋“ฑ๋ก ์‹œ๋„ CoreException result = assertThrows(CoreException.class, () -> { - userService.registerUser(validId, validOtherEmail, validOtherBirthday); + userService.registerUser(validId, "zz@cc.xx", "1992-06-07", "female"); }); // assert diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java index 2531560ee..4e5f97b13 100644 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java @@ -50,7 +50,8 @@ void returnUserInfo_whenRegisterSuccess() { UserV1Dto.UserRegisterRequest request = new UserV1Dto.UserRegisterRequest( "user123", "xx@yy.zz", - "1993-03-13" + "1993-03-13", + "male" ); // act @@ -63,9 +64,31 @@ void returnUserInfo_whenRegisterSuccess() { () -> assertTrue(response.getStatusCode().is2xxSuccessful()), () -> assertThat(response.getBody().data().id()).isEqualTo(request.id()), () -> assertThat(response.getBody().data().email()).isEqualTo(request.email()), - () -> assertThat(response.getBody().data().birthday()).isEqualTo(request.birthday()) + () -> assertThat(response.getBody().data().birthday()).isEqualTo(request.birthday()), + () -> assertThat(response.getBody().data().gender()).isEqualTo(request.gender()) ); } + // ํšŒ์› ๊ฐ€์ž… ์‹œ์— ์„ฑ๋ณ„์ด ์—†์„ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. + @DisplayName("ํšŒ์› ๊ฐ€์ž… ์‹œ์— ์„ฑ๋ณ„์ด ์—†์„ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnBadRequest_whenGenderIsMissing() { + // arrange + UserV1Dto.UserRegisterRequest request = new UserV1Dto.UserRegisterRequest( + "user123", + "xx@yy.zz", + "1993-03-13", + null + ); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + ResponseEntity> response = testRestTemplate.exchange(ENDPOINT_USER, HttpMethod.POST, new HttpEntity<>(request), responseType); + + // assert + assertThat(response.getStatusCode().value()).isEqualTo(400); + } + } } From d5280eafe4f85b6af14bb8641c4f3b1925a2d140 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A0=95=EB=B0=B0?= Date: Fri, 31 Oct 2025 07:15:35 +0900 Subject: [PATCH 11/85] =?UTF-8?q?round1:=20=EB=82=B4=20=EC=A0=95=EB=B3=B4?= =?UTF-8?q?=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•  ๊ฒฝ์šฐ, ํšŒ์› ์ •๋ณด๊ฐ€ ๋ฐ˜ํ™˜๋œ๋‹ค. ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ, null ์ด ๋ฐ˜ํ™˜๋œ๋‹ค. ๋‚ด ์ •๋ณด ์กฐํšŒ์— ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ํ•ด๋‹นํ•˜๋Š” ์œ ์ € ์ •๋ณด๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค. ์กด์žฌํ•˜์ง€ ์•Š๋Š” ID ๋กœ ์กฐํšŒํ•  ๊ฒฝ์šฐ, `404 Not Found` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. --- .../loopers/application/user/UserFacade.java | 9 +++ .../interfaces/api/user/UserV1ApiSpec.java | 14 +++++ .../interfaces/api/user/UserV1Controller.java | 8 +++ .../user/UserServiceIntegrationTest.java | 39 ++++++++++++ .../interfaces/api/UserV1ApiE2ETest.java | 62 +++++++++++++++++-- 5 files changed, 128 insertions(+), 4 deletions(-) diff --git a/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java index 0037eeca5..848eed169 100644 --- a/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java @@ -2,9 +2,13 @@ import com.loopers.domain.user.User; import com.loopers.domain.user.UserService; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; +import java.util.Optional; + @RequiredArgsConstructor @Component public class UserFacade { @@ -14,4 +18,9 @@ public UserInfo registerUser(String userId, String email, String birthday, Strin User registeredUser = userService.registerUser(userId, email, birthday, gender); return UserInfo.from(registeredUser); } + + public UserInfo getUserInfo(String userId) { + Optional user = userService.findByUserId(userId); + return UserInfo.from(user.orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."))); + } } diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1ApiSpec.java index f76f204d5..bb1a413c8 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1ApiSpec.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1ApiSpec.java @@ -20,4 +20,18 @@ ApiResponse registerUser( ) UserV1Dto.UserRegisterRequest request ); + + @Operation( + method = "GET", + summary = "๋‚ด ์ •๋ณด ์กฐํšŒ", + description = "ํšŒ์› ์ •๋ณด๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค." + ) + ApiResponse getUserInfo( + @Schema( + name = "ํšŒ์› ID", + description = "์กฐํšŒํ•  ํšŒ์›์˜ ID" + ) + String userId + ); + } diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java index 5a349a998..059faf73f 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java @@ -24,4 +24,12 @@ public ApiResponse registerUser(@RequestBody UserV1Dto.U UserV1Dto.UserResponse response = UserV1Dto.UserResponse.from(info); return ApiResponse.success(response); } + + @RequestMapping(method = RequestMethod.GET, path = "/{userId}") + @Override + public ApiResponse getUserInfo(@PathVariable String userId) { + UserInfo info = userFacade.getUserInfo(userId); + UserV1Dto.UserResponse response = UserV1Dto.UserResponse.from(info); + return ApiResponse.success(response); + } } diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java index d12e12a4f..24bf57b2c 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java @@ -67,4 +67,43 @@ void throwsExceptionWhenRegisterWithExistingUserId() { assertThat(result.getMessage()).isEqualTo("์ด๋ฏธ ์กด์žฌํ•˜๋Š” ์‚ฌ์šฉ์ž ID ์ž…๋‹ˆ๋‹ค."); } + @DisplayName("ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•  ๊ฒฝ์šฐ, ํšŒ์› ์ •๋ณด๊ฐ€ ๋ฐ˜ํ™˜๋œ๋‹ค.") + @Test + void returnsUserInfo_whenUserExists() { + // arrange + String validId = "user123"; + String validEmail = "xx@yy.zz"; + String validBirthday = "1993-03-13"; + String validGender = "male"; + // ๊ธฐ์กด ์œ ์ € ๋“ฑ๋ก + userService.registerUser(validId, validEmail, validBirthday, validGender); + + // act + Optional foundUser = userService.findByUserId(validId); + + // assert + assertThat(foundUser).isPresent(); + assertThat(foundUser.get().getUserId()).isEqualTo("user123"); + } + + @DisplayName("ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ, null ์ด ๋ฐ˜ํ™˜๋œ๋‹ค.") + @Test + void returnsNull_whenUserDoesNotExist() { + // arrange + String validId = "user123"; + String validEmail = "xx@yy.zz"; + String validBirthday = "1993-03-13"; + String validGender = "male"; + // ๊ธฐ์กด ์œ ์ € ๋“ฑ๋ก + userService.registerUser(validId, validEmail, validBirthday, validGender); + String nonExistId = "nonexist"; + + + // act + Optional foundUser = userService.findByUserId(nonExistId); + + // assert + assertThat(foundUser).isNotPresent(); + } + } diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java index 4e5f97b13..ebba3f89d 100644 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java @@ -2,10 +2,7 @@ import com.loopers.interfaces.api.user.UserV1Dto; import com.loopers.utils.DatabaseCleanUp; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.client.TestRestTemplate; @@ -91,4 +88,61 @@ void returnBadRequest_whenGenderIsMissing() { } } + + @DisplayName("GET /api/v1/users/{userId}") + @Nested + class Get { + private final String validUserId = "user123"; + private final String validEmail = "xx@yy.zz"; + private final String validBirthday = "1993-03-13"; + private final String validGender = "male"; + + // ํšŒ์›๊ฐ€์ž… ์ •๋ณด ์ž‘์„ฑ + @BeforeEach + void setupUser() { + UserV1Dto.UserRegisterRequest request = new UserV1Dto.UserRegisterRequest( + validUserId, + validEmail, + validBirthday, + validGender + ); + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + testRestTemplate.exchange(ENDPOINT_USER, HttpMethod.POST, new HttpEntity<>(request), responseType); + } + + @DisplayName("๋‚ด ์ •๋ณด ์กฐํšŒ์— ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ํ•ด๋‹นํ•˜๋Š” ์œ ์ € ์ •๋ณด๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnUserInfo_whenGetUserInfoSuccess() { + // arrange: setupUser() ์ฐธ์กฐ + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + ResponseEntity> response = testRestTemplate.exchange(ENDPOINT_USER + "/" + validUserId, HttpMethod.GET, null, responseType); + + // assert + assertAll( + () -> assertTrue(response.getStatusCode().is2xxSuccessful()), + () -> assertThat(response.getBody().data().id()).isEqualTo("user123"), + () -> assertThat(response.getBody().data().email()).isEqualTo("xx@yy.zz"), + () -> assertThat(response.getBody().data().birthday()).isEqualTo("1993-03-13"), + () -> assertThat(response.getBody().data().gender()).isEqualTo("male") + ); + } + + @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ID ๋กœ ์กฐํšŒํ•  ๊ฒฝ์šฐ, `404 Not Found` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnNotFound_whenUserIdDoesNotExist() { + // arrange + String invalidUserId = "nonexist"; + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + ResponseEntity> response = testRestTemplate.exchange(ENDPOINT_USER + "/" + invalidUserId, HttpMethod.GET, null, responseType); + + // assert + assertThat(response.getStatusCode().value()).isEqualTo(404); + } + } } From 05b7b2a7471b87151eac4d468bec4e624fda2693 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A0=95=EB=B0=B0?= Date: Fri, 31 Oct 2025 15:20:10 +0900 Subject: [PATCH 12/85] =?UTF-8?q?round1:=20=ED=8F=AC=EC=9D=B8=ED=8A=B8=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•  ๊ฒฝ์šฐ, ๋ณด์œ  ํฌ์ธํŠธ๊ฐ€ ๋ฐ˜ํ™˜๋œ๋‹ค. ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ, null ์ด ๋ฐ˜ํ™˜๋œ๋‹ค. ํฌ์ธํŠธ ์กฐํšŒ์— ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ๋ณด์œ  ํฌ์ธํŠธ๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค. `X-USER-ID` ํ—ค๋”๊ฐ€ ์—†์„ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. --- .../application/point/PointFacade.java | 18 ++++ .../java/com/loopers/domain/user/User.java | 1 + .../com/loopers/domain/user/UserService.java | 5 + .../interfaces/api/point/PointV1ApiSpec.java | 25 +++++ .../api/point/PointV1Controller.java | 31 ++++++ .../interfaces/api/point/PointV1Dto.java | 14 +++ .../user/UserServiceIntegrationTest.java | 38 +++++++ .../interfaces/api/PointV1ApiE2ETest.java | 102 ++++++++++++++++++ 8 files changed, 234 insertions(+) create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1ApiSpec.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Controller.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Dto.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/interfaces/api/PointV1ApiE2ETest.java diff --git a/apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java new file mode 100644 index 000000000..ac25b521c --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java @@ -0,0 +1,18 @@ +package com.loopers.application.point; + +import com.loopers.domain.user.UserService; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class PointFacade { + private final UserService userService; + + public Long getCurrentPoint(String userId) { + return userService.getCurrentPoint(userId) + .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java index a1074a66f..f0d617ddf 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java @@ -19,6 +19,7 @@ protected User() { private String email; private String birthday; private String gender; + private Long currentPoint = 0L; private User(String userId, String email, String birthday, String gender) { this.userId = userId; diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java index 8f45495e1..c0f70b1d3 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java @@ -27,4 +27,9 @@ public User registerUser(String userId, String email, String birthday, String ge public Optional findByUserId(String userId) { return userJpaRepository.findByUserId(userId); } + + @Transactional(readOnly = true) + public Optional getCurrentPoint(String userId) { + return findByUserId(userId).map(User::getCurrentPoint); + } } diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1ApiSpec.java new file mode 100644 index 000000000..8efd0d8fb --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1ApiSpec.java @@ -0,0 +1,25 @@ +package com.loopers.interfaces.api.point; + +import com.loopers.interfaces.api.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.tags.Tag; + +@Tag(name = "Point V1 API", description = "์‚ฌ์šฉ์ž API ์ž…๋‹ˆ๋‹ค.") +public interface PointV1ApiSpec { + + // /points + @Operation( + method = "GET", + summary = "ํฌ์ธํŠธ ์กฐํšŒ", + description = "ํšŒ์›์˜ ๋ณด์œ  ํฌ์ธํŠธ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค." + ) + // X-USER-ID ํ—ค๋”๊ฐ’ ์‚ฌ์šฉ + ApiResponse getUserPoints( + @Schema( + name = "ํšŒ์› ID", + description = "ํฌ์ธํŠธ๋ฅผ ์กฐํšŒํ•  ํšŒ์›์˜ ID" + ) + String userId + ); +} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Controller.java new file mode 100644 index 000000000..d5530e9c9 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Controller.java @@ -0,0 +1,31 @@ +package com.loopers.interfaces.api.point; + + +import com.loopers.application.point.PointFacade; +import com.loopers.interfaces.api.ApiResponse; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/v1/points") +public class PointV1Controller implements PointV1ApiSpec { + private final PointFacade pointFacade; + + @RequestMapping(method = RequestMethod.GET) + @Override + public ApiResponse getUserPoints(@RequestHeader(value = "X-USER-ID", required = false) String userId) { + if (StringUtils.isBlank(userId)) { + throw new CoreException(ErrorType.BAD_REQUEST); + } + Long currentPoint = pointFacade.getCurrentPoint(userId); + PointV1Dto.PointResponse response = PointV1Dto.PointResponse.from(currentPoint); + return ApiResponse.success(response); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Dto.java new file mode 100644 index 000000000..c90bb7072 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Dto.java @@ -0,0 +1,14 @@ +package com.loopers.interfaces.api.point; + +public class PointV1Dto { + + public record PointResponse( + Long currentPoint + ) { + public static PointV1Dto.PointResponse from(Long currentPoint) { + return new PointV1Dto.PointResponse( + currentPoint + ); + } + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java index 24bf57b2c..b7698e76c 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java @@ -106,4 +106,42 @@ void returnsNull_whenUserDoesNotExist() { assertThat(foundUser).isNotPresent(); } + @DisplayName("ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•  ๊ฒฝ์šฐ, ๋ณด์œ  ํฌ์ธํŠธ๊ฐ€ ๋ฐ˜ํ™˜๋œ๋‹ค.") + @Test + void returnsUserPoints_whenUserExists() { + // arrange + String validId = "user123"; + String validEmail = "xx@yy.zz"; + String validBirthday = "1993-03-13"; + String validGender = "male"; + // ์œ ์ € ๋“ฑ๋ก + userService.registerUser(validId, validEmail, validBirthday, validGender); + String existingUserId = "user123"; + + // act + Optional currentPoint = userService.getCurrentPoint(existingUserId); + + // assert + assertThat(currentPoint).isPresent(); + assertThat(currentPoint.get()).isEqualTo(0L); + } + + @DisplayName("ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ, null ์ด ๋ฐ˜ํ™˜๋œ๋‹ค.") + @Test + void returnsNullPoints_whenUserDoesNotExist() { + // arrange + String validId = "user123"; + String validEmail = "xx@yy.zz"; + String validBirthday = "1993-03-13"; + String validGender = "male"; + // ์œ ์ € ๋“ฑ๋ก + userService.registerUser(validId, validEmail, validBirthday, validGender); + String nonExistingId = "nonexist"; + + // act + Optional currentPoint = userService.getCurrentPoint(nonExistingId); + + // assert + assertThat(currentPoint).isNotPresent(); + } } diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/PointV1ApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/PointV1ApiE2ETest.java new file mode 100644 index 000000000..e31f5e604 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/PointV1ApiE2ETest.java @@ -0,0 +1,102 @@ +package com.loopers.interfaces.api; + +import com.loopers.interfaces.api.point.PointV1Dto; +import com.loopers.interfaces.api.user.UserV1Dto; +import com.loopers.utils.DatabaseCleanUp; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class PointV1ApiE2ETest { + private final String ENDPOINT_USER = "/api/v1/users"; + private final String ENDPOINT_POINT = "/api/v1/points"; + + private final TestRestTemplate testRestTemplate; + private final DatabaseCleanUp databaseCleanUp; + + @Autowired + public PointV1ApiE2ETest( + TestRestTemplate testRestTemplate, + DatabaseCleanUp databaseCleanUp + ) { + this.testRestTemplate = testRestTemplate; + this.databaseCleanUp = databaseCleanUp; + } + + @AfterEach + void tearDown() { + databaseCleanUp.truncateAllTables(); + } + + @DisplayName("GET /api/v1/points") + @Nested + class GetPoints { + private final String validUserId = "user123"; + private final String validEmail = "xx@yy.zz"; + private final String validBirthday = "1993-03-13"; + private final String validGender = "male"; + + // ํšŒ์›๊ฐ€์ž… ์ •๋ณด ์ž‘์„ฑ + @BeforeEach + void setupUser() { + UserV1Dto.UserRegisterRequest request = new UserV1Dto.UserRegisterRequest( + validUserId, + validEmail, + validBirthday, + validGender + ); + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + testRestTemplate.exchange(ENDPOINT_USER, HttpMethod.POST, new HttpEntity<>(request), responseType); + } + + @DisplayName("ํฌ์ธํŠธ ์กฐํšŒ์— ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ๋ณด์œ  ํฌ์ธํŠธ๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnUserPoints_whenGetUserPointsSuccess() { + // arrange: setupUser() ์ฐธ์กฐ + String xUserIdHeader = "user123"; + HttpHeaders headers = new HttpHeaders(); + headers.add("X-USER-ID", xUserIdHeader); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + HttpEntity requestEntity = new HttpEntity<>(null, headers); + ResponseEntity> response = testRestTemplate.exchange(ENDPOINT_POINT, HttpMethod.GET, requestEntity, responseType); + + // assert + assertAll( + () -> assertTrue(response.getStatusCode().is2xxSuccessful()), + () -> assertThat(response.getBody().data().currentPoint()).isEqualTo(0L) + ); + } + + //`X-USER-ID` ํ—ค๋”๊ฐ€ ์—†์„ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. + @DisplayName("`X-USER-ID` ํ—ค๋”๊ฐ€ ์—†์„ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnBadRequest_whenXUserIdHeaderIsMissing() { + // arrange: setupUser() ์ฐธ์กฐ + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + HttpEntity requestEntity = new HttpEntity<>(null, null); + ResponseEntity> response = testRestTemplate.exchange(ENDPOINT_POINT, HttpMethod.GET, requestEntity, responseType); + + // assert + assertThat(response.getStatusCode().value()).isEqualTo(400); + } + } + +} From 227d93260d2349c04b99731db0db6f06103cb53b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A0=95=EB=B0=B0?= Date: Fri, 31 Oct 2025 17:00:27 +0900 Subject: [PATCH 13/85] =?UTF-8?q?round1:=20=ED=8F=AC=EC=9D=B8=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=A9=EC=A0=84=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 0 ์ดํ•˜์˜ ์ •์ˆ˜๋กœ ํฌ์ธํŠธ๋ฅผ ์ถฉ์ „ ์‹œ ์‹คํŒจํ•œ๋‹ค. ์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ์ € ID ๋กœ ์ถฉ์ „์„ ์‹œ๋„ํ•œ ๊ฒฝ์šฐ, ์‹คํŒจํ•œ๋‹ค. ์กด์žฌํ•˜๋Š” ์œ ์ €๊ฐ€ 1000์›์„ ์ถฉ์ „ํ•  ๊ฒฝ์šฐ, ์ถฉ์ „๋œ ๋ณด์œ  ์ด๋Ÿ‰์„ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค. ์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ์ €๋กœ ์š”์ฒญํ•  ๊ฒฝ์šฐ, `404 Not Found` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. --- .../application/point/PointFacade.java | 6 +++ .../java/com/loopers/domain/point/Point.java | 35 ++++++++++++++++ .../domain/point/PointJpaRepository.java | 9 ++++ .../loopers/domain/point/PointService.java | 28 +++++++++++++ .../java/com/loopers/domain/user/User.java | 4 ++ .../com/loopers/domain/user/UserService.java | 5 +++ .../interfaces/api/point/PointV1ApiSpec.java | 19 +++++++++ .../api/point/PointV1Controller.java | 15 +++++-- .../interfaces/api/point/PointV1Dto.java | 5 +++ .../loopers/domain/point/PointModelTest.java | 37 ++++++++++++++++ .../point/PointServiceIntegrationTest.java | 30 +++++++++++++ .../interfaces/api/PointV1ApiE2ETest.java | 42 +++++++++++++++++++ 12 files changed, 231 insertions(+), 4 deletions(-) create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/point/PointJpaRepository.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/point/PointModelTest.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java diff --git a/apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java index ac25b521c..1a9af650f 100644 --- a/apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java @@ -1,5 +1,6 @@ package com.loopers.application.point; +import com.loopers.domain.point.PointService; import com.loopers.domain.user.UserService; import com.loopers.support.error.CoreException; import com.loopers.support.error.ErrorType; @@ -10,9 +11,14 @@ @Component public class PointFacade { private final UserService userService; + private final PointService pointService; public Long getCurrentPoint(String userId) { return userService.getCurrentPoint(userId) .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); } + + public Long chargePoints(String userId, int amount) { + return pointService.chargePoint(userId, amount); + } } diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java b/apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java new file mode 100644 index 000000000..d27ebbe22 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java @@ -0,0 +1,35 @@ +package com.loopers.domain.point; + +import com.loopers.domain.BaseEntity; +import com.loopers.domain.user.User; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import jakarta.persistence.Entity; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; + +@Entity +@Table(name = "point") +public class Point extends BaseEntity { + @ManyToOne + @JoinColumn(referencedColumnName = "id", nullable = false, updatable = false) + private User user; + private Long amount; + + protected Point() { + } + + private Point(User user, Long amount) { + this.user = user; + this.amount = amount; + } + + public static Point create(User user, int amount) { + if (amount <= 0) { + throw new CoreException(ErrorType.BAD_REQUEST, "์ถฉ์ „ ํฌ์ธํŠธ๋Š” 0๋ณด๋‹ค ์ปค์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + } + + return new Point(user, (long) amount); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/point/PointJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/point/PointJpaRepository.java new file mode 100644 index 000000000..2c1e365a2 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/point/PointJpaRepository.java @@ -0,0 +1,9 @@ +package com.loopers.domain.point; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface PointJpaRepository extends JpaRepository { + +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java b/apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java new file mode 100644 index 000000000..ce0960041 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java @@ -0,0 +1,28 @@ +package com.loopers.domain.point; + +import com.loopers.domain.user.User; +import com.loopers.domain.user.UserService; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Service +public class PointService { + private final PointJpaRepository pointJpaRepository; + private final UserService userService; + + @Transactional + public Long chargePoint(String userId, int amount) { + User user = userService.findByUserIdForUpdate(userId).orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); + + Point point = Point.create(user, amount); + + user.setCurrentPoint(user.getCurrentPoint() + amount); + pointJpaRepository.save(point); + + return user.getCurrentPoint(); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java index f0d617ddf..908a5efac 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java @@ -3,9 +3,11 @@ import com.loopers.domain.BaseEntity; import com.loopers.support.error.CoreException; import com.loopers.support.error.ErrorType; +import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Table; import lombok.Getter; +import lombok.Setter; import org.apache.commons.lang3.StringUtils; @Entity @@ -15,10 +17,12 @@ public class User extends BaseEntity { protected User() { } + @Column(nullable = false, unique = true, length = 10) private String userId; private String email; private String birthday; private String gender; + @Setter private Long currentPoint = 0L; private User(String userId, String email, String birthday, String gender) { diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java index c0f70b1d3..be907604a 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java @@ -28,6 +28,11 @@ public Optional findByUserId(String userId) { return userJpaRepository.findByUserId(userId); } + // find by user id with lock for update + @Transactional + public Optional findByUserIdForUpdate(String userId) { + return userJpaRepository.findByUserId(userId); + } @Transactional(readOnly = true) public Optional getCurrentPoint(String userId) { return findByUserId(userId).map(User::getCurrentPoint); diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1ApiSpec.java index 8efd0d8fb..bfb935ad2 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1ApiSpec.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1ApiSpec.java @@ -22,4 +22,23 @@ ApiResponse getUserPoints( ) String userId ); + + // /points post ํฌ์ธํŠธ ์ถฉ์ „ + @Operation( + method = "POST", + summary = "ํฌ์ธํŠธ ์ถฉ์ „", + description = "ํšŒ์›์˜ ํฌ์ธํŠธ๋ฅผ ์ถฉ์ „ํ•ฉ๋‹ˆ๋‹ค." + ) + ApiResponse chargeUserPoints( + @Schema( + name = "ํšŒ์› ID", + description = "ํฌ์ธํŠธ๋ฅผ ์ถฉ์ „ํ•  ํšŒ์›์˜ ID" + ) + String userId, + @Schema( + name = "์ถฉ์ „ํ•  ํฌ์ธํŠธ", + description = "์ถฉ์ „ํ•  ํฌ์ธํŠธ ๊ธˆ์•ก. ์–‘์ˆ˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค." + ) + PointV1Dto.PointChargeRequest request + ); } diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Controller.java index d5530e9c9..d2fb3ce63 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Controller.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Controller.java @@ -7,10 +7,7 @@ import com.loopers.support.error.ErrorType; import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.StringUtils; -import org.springframework.web.bind.annotation.RequestHeader; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; @RequiredArgsConstructor @RestController @@ -28,4 +25,14 @@ public ApiResponse getUserPoints(@RequestHeader(value PointV1Dto.PointResponse response = PointV1Dto.PointResponse.from(currentPoint); return ApiResponse.success(response); } + + @RequestMapping(method = RequestMethod.POST) + @Override + public ApiResponse chargeUserPoints( + @RequestHeader(value = "X-USER-ID", required = false) String userId, + @RequestBody PointV1Dto.PointChargeRequest request) { + Long chargedPoint = pointFacade.chargePoints(userId, request.amount()); + PointV1Dto.PointResponse response = PointV1Dto.PointResponse.from(chargedPoint); + return ApiResponse.success(response); + } } diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Dto.java index c90bb7072..a42ddec01 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Dto.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Dto.java @@ -11,4 +11,9 @@ public static PointV1Dto.PointResponse from(Long currentPoint) { ); } } + + public record PointChargeRequest( + int amount + ) { + } } diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/point/PointModelTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/point/PointModelTest.java new file mode 100644 index 000000000..6eb3f5f72 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/point/PointModelTest.java @@ -0,0 +1,37 @@ +package com.loopers.domain.point; + +import com.loopers.domain.user.User; +import com.loopers.support.error.CoreException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class PointModelTest { + @DisplayName("ํฌ์ธํŠธ ์ถฉ์ „์„ ํ•  ๋•Œ, ") + @Nested + class Create { + // 0 ์ดํ•˜์˜ ์ •์ˆ˜๋กœ ํฌ์ธํŠธ๋ฅผ ์ถฉ์ „ ์‹œ ์‹คํŒจํ•œ๋‹ค. + @DisplayName("0 ์ดํ•˜์˜ ์ •์ˆ˜๋กœ ํฌ์ธํŠธ๋ฅผ ์ถฉ์ „ ์‹œ ์‹คํŒจํ•œ๋‹ค.") + @ParameterizedTest + @ValueSource(ints = {0, -10, -100}) + void throwsException_whenPointIsZeroOrNegative(int invalidPoint) { + // arrange + String validId = "user123"; + String validEmail = "xx@yy.zz"; + String validBirthday = "1993-03-13"; + String validGender = "male"; + + User user = User.create(validId, validEmail, validBirthday, validGender); + + // act + CoreException result = assertThrows(CoreException.class, () -> Point.create(user, invalidPoint)); + + // assert + assertThat(result.getMessage()).isEqualTo("์ถฉ์ „ ํฌ์ธํŠธ๋Š” 0๋ณด๋‹ค ์ปค์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + } + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java new file mode 100644 index 000000000..a2097aef9 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java @@ -0,0 +1,30 @@ +package com.loopers.domain.point; + +import com.loopers.support.error.CoreException; +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +@SpringBootTest +@Transactional +public class PointServiceIntegrationTest { + @Autowired + private PointService pointService; + + //์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ์ € ID ๋กœ ์ถฉ์ „์„ ์‹œ๋„ํ•œ ๊ฒฝ์šฐ, ์‹คํŒจํ•œ๋‹ค. + @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ์ € ID ๋กœ ์ถฉ์ „์„ ์‹œ๋„ํ•œ ๊ฒฝ์šฐ, ์‹คํŒจํ•œ๋‹ค.") + @Test + void throwsExceptionWhenChargePointWithNonExistingUserId() { + // arrange + String nonExistingUserId = "nonexist"; + int chargeAmount = 1000; + + // act & assert + assertThrows(CoreException.class, () -> pointService.chargePoint(nonExistingUserId, chargeAmount)); + } + +} diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/PointV1ApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/PointV1ApiE2ETest.java index e31f5e604..b4dd08e3c 100644 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/PointV1ApiE2ETest.java +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/PointV1ApiE2ETest.java @@ -97,6 +97,48 @@ void returnBadRequest_whenXUserIdHeaderIsMissing() { // assert assertThat(response.getStatusCode().value()).isEqualTo(400); } + + @DisplayName("์กด์žฌํ•˜๋Š” ์œ ์ €๊ฐ€ 1000์›์„ ์ถฉ์ „ํ•  ๊ฒฝ์šฐ, ์ถฉ์ „๋œ ๋ณด์œ  ์ด๋Ÿ‰์„ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnChargedPoints_whenChargeUserPointsSuccess() { + // arrange: setupUser() ์ฐธ์กฐ + String xUserIdHeader = "user123"; + HttpHeaders headers = new HttpHeaders(); + headers.add("X-USER-ID", xUserIdHeader); + PointV1Dto.PointChargeRequest request = new PointV1Dto.PointChargeRequest(1000); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + HttpEntity requestEntity = new HttpEntity<>(request, headers); + ResponseEntity> response = testRestTemplate.exchange(ENDPOINT_POINT, HttpMethod.POST, requestEntity, responseType); + + // assert + assertAll( + () -> assertTrue(response.getStatusCode().is2xxSuccessful()), + () -> assertThat(response.getBody().data().currentPoint()).isEqualTo(1000L) + ); + } + + //์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ์ €๋กœ ์š”์ฒญํ•  ๊ฒฝ์šฐ, `404 Not Found` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. + @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ์ €๋กœ ์š”์ฒญํ•  ๊ฒฝ์šฐ, `404 Not Found` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnNotFound_whenChargePointsForNonExistentUser() { + // arrange: setupUser() ์ฐธ์กฐ + String xUserIdHeader = "nonexist"; + HttpHeaders headers = new HttpHeaders(); + headers.add("X-USER-ID", xUserIdHeader); + PointV1Dto.PointChargeRequest request = new PointV1Dto.PointChargeRequest(1000); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + HttpEntity requestEntity = new HttpEntity<>(request, headers); + ResponseEntity> response = testRestTemplate.exchange(ENDPOINT_POINT, HttpMethod.POST, requestEntity, responseType); + + // assert + assertThat(response.getStatusCode().value()).isEqualTo(404); + } } } From 003b54351c073361cfd9272616e050f3737dc80c Mon Sep 17 00:00:00 2001 From: adminhelper <201401339hs@gmail.com> Date: Tue, 28 Oct 2025 02:30:05 +0900 Subject: [PATCH 14/85] =?UTF-8?q?docs:=201round.md=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/1round.md | 67 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 docs/1round.md diff --git a/docs/1round.md b/docs/1round.md new file mode 100644 index 000000000..922b0bcea --- /dev/null +++ b/docs/1round.md @@ -0,0 +1,67 @@ +## ๐Ÿงช Implementation Quest + +> ์ง€์ •๋œ **๋‹จ์œ„ ํ…Œ์ŠคํŠธ / ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ / E2E ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค**๋ฅผ ํ•„์ˆ˜๋กœ ๊ตฌํ˜„ํ•˜๊ณ , ๋ชจ๋“  ํ…Œ์ŠคํŠธ๋ฅผ ํ†ต๊ณผ์‹œํ‚ค๋Š” ๊ฒƒ์„ ๋ชฉํ‘œ๋กœ ํ•ฉ๋‹ˆ๋‹ค. +> + +### ํšŒ์› ๊ฐ€์ž… + +**๐Ÿงฑ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ** + +- [ ] ID ๊ฐ€ `์˜๋ฌธ ๋ฐ ์ˆซ์ž 10์ž ์ด๋‚ด` ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. +- [ ] ์ด๋ฉ”์ผ์ด `xx@yy.zz` ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. +- [ ] ์ƒ๋…„์›”์ผ์ด `yyyy-MM-dd` ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. + +**๐Ÿ”— ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ** + +- [ ] ํšŒ์› ๊ฐ€์ž…์‹œ User ์ €์žฅ์ด ์ˆ˜ํ–‰๋œ๋‹ค. ( spy ๊ฒ€์ฆ ) +- [ ] ์ด๋ฏธ ๊ฐ€์ž…๋œ ID ๋กœ ํšŒ์›๊ฐ€์ž… ์‹œ๋„ ์‹œ, ์‹คํŒจํ•œ๋‹ค. + +**๐ŸŒ E2E ํ…Œ์ŠคํŠธ** + +- [ ] ํšŒ์› ๊ฐ€์ž…์ด ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ์ƒ์„ฑ๋œ ์œ ์ € ์ •๋ณด๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค. +- [ ] ํšŒ์› ๊ฐ€์ž… ์‹œ์— ์„ฑ๋ณ„์ด ์—†์„ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. + +### ๋‚ด ์ •๋ณด ์กฐํšŒ + +**๐Ÿ”— ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ** + +- [ ] ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•  ๊ฒฝ์šฐ, ํšŒ์› ์ •๋ณด๊ฐ€ ๋ฐ˜ํ™˜๋œ๋‹ค. +- [ ] ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ, null ์ด ๋ฐ˜ํ™˜๋œ๋‹ค. + +**๐ŸŒ E2E ํ…Œ์ŠคํŠธ** + +- [ ] ๋‚ด ์ •๋ณด ์กฐํšŒ์— ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ํ•ด๋‹นํ•˜๋Š” ์œ ์ € ์ •๋ณด๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค. +- [ ] ์กด์žฌํ•˜์ง€ ์•Š๋Š” ID ๋กœ ์กฐํšŒํ•  ๊ฒฝ์šฐ, `404 Not Found` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. + +### ํฌ์ธํŠธ ์กฐํšŒ + +**๐Ÿ”— ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ** + +- [ ] ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•  ๊ฒฝ์šฐ, ๋ณด์œ  ํฌ์ธํŠธ๊ฐ€ ๋ฐ˜ํ™˜๋œ๋‹ค. +- [ ] ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ, null ์ด ๋ฐ˜ํ™˜๋œ๋‹ค. + +**๐ŸŒ E2E ํ…Œ์ŠคํŠธ** + +- [ ] ํฌ์ธํŠธ ์กฐํšŒ์— ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ๋ณด์œ  ํฌ์ธํŠธ๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค. +- [ ] `X-USER-ID` ํ—ค๋”๊ฐ€ ์—†์„ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. + +### ํฌ์ธํŠธ ์ถฉ์ „ + +**๐Ÿงฑ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ** + +- [ ] 0 ์ดํ•˜์˜ ์ •์ˆ˜๋กœ ํฌ์ธํŠธ๋ฅผ ์ถฉ์ „ ์‹œ ์‹คํŒจํ•œ๋‹ค. + +**๐Ÿ”— ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ** + +- [ ] ์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ์ € ID ๋กœ ์ถฉ์ „์„ ์‹œ๋„ํ•œ ๊ฒฝ์šฐ, ์‹คํŒจํ•œ๋‹ค. + +**๐ŸŒ E2E ํ…Œ์ŠคํŠธ** + +- [ ] ์กด์žฌํ•˜๋Š” ์œ ์ €๊ฐ€ 1000์›์„ ์ถฉ์ „ํ•  ๊ฒฝ์šฐ, ์ถฉ์ „๋œ ๋ณด์œ  ์ด๋Ÿ‰์„ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค. +- [ ] ์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ์ €๋กœ ์š”์ฒญํ•  ๊ฒฝ์šฐ, `404 Not Found` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. + +## โœ… Checklist + +- [ ] ํ…Œ์ŠคํŠธ๋Š” ๋ชจ๋‘ ํ†ต๊ณผํ•ด์•ผ ํ•˜๋ฉฐ, `@Test` ๊ธฐ๋ฐ˜์œผ๋กœ ๋ช…์‹œ์ ์œผ๋กœ ์ž‘์„ฑ +- [ ] ๊ฐ ํ…Œ์ŠคํŠธ๋Š” ํ…Œ์ŠคํŠธ ๋ช…/์„ค๋ช…/์ž…๋ ฅ/์˜ˆ์ƒ ๊ฒฐ๊ณผ๊ฐ€ ๋ถ„๋ช…ํ•ด์•ผ ํ•จ +- [ ] E2E ํ…Œ์ŠคํŠธ๋Š” ์‹ค์ œ HTTP ์š”์ฒญ์„ ํ†ตํ•ด ํ๋ฆ„์„ ๊ฒ€์ฆํ•  ๊ฒƒ \ No newline at end of file From 00498cd9969b6df9d026a270376da4d27c2f85a5 Mon Sep 17 00:00:00 2001 From: adminhelper <201401339hs@gmail.com> Date: Tue, 28 Oct 2025 03:22:55 +0900 Subject: [PATCH 15/85] =?UTF-8?q?feat:=20User=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/loopers/domain/user/User.java | 13 +++++++++ .../com/loopers/domain/user/UserBirth.java | 26 ++++++++++++++++++ .../com/loopers/domain/user/UserEmail.java | 27 +++++++++++++++++++ .../java/com/loopers/domain/user/UserId.java | 24 +++++++++++++++++ 4 files changed, 90 insertions(+) create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/user/User.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/user/UserBirth.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/user/UserEmail.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/user/UserId.java diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java new file mode 100644 index 000000000..9e782a7e5 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java @@ -0,0 +1,13 @@ +package com.loopers.domain.user; + +public class User { + private final UserId userId; + private final UserEmail email; + private final UserBirth birth; + + public User(UserId userId, UserEmail email, UserBirth birth) { + this.userId = userId; + this.email = email; + this.birth = birth; + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserBirth.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserBirth.java new file mode 100644 index 000000000..0bdc950e3 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserBirth.java @@ -0,0 +1,26 @@ +package com.loopers.domain.user; + +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; + +import java.util.regex.Pattern; + +public class UserBirth { + + private static final Pattern PATTERN = Pattern.compile("^\\d{4}-\\d{2}-\\d{2}$"); + + + private final String birth; + + public UserBirth(String birth) { + if (birth == null || birth.isBlank()) { + throw new CoreException(ErrorType.BAD_REQUEST, "์ƒ๋…„์›”์ผ์ด ๋น„์–ด ์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + + if(!PATTERN.matcher(birth).matches()) { + throw new CoreException(ErrorType.BAD_REQUEST, "์ƒ๋…„์›”์ผ ํ˜•์‹์ด ๋งž์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); + } + + this.birth = birth; + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserEmail.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserEmail.java new file mode 100644 index 000000000..a3005c312 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserEmail.java @@ -0,0 +1,27 @@ +package com.loopers.domain.user; + +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; + +import java.util.regex.Pattern; + +public class UserEmail { + + private final static Pattern PATTERN = Pattern.compile("^[\\w.+-]+@[\\w.-]+\\.[a-zA-Z]{2,}$"); + + private String email; + + public UserEmail(String email) { + if(email == null || email.isBlank()) { + throw new CoreException(ErrorType.BAD_REQUEST, "์ด๋ฉ”์ผ์ด ๋น„์–ด ์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + + if(!PATTERN.matcher(email).matches()) { + throw new CoreException(ErrorType.BAD_REQUEST, "์ด๋ฉ”์ผ ํ˜•์‹์— ๋งž์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ex)xx@yy.zz"); + } + + this.email = email; + } + + +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserId.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserId.java new file mode 100644 index 000000000..9adb13bbd --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserId.java @@ -0,0 +1,24 @@ +package com.loopers.domain.user; + +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; + +import java.util.regex.Pattern; + +public class UserId { + + private static final Pattern PATTERN = Pattern.compile("^[a-zA-Z0-9]{1,10}$"); + + private final String id; + + public UserId(String id) { + if(id == null || id.isEmpty()) { + throw new CoreException(ErrorType.BAD_REQUEST, "์‚ฌ์šฉ์ž ID๊ฐ€ ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + + if (!PATTERN.matcher(id).matches()) { + throw new CoreException(ErrorType.BAD_REQUEST, "์‚ฌ์šฉ์ž ID๊ฐ€ ์˜๋ฌธ ๋ฐ ์ˆซ์ž 10์ž ์ด๋‚ด์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + } + this.id = id; + } +} From 1ad67daafe6c9ae1398249e339b0c9bb20632869 Mon Sep 17 00:00:00 2001 From: adminhelper <201401339hs@gmail.com> Date: Tue, 28 Oct 2025 03:23:37 +0900 Subject: [PATCH 16/85] =?UTF-8?q?test:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EB=8B=A8=EC=9C=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../loopers/domain/user/UserBirthTest.java | 25 +++++++++++++++ .../loopers/domain/user/UserEmailTest.java | 31 +++++++++++++++++++ .../com/loopers/domain/user/UserIdTest.java | 31 +++++++++++++++++++ 3 files changed, 87 insertions(+) create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/user/UserBirthTest.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/user/UserEmailTest.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/user/UserIdTest.java diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserBirthTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserBirthTest.java new file mode 100644 index 000000000..7d603fa04 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserBirthTest.java @@ -0,0 +1,25 @@ +package com.loopers.domain.user; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +@DisplayName("UserBirth Test") +class UserBirthTest { + @DisplayName("UserBirth ๋ชจ๋ธ์„ ์ƒ์„ฑํ•  ๋•Œ") + @Nested + class Create { + @DisplayName("UserBirth์ด ํ˜•์‹์— ๋งž์œผ๋ฉด, ์ •์ƒ์ ์œผ๋กœ ์ƒ์„ฑ๋œ๋‹ค.") + @Test + void createUserBirth_whenValid() { + String birth = "1994-12-01"; + + UserBirth userBirth = new UserBirth(birth); + + assertThat(userBirth).extracting("birth").isEqualTo(birth); + } + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserEmailTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserEmailTest.java new file mode 100644 index 000000000..10a527b42 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserEmailTest.java @@ -0,0 +1,31 @@ +package com.loopers.domain.user; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +@DisplayName("UserEmail Test") +class UserEmailTest { + + @DisplayName("UserEmail ๋ชจ๋ธ์„ ์ƒ์„ฑํ•  ๋•Œ") + @Nested + class Create { + @DisplayName("UserEmail์ด ํ˜•์‹์— ๋งž์œผ๋ฉด, ์ •์ƒ์ ์œผ๋กœ ์ƒ์„ฑ๋œ๋‹ค.") + @Test + void createUserEmail_whenValid() { + + // given + String email = "yh45g@gmail.com"; + + // when + UserEmail userEmail = new UserEmail(email); + + // then + Assertions.assertThat(userEmail).extracting("email").isEqualTo(email); + + } + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserIdTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserIdTest.java new file mode 100644 index 000000000..67e23e206 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserIdTest.java @@ -0,0 +1,31 @@ +package com.loopers.domain.user; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +@DisplayName("UserId Test") +class UserIdTest { + + @DisplayName("UserId ๋ชจ๋ธ์„ ์ƒ์„ฑํ•  ๋•Œ") + @Nested + class Create { + @DisplayName("userId๊ฐ€ ํ˜•์‹์— ๋งž์œผ๋ฉด, ์ •์ƒ์ ์œผ๋กœ ์ƒ์„ฑ๋œ๋‹ค.") + @Test + void createUserId_whenValid() { + + // given + String id = "yh45g"; + + // when + UserId userId = new UserId(id); + + // then + Assertions.assertThat(userId).extracting("id").isEqualTo(id); + + } + } +} From 4a1336604a8f08fcc0ada12fe21244770040feb3 Mon Sep 17 00:00:00 2001 From: bookers-web Date: Tue, 28 Oct 2025 18:57:19 +0900 Subject: [PATCH 17/85] =?UTF-8?q?feat=20:=20User=20=EA=B0=92=EA=B0=9D?= =?UTF-8?q?=EC=B2=B4=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=99=84=EB=A3=8C=20?= =?UTF-8?q?=ED=9B=84=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/loopers/domain/user/UserBirth.java | 26 ---------------- .../com/loopers/domain/user/UserEmail.java | 27 ---------------- .../java/com/loopers/domain/user/UserId.java | 24 -------------- .../loopers/domain/user/UserBirthTest.java | 25 --------------- .../loopers/domain/user/UserEmailTest.java | 31 ------------------- .../com/loopers/domain/user/UserIdTest.java | 31 ------------------- 6 files changed, 164 deletions(-) delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/user/UserBirth.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/user/UserEmail.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/user/UserId.java delete mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/user/UserBirthTest.java delete mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/user/UserEmailTest.java delete mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/user/UserIdTest.java diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserBirth.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserBirth.java deleted file mode 100644 index 0bdc950e3..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserBirth.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.loopers.domain.user; - -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; - -import java.util.regex.Pattern; - -public class UserBirth { - - private static final Pattern PATTERN = Pattern.compile("^\\d{4}-\\d{2}-\\d{2}$"); - - - private final String birth; - - public UserBirth(String birth) { - if (birth == null || birth.isBlank()) { - throw new CoreException(ErrorType.BAD_REQUEST, "์ƒ๋…„์›”์ผ์ด ๋น„์–ด ์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); - } - - if(!PATTERN.matcher(birth).matches()) { - throw new CoreException(ErrorType.BAD_REQUEST, "์ƒ๋…„์›”์ผ ํ˜•์‹์ด ๋งž์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); - } - - this.birth = birth; - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserEmail.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserEmail.java deleted file mode 100644 index a3005c312..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserEmail.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.loopers.domain.user; - -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; - -import java.util.regex.Pattern; - -public class UserEmail { - - private final static Pattern PATTERN = Pattern.compile("^[\\w.+-]+@[\\w.-]+\\.[a-zA-Z]{2,}$"); - - private String email; - - public UserEmail(String email) { - if(email == null || email.isBlank()) { - throw new CoreException(ErrorType.BAD_REQUEST, "์ด๋ฉ”์ผ์ด ๋น„์–ด ์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); - } - - if(!PATTERN.matcher(email).matches()) { - throw new CoreException(ErrorType.BAD_REQUEST, "์ด๋ฉ”์ผ ํ˜•์‹์— ๋งž์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ex)xx@yy.zz"); - } - - this.email = email; - } - - -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserId.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserId.java deleted file mode 100644 index 9adb13bbd..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserId.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.loopers.domain.user; - -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; - -import java.util.regex.Pattern; - -public class UserId { - - private static final Pattern PATTERN = Pattern.compile("^[a-zA-Z0-9]{1,10}$"); - - private final String id; - - public UserId(String id) { - if(id == null || id.isEmpty()) { - throw new CoreException(ErrorType.BAD_REQUEST, "์‚ฌ์šฉ์ž ID๊ฐ€ ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); - } - - if (!PATTERN.matcher(id).matches()) { - throw new CoreException(ErrorType.BAD_REQUEST, "์‚ฌ์šฉ์ž ID๊ฐ€ ์˜๋ฌธ ๋ฐ ์ˆซ์ž 10์ž ์ด๋‚ด์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค."); - } - this.id = id; - } -} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserBirthTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserBirthTest.java deleted file mode 100644 index 7d603fa04..000000000 --- a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserBirthTest.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.loopers.domain.user; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.*; - -@DisplayName("UserBirth Test") -class UserBirthTest { - @DisplayName("UserBirth ๋ชจ๋ธ์„ ์ƒ์„ฑํ•  ๋•Œ") - @Nested - class Create { - @DisplayName("UserBirth์ด ํ˜•์‹์— ๋งž์œผ๋ฉด, ์ •์ƒ์ ์œผ๋กœ ์ƒ์„ฑ๋œ๋‹ค.") - @Test - void createUserBirth_whenValid() { - String birth = "1994-12-01"; - - UserBirth userBirth = new UserBirth(birth); - - assertThat(userBirth).extracting("birth").isEqualTo(birth); - } - } -} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserEmailTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserEmailTest.java deleted file mode 100644 index 10a527b42..000000000 --- a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserEmailTest.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.loopers.domain.user; - -import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.*; - -@DisplayName("UserEmail Test") -class UserEmailTest { - - @DisplayName("UserEmail ๋ชจ๋ธ์„ ์ƒ์„ฑํ•  ๋•Œ") - @Nested - class Create { - @DisplayName("UserEmail์ด ํ˜•์‹์— ๋งž์œผ๋ฉด, ์ •์ƒ์ ์œผ๋กœ ์ƒ์„ฑ๋œ๋‹ค.") - @Test - void createUserEmail_whenValid() { - - // given - String email = "yh45g@gmail.com"; - - // when - UserEmail userEmail = new UserEmail(email); - - // then - Assertions.assertThat(userEmail).extracting("email").isEqualTo(email); - - } - } -} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserIdTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserIdTest.java deleted file mode 100644 index 67e23e206..000000000 --- a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserIdTest.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.loopers.domain.user; - -import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.*; - -@DisplayName("UserId Test") -class UserIdTest { - - @DisplayName("UserId ๋ชจ๋ธ์„ ์ƒ์„ฑํ•  ๋•Œ") - @Nested - class Create { - @DisplayName("userId๊ฐ€ ํ˜•์‹์— ๋งž์œผ๋ฉด, ์ •์ƒ์ ์œผ๋กœ ์ƒ์„ฑ๋œ๋‹ค.") - @Test - void createUserId_whenValid() { - - // given - String id = "yh45g"; - - // when - UserId userId = new UserId(id); - - // then - Assertions.assertThat(userId).extracting("id").isEqualTo(id); - - } - } -} From bc962b2dae67d277e3e5a57fd2589fe898961576 Mon Sep 17 00:00:00 2001 From: bookers-web Date: Tue, 28 Oct 2025 18:57:33 +0900 Subject: [PATCH 18/85] =?UTF-8?q?feat=20:=20User=20=EA=B0=9D=EC=B2=B4=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 --- .../java/com/loopers/domain/user/User.java | 68 ++++++++++++++++--- .../com/loopers/domain/user/UserTest.java | 18 +++++ 2 files changed, 77 insertions(+), 9 deletions(-) create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/user/UserTest.java diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java index 9e782a7e5..fec224c80 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java @@ -1,13 +1,63 @@ package com.loopers.domain.user; -public class User { - private final UserId userId; - private final UserEmail email; - private final UserBirth birth; - - public User(UserId userId, UserEmail email, UserBirth birth) { - this.userId = userId; - this.email = email; - this.birth = birth; +import com.loopers.domain.BaseEntity; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; + +import java.util.regex.Pattern; + +@Entity +@Table(name = "user") +public class User extends BaseEntity { + + private static final Pattern USERID_PATTERN = Pattern.compile("^[a-zA-Z0-9]{1,10}$"); + private static final Pattern BIRTH_PATTERN = Pattern.compile("^\\d{4}-\\d{2}-\\d{2}$"); + private static final Pattern EMAIL_PATTERN = Pattern.compile("^[\\w.+-]+@[\\w.-]+\\.[a-zA-Z]{2,}$"); + + private String userId; + private String email; + private String birth; + + protected User() {} + + public User(String userId, String email, String birth) { + this.userId = requireValidUserId(userId); + this.email = requireValidEmail(email); + this.birth = requireValidBirthDate(birth); + } + + String requireValidUserId(String userId) { + if(userId == null || userId.isEmpty()) { + throw new CoreException(ErrorType.BAD_REQUEST, "์‚ฌ์šฉ์ž ID๊ฐ€ ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + + if (!USERID_PATTERN.matcher(userId).matches()) { + throw new CoreException(ErrorType.BAD_REQUEST, "์‚ฌ์šฉ์ž ID๊ฐ€ ์˜๋ฌธ ๋ฐ ์ˆซ์ž 10์ž ์ด๋‚ด์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + } + return userId; + } + + String requireValidEmail(String email) { + if(email == null || email.isBlank()) { + throw new CoreException(ErrorType.BAD_REQUEST, "์ด๋ฉ”์ผ์ด ๋น„์–ด ์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + + if(!EMAIL_PATTERN.matcher(email).matches()) { + throw new CoreException(ErrorType.BAD_REQUEST, "์ด๋ฉ”์ผ ํ˜•์‹์— ๋งž์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ex)xx@yy.zz"); + } + return email; + } + + String requireValidBirthDate(String birth) { + if (birth == null || birth.isBlank()) { + throw new CoreException(ErrorType.BAD_REQUEST, "์ƒ๋…„์›”์ผ์ด ๋น„์–ด ์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + + if(!BIRTH_PATTERN.matcher(birth).matches()) { + throw new CoreException(ErrorType.BAD_REQUEST, "์ƒ๋…„์›”์ผ ํ˜•์‹์ด ๋งž์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); + } + return birth; } } diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserTest.java new file mode 100644 index 000000000..1d938accc --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserTest.java @@ -0,0 +1,18 @@ +package com.loopers.domain.user; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * packageName : com.loopers.domain.user + * fileName : UserTest + * author : byeonsungmun + * date : 25. 10. 28. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 25. 10. 28. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +class UserTest { + +} From 0605dc42e8612898d0ad5ca4d094d09e0b158283 Mon Sep 17 00:00:00 2001 From: adminhelper <201401339hs@gmail.com> Date: Wed, 29 Oct 2025 03:25:07 +0900 Subject: [PATCH 19/85] =?UTF-8?q?docs:=201round.md=20=EB=AC=B8=EC=84=9C?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/1round.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/1round.md b/docs/1round.md index 922b0bcea..28d422eb9 100644 --- a/docs/1round.md +++ b/docs/1round.md @@ -7,9 +7,9 @@ **๐Ÿงฑ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ** -- [ ] ID ๊ฐ€ `์˜๋ฌธ ๋ฐ ์ˆซ์ž 10์ž ์ด๋‚ด` ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. -- [ ] ์ด๋ฉ”์ผ์ด `xx@yy.zz` ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. -- [ ] ์ƒ๋…„์›”์ผ์ด `yyyy-MM-dd` ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. +- [x] ID ๊ฐ€ `์˜๋ฌธ ๋ฐ ์ˆซ์ž 10์ž ์ด๋‚ด` ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. +- [x] ์ด๋ฉ”์ผ์ด `xx@yy.zz` ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. +- [x] ์ƒ๋…„์›”์ผ์ด `yyyy-MM-dd` ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. **๐Ÿ”— ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ** From f1d94ed833be33a3c1125a406946a878084c67cf Mon Sep 17 00:00:00 2001 From: adminhelper <201401339hs@gmail.com> Date: Wed, 29 Oct 2025 03:25:35 +0900 Subject: [PATCH 20/85] =?UTF-8?q?feat:=20UserRepository,=20UserService=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../loopers/domain/user/UserRepository.java | 10 ++++++++ .../com/loopers/domain/user/UserService.java | 25 +++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java new file mode 100644 index 000000000..6ac3b7b98 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java @@ -0,0 +1,10 @@ +package com.loopers.domain.user; + +import java.util.Optional; + +public interface UserRepository { + + Optional findByUserId(String id); + + User save(User user); +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java new file mode 100644 index 000000000..b5dad2ef6 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java @@ -0,0 +1,25 @@ +package com.loopers.domain.user; + +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Component +@RequiredArgsConstructor +public class UserService { + + private final UserRepository userRepository; + + @Transactional + User register(String userId, String email, String birth) { + userRepository.findByUserId(userId).ifPresent(user -> { + throw new CoreException(ErrorType.CONFLICT); + }); + + User user = new User(userId, email, birth); + return userRepository.save(user); + } + +} From a51021cf7e2c345d1e13aee7fc29402d9b885a32 Mon Sep 17 00:00:00 2001 From: adminhelper <201401339hs@gmail.com> Date: Wed, 29 Oct 2025 03:26:47 +0900 Subject: [PATCH 21/85] =?UTF-8?q?test:=20=ED=9A=8C=EC=9B=90=20=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=ED=86=B5=ED=95=A9=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/UserServiceIntegrationTest.java | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java new file mode 100644 index 000000000..f226daf6c --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java @@ -0,0 +1,52 @@ +package com.loopers.domain.user; + +import com.loopers.support.error.CoreException; +import com.loopers.utils.DatabaseCleanUp; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +@SpringBootTest +class UserServiceIntegrationTest { + + @Autowired + private UserRepository userRepository; + + @Autowired + private UserService userService; + + @Autowired + private DatabaseCleanUp databaseCleanUp; + + @AfterEach + void tearDown() { + databaseCleanUp.truncateAllTables(); + } + + @DisplayName("ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ") + @Nested + class UserRegister { + + @DisplayName("ํšŒ์› ๊ฐ€์ž…์‹œ User ์ €์žฅ์ด ์ˆ˜ํ–‰๋œ๋‹ค.") + @Test + void save_whenUserRegister() { + String userId = "yh45g"; + String email = "yh45g@loopers.com"; + String brith = "1994-12-05"; + + User user = new User(userId, email, brith); + UserRepository userRepositorySpy = spy(userRepository); + UserService userServiceSpy = new UserService(userRepositorySpy); + userServiceSpy.register(userId, email, brith); + + verify(userRepositorySpy).save(user); + } + } +} From a946ac0a78d4bd7cbe562f61fc3f81baf7dc1673 Mon Sep 17 00:00:00 2001 From: adminhelper <201401339hs@gmail.com> Date: Wed, 29 Oct 2025 03:32:17 +0900 Subject: [PATCH 22/85] =?UTF-8?q?fix:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20import=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/loopers/domain/user/UserRepository.java | 5 ++++- .../src/main/java/com/loopers/domain/user/UserService.java | 4 ++-- .../com/loopers/domain/user/UserServiceIntegrationTest.java | 2 -- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java index 6ac3b7b98..06234b87f 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java @@ -1,10 +1,13 @@ package com.loopers.domain.user; +import org.springframework.stereotype.Component; + import java.util.Optional; +@Component public interface UserRepository { Optional findByUserId(String id); - User save(User user); + void save(User user); } diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java index b5dad2ef6..8603ad673 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java @@ -13,13 +13,13 @@ public class UserService { private final UserRepository userRepository; @Transactional - User register(String userId, String email, String birth) { + public void register(String userId, String email, String birth) { userRepository.findByUserId(userId).ifPresent(user -> { throw new CoreException(ErrorType.CONFLICT); }); User user = new User(userId, email, birth); - return userRepository.save(user); + userRepository.save(user); } } diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java index f226daf6c..a8b887ef2 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java @@ -1,6 +1,5 @@ package com.loopers.domain.user; -import com.loopers.support.error.CoreException; import com.loopers.utils.DatabaseCleanUp; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; @@ -9,7 +8,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; From 33c0ab862b18e90eabdbf0384325884d0b727b93 Mon Sep 17 00:00:00 2001 From: adminhelper <201401339hs@gmail.com> Date: Thu, 30 Oct 2025 04:43:19 +0900 Subject: [PATCH 23/85] =?UTF-8?q?test:=20=EB=82=B4=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EB=B0=8F=20E2E=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../loopers/application/user/UserFacade.java | 25 ++++ .../loopers/application/user/UserInfo.java | 14 ++ .../java/com/loopers/domain/user/User.java | 22 ++- .../loopers/domain/user/UserRepository.java | 7 +- .../com/loopers/domain/user/UserService.java | 13 +- .../user/UserJpaRepository.java | 10 ++ .../user/UserRepositoryImpl.java | 27 ++++ .../interfaces/api/user/UserV1ApiSpec.java | 28 ++++ .../interfaces/api/user/UserV1Controller.java | 31 ++++ .../interfaces/api/user/UserV1Dto.java | 24 ++++ .../user/UserServiceIntegrationTest.java | 29 ++-- .../api/user/UserV1ControllerTest.java | 135 ++++++++++++++++++ 12 files changed, 348 insertions(+), 17 deletions(-) create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/user/UserInfo.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserJpaRepository.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1ApiSpec.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/interfaces/api/user/UserV1ControllerTest.java diff --git a/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java new file mode 100644 index 000000000..4df3eff79 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java @@ -0,0 +1,25 @@ +package com.loopers.application.user; + +import com.loopers.domain.user.User; +import com.loopers.domain.user.UserService; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class UserFacade { + private final UserService userService; + + public UserInfo register(String userId, String email, String birth, String gender) { + User user = userService.register(userId, email, birth, gender); + return UserInfo.from(user); + } + + public UserInfo getUser(String userId) { + return userService.findUserByUserId(userId) + .map(UserInfo::from) + .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "ํšŒ์› ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/user/UserInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/user/UserInfo.java new file mode 100644 index 000000000..08f5cea43 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/user/UserInfo.java @@ -0,0 +1,14 @@ +package com.loopers.application.user; + +import com.loopers.domain.user.User; + +public record UserInfo(String userId, String email, String birth, String gender) { + public static UserInfo from(User user) { + return new UserInfo( + user.getUserId(), + user.getEmail(), + user.getBirth(), + user.getGender() + ); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java index fec224c80..67ce14001 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java @@ -1,31 +1,44 @@ package com.loopers.domain.user; import com.loopers.domain.BaseEntity; +import com.loopers.domain.point.Point; import com.loopers.support.error.CoreException; import com.loopers.support.error.ErrorType; +import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Table; +import lombok.Getter; import java.util.regex.Pattern; @Entity @Table(name = "user") +@Getter public class User extends BaseEntity { private static final Pattern USERID_PATTERN = Pattern.compile("^[a-zA-Z0-9]{1,10}$"); private static final Pattern BIRTH_PATTERN = Pattern.compile("^\\d{4}-\\d{2}-\\d{2}$"); private static final Pattern EMAIL_PATTERN = Pattern.compile("^[\\w.+-]+@[\\w.-]+\\.[a-zA-Z]{2,}$"); + @Column(unique = true, nullable = false) private String userId; + + @Column(nullable = false) private String email; + + @Column(nullable = false) private String birth; + @Column(nullable = false) + private String gender; + protected User() {} - public User(String userId, String email, String birth) { + public User(String userId, String email, String birth, String gender) { this.userId = requireValidUserId(userId); this.email = requireValidEmail(email); this.birth = requireValidBirthDate(birth); + this.gender = requireValidGender(gender); } String requireValidUserId(String userId) { @@ -60,4 +73,11 @@ String requireValidBirthDate(String birth) { } return birth; } + + String requireValidGender(String gender) { + if(gender == null || gender.isBlank()) { + throw new CoreException(ErrorType.BAD_REQUEST, "์„ฑ๋ณ„์ด ๋น„์–ด์žˆ์Šต๋‹ˆ๋‹ค."); + } + return gender; + } } diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java index 06234b87f..f4b26266e 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java @@ -1,13 +1,10 @@ package com.loopers.domain.user; -import org.springframework.stereotype.Component; - import java.util.Optional; -@Component public interface UserRepository { - Optional findByUserId(String id); + Optional findByUserId(String userId); - void save(User user); + User save(User user); } diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java index 8603ad673..8dcf999e8 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java @@ -6,6 +6,8 @@ import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; +import java.util.Optional; + @Component @RequiredArgsConstructor public class UserService { @@ -13,13 +15,18 @@ public class UserService { private final UserRepository userRepository; @Transactional - public void register(String userId, String email, String birth) { + public User register(String userId, String email, String birth, String gender) { userRepository.findByUserId(userId).ifPresent(user -> { throw new CoreException(ErrorType.CONFLICT); }); - User user = new User(userId, email, birth); - userRepository.save(user); + User user = new User(userId, email, birth, gender); + return userRepository.save(user); + } + + @Transactional(readOnly = true) + public Optional findUserByUserId(String userId){ + return userRepository.findByUserId(userId); } } diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserJpaRepository.java new file mode 100644 index 000000000..1a78b9a69 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserJpaRepository.java @@ -0,0 +1,10 @@ +package com.loopers.infrastructure.user; + +import com.loopers.domain.user.User; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface UserJpaRepository extends JpaRepository { + Optional findByUserId(String userId); +} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java new file mode 100644 index 000000000..25f05bc6e --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java @@ -0,0 +1,27 @@ +package com.loopers.infrastructure.user; + +import com.loopers.domain.user.User; +import com.loopers.domain.user.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +@RequiredArgsConstructor +@Component +public class UserRepositoryImpl implements UserRepository { + + private final UserJpaRepository userJpaRepository; + + @Override + public Optional findByUserId(String userId) { + return userJpaRepository.findByUserId(userId); + } + + @Override + public User save(User user) { + userJpaRepository.save(user); + return user; + } +} + diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1ApiSpec.java new file mode 100644 index 000000000..f11f386e2 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1ApiSpec.java @@ -0,0 +1,28 @@ +package com.loopers.interfaces.api.user; + +import com.loopers.interfaces.api.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.tags.Tag; + +@Tag(name = "Users V1 API", description = "Users API์ž…๋‹ˆ๋‹ค.") +public interface UserV1ApiSpec { + + @Operation( + summary = "ํšŒ์› ๊ฐ€์ž…", + description = "ํšŒ์› ๊ฐ€์ž…์„ ํ•ฉ๋‹ˆ๋‹ค." + ) + ApiResponse register( + @Schema(description = "ํšŒ์›๊ฐ€์ž…") + UserV1Dto.RegisterRequest request + ); + + @Operation( + summary = "ํšŒ์› ์กฐํšŒ", + description = "ํ•ด๋‹น ํšŒ์›์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค." + ) + ApiResponse getUser( + @Schema(name = "ํšŒ์› ID", description = "์กฐํšŒํ•  ํšŒ์› ID") + String userId + ); +} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java new file mode 100644 index 000000000..aed39ae1f --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java @@ -0,0 +1,31 @@ +package com.loopers.interfaces.api.user; + +import com.loopers.application.user.UserFacade; +import com.loopers.application.user.UserInfo; +import com.loopers.interfaces.api.ApiResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/v1/users") +public class UserV1Controller implements UserV1ApiSpec { + + private final UserFacade userFacade; + + @Override + @PostMapping("/register") + public ApiResponse register(@RequestBody UserV1Dto.RegisterRequest request) { + UserInfo userInfo = userFacade.register(request.userId(), request.mail(), request.birth(), request.gender()); + UserV1Dto.UserResponse response = UserV1Dto.UserResponse.from(userInfo); + return ApiResponse.success(response); + } + + @Override + @GetMapping("/{userId}") + public ApiResponse getUser(@PathVariable String userId) { + UserInfo userInfo = userFacade.getUser(userId); + UserV1Dto.UserResponse response = UserV1Dto.UserResponse.from(userInfo); + return ApiResponse.success(response); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java new file mode 100644 index 000000000..cc634be67 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java @@ -0,0 +1,24 @@ +package com.loopers.interfaces.api.user; + +import com.loopers.application.user.UserInfo; +import jakarta.validation.Valid; + +public class UserV1Dto { + public record RegisterRequest( + String userId, + String mail, + String birth, + String gender + ){} + + public record UserResponse(String userId, String email, String birth, String gender) { + public static UserResponse from(UserInfo info) { + return new UserResponse( + info.userId(), + info.email(), + info.birth(), + info.gender() + ); + } + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java index a8b887ef2..edcfca9a9 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java @@ -1,13 +1,12 @@ package com.loopers.domain.user; +import com.loopers.support.error.CoreException; import com.loopers.utils.DatabaseCleanUp; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; @@ -28,7 +27,7 @@ void tearDown() { databaseCleanUp.truncateAllTables(); } - @DisplayName("ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ") + @DisplayName("ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ") @Nested class UserRegister { @@ -38,13 +37,27 @@ void save_whenUserRegister() { String userId = "yh45g"; String email = "yh45g@loopers.com"; String brith = "1994-12-05"; + String gender = "Male"; - User user = new User(userId, email, brith); UserRepository userRepositorySpy = spy(userRepository); UserService userServiceSpy = new UserService(userRepositorySpy); - userServiceSpy.register(userId, email, brith); + userServiceSpy.register(userId, email, brith, gender); - verify(userRepositorySpy).save(user); + verify(userRepositorySpy).save(any(User.class)); + } + + @DisplayName("์ด๋ฏธ ๊ฐ€์ž…๋œ ID ๋กœ ํšŒ์›๊ฐ€์ž… ์‹œ๋„ ์‹œ, ์‹คํŒจํ•œ๋‹ค.") + @Test + void register_whenUserIdAlreadyExists_thenFail() { + String userId = "yh45g"; + String email = "yh45g@loopers.com"; + String brith = "1994-12-05"; + String gender = "Male"; + + userService.register(userId, email, brith, gender); + + Assertions.assertThrows(CoreException.class, () + -> userService.register(userId, email, brith, gender)); } } } diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/user/UserV1ControllerTest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/user/UserV1ControllerTest.java new file mode 100644 index 000000000..c0648f86c --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/user/UserV1ControllerTest.java @@ -0,0 +1,135 @@ +package com.loopers.interfaces.api.user; + +import com.loopers.domain.user.User; +import com.loopers.infrastructure.user.UserJpaRepository; +import com.loopers.interfaces.api.ApiResponse; +import com.loopers.utils.DatabaseCleanUp; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.*; + +import java.util.function.Function; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class UserV1ControllerTest { + + private static final String REGISTER_ENDPOINT = "/api/v1/users/register"; + private static final Function GETUSER_ENDPOINT = id -> "/api/v1/users/" + id; + private final TestRestTemplate testRestTemplate; + private final UserJpaRepository userJpaRepository; + private final DatabaseCleanUp databaseCleanUp; + + @Autowired + public UserV1ControllerTest(TestRestTemplate testRestTemplate, UserJpaRepository userJpaRepository, DatabaseCleanUp databaseCleanUp) { + this.testRestTemplate = testRestTemplate; + this.userJpaRepository = userJpaRepository; + this.databaseCleanUp = databaseCleanUp; + } + + @AfterEach + void tearDown() { + databaseCleanUp.truncateAllTables(); + } + + @DisplayName("POST /api/v1/users") + @Nested + class RegisterUser { + @DisplayName("ํšŒ์› ๊ฐ€์ž…์ด ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ์ƒ์„ฑ๋œ ์œ ์ € ์ •๋ณด๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void registerUser_whenSuccessResponseUser() { + String userId = "yh45g"; + String email = "yh45g@loopers.com"; + String birth = "1994-12-05"; + String gender = "MALE"; + + UserV1Dto.RegisterRequest request = new UserV1Dto.RegisterRequest(userId, email, birth, gender); + + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; + + ResponseEntity> response = + testRestTemplate.exchange(REGISTER_ENDPOINT, HttpMethod.POST, new HttpEntity<>(request), responseType); + + assertAll( + () -> assertTrue(response.getStatusCode().is2xxSuccessful()), + () -> assertThat(response.getBody().data().userId()).isEqualTo(userId), + () -> assertThat(response.getBody().data().email()).isEqualTo(email), + () -> assertThat(response.getBody().data().birth()).isEqualTo(birth), + () -> assertThat(response.getBody().data().gender()).isEqualTo(gender) + ); + } + @DisplayName("ํšŒ์› ๊ฐ€์ž… ์‹œ์— ์„ฑ๋ณ„์ด ์—†์„ ๊ฒฝ์šฐ, 400 BAD_REQUEST ์‘๋‹ต์„ ๋ฐ›๋Š”๋‹ค.") + @Test + void throwsBadRequest_whenGenderIsNotProvided() { + String userId = "yh45g"; + String email = "yh45g@loopers.com"; + String birth = "1994-12-05"; + String gender = null; + + UserV1Dto.RegisterRequest request = new UserV1Dto.RegisterRequest(userId, email, birth, gender); + + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; + ResponseEntity> response = + testRestTemplate.exchange(REGISTER_ENDPOINT, HttpMethod.POST, new HttpEntity<>(request), responseType); + + assertAll( + () -> assertTrue(response.getStatusCode().is4xxClientError()), + () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST) + ); + } + } + + @DisplayName("GET /api/v1/users/{userId}") + @Nested + class GetUserById { + @DisplayName("ํ•ด๋‹น ID์˜ ํšŒ์›์ด ์กด์žฌํ•  ๊ฒฝ์šฐ, ํšŒ์› ์ •๋ณด๊ฐ€ ๋ฐ˜ํ™˜๋œ๋‹ค.") + @Test + void getUserById_whenSuccessResponseUser() { + String userId = "yh45g"; + String email = "yh45g@loopers.com"; + String birth = "1994-12-05"; + String gender = "MALE"; + + userJpaRepository.save(new User(userId, email, birth, gender)); + + String requestUrl = GETUSER_ENDPOINT.apply(userId); + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; + ResponseEntity> response = + testRestTemplate.exchange(requestUrl, HttpMethod.GET, new HttpEntity<>(null), responseType); + + assertAll( + () -> assertTrue(response.getStatusCode().is2xxSuccessful()), + () -> assertThat(response.getBody().data().userId()).isEqualTo(userId), + () -> assertThat(response.getBody().data().email()).isEqualTo(email), + () -> assertThat(response.getBody().data().birth()).isEqualTo(birth), + () -> assertThat(response.getBody().data().gender()).isEqualTo(gender) + ); + } + + @DisplayName("ํ•ด๋‹น ID์˜ ํšŒ์›์ด ์กด์žฌํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ, null ์ด ๋ฐ˜ํ™˜๋œ๋‹ค.") + @Test + void throwsException_whenInvalidUserIdIsProvided() { + String userId = "notUserId"; + String requestUrl = GETUSER_ENDPOINT.apply(userId); + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; + ResponseEntity> response = + testRestTemplate.exchange(requestUrl, HttpMethod.GET, new HttpEntity<>(null), responseType); + + HttpHeaders headers = new HttpHeaders(); + assertAll( + () -> assertTrue(response.getStatusCode().is4xxClientError()), + () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND) + ); + } + } + +} From b73c2c6d549816528d3c5165e7e200ef34a4d95f Mon Sep 17 00:00:00 2001 From: adminhelper <201401339hs@gmail.com> Date: Thu, 30 Oct 2025 04:43:49 +0900 Subject: [PATCH 24/85] docs: 1round.md check list update --- docs/1round.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/1round.md b/docs/1round.md index 28d422eb9..b8931c23b 100644 --- a/docs/1round.md +++ b/docs/1round.md @@ -13,25 +13,25 @@ **๐Ÿ”— ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ** -- [ ] ํšŒ์› ๊ฐ€์ž…์‹œ User ์ €์žฅ์ด ์ˆ˜ํ–‰๋œ๋‹ค. ( spy ๊ฒ€์ฆ ) -- [ ] ์ด๋ฏธ ๊ฐ€์ž…๋œ ID ๋กœ ํšŒ์›๊ฐ€์ž… ์‹œ๋„ ์‹œ, ์‹คํŒจํ•œ๋‹ค. +- [x] ํšŒ์› ๊ฐ€์ž…์‹œ User ์ €์žฅ์ด ์ˆ˜ํ–‰๋œ๋‹ค. ( spy ๊ฒ€์ฆ ) +- [x] ์ด๋ฏธ ๊ฐ€์ž…๋œ ID ๋กœ ํšŒ์›๊ฐ€์ž… ์‹œ๋„ ์‹œ, ์‹คํŒจํ•œ๋‹ค. **๐ŸŒ E2E ํ…Œ์ŠคํŠธ** -- [ ] ํšŒ์› ๊ฐ€์ž…์ด ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ์ƒ์„ฑ๋œ ์œ ์ € ์ •๋ณด๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค. -- [ ] ํšŒ์› ๊ฐ€์ž… ์‹œ์— ์„ฑ๋ณ„์ด ์—†์„ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. +- [x] ํšŒ์› ๊ฐ€์ž…์ด ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ์ƒ์„ฑ๋œ ์œ ์ € ์ •๋ณด๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค. +- [x] ํšŒ์› ๊ฐ€์ž… ์‹œ์— ์„ฑ๋ณ„์ด ์—†์„ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. ### ๋‚ด ์ •๋ณด ์กฐํšŒ **๐Ÿ”— ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ** -- [ ] ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•  ๊ฒฝ์šฐ, ํšŒ์› ์ •๋ณด๊ฐ€ ๋ฐ˜ํ™˜๋œ๋‹ค. -- [ ] ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ, null ์ด ๋ฐ˜ํ™˜๋œ๋‹ค. +- [x] ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•  ๊ฒฝ์šฐ, ํšŒ์› ์ •๋ณด๊ฐ€ ๋ฐ˜ํ™˜๋œ๋‹ค. +- [x] ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ, null ์ด ๋ฐ˜ํ™˜๋œ๋‹ค. **๐ŸŒ E2E ํ…Œ์ŠคํŠธ** -- [ ] ๋‚ด ์ •๋ณด ์กฐํšŒ์— ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ํ•ด๋‹นํ•˜๋Š” ์œ ์ € ์ •๋ณด๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค. -- [ ] ์กด์žฌํ•˜์ง€ ์•Š๋Š” ID ๋กœ ์กฐํšŒํ•  ๊ฒฝ์šฐ, `404 Not Found` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. +- [x] ๋‚ด ์ •๋ณด ์กฐํšŒ์— ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ํ•ด๋‹นํ•˜๋Š” ์œ ์ € ์ •๋ณด๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค. +- [x] ์กด์žฌํ•˜์ง€ ์•Š๋Š” ID ๋กœ ์กฐํšŒํ•  ๊ฒฝ์šฐ, `404 Not Found` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. ### ํฌ์ธํŠธ ์กฐํšŒ From 8d5e90a8a0129ead9315ac4bbb80b1560fc68d45 Mon Sep 17 00:00:00 2001 From: adminhelper <201401339hs@gmail.com> Date: Thu, 30 Oct 2025 04:44:08 +0900 Subject: [PATCH 25/85] =?UTF-8?q?feat:=20=ED=8F=AC=EC=9D=B8=ED=8A=B8=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EB=8F=84=EB=A9=94=EC=9D=B8=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 --- .../java/com/loopers/domain/point/Point.java | 20 +++++++++++++++++++ .../loopers/domain/point/PointRepository.java | 4 ++++ .../loopers/domain/point/PointService.java | 6 ++++++ .../point/PointJpaRepository.java | 7 +++++++ 4 files changed, 37 insertions(+) create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/point/PointRepository.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointJpaRepository.java diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java b/apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java new file mode 100644 index 000000000..26266d141 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java @@ -0,0 +1,20 @@ +package com.loopers.domain.point; + + +import com.loopers.domain.BaseEntity; +import com.loopers.domain.user.User; +import jakarta.persistence.*; + +@Entity +@Table(name = "point") +public class Point extends BaseEntity { + + @OneToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "user_id", unique = true, nullable = false) + private User user; + + @Column(nullable = false) + private Long Amount; + + +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/point/PointRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/point/PointRepository.java new file mode 100644 index 000000000..0a50068ca --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/point/PointRepository.java @@ -0,0 +1,4 @@ +package com.loopers.domain.point; + +public interface PointRepository { +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java b/apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java new file mode 100644 index 000000000..03807f888 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java @@ -0,0 +1,6 @@ +package com.loopers.domain.point; + +public class PointService { + + +} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointJpaRepository.java new file mode 100644 index 000000000..aa1089a9e --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointJpaRepository.java @@ -0,0 +1,7 @@ +package com.loopers.infrastructure.point; + +import com.loopers.domain.point.Point; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface PointJpaRepository extends JpaRepository { +} From d6711cfa75f8019e9555edb24d390d1af0f34d4b Mon Sep 17 00:00:00 2001 From: adminhelper <201401339hs@gmail.com> Date: Thu, 30 Oct 2025 04:46:37 +0900 Subject: [PATCH 26/85] =?UTF-8?q?feat:=20=EB=82=B4=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EB=B0=8F=20E2E=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/loopers/domain/user/User.java | 1 - .../java/com/loopers/interfaces/api/user/UserV1ApiSpec.java | 2 +- .../main/java/com/loopers/interfaces/api/user/UserV1Dto.java | 1 - .../com/loopers/interfaces/api/user/UserV1ControllerTest.java | 1 - 4 files changed, 1 insertion(+), 4 deletions(-) diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java index 67ce14001..287b84cf8 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java @@ -1,7 +1,6 @@ package com.loopers.domain.user; import com.loopers.domain.BaseEntity; -import com.loopers.domain.point.Point; import com.loopers.support.error.CoreException; import com.loopers.support.error.ErrorType; import jakarta.persistence.Column; diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1ApiSpec.java index f11f386e2..dec948cdf 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1ApiSpec.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1ApiSpec.java @@ -5,7 +5,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.tags.Tag; -@Tag(name = "Users V1 API", description = "Users API์ž…๋‹ˆ๋‹ค.") +@Tag(name = "Users V1 API", description = "Users API ์ž…๋‹ˆ๋‹ค.") public interface UserV1ApiSpec { @Operation( diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java index cc634be67..16aab05e2 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java @@ -1,7 +1,6 @@ package com.loopers.interfaces.api.user; import com.loopers.application.user.UserInfo; -import jakarta.validation.Valid; public class UserV1Dto { public record RegisterRequest( diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/user/UserV1ControllerTest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/user/UserV1ControllerTest.java index c0648f86c..5a297152b 100644 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/user/UserV1ControllerTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/user/UserV1ControllerTest.java @@ -124,7 +124,6 @@ void throwsException_whenInvalidUserIdIsProvided() { ResponseEntity> response = testRestTemplate.exchange(requestUrl, HttpMethod.GET, new HttpEntity<>(null), responseType); - HttpHeaders headers = new HttpHeaders(); assertAll( () -> assertTrue(response.getStatusCode().is4xxClientError()), () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND) From 7a566848790f056205df2f593f7d90324e48cb79 Mon Sep 17 00:00:00 2001 From: adminhelper <201401339hs@gmail.com> Date: Fri, 31 Oct 2025 05:02:34 +0900 Subject: [PATCH 27/85] =?UTF-8?q?docs:=201round.md=20=EC=9A=94=EA=B5=AC?= =?UTF-8?q?=EC=82=AC=ED=95=AD=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/1round.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/1round.md b/docs/1round.md index b8931c23b..106d6c809 100644 --- a/docs/1round.md +++ b/docs/1round.md @@ -37,31 +37,31 @@ **๐Ÿ”— ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ** -- [ ] ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•  ๊ฒฝ์šฐ, ๋ณด์œ  ํฌ์ธํŠธ๊ฐ€ ๋ฐ˜ํ™˜๋œ๋‹ค. -- [ ] ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ, null ์ด ๋ฐ˜ํ™˜๋œ๋‹ค. +- [x] ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•  ๊ฒฝ์šฐ, ๋ณด์œ  ํฌ์ธํŠธ๊ฐ€ ๋ฐ˜ํ™˜๋œ๋‹ค. +- [x] ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ, null ์ด ๋ฐ˜ํ™˜๋œ๋‹ค. **๐ŸŒ E2E ํ…Œ์ŠคํŠธ** -- [ ] ํฌ์ธํŠธ ์กฐํšŒ์— ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ๋ณด์œ  ํฌ์ธํŠธ๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค. -- [ ] `X-USER-ID` ํ—ค๋”๊ฐ€ ์—†์„ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. +- [x] ํฌ์ธํŠธ ์กฐํšŒ์— ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ๋ณด์œ  ํฌ์ธํŠธ๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค. +- [x] `X-USER-ID` ํ—ค๋”๊ฐ€ ์—†์„ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. ### ํฌ์ธํŠธ ์ถฉ์ „ **๐Ÿงฑ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ** -- [ ] 0 ์ดํ•˜์˜ ์ •์ˆ˜๋กœ ํฌ์ธํŠธ๋ฅผ ์ถฉ์ „ ์‹œ ์‹คํŒจํ•œ๋‹ค. +- [X] 0 ์ดํ•˜์˜ ์ •์ˆ˜๋กœ ํฌ์ธํŠธ๋ฅผ ์ถฉ์ „ ์‹œ ์‹คํŒจํ•œ๋‹ค. **๐Ÿ”— ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ** -- [ ] ์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ์ € ID ๋กœ ์ถฉ์ „์„ ์‹œ๋„ํ•œ ๊ฒฝ์šฐ, ์‹คํŒจํ•œ๋‹ค. +- [X] ์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ์ € ID ๋กœ ์ถฉ์ „์„ ์‹œ๋„ํ•œ ๊ฒฝ์šฐ, ์‹คํŒจํ•œ๋‹ค. **๐ŸŒ E2E ํ…Œ์ŠคํŠธ** -- [ ] ์กด์žฌํ•˜๋Š” ์œ ์ €๊ฐ€ 1000์›์„ ์ถฉ์ „ํ•  ๊ฒฝ์šฐ, ์ถฉ์ „๋œ ๋ณด์œ  ์ด๋Ÿ‰์„ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค. -- [ ] ์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ์ €๋กœ ์š”์ฒญํ•  ๊ฒฝ์šฐ, `404 Not Found` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. +- [X] ์กด์žฌํ•˜๋Š” ์œ ์ €๊ฐ€ 1000์›์„ ์ถฉ์ „ํ•  ๊ฒฝ์šฐ, ์ถฉ์ „๋œ ๋ณด์œ  ์ด๋Ÿ‰์„ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค. +- [X] ์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ์ €๋กœ ์š”์ฒญํ•  ๊ฒฝ์šฐ, `404 Not Found` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. ## โœ… Checklist -- [ ] ํ…Œ์ŠคํŠธ๋Š” ๋ชจ๋‘ ํ†ต๊ณผํ•ด์•ผ ํ•˜๋ฉฐ, `@Test` ๊ธฐ๋ฐ˜์œผ๋กœ ๋ช…์‹œ์ ์œผ๋กœ ์ž‘์„ฑ -- [ ] ๊ฐ ํ…Œ์ŠคํŠธ๋Š” ํ…Œ์ŠคํŠธ ๋ช…/์„ค๋ช…/์ž…๋ ฅ/์˜ˆ์ƒ ๊ฒฐ๊ณผ๊ฐ€ ๋ถ„๋ช…ํ•ด์•ผ ํ•จ -- [ ] E2E ํ…Œ์ŠคํŠธ๋Š” ์‹ค์ œ HTTP ์š”์ฒญ์„ ํ†ตํ•ด ํ๋ฆ„์„ ๊ฒ€์ฆํ•  ๊ฒƒ \ No newline at end of file +- [X] ํ…Œ์ŠคํŠธ๋Š” ๋ชจ๋‘ ํ†ต๊ณผํ•ด์•ผ ํ•˜๋ฉฐ, `@Test` ๊ธฐ๋ฐ˜์œผ๋กœ ๋ช…์‹œ์ ์œผ๋กœ ์ž‘์„ฑ +- [X] ๊ฐ ํ…Œ์ŠคํŠธ๋Š” ํ…Œ์ŠคํŠธ ๋ช…/์„ค๋ช…/์ž…๋ ฅ/์˜ˆ์ƒ ๊ฒฐ๊ณผ๊ฐ€ ๋ถ„๋ช…ํ•ด์•ผ ํ•จ +- [X] E2E ํ…Œ์ŠคํŠธ๋Š” ์‹ค์ œ HTTP ์š”์ฒญ์„ ํ†ตํ•ด ํ๋ฆ„์„ ๊ฒ€์ฆํ•  ๊ฒƒ \ No newline at end of file From ded9e381c05c545f59c24e8e9f0f4d90f7f4d980 Mon Sep 17 00:00:00 2001 From: adminhelper <201401339hs@gmail.com> Date: Fri, 31 Oct 2025 05:06:36 +0900 Subject: [PATCH 28/85] =?UTF-8?q?chore:=20=ED=8C=8C=EC=9D=BC=20=EC=9C=84?= =?UTF-8?q?=EC=B9=98=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../interfaces/api/{ => example}/ExampleV1ApiE2ETest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename apps/commerce-api/src/test/java/com/loopers/interfaces/api/{ => example}/ExampleV1ApiE2ETest.java (98%) diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/ExampleV1ApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/example/ExampleV1ApiE2ETest.java similarity index 98% rename from apps/commerce-api/src/test/java/com/loopers/interfaces/api/ExampleV1ApiE2ETest.java rename to apps/commerce-api/src/test/java/com/loopers/interfaces/api/example/ExampleV1ApiE2ETest.java index 1bb3dba65..70f256149 100644 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/ExampleV1ApiE2ETest.java +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/example/ExampleV1ApiE2ETest.java @@ -1,8 +1,8 @@ -package com.loopers.interfaces.api; +package com.loopers.interfaces.api.example; import com.loopers.domain.example.ExampleModel; import com.loopers.infrastructure.example.ExampleJpaRepository; -import com.loopers.interfaces.api.example.ExampleV1Dto; +import com.loopers.interfaces.api.ApiResponse; import com.loopers.utils.DatabaseCleanUp; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; From 5b83c0381587841ba3e14015d9233013c6d37122 Mon Sep 17 00:00:00 2001 From: adminhelper <201401339hs@gmail.com> Date: Fri, 31 Oct 2025 05:07:19 +0900 Subject: [PATCH 29/85] =?UTF-8?q?feat:=20=ED=8F=AC=EC=9D=B8=ED=8A=B8=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/point/PointFacade.java | 28 ++++++++++++++ .../loopers/application/point/PointInfo.java | 13 +++++++ .../java/com/loopers/domain/point/Point.java | 37 +++++++++++++++---- .../loopers/domain/point/PointRepository.java | 6 +++ .../loopers/domain/point/PointService.java | 24 ++++++++++++ .../point/PointJpaRepository.java | 4 ++ .../point/PointRepositoryImpl.java | 26 +++++++++++++ .../interfaces/api/point/PointV1ApiSpec.java | 27 ++++++++++++++ .../api/point/PointV1Controller.java | 31 ++++++++++++++++ .../interfaces/api/point/PointV1Dto.java | 17 +++++++++ 10 files changed, 205 insertions(+), 8 deletions(-) create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/point/PointInfo.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointRepositoryImpl.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1ApiSpec.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Controller.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Dto.java diff --git a/apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java new file mode 100644 index 000000000..009be1cec --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java @@ -0,0 +1,28 @@ +package com.loopers.application.point; + +import com.loopers.domain.point.Point; +import com.loopers.domain.point.PointService; +import com.loopers.interfaces.api.point.PointV1Dto; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class PointFacade { + private final PointService pointService; + + public PointInfo getPoint(String userId) { + Point point = pointService.findPointByUserId(userId); + + if (point == null) { + throw new CoreException(ErrorType.NOT_FOUND, "์‚ฌ์šฉ์ž ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); + } + return PointInfo.from(point); + } + + public PointInfo chargePoint(PointV1Dto.ChargePointRequest request) { + return PointInfo.from(pointService.chargePoint(request.userId(), request.chargeAmount())); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/point/PointInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/point/PointInfo.java new file mode 100644 index 000000000..2c357dc7e --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/point/PointInfo.java @@ -0,0 +1,13 @@ +package com.loopers.application.point; + +import com.loopers.domain.point.Point; + +public record PointInfo(String userId, Long amount) { + public static PointInfo from(Point info) { + return new PointInfo( + info.getUserId(), + info.getAmount() + ); + } + +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java b/apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java index 26266d141..bef72fb00 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java @@ -1,20 +1,41 @@ package com.loopers.domain.point; - import com.loopers.domain.BaseEntity; -import com.loopers.domain.user.User; -import jakarta.persistence.*; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import lombok.Getter; @Entity @Table(name = "point") +@Getter public class Point extends BaseEntity { - @OneToOne(fetch = FetchType.LAZY, optional = false) - @JoinColumn(name = "user_id", unique = true, nullable = false) - private User user; + private String userId; + + private Long amount; + + protected Point() {} + + public Point(String userId, Long amount) { + this.userId = requireValidUserId(userId); + this.amount = amount; + } - @Column(nullable = false) - private Long Amount; + String requireValidUserId(String userId) { + if(userId == null || userId.isEmpty()) { + throw new CoreException(ErrorType.BAD_REQUEST, "์‚ฌ์šฉ์ž ID๊ฐ€ ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + return userId; + } + public Point charge(Long chargeAmount) { + if (chargeAmount == null || chargeAmount <= 0) { + throw new CoreException(ErrorType.BAD_REQUEST, "0์› ์ดํ•˜๋กœ ํฌ์ธํŠธ๋ฅผ ์ถฉ์ „ ํ• ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + this.amount += chargeAmount; + return new Point(this.userId, this.amount); + } } diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/point/PointRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/point/PointRepository.java index 0a50068ca..314022491 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/point/PointRepository.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/point/PointRepository.java @@ -1,4 +1,10 @@ package com.loopers.domain.point; +import java.util.Optional; + public interface PointRepository { + + Optional findByUserId(String userId); + + Point save(Point point); } diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java b/apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java index 03807f888..2ecc7b456 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java @@ -1,6 +1,30 @@ package com.loopers.domain.point; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Component public class PointService { + private final PointRepository pointRepository; + + + @Transactional(readOnly = true) + public Point findPointByUserId(String userId) { + return pointRepository.findByUserId(userId).orElse(null); + } + + @Transactional + public Point chargePoint(String userId, Long chargeAmount) { + Point point = pointRepository.findByUserId(userId) + .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "ํฌ์ธํŠธ๋ฅผ ์ถฉ์ „ํ• ์ˆ˜ ์—†๋Š” ์‚ฌ์šฉ์ž์ž…๋‹ˆ๋‹ค.")); + point.charge(chargeAmount); + return pointRepository.save(point); + } + } diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointJpaRepository.java index aa1089a9e..a35a56151 100644 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointJpaRepository.java +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointJpaRepository.java @@ -3,5 +3,9 @@ import com.loopers.domain.point.Point; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + public interface PointJpaRepository extends JpaRepository { + + Optional findByUserId(String userId); } diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointRepositoryImpl.java new file mode 100644 index 000000000..1663b1d4f --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointRepositoryImpl.java @@ -0,0 +1,26 @@ +package com.loopers.infrastructure.point; + +import com.loopers.domain.point.Point; +import com.loopers.domain.point.PointRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +@RequiredArgsConstructor +@Component +public class PointRepositoryImpl implements PointRepository { + + private final PointJpaRepository pointJpaRepository; + + @Override + public Optional findByUserId(String userId) { + return pointJpaRepository.findByUserId(userId); + } + + @Override + public Point save(Point point) { + pointJpaRepository.save(point); + return point; + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1ApiSpec.java new file mode 100644 index 000000000..b954c1a4e --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1ApiSpec.java @@ -0,0 +1,27 @@ +package com.loopers.interfaces.api.point; + +import com.loopers.interfaces.api.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.tags.Tag; +@Tag(name = "Point V1 API", description = "Point API ์ž…๋‹ˆ๋‹ค.") +public interface PointV1ApiSpec { + + @Operation( + summary = "ํฌ์ธํŠธ ํšŒ์› ์กฐํšŒ", + description = "ํšŒ์›์˜ ํฌ์ธํŠธ๋ฅผ ์กฐํšŒํ•œ๋‹ค." + ) + ApiResponse getPoint( + @Schema(name = "ํšŒ์› Id", description = "์กฐํšŒํ•  ํšŒ์› ID") + String userId + ); + + @Operation( + summary = "ํฌ์ธํŠธ ์ถฉ์ „", + description = "ํšŒ์›์˜ ํฌ์ธํŠธ๋ฅผ ์ถฉ์ „ํ•œ๋‹ค." + ) + ApiResponse chargePoint( + @Schema(name = "ํฌ์ธํŠธ ์ถฉ์ „ ์š”์ฒญ", description = "์กฐํšŒํ•  ํšŒ์› ID") + PointV1Dto.ChargePointRequest request + ); +} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Controller.java new file mode 100644 index 000000000..866fce9b3 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Controller.java @@ -0,0 +1,31 @@ +package com.loopers.interfaces.api.point; + +import com.loopers.application.point.PointFacade; +import com.loopers.application.point.PointInfo; +import com.loopers.interfaces.api.ApiResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/v1/points") +public class PointV1Controller implements PointV1ApiSpec { + + private final PointFacade pointFacade; + + @Override + @GetMapping + public ApiResponse getPoint(@RequestHeader("X-USER-ID") String userId) { + PointInfo pointInfo = pointFacade.getPoint(userId); + PointV1Dto.PointResponse response = PointV1Dto.PointResponse.from(pointInfo); + return ApiResponse.success(response); + } + + @Override + @PatchMapping("/charge") + public ApiResponse chargePoint(@RequestBody PointV1Dto.ChargePointRequest request) { + PointInfo pointInfo = pointFacade.chargePoint(request); + PointV1Dto.PointResponse response = PointV1Dto.PointResponse.from(pointInfo); + return ApiResponse.success(response); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Dto.java new file mode 100644 index 000000000..406e84cc5 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Dto.java @@ -0,0 +1,17 @@ +package com.loopers.interfaces.api.point; + +import com.loopers.application.point.PointInfo; + +public class PointV1Dto { + + public record ChargePointRequest(String userId, Long chargeAmount) {} + + public record PointResponse(String userId, Long amount){ + public static PointResponse from(PointInfo info) { + return new PointResponse( + info.userId(), + info.amount() + ); + } + } +} From 3158c2bce49c3e9a6cea90cc499c54688ecb169a Mon Sep 17 00:00:00 2001 From: adminhelper <201401339hs@gmail.com> Date: Fri, 31 Oct 2025 05:07:37 +0900 Subject: [PATCH 30/85] =?UTF-8?q?test:=20=ED=8F=AC=EC=9D=B8=ED=8A=B8=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=20=EB=8B=A8=EC=9C=84=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/loopers/domain/point/PointTest.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/point/PointTest.java diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/point/PointTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/point/PointTest.java new file mode 100644 index 000000000..bcc911321 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/point/PointTest.java @@ -0,0 +1,23 @@ +package com.loopers.domain.point; + +import com.loopers.support.error.CoreException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +class PointTest { + @DisplayName("Point ๋‹จ์œ„ ํ…Œ์ŠคํŠธ") + @Nested + class Charge { + @DisplayName("0 ์ดํ•˜์˜ ์ •์ˆ˜๋กœ ํฌ์ธํŠธ๋ฅผ ์ถฉ์ „ ์‹œ ์‹คํŒจํ•œ๋‹ค.") + @Test + void throwsChargeAmountFailException_whenZeroAmountOrNegative() { + Point point = new Point("yh45g", 0L); + assertThrows(CoreException.class, () -> + point.charge(0L)); + } + } + +} From e287600296c6797ca187820d7036f229713cc969 Mon Sep 17 00:00:00 2001 From: adminhelper <201401339hs@gmail.com> Date: Fri, 31 Oct 2025 05:07:42 +0900 Subject: [PATCH 31/85] =?UTF-8?q?test:=20=ED=8F=AC=EC=9D=B8=ED=8A=B8=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=20=ED=86=B5=ED=95=A9=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../point/PointServiceIntegrationTest.java | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java new file mode 100644 index 000000000..544db751b --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java @@ -0,0 +1,86 @@ +package com.loopers.domain.point; + +import com.loopers.domain.user.User; +import com.loopers.domain.user.UserRepository; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import com.loopers.utils.DatabaseCleanUp; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@SpringBootTest +class PointServiceIntegrationTest { + + @Autowired + private PointRepository pointRepository; + + @Autowired + private UserRepository userRepository; + + @Autowired + private PointService pointService; + + @Autowired + private DatabaseCleanUp databaseCleanUp; + + @AfterEach + void tearDown() { + databaseCleanUp.truncateAllTables(); + } + + @DisplayName("ํฌ์ธํŠธ ์กฐํšŒ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ") + @Nested + class PointUser { + + @DisplayName("ํ•ด๋‹น ID์˜ ํšŒ์›์ด ์กด์žฌํ•  ๊ฒฝ์šฐ, ๋ณด์œ  ํฌ์ธํŠธ๊ฐ€ ๋ฐ˜ํ™˜๋œ๋‹ค.") + @Test + void returnPointInfo_whenValidIdIsProvided() { + String id = "yh45g"; + String email = "yh45g@loopers.com"; + String birth = "1994-12-05"; + String gender = "MALE"; + + userRepository.save(new User(id, email, birth, gender)); + pointRepository.save(new Point(id, 0L)); + + Point result = pointService.findPointByUserId(id); + + assertThat(result.getUserId()).isEqualTo(id); + assertThat(result.getAmount()).isEqualTo(0L); + } + + @DisplayName("ํšŒ์›์ด ์กด์žฌ ํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ, null ์ด ๋ฐ˜ํ™˜๋œ๋‹ค.") + @Test + void returnNull_whenInvalidUserIdIsProvided() { + String id = "yh45g"; + + Point point = pointService.findPointByUserId(id); + + assertThat(point).isNull(); + } + } + + @DisplayName("ํฌ์ธํŠธ ์ถฉ์ „ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ") + @Nested + class Charge { + + @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ์ € ID ๋กœ ์ถฉ์ „์„ ์‹œ๋„ํ•œ ๊ฒฝ์šฐ, ์‹คํŒจํ•œ๋‹ค.") + @Test + void throwsChargeAmountFailException_whenUserIDIsNotProvided() { + String id = "yh45g"; + + CoreException exception = assertThrows(CoreException.class, () -> { + pointService.chargePoint(id, 1000L); + }); + + assertThat(exception.getErrorType()).isEqualTo(ErrorType.NOT_FOUND); + } + } +} From 9c5e6ea939ed49903969a5743b4c79cfe55e5df8 Mon Sep 17 00:00:00 2001 From: adminhelper <201401339hs@gmail.com> Date: Fri, 31 Oct 2025 05:07:49 +0900 Subject: [PATCH 32/85] =?UTF-8?q?test:=20=ED=8F=AC=EC=9D=B8=ED=8A=B8=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=20E2E=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/point/PointV1ControllerTest.java | 140 ++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 apps/commerce-api/src/test/java/com/loopers/interfaces/api/point/PointV1ControllerTest.java diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/point/PointV1ControllerTest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/point/PointV1ControllerTest.java new file mode 100644 index 000000000..8e8507f72 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/point/PointV1ControllerTest.java @@ -0,0 +1,140 @@ +package com.loopers.interfaces.api.point; + +import com.loopers.domain.point.Point; +import com.loopers.domain.point.PointRepository; +import com.loopers.domain.user.User; +import com.loopers.domain.user.UserRepository; +import com.loopers.interfaces.api.ApiResponse; +import com.loopers.utils.DatabaseCleanUp; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.*; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class PointV1ControllerTest { + + private static final String GET_USER_POINT_ENDPOINT = "/api/v1/points"; + private static final String POST_USER_POINT_ENDPOINT = "/api/v1/points/charge"; + + @Autowired + private PointRepository pointRepository; + + @Autowired + private UserRepository userRepository; + + @Autowired + private DatabaseCleanUp databaseCleanUp; + + @Autowired + private TestRestTemplate testRestTemplate; + + @AfterEach + void tearDown() { + databaseCleanUp.truncateAllTables(); + } + + @DisplayName("GET /api/v1/points") + @Nested + class UserPoint { + + @DisplayName("ํฌ์ธํŠธ ์กฐํšŒ์— ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ๋ณด์œ  ํฌ์ธํŠธ๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnPoint_whenValidUserIdIsProvided() { + String id = "yh45g"; + String email = "yh45g@loopers.com"; + String birth = "1994-12-05"; + String gender = "MALE"; + Long amount = 1000L; + + userRepository.save(new User(id, email, birth, gender)); + pointRepository.save(new Point(id, amount)); + + HttpHeaders headers = new HttpHeaders(); + headers.add("X-USER-ID", id); + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; + ResponseEntity> response = + testRestTemplate.exchange(GET_USER_POINT_ENDPOINT, HttpMethod.GET, new HttpEntity<>(null, headers), responseType); + + assertAll( + () -> assertTrue(response.getStatusCode().is2xxSuccessful()), + () -> assertThat(response.getBody().data().userId()).isEqualTo(id), + () -> assertThat(response.getBody().data().amount()).isEqualTo(1000L) + ); + } + + @DisplayName("ํ•ด๋‹น ID์˜ ํšŒ์›์ด ์กด์žฌํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ, null์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnNull_whenUserIdExists() { + String id = "yh45g"; + + HttpHeaders headers = new HttpHeaders(); + headers.add("X-USER-ID", id); + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; + ResponseEntity> response = + testRestTemplate.exchange(GET_USER_POINT_ENDPOINT, HttpMethod.GET, new HttpEntity<>(null, headers), responseType); + + assertAll( + () -> assertTrue(response.getStatusCode().is4xxClientError()), + () -> assertThat(response.getBody().data()).isNull() + ); + } + } + + @DisplayName("POST /api/v1/points/charge") + @Nested + class Charge { + + @DisplayName("์กด์žฌํ•˜๋Š” ์œ ์ €๊ฐ€ 1000์›์„ ์ถฉ์ „ํ•  ๊ฒฝ์šฐ, ์ถฉ์ „๋œ ๋ณด์œ  ์ด๋Ÿ‰์„ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnsTotalPoint_whenChargeUserPoint() { + String id = "yh45g"; + String email = "yh45g@loopers.com"; + String birth = "1994-12-05"; + String gender = "MALE"; + + userRepository.save(new User(id, email, birth, gender)); + pointRepository.save(new Point(id, 0L)); + + PointV1Dto.ChargePointRequest request = new PointV1Dto.ChargePointRequest(id, 1000L); + + HttpHeaders headers = new HttpHeaders(); + headers.add("X-USER-ID", id); + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; + ResponseEntity> response = + testRestTemplate.exchange(POST_USER_POINT_ENDPOINT, HttpMethod.PATCH, new HttpEntity<>(request, headers), responseType); + + assertAll( + () -> assertTrue(response.getStatusCode().is2xxSuccessful()), + () -> assertThat(response.getBody().data().userId()).isEqualTo(id), + () -> assertThat(response.getBody().data().amount()).isEqualTo(1000L) + ); + } + + @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ์ €๋กœ ์š”์ฒญํ•  ๊ฒฝ์šฐ, 404 Not Found ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void throwsException_whenInvalidUserIdIsProvided() { + String id = "yh45g"; + + HttpHeaders headers = new HttpHeaders(); + headers.add("X-USER-ID", id); + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; + ResponseEntity> response = + testRestTemplate.exchange(POST_USER_POINT_ENDPOINT, HttpMethod.PATCH, new HttpEntity<>(null, headers), responseType); + + assertAll( + () -> assertTrue(response.getStatusCode().is4xxClientError()), + () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND) + ); + } + } +} From 3774002312fc9fddcc417b938e19c46731f1591a Mon Sep 17 00:00:00 2001 From: adminhelper <201401339hs@gmail.com> Date: Fri, 31 Oct 2025 05:08:22 +0900 Subject: [PATCH 33/85] =?UTF-8?q?refactor:=20=ED=9A=8C=EC=9B=90=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../loopers/application/user/UserFacade.java | 8 ++- .../com/loopers/domain/user/UserService.java | 6 +- .../user/UserJpaRepository.java | 1 + .../interfaces/api/user/UserV1ApiSpec.java | 2 +- .../user/UserServiceIntegrationTest.java | 40 +++++++++++- .../com/loopers/domain/user/UserTest.java | 61 +++++++++++++++---- .../api/user/UserV1ControllerTest.java | 37 +++++------ 7 files changed, 114 insertions(+), 41 deletions(-) diff --git a/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java index 4df3eff79..f42bd5206 100644 --- a/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java @@ -18,8 +18,10 @@ public UserInfo register(String userId, String email, String birth, String gende } public UserInfo getUser(String userId) { - return userService.findUserByUserId(userId) - .map(UserInfo::from) - .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "ํšŒ์› ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); + User user = userService.findUserByUserId(userId); + if (user == null) { + throw new CoreException(ErrorType.NOT_FOUND, "์‚ฌ์šฉ์ž ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); + } + return UserInfo.from(user); } } diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java index 8dcf999e8..db386e1e8 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java @@ -6,8 +6,6 @@ import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; -import java.util.Optional; - @Component @RequiredArgsConstructor public class UserService { @@ -25,8 +23,8 @@ public User register(String userId, String email, String birth, String gender) { } @Transactional(readOnly = true) - public Optional findUserByUserId(String userId){ - return userRepository.findByUserId(userId); + public User findUserByUserId(String userId){ + return userRepository.findByUserId(userId).orElse(null); } } diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserJpaRepository.java index 1a78b9a69..f80a5bc52 100644 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserJpaRepository.java +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserJpaRepository.java @@ -6,5 +6,6 @@ import java.util.Optional; public interface UserJpaRepository extends JpaRepository { + Optional findByUserId(String userId); } diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1ApiSpec.java index dec948cdf..235e8e9fa 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1ApiSpec.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1ApiSpec.java @@ -13,7 +13,7 @@ public interface UserV1ApiSpec { description = "ํšŒ์› ๊ฐ€์ž…์„ ํ•ฉ๋‹ˆ๋‹ค." ) ApiResponse register( - @Schema(description = "ํšŒ์›๊ฐ€์ž…") + @Schema(name = "ํšŒ์› ๊ฐ€์ž… ์š”์ฒญ", description = "ํšŒ์›๊ฐ€์ž…") UserV1Dto.RegisterRequest request ); diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java index edcfca9a9..c342c8aa7 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java @@ -6,6 +6,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; @@ -27,7 +29,7 @@ void tearDown() { databaseCleanUp.truncateAllTables(); } - @DisplayName("ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ") + @DisplayName("ํšŒ์› ๊ฐ€์ž… ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ") @Nested class UserRegister { @@ -48,7 +50,7 @@ void save_whenUserRegister() { @DisplayName("์ด๋ฏธ ๊ฐ€์ž…๋œ ID ๋กœ ํšŒ์›๊ฐ€์ž… ์‹œ๋„ ์‹œ, ์‹คํŒจํ•œ๋‹ค.") @Test - void register_whenUserIdAlreadyExists_thenFail() { + void throwsException_whenDuplicateUserId() { String userId = "yh45g"; String email = "yh45g@loopers.com"; String brith = "1994-12-05"; @@ -60,4 +62,38 @@ void register_whenUserIdAlreadyExists_thenFail() { -> userService.register(userId, email, brith, gender)); } } + + @DisplayName("๋‚ด ์ •๋ณด ์กฐํšŒ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ") + @Nested + class Get { + + @DisplayName("ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•  ๊ฒฝ์šฐ, ํšŒ์› ์ •๋ณด๊ฐ€ ๋ฐ˜ํ™˜๋œ๋‹ค.") + @Test + void returnsUser_whenValidIdIsProvided() { + String userId = "yh45g"; + String email = "yh45g@loopers.com"; + String brith = "1994-12-05"; + String gender = "Male"; + + userService.register(userId, email, brith, gender); + + User user = userService.findUserByUserId(userId); + + assertAll( + () -> assertThat(user.getUserId()).isEqualTo(userId), + () -> assertThat(user.getEmail()).isEqualTo(email), + () -> assertThat(user.getBirth()).isEqualTo(brith), + () -> assertThat(user.getGender()).isEqualTo(gender) + ); + } + + @DisplayName("ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ, null ์ด ๋ฐ˜ํ™˜๋œ๋‹ค.") + @Test + void returnNull_whenInvalidUserIdIsProvided() { + String userId = "yh45g"; + User user = userService.findUserByUserId(userId); + + assertThat(user).isNull(); + } + } } diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserTest.java index 1d938accc..a8f8948ca 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserTest.java @@ -1,18 +1,53 @@ package com.loopers.domain.user; -import static org.junit.jupiter.api.Assertions.*; - -/** - * packageName : com.loopers.domain.user - * fileName : UserTest - * author : byeonsungmun - * date : 25. 10. 28. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 25. 10. 28. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ +import com.loopers.support.error.CoreException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertThrows; + class UserTest { + @DisplayName("User ๋‹จ์œ„ ํ…Œ์ŠคํŠธ") + @Nested + class Create { + @DisplayName("ID ๊ฐ€ ์˜๋ฌธ ๋ฐ ์ˆซ์ž 10์ž ์ด๋‚ด ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค.") + @Test + void throwsException_whenInvalidUserIdFormat() { + // given + String invalidUserId = "invalid_id_123"; // 10์ž ์ดˆ๊ณผ + ํŠน์ˆ˜๋ฌธ์ž ํฌํ•จ + String email = "valid@loopers.com"; + String birth = "1994-12-05"; + String gender = "MALE"; + + // when & then + assertThrows(CoreException.class, () -> new User(invalidUserId, email, birth, gender)); + } + + @DisplayName("์ด๋ฉ”์ผ์ด `xx@yy.zz` ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค.") + @Test + void throwsException_whenInvalidEmailFormat() { + // given + String userId = "yh45g"; + String invalidEmail = "invalid-email-format"; // '@' ์—†์Œ + String birth = "1994-12-05"; + String gender = "MALE"; + + // when & then + assertThrows(CoreException.class, () -> new User(userId, invalidEmail, birth, gender)); + } + + @DisplayName("์ƒ๋…„์›”์ผ์ด `yyyy-MM-dd` ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค.") + @Test + void throwsException_whenInvalidBirthFormat() { + // given + String userId = "yh45g"; + String invalidEmail = "invalid-email-format"; // '@' ์—†์Œ + String birth = "1994-12-05"; + String gender = "MALE"; + // when & then + assertThrows(CoreException.class, () -> new User(userId, invalidEmail, birth, gender)); + } + } } diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/user/UserV1ControllerTest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/user/UserV1ControllerTest.java index 5a297152b..b99da8b4e 100644 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/user/UserV1ControllerTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/user/UserV1ControllerTest.java @@ -12,7 +12,10 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.core.ParameterizedTypeReference; -import org.springframework.http.*; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import java.util.function.Function; @@ -23,18 +26,17 @@ @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class UserV1ControllerTest { - private static final String REGISTER_ENDPOINT = "/api/v1/users/register"; - private static final Function GETUSER_ENDPOINT = id -> "/api/v1/users/" + id; - private final TestRestTemplate testRestTemplate; - private final UserJpaRepository userJpaRepository; - private final DatabaseCleanUp databaseCleanUp; + private static final String USER_REGISTER_ENDPOINT = "/api/v1/users/register"; + private static final Function GET_USER_ENDPOINT = id -> "/api/v1/users/" + id; @Autowired - public UserV1ControllerTest(TestRestTemplate testRestTemplate, UserJpaRepository userJpaRepository, DatabaseCleanUp databaseCleanUp) { - this.testRestTemplate = testRestTemplate; - this.userJpaRepository = userJpaRepository; - this.databaseCleanUp = databaseCleanUp; - } + private TestRestTemplate testRestTemplate; + + @Autowired + private UserJpaRepository userJpaRepository; + + @Autowired + private DatabaseCleanUp databaseCleanUp; @AfterEach void tearDown() { @@ -57,7 +59,7 @@ void registerUser_whenSuccessResponseUser() { ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; ResponseEntity> response = - testRestTemplate.exchange(REGISTER_ENDPOINT, HttpMethod.POST, new HttpEntity<>(request), responseType); + testRestTemplate.exchange(USER_REGISTER_ENDPOINT, HttpMethod.POST, new HttpEntity<>(request), responseType); assertAll( () -> assertTrue(response.getStatusCode().is2xxSuccessful()), @@ -79,7 +81,7 @@ void throwsBadRequest_whenGenderIsNotProvided() { ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; ResponseEntity> response = - testRestTemplate.exchange(REGISTER_ENDPOINT, HttpMethod.POST, new HttpEntity<>(request), responseType); + testRestTemplate.exchange(USER_REGISTER_ENDPOINT, HttpMethod.POST, new HttpEntity<>(request), responseType); assertAll( () -> assertTrue(response.getStatusCode().is4xxClientError()), @@ -91,7 +93,7 @@ void throwsBadRequest_whenGenderIsNotProvided() { @DisplayName("GET /api/v1/users/{userId}") @Nested class GetUserById { - @DisplayName("ํ•ด๋‹น ID์˜ ํšŒ์›์ด ์กด์žฌํ•  ๊ฒฝ์šฐ, ํšŒ์› ์ •๋ณด๊ฐ€ ๋ฐ˜ํ™˜๋œ๋‹ค.") + @DisplayName("๋‚ด ์ •๋ณด ์กฐํšŒ์— ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ํ•ด๋‹นํ•˜๋Š” ์œ ์ € ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") @Test void getUserById_whenSuccessResponseUser() { String userId = "yh45g"; @@ -101,7 +103,7 @@ void getUserById_whenSuccessResponseUser() { userJpaRepository.save(new User(userId, email, birth, gender)); - String requestUrl = GETUSER_ENDPOINT.apply(userId); + String requestUrl = GET_USER_ENDPOINT.apply(userId); ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; ResponseEntity> response = testRestTemplate.exchange(requestUrl, HttpMethod.GET, new HttpEntity<>(null), responseType); @@ -115,11 +117,11 @@ void getUserById_whenSuccessResponseUser() { ); } - @DisplayName("ํ•ด๋‹น ID์˜ ํšŒ์›์ด ์กด์žฌํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ, null ์ด ๋ฐ˜ํ™˜๋œ๋‹ค.") + @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ID ๋กœ ์กฐํšŒํ•  ๊ฒฝ์šฐ, 404 Not Found ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") @Test void throwsException_whenInvalidUserIdIsProvided() { String userId = "notUserId"; - String requestUrl = GETUSER_ENDPOINT.apply(userId); + String requestUrl = GET_USER_ENDPOINT.apply(userId); ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; ResponseEntity> response = testRestTemplate.exchange(requestUrl, HttpMethod.GET, new HttpEntity<>(null), responseType); @@ -130,5 +132,4 @@ void throwsException_whenInvalidUserIdIsProvided() { ); } } - } From 8e5643aa0e3276c4028fb05abe30fa9108966f84 Mon Sep 17 00:00:00 2001 From: adminhelper <201401339hs@gmail.com> Date: Fri, 31 Oct 2025 05:10:30 +0900 Subject: [PATCH 34/85] =?UTF-8?q?chore:=20=ED=9A=8C=EC=9B=90=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/loopers/domain/point/Point.java | 4 ++-- .../com/loopers/domain/point/PointServiceIntegrationTest.java | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java b/apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java index bef72fb00..02e1f9f7f 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java @@ -30,12 +30,12 @@ String requireValidUserId(String userId) { return userId; } - public Point charge(Long chargeAmount) { + public void charge(Long chargeAmount) { if (chargeAmount == null || chargeAmount <= 0) { throw new CoreException(ErrorType.BAD_REQUEST, "0์› ์ดํ•˜๋กœ ํฌ์ธํŠธ๋ฅผ ์ถฉ์ „ ํ• ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); } this.amount += chargeAmount; - return new Point(this.userId, this.amount); + new Point(this.userId, this.amount); } } diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java index 544db751b..2ba880463 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java @@ -76,9 +76,7 @@ class Charge { void throwsChargeAmountFailException_whenUserIDIsNotProvided() { String id = "yh45g"; - CoreException exception = assertThrows(CoreException.class, () -> { - pointService.chargePoint(id, 1000L); - }); + CoreException exception = assertThrows(CoreException.class, () -> pointService.chargePoint(id, 1000L)); assertThat(exception.getErrorType()).isEqualTo(ErrorType.NOT_FOUND); } From ef3eebdce67a2a6566c5636de732acb9778095fd Mon Sep 17 00:00:00 2001 From: bookers-web Date: Fri, 31 Oct 2025 15:26:04 +0900 Subject: [PATCH 35/85] =?UTF-8?q?style:=20=EC=BD=94=EB=93=9C=20=EC=9E=90?= =?UTF-8?q?=EB=8F=99=EC=A0=95=EB=A0=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/loopers/domain/point/Point.java | 1 - .../main/java/com/loopers/domain/point/PointService.java | 6 +----- .../main/java/com/loopers/domain/user/UserService.java | 2 +- .../com/loopers/interfaces/api/point/PointV1ApiSpec.java | 1 + .../java/com/loopers/interfaces/api/point/PointV1Dto.java | 5 +++-- .../com/loopers/interfaces/api/user/UserV1ApiSpec.java | 8 ++++---- .../java/com/loopers/interfaces/api/user/UserV1Dto.java | 3 ++- 7 files changed, 12 insertions(+), 14 deletions(-) diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java b/apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java index 02e1f9f7f..b8f453f14 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java @@ -37,5 +37,4 @@ public void charge(Long chargeAmount) { this.amount += chargeAmount; new Point(this.userId, this.amount); } - } diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java b/apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java index 2ecc7b456..dfc0788c6 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java @@ -12,7 +12,6 @@ public class PointService { private final PointRepository pointRepository; - @Transactional(readOnly = true) public Point findPointByUserId(String userId) { return pointRepository.findByUserId(userId).orElse(null); @@ -20,11 +19,8 @@ public Point findPointByUserId(String userId) { @Transactional public Point chargePoint(String userId, Long chargeAmount) { - Point point = pointRepository.findByUserId(userId) - .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "ํฌ์ธํŠธ๋ฅผ ์ถฉ์ „ํ• ์ˆ˜ ์—†๋Š” ์‚ฌ์šฉ์ž์ž…๋‹ˆ๋‹ค.")); + Point point = pointRepository.findByUserId(userId).orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "ํฌ์ธํŠธ๋ฅผ ์ถฉ์ „ํ• ์ˆ˜ ์—†๋Š” ์‚ฌ์šฉ์ž์ž…๋‹ˆ๋‹ค.")); point.charge(chargeAmount); return pointRepository.save(point); } - - } diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java index db386e1e8..da2878300 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java @@ -23,7 +23,7 @@ public User register(String userId, String email, String birth, String gender) { } @Transactional(readOnly = true) - public User findUserByUserId(String userId){ + public User findUserByUserId(String userId) { return userRepository.findByUserId(userId).orElse(null); } diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1ApiSpec.java index b954c1a4e..faa21f303 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1ApiSpec.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1ApiSpec.java @@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.tags.Tag; + @Tag(name = "Point V1 API", description = "Point API ์ž…๋‹ˆ๋‹ค.") public interface PointV1ApiSpec { diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Dto.java index 406e84cc5..b0b3d050e 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Dto.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Dto.java @@ -4,9 +4,10 @@ public class PointV1Dto { - public record ChargePointRequest(String userId, Long chargeAmount) {} + public record ChargePointRequest(String userId, Long chargeAmount) { + } - public record PointResponse(String userId, Long amount){ + public record PointResponse(String userId, Long amount) { public static PointResponse from(PointInfo info) { return new PointResponse( info.userId(), diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1ApiSpec.java index 235e8e9fa..1bed68e62 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1ApiSpec.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1ApiSpec.java @@ -13,8 +13,8 @@ public interface UserV1ApiSpec { description = "ํšŒ์› ๊ฐ€์ž…์„ ํ•ฉ๋‹ˆ๋‹ค." ) ApiResponse register( - @Schema(name = "ํšŒ์› ๊ฐ€์ž… ์š”์ฒญ", description = "ํšŒ์›๊ฐ€์ž…") - UserV1Dto.RegisterRequest request + @Schema(name = "ํšŒ์› ๊ฐ€์ž… ์š”์ฒญ", description = "ํšŒ์›๊ฐ€์ž…") + UserV1Dto.RegisterRequest request ); @Operation( @@ -22,7 +22,7 @@ ApiResponse register( description = "ํ•ด๋‹น ํšŒ์›์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค." ) ApiResponse getUser( - @Schema(name = "ํšŒ์› ID", description = "์กฐํšŒํ•  ํšŒ์› ID") - String userId + @Schema(name = "ํšŒ์› ID", description = "์กฐํšŒํ•  ํšŒ์› ID") + String userId ); } diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java index 16aab05e2..263214848 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java @@ -8,7 +8,8 @@ public record RegisterRequest( String mail, String birth, String gender - ){} + ) { + } public record UserResponse(String userId, String email, String birth, String gender) { public static UserResponse from(UserInfo info) { From d7386544e6f8468272cd5be72e14007ee2eaa32e Mon Sep 17 00:00:00 2001 From: bookers-web Date: Fri, 31 Oct 2025 15:46:55 +0900 Subject: [PATCH 36/85] =?UTF-8?q?remove:=20sample=20code=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/example/ExampleFacade.java | 17 --- .../application/example/ExampleInfo.java | 13 -- .../loopers/domain/example/ExampleModel.java | 44 ------- .../domain/example/ExampleRepository.java | 7 -- .../domain/example/ExampleService.java | 20 --- .../example/ExampleJpaRepository.java | 6 - .../example/ExampleRepositoryImpl.java | 19 --- .../api/example/ExampleV1ApiSpec.java | 19 --- .../api/example/ExampleV1Controller.java | 28 ----- .../interfaces/api/example/ExampleV1Dto.java | 15 --- .../domain/example/ExampleModelTest.java | 65 ---------- .../ExampleServiceIntegrationTest.java | 72 ----------- .../api/example/ExampleV1ApiE2ETest.java | 114 ------------------ 13 files changed, 439 deletions(-) delete mode 100644 apps/commerce-api/src/main/java/com/loopers/application/example/ExampleFacade.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/application/example/ExampleInfo.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleModel.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleRepository.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleService.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/example/ExampleJpaRepository.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/example/ExampleRepositoryImpl.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1ApiSpec.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1Controller.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1Dto.java delete mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/example/ExampleModelTest.java delete mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/example/ExampleServiceIntegrationTest.java delete mode 100644 apps/commerce-api/src/test/java/com/loopers/interfaces/api/example/ExampleV1ApiE2ETest.java diff --git a/apps/commerce-api/src/main/java/com/loopers/application/example/ExampleFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/example/ExampleFacade.java deleted file mode 100644 index 552a9ad62..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/application/example/ExampleFacade.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.loopers.application.example; - -import com.loopers.domain.example.ExampleModel; -import com.loopers.domain.example.ExampleService; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; - -@RequiredArgsConstructor -@Component -public class ExampleFacade { - private final ExampleService exampleService; - - public ExampleInfo getExample(Long id) { - ExampleModel example = exampleService.getExample(id); - return ExampleInfo.from(example); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/example/ExampleInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/example/ExampleInfo.java deleted file mode 100644 index 877aba96c..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/application/example/ExampleInfo.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.loopers.application.example; - -import com.loopers.domain.example.ExampleModel; - -public record ExampleInfo(Long id, String name, String description) { - public static ExampleInfo from(ExampleModel model) { - return new ExampleInfo( - model.getId(), - model.getName(), - model.getDescription() - ); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleModel.java b/apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleModel.java deleted file mode 100644 index c588c4a8a..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleModel.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.loopers.domain.example; - -import com.loopers.domain.BaseEntity; -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import jakarta.persistence.Entity; -import jakarta.persistence.Table; - -@Entity -@Table(name = "example") -public class ExampleModel extends BaseEntity { - - private String name; - private String description; - - protected ExampleModel() {} - - public ExampleModel(String name, String description) { - if (name == null || name.isBlank()) { - throw new CoreException(ErrorType.BAD_REQUEST, "์ด๋ฆ„์€ ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); - } - if (description == null || description.isBlank()) { - throw new CoreException(ErrorType.BAD_REQUEST, "์„ค๋ช…์€ ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); - } - - this.name = name; - this.description = description; - } - - public String getName() { - return name; - } - - public String getDescription() { - return description; - } - - public void update(String newDescription) { - if (newDescription == null || newDescription.isBlank()) { - throw new CoreException(ErrorType.BAD_REQUEST, "์„ค๋ช…์€ ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); - } - this.description = newDescription; - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleRepository.java deleted file mode 100644 index 3625e5662..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.loopers.domain.example; - -import java.util.Optional; - -public interface ExampleRepository { - Optional find(Long id); -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleService.java b/apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleService.java deleted file mode 100644 index c0e8431e8..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleService.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.loopers.domain.example; - -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; - -@RequiredArgsConstructor -@Component -public class ExampleService { - - private final ExampleRepository exampleRepository; - - @Transactional(readOnly = true) - public ExampleModel getExample(Long id) { - return exampleRepository.find(id) - .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "[id = " + id + "] ์˜ˆ์‹œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/example/ExampleJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/example/ExampleJpaRepository.java deleted file mode 100644 index ce6d3ead0..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/example/ExampleJpaRepository.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.loopers.infrastructure.example; - -import com.loopers.domain.example.ExampleModel; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface ExampleJpaRepository extends JpaRepository {} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/example/ExampleRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/example/ExampleRepositoryImpl.java deleted file mode 100644 index 37f2272f0..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/example/ExampleRepositoryImpl.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.loopers.infrastructure.example; - -import com.loopers.domain.example.ExampleModel; -import com.loopers.domain.example.ExampleRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; - -import java.util.Optional; - -@RequiredArgsConstructor -@Component -public class ExampleRepositoryImpl implements ExampleRepository { - private final ExampleJpaRepository exampleJpaRepository; - - @Override - public Optional find(Long id) { - return exampleJpaRepository.findById(id); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1ApiSpec.java deleted file mode 100644 index 219e3101e..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1ApiSpec.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.loopers.interfaces.api.example; - -import com.loopers.interfaces.api.ApiResponse; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.tags.Tag; - -@Tag(name = "Example V1 API", description = "Loopers ์˜ˆ์‹œ API ์ž…๋‹ˆ๋‹ค.") -public interface ExampleV1ApiSpec { - - @Operation( - summary = "์˜ˆ์‹œ ์กฐํšŒ", - description = "ID๋กœ ์˜ˆ์‹œ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค." - ) - ApiResponse getExample( - @Schema(name = "์˜ˆ์‹œ ID", description = "์กฐํšŒํ•  ์˜ˆ์‹œ์˜ ID") - Long exampleId - ); -} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1Controller.java deleted file mode 100644 index 917376016..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1Controller.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.loopers.interfaces.api.example; - -import com.loopers.application.example.ExampleFacade; -import com.loopers.application.example.ExampleInfo; -import com.loopers.interfaces.api.ApiResponse; -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; - -@RequiredArgsConstructor -@RestController -@RequestMapping("/api/v1/examples") -public class ExampleV1Controller implements ExampleV1ApiSpec { - - private final ExampleFacade exampleFacade; - - @GetMapping("/{exampleId}") - @Override - public ApiResponse getExample( - @PathVariable(value = "exampleId") Long exampleId - ) { - ExampleInfo info = exampleFacade.getExample(exampleId); - ExampleV1Dto.ExampleResponse response = ExampleV1Dto.ExampleResponse.from(info); - return ApiResponse.success(response); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1Dto.java deleted file mode 100644 index 4ecf0eea5..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1Dto.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.loopers.interfaces.api.example; - -import com.loopers.application.example.ExampleInfo; - -public class ExampleV1Dto { - public record ExampleResponse(Long id, String name, String description) { - public static ExampleResponse from(ExampleInfo info) { - return new ExampleResponse( - info.id(), - info.name(), - info.description() - ); - } - } -} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/example/ExampleModelTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/example/ExampleModelTest.java deleted file mode 100644 index 44ca7576e..000000000 --- a/apps/commerce-api/src/test/java/com/loopers/domain/example/ExampleModelTest.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.loopers.domain.example; - -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertAll; -import static org.junit.jupiter.api.Assertions.assertThrows; - -class ExampleModelTest { - @DisplayName("์˜ˆ์‹œ ๋ชจ๋ธ์„ ์ƒ์„ฑํ•  ๋•Œ, ") - @Nested - class Create { - @DisplayName("์ œ๋ชฉ๊ณผ ์„ค๋ช…์ด ๋ชจ๋‘ ์ฃผ์–ด์ง€๋ฉด, ์ •์ƒ์ ์œผ๋กœ ์ƒ์„ฑ๋œ๋‹ค.") - @Test - void createsExampleModel_whenNameAndDescriptionAreProvided() { - // arrange - String name = "์ œ๋ชฉ"; - String description = "์„ค๋ช…"; - - // act - ExampleModel exampleModel = new ExampleModel(name, description); - - // assert - assertAll( - () -> assertThat(exampleModel.getId()).isNotNull(), - () -> assertThat(exampleModel.getName()).isEqualTo(name), - () -> assertThat(exampleModel.getDescription()).isEqualTo(description) - ); - } - - @DisplayName("์ œ๋ชฉ์ด ๋นˆ์นธ์œผ๋กœ๋งŒ ์ด๋ฃจ์–ด์ ธ ์žˆ์œผ๋ฉด, BAD_REQUEST ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") - @Test - void throwsBadRequestException_whenTitleIsBlank() { - // arrange - String name = " "; - - // act - CoreException result = assertThrows(CoreException.class, () -> { - new ExampleModel(name, "์„ค๋ช…"); - }); - - // assert - assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); - } - - @DisplayName("์„ค๋ช…์ด ๋น„์–ด์žˆ์œผ๋ฉด, BAD_REQUEST ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") - @Test - void throwsBadRequestException_whenDescriptionIsEmpty() { - // arrange - String description = ""; - - // act - CoreException result = assertThrows(CoreException.class, () -> { - new ExampleModel("์ œ๋ชฉ", description); - }); - - // assert - assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); - } - } -} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/example/ExampleServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/example/ExampleServiceIntegrationTest.java deleted file mode 100644 index bbd5fdbe1..000000000 --- a/apps/commerce-api/src/test/java/com/loopers/domain/example/ExampleServiceIntegrationTest.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.loopers.domain.example; - -import com.loopers.infrastructure.example.ExampleJpaRepository; -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import com.loopers.utils.DatabaseCleanUp; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertAll; -import static org.junit.jupiter.api.Assertions.assertThrows; - -@SpringBootTest -class ExampleServiceIntegrationTest { - @Autowired - private ExampleService exampleService; - - @Autowired - private ExampleJpaRepository exampleJpaRepository; - - @Autowired - private DatabaseCleanUp databaseCleanUp; - - @AfterEach - void tearDown() { - databaseCleanUp.truncateAllTables(); - } - - @DisplayName("์˜ˆ์‹œ๋ฅผ ์กฐํšŒํ•  ๋•Œ,") - @Nested - class Get { - @DisplayName("์กด์žฌํ•˜๋Š” ์˜ˆ์‹œ ID๋ฅผ ์ฃผ๋ฉด, ํ•ด๋‹น ์˜ˆ์‹œ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnsExampleInfo_whenValidIdIsProvided() { - // arrange - ExampleModel exampleModel = exampleJpaRepository.save( - new ExampleModel("์˜ˆ์‹œ ์ œ๋ชฉ", "์˜ˆ์‹œ ์„ค๋ช…") - ); - - // act - ExampleModel result = exampleService.getExample(exampleModel.getId()); - - // assert - assertAll( - () -> assertThat(result).isNotNull(), - () -> assertThat(result.getId()).isEqualTo(exampleModel.getId()), - () -> assertThat(result.getName()).isEqualTo(exampleModel.getName()), - () -> assertThat(result.getDescription()).isEqualTo(exampleModel.getDescription()) - ); - } - - @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์˜ˆ์‹œ ID๋ฅผ ์ฃผ๋ฉด, NOT_FOUND ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") - @Test - void throwsException_whenInvalidIdIsProvided() { - // arrange - Long invalidId = 999L; // Assuming this ID does not exist - - // act - CoreException exception = assertThrows(CoreException.class, () -> { - exampleService.getExample(invalidId); - }); - - // assert - assertThat(exception.getErrorType()).isEqualTo(ErrorType.NOT_FOUND); - } - } -} diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/example/ExampleV1ApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/example/ExampleV1ApiE2ETest.java deleted file mode 100644 index 70f256149..000000000 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/example/ExampleV1ApiE2ETest.java +++ /dev/null @@ -1,114 +0,0 @@ -package com.loopers.interfaces.api.example; - -import com.loopers.domain.example.ExampleModel; -import com.loopers.infrastructure.example.ExampleJpaRepository; -import com.loopers.interfaces.api.ApiResponse; -import com.loopers.utils.DatabaseCleanUp; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.web.client.TestRestTemplate; -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpMethod; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; - -import java.util.function.Function; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertAll; -import static org.junit.jupiter.api.Assertions.assertTrue; - -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -class ExampleV1ApiE2ETest { - - private static final Function ENDPOINT_GET = id -> "/api/v1/examples/" + id; - - private final TestRestTemplate testRestTemplate; - private final ExampleJpaRepository exampleJpaRepository; - private final DatabaseCleanUp databaseCleanUp; - - @Autowired - public ExampleV1ApiE2ETest( - TestRestTemplate testRestTemplate, - ExampleJpaRepository exampleJpaRepository, - DatabaseCleanUp databaseCleanUp - ) { - this.testRestTemplate = testRestTemplate; - this.exampleJpaRepository = exampleJpaRepository; - this.databaseCleanUp = databaseCleanUp; - } - - @AfterEach - void tearDown() { - databaseCleanUp.truncateAllTables(); - } - - @DisplayName("GET /api/v1/examples/{id}") - @Nested - class Get { - @DisplayName("์กด์žฌํ•˜๋Š” ์˜ˆ์‹œ ID๋ฅผ ์ฃผ๋ฉด, ํ•ด๋‹น ์˜ˆ์‹œ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnsExampleInfo_whenValidIdIsProvided() { - // arrange - ExampleModel exampleModel = exampleJpaRepository.save( - new ExampleModel("์˜ˆ์‹œ ์ œ๋ชฉ", "์˜ˆ์‹œ ์„ค๋ช…") - ); - String requestUrl = ENDPOINT_GET.apply(exampleModel.getId()); - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; - ResponseEntity> response = - testRestTemplate.exchange(requestUrl, HttpMethod.GET, new HttpEntity<>(null), responseType); - - // assert - assertAll( - () -> assertTrue(response.getStatusCode().is2xxSuccessful()), - () -> assertThat(response.getBody().data().id()).isEqualTo(exampleModel.getId()), - () -> assertThat(response.getBody().data().name()).isEqualTo(exampleModel.getName()), - () -> assertThat(response.getBody().data().description()).isEqualTo(exampleModel.getDescription()) - ); - } - - @DisplayName("์ˆซ์ž๊ฐ€ ์•„๋‹Œ ID ๋กœ ์š”์ฒญํ•˜๋ฉด, 400 BAD_REQUEST ์‘๋‹ต์„ ๋ฐ›๋Š”๋‹ค.") - @Test - void throwsBadRequest_whenIdIsNotProvided() { - // arrange - String requestUrl = "/api/v1/examples/๋‚˜๋‚˜"; - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; - ResponseEntity> response = - testRestTemplate.exchange(requestUrl, HttpMethod.GET, new HttpEntity<>(null), responseType); - - // assert - assertAll( - () -> assertTrue(response.getStatusCode().is4xxClientError()), - () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST) - ); - } - - @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์˜ˆ์‹œ ID๋ฅผ ์ฃผ๋ฉด, 404 NOT_FOUND ์‘๋‹ต์„ ๋ฐ›๋Š”๋‹ค.") - @Test - void throwsException_whenInvalidIdIsProvided() { - // arrange - Long invalidId = -1L; - String requestUrl = ENDPOINT_GET.apply(invalidId); - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; - ResponseEntity> response = - testRestTemplate.exchange(requestUrl, HttpMethod.GET, new HttpEntity<>(null), responseType); - - // assert - assertAll( - () -> assertTrue(response.getStatusCode().is4xxClientError()), - () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND) - ); - } - } -} From b06e034f2c369f6c3c5c7010e18449a2b59cc950 Mon Sep 17 00:00:00 2001 From: bookers-web Date: Fri, 31 Oct 2025 15:47:43 +0900 Subject: [PATCH 37/85] =?UTF-8?q?chore:=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20given=20when=20then=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../point/PointServiceIntegrationTest.java | 9 +++++++++ .../java/com/loopers/domain/point/PointTest.java | 3 +++ .../domain/user/UserServiceIntegrationTest.java | 15 ++++++++++++++- .../api/point/PointV1ControllerTest.java | 16 ++++++++++++++++ .../api/user/UserV1ControllerTest.java | 15 ++++++++++++++- 5 files changed, 56 insertions(+), 2 deletions(-) diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java index 2ba880463..882bca673 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java @@ -42,6 +42,7 @@ class PointUser { @DisplayName("ํ•ด๋‹น ID์˜ ํšŒ์›์ด ์กด์žฌํ•  ๊ฒฝ์šฐ, ๋ณด์œ  ํฌ์ธํŠธ๊ฐ€ ๋ฐ˜ํ™˜๋œ๋‹ค.") @Test void returnPointInfo_whenValidIdIsProvided() { + //given String id = "yh45g"; String email = "yh45g@loopers.com"; String birth = "1994-12-05"; @@ -50,8 +51,10 @@ void returnPointInfo_whenValidIdIsProvided() { userRepository.save(new User(id, email, birth, gender)); pointRepository.save(new Point(id, 0L)); + //when Point result = pointService.findPointByUserId(id); + //then assertThat(result.getUserId()).isEqualTo(id); assertThat(result.getAmount()).isEqualTo(0L); } @@ -59,10 +62,13 @@ void returnPointInfo_whenValidIdIsProvided() { @DisplayName("ํšŒ์›์ด ์กด์žฌ ํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ, null ์ด ๋ฐ˜ํ™˜๋œ๋‹ค.") @Test void returnNull_whenInvalidUserIdIsProvided() { + //given String id = "yh45g"; + //when Point point = pointService.findPointByUserId(id); + //then assertThat(point).isNull(); } } @@ -74,10 +80,13 @@ class Charge { @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ์ € ID ๋กœ ์ถฉ์ „์„ ์‹œ๋„ํ•œ ๊ฒฝ์šฐ, ์‹คํŒจํ•œ๋‹ค.") @Test void throwsChargeAmountFailException_whenUserIDIsNotProvided() { + //given String id = "yh45g"; + //when CoreException exception = assertThrows(CoreException.class, () -> pointService.chargePoint(id, 1000L)); + //then assertThat(exception.getErrorType()).isEqualTo(ErrorType.NOT_FOUND); } } diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/point/PointTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/point/PointTest.java index bcc911321..81bedab7d 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/point/PointTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/point/PointTest.java @@ -14,7 +14,10 @@ class Charge { @DisplayName("0 ์ดํ•˜์˜ ์ •์ˆ˜๋กœ ํฌ์ธํŠธ๋ฅผ ์ถฉ์ „ ์‹œ ์‹คํŒจํ•œ๋‹ค.") @Test void throwsChargeAmountFailException_whenZeroAmountOrNegative() { + //given Point point = new Point("yh45g", 0L); + + //when&then assertThrows(CoreException.class, () -> point.charge(0L)); } diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java index c342c8aa7..71091883f 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java @@ -36,6 +36,7 @@ class UserRegister { @DisplayName("ํšŒ์› ๊ฐ€์ž…์‹œ User ์ €์žฅ์ด ์ˆ˜ํ–‰๋œ๋‹ค.") @Test void save_whenUserRegister() { + //given String userId = "yh45g"; String email = "yh45g@loopers.com"; String brith = "1994-12-05"; @@ -43,21 +44,27 @@ void save_whenUserRegister() { UserRepository userRepositorySpy = spy(userRepository); UserService userServiceSpy = new UserService(userRepositorySpy); + + //when userServiceSpy.register(userId, email, brith, gender); + //then verify(userRepositorySpy).save(any(User.class)); } @DisplayName("์ด๋ฏธ ๊ฐ€์ž…๋œ ID ๋กœ ํšŒ์›๊ฐ€์ž… ์‹œ๋„ ์‹œ, ์‹คํŒจํ•œ๋‹ค.") @Test void throwsException_whenDuplicateUserId() { + //given String userId = "yh45g"; String email = "yh45g@loopers.com"; String brith = "1994-12-05"; String gender = "Male"; + //when userService.register(userId, email, brith, gender); + //then Assertions.assertThrows(CoreException.class, () -> userService.register(userId, email, brith, gender)); } @@ -70,15 +77,17 @@ class Get { @DisplayName("ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•  ๊ฒฝ์šฐ, ํšŒ์› ์ •๋ณด๊ฐ€ ๋ฐ˜ํ™˜๋œ๋‹ค.") @Test void returnsUser_whenValidIdIsProvided() { + //given String userId = "yh45g"; String email = "yh45g@loopers.com"; String brith = "1994-12-05"; String gender = "Male"; + //when userService.register(userId, email, brith, gender); - User user = userService.findUserByUserId(userId); + //then assertAll( () -> assertThat(user.getUserId()).isEqualTo(userId), () -> assertThat(user.getEmail()).isEqualTo(email), @@ -90,9 +99,13 @@ void returnsUser_whenValidIdIsProvided() { @DisplayName("ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ, null ์ด ๋ฐ˜ํ™˜๋œ๋‹ค.") @Test void returnNull_whenInvalidUserIdIsProvided() { + //given String userId = "yh45g"; + + //when User user = userService.findUserByUserId(userId); + //then assertThat(user).isNull(); } } diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/point/PointV1ControllerTest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/point/PointV1ControllerTest.java index 8e8507f72..b725fd807 100644 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/point/PointV1ControllerTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/point/PointV1ControllerTest.java @@ -50,6 +50,7 @@ class UserPoint { @DisplayName("ํฌ์ธํŠธ ์กฐํšŒ์— ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ๋ณด์œ  ํฌ์ธํŠธ๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") @Test void returnPoint_whenValidUserIdIsProvided() { + //given String id = "yh45g"; String email = "yh45g@loopers.com"; String birth = "1994-12-05"; @@ -61,10 +62,13 @@ void returnPoint_whenValidUserIdIsProvided() { HttpHeaders headers = new HttpHeaders(); headers.add("X-USER-ID", id); + + //when ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; ResponseEntity> response = testRestTemplate.exchange(GET_USER_POINT_ENDPOINT, HttpMethod.GET, new HttpEntity<>(null, headers), responseType); + //then assertAll( () -> assertTrue(response.getStatusCode().is2xxSuccessful()), () -> assertThat(response.getBody().data().userId()).isEqualTo(id), @@ -75,14 +79,18 @@ void returnPoint_whenValidUserIdIsProvided() { @DisplayName("ํ•ด๋‹น ID์˜ ํšŒ์›์ด ์กด์žฌํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ, null์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") @Test void returnNull_whenUserIdExists() { + //given String id = "yh45g"; HttpHeaders headers = new HttpHeaders(); headers.add("X-USER-ID", id); + + //when ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; ResponseEntity> response = testRestTemplate.exchange(GET_USER_POINT_ENDPOINT, HttpMethod.GET, new HttpEntity<>(null, headers), responseType); + //then assertAll( () -> assertTrue(response.getStatusCode().is4xxClientError()), () -> assertThat(response.getBody().data()).isNull() @@ -97,6 +105,7 @@ class Charge { @DisplayName("์กด์žฌํ•˜๋Š” ์œ ์ €๊ฐ€ 1000์›์„ ์ถฉ์ „ํ•  ๊ฒฝ์šฐ, ์ถฉ์ „๋œ ๋ณด์œ  ์ด๋Ÿ‰์„ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") @Test void returnsTotalPoint_whenChargeUserPoint() { + //given String id = "yh45g"; String email = "yh45g@loopers.com"; String birth = "1994-12-05"; @@ -109,10 +118,13 @@ void returnsTotalPoint_whenChargeUserPoint() { HttpHeaders headers = new HttpHeaders(); headers.add("X-USER-ID", id); + + //when ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; ResponseEntity> response = testRestTemplate.exchange(POST_USER_POINT_ENDPOINT, HttpMethod.PATCH, new HttpEntity<>(request, headers), responseType); + //then assertAll( () -> assertTrue(response.getStatusCode().is2xxSuccessful()), () -> assertThat(response.getBody().data().userId()).isEqualTo(id), @@ -123,14 +135,18 @@ void returnsTotalPoint_whenChargeUserPoint() { @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ์ €๋กœ ์š”์ฒญํ•  ๊ฒฝ์šฐ, 404 Not Found ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") @Test void throwsException_whenInvalidUserIdIsProvided() { + //given String id = "yh45g"; HttpHeaders headers = new HttpHeaders(); headers.add("X-USER-ID", id); + + //when ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; ResponseEntity> response = testRestTemplate.exchange(POST_USER_POINT_ENDPOINT, HttpMethod.PATCH, new HttpEntity<>(null, headers), responseType); + //then assertAll( () -> assertTrue(response.getStatusCode().is4xxClientError()), () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND) diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/user/UserV1ControllerTest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/user/UserV1ControllerTest.java index b99da8b4e..defe2fcd5 100644 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/user/UserV1ControllerTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/user/UserV1ControllerTest.java @@ -49,6 +49,7 @@ class RegisterUser { @DisplayName("ํšŒ์› ๊ฐ€์ž…์ด ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ์ƒ์„ฑ๋œ ์œ ์ € ์ •๋ณด๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") @Test void registerUser_whenSuccessResponseUser() { + //given String userId = "yh45g"; String email = "yh45g@loopers.com"; String birth = "1994-12-05"; @@ -56,11 +57,12 @@ void registerUser_whenSuccessResponseUser() { UserV1Dto.RegisterRequest request = new UserV1Dto.RegisterRequest(userId, email, birth, gender); + //when ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; - ResponseEntity> response = testRestTemplate.exchange(USER_REGISTER_ENDPOINT, HttpMethod.POST, new HttpEntity<>(request), responseType); + //then assertAll( () -> assertTrue(response.getStatusCode().is2xxSuccessful()), () -> assertThat(response.getBody().data().userId()).isEqualTo(userId), @@ -72,6 +74,7 @@ void registerUser_whenSuccessResponseUser() { @DisplayName("ํšŒ์› ๊ฐ€์ž… ์‹œ์— ์„ฑ๋ณ„์ด ์—†์„ ๊ฒฝ์šฐ, 400 BAD_REQUEST ์‘๋‹ต์„ ๋ฐ›๋Š”๋‹ค.") @Test void throwsBadRequest_whenGenderIsNotProvided() { + //given String userId = "yh45g"; String email = "yh45g@loopers.com"; String birth = "1994-12-05"; @@ -79,10 +82,12 @@ void throwsBadRequest_whenGenderIsNotProvided() { UserV1Dto.RegisterRequest request = new UserV1Dto.RegisterRequest(userId, email, birth, gender); + //when ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; ResponseEntity> response = testRestTemplate.exchange(USER_REGISTER_ENDPOINT, HttpMethod.POST, new HttpEntity<>(request), responseType); + //then assertAll( () -> assertTrue(response.getStatusCode().is4xxClientError()), () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST) @@ -96,6 +101,7 @@ class GetUserById { @DisplayName("๋‚ด ์ •๋ณด ์กฐํšŒ์— ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ํ•ด๋‹นํ•˜๋Š” ์œ ์ € ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") @Test void getUserById_whenSuccessResponseUser() { + //given String userId = "yh45g"; String email = "yh45g@loopers.com"; String birth = "1994-12-05"; @@ -104,10 +110,13 @@ void getUserById_whenSuccessResponseUser() { userJpaRepository.save(new User(userId, email, birth, gender)); String requestUrl = GET_USER_ENDPOINT.apply(userId); + + //when ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; ResponseEntity> response = testRestTemplate.exchange(requestUrl, HttpMethod.GET, new HttpEntity<>(null), responseType); + //then assertAll( () -> assertTrue(response.getStatusCode().is2xxSuccessful()), () -> assertThat(response.getBody().data().userId()).isEqualTo(userId), @@ -120,12 +129,16 @@ void getUserById_whenSuccessResponseUser() { @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ID ๋กœ ์กฐํšŒํ•  ๊ฒฝ์šฐ, 404 Not Found ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") @Test void throwsException_whenInvalidUserIdIsProvided() { + //given String userId = "notUserId"; String requestUrl = GET_USER_ENDPOINT.apply(userId); + + //when ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; ResponseEntity> response = testRestTemplate.exchange(requestUrl, HttpMethod.GET, new HttpEntity<>(null), responseType); + //then assertAll( () -> assertTrue(response.getStatusCode().is4xxClientError()), () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND) From 6c487552bdc209e74b762ab8600898c3df61cc78 Mon Sep 17 00:00:00 2001 From: bookers-web Date: Fri, 31 Oct 2025 16:46:40 +0900 Subject: [PATCH 38/85] =?UTF-8?q?fix:=20Exception=20=EB=A9=94=EC=8B=9C?= =?UTF-8?q?=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/loopers/domain/user/UserService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java index da2878300..57353968a 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java @@ -15,7 +15,7 @@ public class UserService { @Transactional public User register(String userId, String email, String birth, String gender) { userRepository.findByUserId(userId).ifPresent(user -> { - throw new CoreException(ErrorType.CONFLICT); + throw new CoreException(ErrorType.CONFLICT, "์ด๋ฏธ ๊ฐ€์ž…๋œ ์‚ฌ์šฉ์žID ์ž…๋‹ˆ๋‹ค."); }); User user = new User(userId, email, birth, gender); From d9ae7adc3cfd0fb44a493c8638e258ce188e8750 Mon Sep 17 00:00:00 2001 From: simplify-len Date: Mon, 3 Nov 2025 22:08:41 +0900 Subject: [PATCH 39/85] Add GitHub Actions workflow for PR Agent --- .github/workflows/main.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .github/workflows/main.yml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 000000000..1f80db6bf --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,13 @@ +name: PR Agent +on: + pull_request: + types: [opened, synchronize] +jobs: + pr_agent_job: + runs-on: ubuntu-latest + steps: + - name: PR Agent action step + uses: Codium-ai/pr-agent@main + env: + OPENAI_KEY: ${{ secrets.OPENAI_KEY }} + GITHUB_TOKEN: ${{ secrets.G_TOKEN }} From 5bb8d33f7ffcc3e4d48c6b744ca8e4716629c9e7 Mon Sep 17 00:00:00 2001 From: adminhelper <201401339hs@gmail.com> Date: Fri, 7 Nov 2025 16:58:30 +0900 Subject: [PATCH 40/85] =?UTF-8?q?docs=20:=20=EC=9C=A0=EC=A0=80=20=EC=8B=9C?= =?UTF-8?q?=EB=82=98=EB=A6=AC=EC=98=A4=20=EA=B8=B0=EB=B0=98=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=A0=95=EC=9D=98,=20=EC=9A=94=EA=B5=AC=EC=82=AC?= =?UTF-8?q?=ED=95=AD=20=EB=AA=85=EC=84=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/2round/01-requirements.md | 104 +++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 docs/2round/01-requirements.md diff --git a/docs/2round/01-requirements.md b/docs/2round/01-requirements.md new file mode 100644 index 000000000..3296c21c6 --- /dev/null +++ b/docs/2round/01-requirements.md @@ -0,0 +1,104 @@ +# ์œ ์ €-์‹œ๋‚˜๋ฆฌ์˜ค + +## ์ƒํ’ˆ ๋ชฉ๋ก +1. ์‚ฌ์šฉ์ž๊ฐ€ ๋ชจ๋“  ์ƒํ’ˆ์— ๋Œ€ํ•ด์„œ ๋ณผ์ˆ˜ ์žˆ๋‹ค. +2. ํŒ๋งค์ค‘์ธ ์ƒํ’ˆ์— ๋Œ€ํ•œ ํŒ๋งค๋ช…, ํŒ๋งค๊ธˆ์•ก, ํŒ๋งค๋ธŒ๋žœ๋“œ, ์ด๋ฏธ์ง€, ์ข‹์•„์š”ํ•œ ์ƒํ’ˆ์„ ๋ณผ์ˆ˜์žˆ๋‹ค. +3. ์‚ฌ์šฉ์ž๋Š” ์›ํ•˜๋Š” ๋ธŒ๋žœ๋“œ๋ณ„๋กœ ์ƒํ’ˆ์„ ๋ณผ์ˆ˜ ์žˆ๋‹ค. +4. ์‚ฌ์šฉ์ž๋Š” ์›ํ•˜๋Š” ์ƒํ’ˆ์—๋Œ€ํ•ด์„œ ์ข‹์•„์š”๋ฅผ ๋ˆ„๋ฅผ์ˆ˜์žˆ๋‹ค. +5. ์‚ฌ์šฉ์ž๋Š” ์›ํ•˜๋Š” ์กฐ๊ฑด์— ๋”ฐ๋ผ ์ƒํ’ˆ์„ ๋ณผ์ˆ˜์žˆ๋‹ค. +6. ์‚ฌ์šฉ์ž๋Š” ์›ํ•˜๋Š” ํŽ˜์ด์ง€๋กœ ์ด๋™ํ• ์ˆ˜์žˆ๋‹ค. + +-[๊ธฐ๋Šฅ] +1. ์ „์ฒด ์ƒํ’ˆ ๋ชฉ๋ก ์กฐํšŒ +2. ๋ธŒ๋žœ๋“œ ์ƒํ’ˆ ๋ชฉ๋ก ์กฐํšŒ +3. ์ƒํ’ˆ ์ข‹์•„์š” ๋“ฑ๋ก +4. ํ•„ํ„ฐ๋ง (์ตœ์‹ ์ˆœ, ํŒ๋งค์ˆœ, ์ข‹์•„์š” ์ˆœ, ๋‚จ์ž/์—ฌ์ž) +5. ํŽ˜์ด์ง• + +-[์ œ์•ฝ] +1. ๋กœ๊ทธ์ธ์€ X-USER-ID๋กœ ํŒ๋‹จ, ๋น„ํšŒ์›์ผ๊ฒฝ์šฐ "๋กœ๊ทธ์ธ ํ›„ ์ข‹์•„์š” ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค" ๋ฉ”์‹œ์ง€ ์ „๋‹ฌํ•œ๋‹ค +2. ์ƒํ’ˆ์ด ์—†์„๊ฒฝ์šฐ 404_NOTFOUND ์ œ๊ณตํ•œ๋‹ค. + +--- +## ์ƒํ’ˆ ์ƒ์„ธ +1. ์‚ฌ์šฉ์ž๋Š” ์›ํ•˜๋Š” ํŒ๋งค์ค‘ ์ƒํ’ˆ(ํŒ๋งค๋ช…,ํŒ๋งค๊ธˆ์•ก,ํŒ๋งค๋ธŒ๋žœ๋“œ,์ด๋ฏธ์ง€,์ข‹์•„์š”ํ•œ ์ƒํ’ˆ์„ ๋ณผ์ˆ˜์žˆ๋‹ค. + -[๊ธฐ๋Šฅ] +1. ์‚ฌ์šฉ์ž๊ฐ€ ์›ํ•˜๋Š” ์ƒํ’ˆ๋ฒˆํ˜ธ๋กœ ์กฐํšŒ +2. ์ƒํ’ˆ ์ข‹์•„์š” ๋“ฑ๋ก / ์ทจ์†Œ + -[์ œ์•ฝ] +1. ๋กœ๊ทธ์ธ์€ X-USER-ID๋กœ ํŒ๋‹จ, ๋น„ํšŒ์›์ผ๊ฒฝ์šฐ "๋กœ๊ทธ์ธ ํ›„ ์ข‹์•„์š” ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค" ๋ฉ”์‹œ์ง€ ์ „๋‹ฌํ•œ๋‹ค +2. ์ƒํ’ˆ์ด ์กด์žฌํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ 404_NOTFOUND ์ œ๊ณตํ•œ๋‹ค. + +--- +## ์ข‹์•„์š” +1. ์‚ฌ์šฉ์ž๊ฐ€ ์›ํ•˜๋Š” ์ƒํ’ˆ์— ๋Œ€ํ•ด์„œ ์ข‹์•„์š”๋ฅผ ๋ˆ„๋ฅผ ์ˆ˜ ์žˆ๋‹ค. +2. ์‚ฌ์šฉ์ž๊ฐ€ ์›ํ•˜๋Š” ์ƒํ’ˆ์— ๋Œ€ํ•ด์„œ ์ข‹์•„์š”๋ฅผ ์ทจ์†Œ ํ•  ์ˆ˜ ์žˆ๋‹ค. +2. ์‚ฌ์šฉ์ž๊ฐ€ ์ข‹์•„ํ•˜๋Š” ์ƒํ’ˆ์— ๋Œ€ํ•ด ๋ชฉ๋ก์„ ๋ณผ์ˆ˜์žˆ๋‹ค. + -[๊ธฐ๋Šฅ] +1. ์ข‹์•„์š” ๋ˆ„๋ฅธ ์ƒํ’ˆ์—๋Œ€ํ•ด ๋ชฉ๋ก ์กฐํšŒ +2. ์‚ฌ์šฉ๊ฐ€ ์›ํ•˜๋Š” ์ƒํ’ˆ์— ๋Œ€ํ•œ ๋“ฑ๋ก/์ทจ์†Œ, ๋‹จ ๋“ฑ๋ก/ํ•ด์ œ (๋ฉฑ๋“ฑ์„ฑ) + -[์ œ์•ฝ] +1. ๋กœ๊ทธ์ธ์€ X-USER-ID๋กœ ํŒ๋‹จ, ๋น„ํšŒ์›์ผ๊ฒฝ์šฐ "๋กœ๊ทธ์ธ ํ›„ ์ข‹์•„์š” ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค" ๋ฉ”์‹œ์ง€ ์ „๋‹ฌํ•œ๋‹ค +2. ํ•ด๋‹น ์ƒํ’ˆ์— ์ข‹์•„์š”๋ฅผ ์ฒ˜์Œ ๋“ฑ๋ก ํ• ๋•Œ๋Š” 201_Created ์ œ๊ณตํ•œ๋‹ค +3. ํ•ด๋‹น ์ƒํ’ˆ์— ์ข‹์•„์š”๋ฅผ ํ•œ๋ฒˆ๋” ๋“ฑ๋ก ํ• ๋•Œ๋Š” 200_OK ์ œ๊ณตํ•œ๋‹ค +4. ํ•ด๋‹น ์ƒํ’ˆ์— ์ข‹์•„์š”๊ฐ€ ๋“ฑ๋ก ๋œ ์ƒํƒœ์—์„œ ์ทจ์†Œ ํ• ๋•Œ๋Š” 204 No Content ์ œ๊ณตํ•œ๋‹ค +5. ํ•ด๋‹น ์ƒํ’ˆ์— ์ข‹์•„์š”๊ฐ€ ์ทจ์†Œ๊ฐ€ ๋œ ์ƒํƒœ์—์„œ ํ•œ๋ฒˆ๋” ์ทจ์†Œ ํ• ๋•Œ๋Š” 204 No Content ์ œ๊ณตํ•œ๋‹ค +--- +## ๋ธŒ๋žœ๋“œ +1. ์‚ฌ์šฉ์ž๋Š” ๋ชจ๋“  ๋ธŒ๋žœ๋“œ์˜ ์ƒํ’ˆ์„ ๋ณผ์ˆ˜์žˆ๋‹ค. +2. ์‚ฌ์šฉ์ž๋Š” ํŠน์ • ๋ธŒ๋žœ๋“œ์— ๋Œ€ํ•œ ์ƒํ’ˆ๋งŒ ๋ณผ์ˆ˜์žˆ๋‹ค. +3. ์‚ฌ์šฉ์ž๋Š” ํŠน์ • ๊ธฐ์ค€์œผ๋กœ ์ƒํ’ˆ์„ ๋ณผ์ˆ˜์žˆ๋‹ค. (์ตœ์‹ ์ˆœ, ํŒ๋งค์ˆœ, ์ข‹์•„์š” ์ˆœ) +4. ์‚ฌ์šฉ์ž๋Š” ์›ํ•˜๋Š” ํŽ˜์ด์ง€๋กœ ์ด๋™ํ• ์ˆ˜์žˆ๋‹ค. + [๊ธฐ๋Šฅ] +1. ๋ชจ๋“  ๋ธŒ๋žœ๋“œ ์ƒํ’ˆ ์กฐํšŒ +2. ํŠน์ • ๋ธŒ๋žœ๋“œ ์ƒํ’ˆ ์กฐํšŒ +3. ํ•„ํ„ฐ๋ง (์ตœ์‹ ์ˆœ, ํŒ๋งค์ˆœ, ์ข‹์•„์š” ์ˆœ, ๋‚จ์ž/์—ฌ์ž) +4. ํŽ˜์ด์ง• + [์ œ์•ฝ] +1. ๋ธŒ๋žœ๋“œ๊ฐ€ ์—†์„์‹œ 404_NOTFOUND๋ฅผ ์ œ๊ณตํ•œ๋‹ค +--- +## ์ฃผ๋ฌธ +1. ์‚ฌ์šฉ์ž๊ฐ€ ์ƒํ’ˆ ๋ชฉ๋ก์—์„œ ์›ํ•˜๋Š” ์ƒํ’ˆ์„ ์„ ํƒํ•˜์—ฌ ์ฃผ๋ฌธํ•  ์ˆ˜ ์žˆ๋‹ค. +2. ํ•œ๊ฐœ์˜ ์ฃผ๋ฌธ์€ ์—ฌ๋Ÿฌ ์ƒํ’ˆ์„ ํฌํ•จํ•  ์ˆ˜ ์žˆ๋‹ค. +3. ์‚ฌ์šฉ์ž๊ฐ€ ์ฃผ๋ฌธ ๋‚ด์—ญ์„ ์กฐํšŒํ•ด ์–ด๋–ค ์ƒํ’ˆ์„ ์ฃผ๋ฌธํ–ˆ๋Š”์ง€ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค. +4. ์‚ฌ์šฉ์ž๋Š” ๊ฒฐ์ œ ์ „์ด๋ผ๋ฉด ์ฃผ๋ฌธ์„ ์ทจ์†Œํ•  ์ˆ˜ ์žˆ๋‹ค. +5. ์‚ฌ์šฉ์ž๋Š” ์ฃผ๋ฌธ ์ƒ์„ธ ํ™”๋ฉด์—์„œ ์ƒํ’ˆ ์ •๋ณด, ์ˆ˜๋Ÿ‰, ๊ฒฐ์ œ ๊ธˆ์•ก, ์ƒํƒœ ๋“ฑ์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค. + [๊ธฐ๋Šฅ] +1. ์ฃผ๋ฌธ ์ƒ์„ฑ +2. ์ฃผ๋ฌธ ๋ชฉ๋ก ์กฐํšŒ +3. ์ฃผ๋ฌธ ์ƒ์„ธ ์กฐํšŒ +4. ์ฃผ๋ฌธ ์ทจ์†Œ +5. ์ฃผ๋ฌธ์— ๋Œ€ํ•œ ์ƒํƒœ๊ด€๋ฆฌ + [์ œ์•ฝ] +1. ์ฃผ๋ฌธ ์‹œ ์ƒํ’ˆ ์žฌ๊ณ  ํ™•์ธ ๋ฐ ๋ถ€์กฑ ์‹œ ์˜ˆ์™ธ ๋ฐœ์ƒ +2. ํฌ์ธํŠธ ์ž”์•ก ๋ถ€์กฑ ์‹œ ์ฃผ๋ฌธ ๋ถˆ๊ฐ€ +3. ๋™์ผํ•œ ์ฃผ๋ฌธ ์š”์ฒญ์ด ์ค‘๋ณต์œผ๋กœ ๋“ค์–ด์™€๋„ ํ•œ ๋ฒˆ๋งŒ ์ฒ˜๋ฆฌ +--- +## ๊ฒฐ์ œ +1. ์‚ฌ์šฉ์ž๋Š” ์ฃผ๋ฌธํ•œ ์ƒํ’ˆ์— ๋Œ€ํ•ด ํฌ์ธํŠธ๋กœ ๊ฒฐ์ œํ•  ์ˆ˜ ์žˆ๋‹ค. +2. ๊ฒฐ์ œ๊ฐ€ ์™„๋ฃŒ๋˜๋ฉด ์ฃผ๋ฌธ ์ƒํƒœ๊ฐ€ ๊ฒฐ์ œ ์™„๋ฃŒ๋กœ ๋ณ€๊ฒฝ๋œ๋‹ค. +3. ๊ฒฐ์ œ ์ค‘ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ๊ฒฐ์ œ ์‹คํŒจ๋กœ ์ฒ˜๋ฆฌ๋˜๋ฉฐ ํฌ์ธํŠธ์™€ ์žฌ๊ณ ๋Š” ๋ณต๊ตฌ๋œ๋‹ค. +4. ๊ฒฐ์ œ ์™„๋ฃŒ ํ›„์—๋Š” ์ฃผ๋ฌธ ์ทจ์†Œ๊ฐ€ ๋ถˆ๊ฐ€๋Šฅํ•˜๋‹ค. + [๊ธฐ๋Šฅ] +1. ๊ฒฐ์ œ์š”์ฒญ +2. ๊ฒฐ์ œ ๊ฒฐ๊ณผ ๋ฐ˜์˜ +3. ๊ฒฐ์ œ ์‹คํŒจ ์ฒ˜๋ฆฌ +4. ๊ฒฐ์ œ ๋‚ด์—ญ ์กฐํšŒ + [์ œ์•ฝ] +1. ๋™์ผ ์ฃผ๋ฌธ์— ๋Œ€ํ•ด ์ค‘๋ณต ๊ฒฐ์ œ ์š”์ฒญ ์‹œ ํ•œ ๋ฒˆ๋งŒ ์ฒ˜๋ฆฌ +2. ํฌ์ธํŠธ ์ฐจ๊ฐ์‹คํŒจ ์‹œ ๋ณต๊ตฌ +3. ์™ธ๋ถ€๊ฒฐ์ œ ์‹œ์Šคํ…œ ๊ฒฐ์ œ ์‹œ์Šคํ…œ ์ฒ˜๋ฆฌ ์‹คํŒจ์‹œ ์˜ˆ์™ธ์ฒ˜๋ฆฌ + +---- +## Ubiquitous +| ํ•œ๊ตญ์–ด | ์˜์–ด | +|--------|------| +| ์‚ฌ์šฉ์ž | User | +| ํฌ์ธํŠธ | Point | +| ์ƒํ’ˆ | Product | +| ๋ธŒ๋žœ๋“œ | Brand | +| ์ข‹์•„์š” | Like | +| ์ฃผ๋ฌธ | Order | +| ์žฌ๊ณ  | Stock | +| ๊ฐ€๊ฒฉ | Price | +| ๊ฒฐ์ œ | Payment | \ No newline at end of file From d50511593302038e3d4e01b13790016c1654507a Mon Sep 17 00:00:00 2001 From: adminhelper <201401339hs@gmail.com> Date: Fri, 7 Nov 2025 16:58:45 +0900 Subject: [PATCH 41/85] =?UTF-8?q?docs=20:=20=EC=8B=9C=ED=80=80=EC=8A=A4=20?= =?UTF-8?q?=EB=8B=A4=EC=9D=B4=EC=96=B4=EA=B7=B8=EB=9E=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/2round/02-sequence-diagrams.md | 164 ++++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 docs/2round/02-sequence-diagrams.md diff --git a/docs/2round/02-sequence-diagrams.md b/docs/2round/02-sequence-diagrams.md new file mode 100644 index 000000000..5264a4dc0 --- /dev/null +++ b/docs/2round/02-sequence-diagrams.md @@ -0,0 +1,164 @@ +# ์‹œํ€€์Šค ๋‹ค์ด์–ด๊ทธ๋žจ + +### 1. ์ƒํ’ˆ ๋ชฉ๋ก ์กฐํšŒ +```mermaid +sequenceDiagram + participant User + participant ProductController + participant ProductService + participant ProductRepository + participant BrandRepository + participant LikeRepository + + User->>ProductController: GET /api/v1/products + ProductController->>ProductService: getProductList + ProductService->>ProductRepository: findAllWithPaging + ProductService->>BrandRepository: findBrandInfoForProducts() + ProductService->>LikeRepository: countLikesForProducts() + ProductRepository-->>ProductService: productList + ProductService-->>ProductController: productListResponse + ProductController-->>User: 200 OK (์ƒํ’ˆ ๋ชฉ๋ก + ๋ธŒ๋žœ๋“œ + ์ข‹์•„์š” ์ˆ˜) +``` +--- +### 2. ์ƒํ’ˆ ์ƒ์„ธ ์กฐํšŒ +```mermaid +sequenceDiagram + participant User + participant ProductController + participant ProductService + participant ProductRepository + participant BrandRepository + participant LikeRepository + + User->>ProductController: GET /api/v1/products/{productId} + ProductController->>ProductService: getProductDetail(productId, userId) + ProductService->>ProductRepository: findById(productId) + ProductService->>BrandRepository: findBrandInfo(brandId) + ProductService->>LikeRepository: existsByUserIdAndProductId(userId, productId) + ProductRepository-->>ProductService: productDetail + ProductService-->>ProductController: productDetailResponse + ProductController-->>User: 200 OK (์ƒํ’ˆ ์ƒ์„ธ ์ •๋ณด) +``` +--- +### 3. ์ƒํ’ˆ ์ข‹์•„์š” ๋“ฑ๋ก/์ทจ์†Œ +```mermaid +sequenceDiagram + participant User + participant LikeController + participant LikeService + participant LikeRepository + + User->>LikeController: POST /api/v1/like/products/{productId} + LikeController->>LikeService: toggleLike(userId, productId) + LikeService->>LikeRepository: existsByUserIdAndProductId(userId, productId) + alt ์ข‹์•„์š”๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Œ + LikeService->>LikeRepository: save(userId, productId) + LikeService-->>LikeController: 201 Created + else ์ด๋ฏธ ์ข‹์•„์š” ๋˜์–ด์žˆ์Œ + LikeService->>LikeRepository: delete(userId, productId) + LikeService-->>LikeController: 204 No Content + end + LikeController-->>User: ์‘๋‹ต (์ƒํƒœ์ฝ”๋“œ์— ๋”ฐ๋ผ ๋‹ค๋ฆ„) +``` +--- + +### 4. ๋ธŒ๋žœ๋“œ๋ณ„ ์ƒํ’ˆ ์กฐํšŒ +```mermaid +sequenceDiagram + participant User + participant BrandController + participant BrandService + participant ProductRepository + participant BrandRepository + + User->>BrandController: GET /api/v1/brands/{brandId}/products + BrandController->>BrandService: getProductsByBrand(brandId, sort, page) + BrandService->>BrandRepository: findById(brandId) + BrandService->>ProductRepository: findByBrandId(brandId, sort, page) + BrandRepository-->>BrandService: brandInfo + ProductRepository-->>BrandService: productList + BrandService-->>BrandController: productListResponse + BrandController-->>User: 200 OK (๋ธŒ๋žœ๋“œ ์ƒํ’ˆ ๋ชฉ๋ก) +``` +--- +### 5. ์ฃผ๋ฌธ ์ƒ์„ฑ +```mermaid +sequenceDiagram + participant User + participant OrderController + participant OrderService + participant ProductReader + participant StockService + participant PointService + participant OrderRepository + + User->>OrderController: POST /api/v1/orders (items[]) + OrderController->>OrderService: createOrder(userId, items) + OrderService->>ProductReader: getProductsByIds(productIds) + loop ๊ฐ ์ƒํ’ˆ์— ๋Œ€ํ•ด + OrderService->>StockService: checkAndDecreaseStock(productId, quantity) + end + OrderService->>PointService: deductPoint(userId, totalPrice) + alt ์žฌ๊ณ  ๋˜๋Š” ํฌ์ธํŠธ ๋ถ€์กฑ + OrderService-->>OrderController: throw Exception + OrderController-->>User: 400 Bad Request + else ์ •์ƒ + OrderService->>OrderRepository: save(order, orderItems) + OrderService-->>OrderController: OrderResponse + OrderController-->>User: 201 Created (์ฃผ๋ฌธ ์™„๋ฃŒ) + end +``` +--- +### 6. ์ฃผ๋ฌธ ๋ชฉ๋ก ๋ฐ ์ƒ์„ธ ์กฐํšŒ +```mermaid +sequenceDiagram + participant User + participant OrderController + participant OrderService + participant OrderRepository + participant ProductRepository + + User->>OrderController: GET /api/v1/orders + OrderController->>OrderService: getOrderList(userId) + OrderService->>OrderRepository: findByUserId(userId) + OrderRepository-->>OrderService: orderList + OrderService-->>OrderController: orderListResponse + OrderController-->>User: 200 OK (์ฃผ๋ฌธ ๋ชฉ๋ก) + + User->>OrderController: GET /api/v1/orders/{orderId} + OrderController->>OrderService: getOrderDetail(orderId, userId) + OrderService->>OrderRepository: findById(orderId) + OrderService->>ProductRepository: findProductsInOrder(orderId) + OrderRepository-->>OrderService: orderDetail + OrderService-->>OrderController: orderDetailResponse + OrderController-->>User: 200 OK (์ฃผ๋ฌธ ์ƒ์„ธ) +``` +--- +### 7. ๊ฒฐ์ œ ์ฒ˜๋ฆฌ +```mermaid +sequenceDiagram + participant User + participant PaymentController + participant PaymentService + participant PaymentGateway + participant OrderRepository + participant PointService + participant StockService + + User->>PaymentController: POST /api/v1/payments (orderId) + PaymentController->>PaymentService: processPayment(orderId, userId) + PaymentService->>OrderRepository: findById(orderId) + PaymentService->>PaymentGateway: requestPayment(orderId, amount) + alt ๊ฒฐ์ œ ์„ฑ๊ณต + PaymentGateway-->>PaymentService: SUCCESS + PaymentService->>OrderRepository: updateStatus(orderId, PAID) + PaymentService-->>PaymentController: successResponse + PaymentController-->>User: 200 OK (๊ฒฐ์ œ ์™„๋ฃŒ) + else ๊ฒฐ์ œ ์‹คํŒจ + PaymentGateway-->>PaymentService: FAILED + PaymentService->>PointService: rollbackPoint(userId, amount) + PaymentService->>StockService: restoreStock(orderId) + PaymentService->>OrderRepository: updateStatus(orderId, FAILED) + PaymentController-->>User: 500 Internal Server Error (๊ฒฐ์ œ ์‹คํŒจ) + end +``` From 624b780acec71c75f283a61398e661f7faa545ee Mon Sep 17 00:00:00 2001 From: adminhelper <201401339hs@gmail.com> Date: Fri, 7 Nov 2025 16:58:59 +0900 Subject: [PATCH 42/85] =?UTF-8?q?docs=20:=20=EB=8F=84=EB=A9=94=EC=9D=B8=20?= =?UTF-8?q?=EA=B0=9D=EC=B2=B4=20=EC=84=A4=EA=B3=84=20(=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=20=EB=8B=A4=EC=9D=B4=EC=96=B4=EA=B7=B8=EB=9E=A8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/2round/03-class-diagram.md | 83 +++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 docs/2round/03-class-diagram.md diff --git a/docs/2round/03-class-diagram.md b/docs/2round/03-class-diagram.md new file mode 100644 index 000000000..19a4758e6 --- /dev/null +++ b/docs/2round/03-class-diagram.md @@ -0,0 +1,83 @@ +# ํด๋ž˜์Šค ๋‹ค์ด์–ด๊ทธ๋žจ + +```mermaid +classDiagram +direction TB + +class User { + Long id + String userId + String name + String email + String gender +} + +class Point { + Long id + Long userId + Long amount +} + +class Brand { + Long id + String name + String description +} + +class Product { + Long id + Long brandId + String name + Long price + String status +} + +class Stock { + Long productId + int quantity +} + +class Like { + Long id + String userId + Long productId + LocalDateTime createdAt +} + +class Order { + Long id + String userId + String status + Long totalPrice + LocalDateTime createdAt + List orderItems +} + +class OrderItem { + Long id + Long orderId + Long productId + int quantity + Long priceSnapshot +} + +class Payment { + Long id + Long orderId + String status + String paymentRequestId + LocalDateTime createdAt +} + +%% ๊ด€๊ณ„ ์„ค์ • +User --> Point +Brand --> Product +Product --> Stock +Product --> Like +User --> Like +User --> Order +Order --> OrderItem +Order --> Payment +OrderItem --> Product + +``` \ No newline at end of file From a97d77bd5a4c4ee7a1b58dd2b973b34484622828 Mon Sep 17 00:00:00 2001 From: adminhelper <201401339hs@gmail.com> Date: Fri, 7 Nov 2025 16:59:06 +0900 Subject: [PATCH 43/85] =?UTF-8?q?docs=20:=20=EC=A0=84=EC=B2=B4=20=ED=85=8C?= =?UTF-8?q?=EC=9D=B4=EB=B8=94=20=EA=B5=AC=EC=A1=B0=20=EB=B0=8F=20=EA=B4=80?= =?UTF-8?q?=EA=B3=84=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/2round/04-erd.md | 79 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 docs/2round/04-erd.md diff --git a/docs/2round/04-erd.md b/docs/2round/04-erd.md new file mode 100644 index 000000000..afc7cb8be --- /dev/null +++ b/docs/2round/04-erd.md @@ -0,0 +1,79 @@ +# ์—”ํ‹ฐํ‹ฐ ๋‹ค์ด์–ด๊ทธ๋žจ +```mermaid +erDiagram + USER { + bigint id PK + varchar user_id + varchar name + varchar email + varchar gender + } + + POINT { + bigint id PK + varchar user_id FK + bigint amount + } + + BRAND { + bigint id PK + varchar name + varchar description + } + + PRODUCT { + bigint id PK + bigint brand_id FK + varchar name + bigint price + varchar status + } + + STOCK { + bigint product_id PK, FK + int quantity + } + + LIKE { + bigint id PK + varchar user_id FK + bigint product_id FK + datetime created_at + } + + ORDERS { + bigint id PK + varchar user_id FK + varchar status + bigint total_price + datetime created_at + } + + ORDER_ITEM { + bigint id PK + bigint order_id FK + bigint product_id FK + int quantity + bigint price_snapshot + } + + PAYMENT { + bigint id PK + bigint order_id FK + varchar status + varchar payment_request_id + datetime created_at + } + + %% ๊ด€๊ณ„ ์„ค์ • (ํ•œ๊ธ€ ๋ฒ„์ „) + USER ||--|| POINT : "" + BRAND ||--o{ PRODUCT : "" + PRODUCT ||--|| STOCK : "" + PRODUCT ||--o{ LIKE : "" + USER ||--o{ LIKE : "" + USER ||--o{ ORDERS : "" + ORDERS ||--o{ ORDER_ITEM : "" + ORDERS ||--|| PAYMENT : "" + ORDER_ITEM }o--|| PRODUCT : "" + +``` \ No newline at end of file From 55f8c8b453da282f4adc3361abb8552e8880bb33 Mon Sep 17 00:00:00 2001 From: adminhelper <201401339hs@gmail.com> Date: Fri, 7 Nov 2025 16:59:16 +0900 Subject: [PATCH 44/85] =?UTF-8?q?docs=20:=202round=20=EB=AC=B8=EC=84=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/2round/2round.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 docs/2round/2round.md diff --git a/docs/2round/2round.md b/docs/2round/2round.md new file mode 100644 index 000000000..84fdc982c --- /dev/null +++ b/docs/2round/2round.md @@ -0,0 +1,37 @@ +## โœ๏ธ Design Quest + +> **์ด์ปค๋จธ์Šค ๋„๋ฉ”์ธ(์ƒํ’ˆ, ๋ธŒ๋žœ๋“œ, ์ข‹์•„์š”, ์ฃผ๋ฌธ ๋“ฑ)์— ๋Œ€ํ•œ ์„ค๊ณ„**๋ฅผ ์™„๋ฃŒํ•˜๊ณ , ๋‹ค์Œ ์ฃผ๋ถ€ํ„ฐ ๊ฐœ๋ฐœ ๊ฐ€๋Šฅํ•œ ์ˆ˜์ค€์˜ ์„ค๊ณ„ ๋ฌธ์„œ๋ฅผ ์ •๋ฆฌํ•˜์—ฌ PR๋กœ ์ œ์ถœํ•ฉ๋‹ˆ๋‹ค. +> + +### ๐Ÿ“‹ ๊ณผ์ œ ์ •๋ณด + +- **์„ค๊ณ„ ๋ฒ”์œ„** + - ์ƒํ’ˆ ๋ชฉ๋ก / ์ƒํ’ˆ ์ƒ์„ธ / ๋ธŒ๋žœ๋“œ ์กฐํšŒ + - ์ƒํ’ˆ ์ข‹์•„์š” ๋“ฑ๋ก/์ทจ์†Œ (๋ฉฑ๋“ฑ ๋™์ž‘) + - ์ฃผ๋ฌธ ์ƒ์„ฑ ๋ฐ ๊ฒฐ์ œ ํ๋ฆ„ (์žฌ๊ณ  ์ฐจ๊ฐ, ํฌ์ธํŠธ ์ฐจ๊ฐ, ์™ธ๋ถ€ ์‹œ์Šคํ…œ ์—ฐ๋™) +- **์ œ์™ธ ๋„๋ฉ”์ธ** + - ํšŒ์›๊ฐ€์ž…, ํฌ์ธํŠธ ์ถฉ์ „ (1์ฃผ์ฐจ ๊ตฌํ˜„ ์™„๋ฃŒ ๊ธฐ์ค€) +- **์š”๊ตฌ์‚ฌํ•ญ ๊ธฐ๋ฐ˜** + - ๋ฃจํ”„ํŒฉ ์ด์ปค๋จธ์Šค ์‹œ๋‚˜๋ฆฌ์˜ค ๋ฌธ์„œ๋ฅผ ๊ธฐ์ค€์œผ๋กœ ๊ธฐ๋Šฅ/์ œ์•ฝ์‚ฌํ•ญ์„ ์„ค๊ณ„์— ๋ฐ˜์˜ํ•ฉ๋‹ˆ๋‹ค. +- **์ œ์ถœ ๋ฐฉ์‹** + 1. ์•„๋ž˜ ํŒŒ์ผ๋“ค์„ ํ”„๋กœ์ ํŠธ ๋‚ด `docs/week2/` ํด๋”์— `.md`๋กœ ์ €์žฅ + 2. Github PR๋กœ ์ œ์ถœ + - PR ์ œ๋ชฉ: `[2์ฃผ์ฐจ] ์„ค๊ณ„ ๋ฌธ์„œ ์ œ์ถœ - ํ™๊ธธ๋™` + - PR ๋ณธ๋ฌธ์— ๋ฆฌ๋ทฐ ํฌ์ธํŠธ ํฌํ•จ (์˜ˆ: ๊ณ ๋ฏผํ•œ ์ง€์  ๋“ฑ) + +### โœ… ์ œ์ถœ ํŒŒ์ผ ๋ชฉ๋ก (.docs/design ๋””๋ ‰ํ† ๋ฆฌ ๋‚ด) + +| ํŒŒ์ผ๋ช… | ๋‚ด์šฉ | +| --- | --- | +| `01-requirements.md` | ์œ ์ € ์‹œ๋‚˜๋ฆฌ์˜ค ๊ธฐ๋ฐ˜ ๊ธฐ๋Šฅ ์ •์˜, ์š”๊ตฌ์‚ฌํ•ญ ๋ช…์„ธ | +| `02-sequence-diagrams.md` | ์‹œํ€€์Šค ๋‹ค์ด์–ด๊ทธ๋žจ ์ตœ์†Œ 2๊ฐœ ์ด์ƒ (Mermaid ๊ธฐ๋ฐ˜ ์ž‘์„ฑ ๊ถŒ์žฅ) | +| `03-class-diagram.md` | ๋„๋ฉ”์ธ ๊ฐ์ฒด ์„ค๊ณ„ (ํด๋ž˜์Šค ๋‹ค์ด์–ด๊ทธ๋žจ or ์„ค๋ช… ์ค‘์‹ฌ) | +| `04-erd.md` | ์ „์ฒด ํ…Œ์ด๋ธ” ๊ตฌ์กฐ ๋ฐ ๊ด€๊ณ„ ์ •๋ฆฌ (ERD Mermaid ์ž‘์„ฑ ๊ฐ€๋Šฅ) | + +## โœ… Checklist + +- [ ] ์ƒํ’ˆ/๋ธŒ๋žœ๋“œ/์ข‹์•„์š”/์ฃผ๋ฌธ ๋„๋ฉ”์ธ์ด ๋ชจ๋‘ ํฌํ•จ๋˜์–ด ์žˆ๋Š”๊ฐ€? +- [ ] ๊ธฐ๋Šฅ ์š”๊ตฌ์‚ฌํ•ญ์ด ์œ ์ € ์ค‘์‹ฌ์œผ๋กœ ์ •๋ฆฌ๋˜์–ด ์žˆ๋Š”๊ฐ€? +- [ ] ์‹œํ€€์Šค ๋‹ค์ด์–ด๊ทธ๋žจ์—์„œ ์ฑ…์ž„ ๊ฐ์ฒด๊ฐ€ ๋“œ๋Ÿฌ๋‚˜๋Š”๊ฐ€? +- [ ] ํด๋ž˜์Šค ๊ตฌ์กฐ๊ฐ€ ๋„๋ฉ”์ธ ์„ค๊ณ„๋ฅผ ์ž˜ ํ‘œํ˜„ํ•˜๊ณ  ์žˆ๋Š”๊ฐ€? +- [ ] ERD ์„ค๊ณ„ ์‹œ ๋ฐ์ดํ„ฐ ์ •ํ•ฉ์„ฑ์„ ๊ณ ๋ คํ•˜์—ฌ ๊ตฌ์„ฑํ•˜์˜€๋Š”๊ฐ€? \ No newline at end of file From 592a4e5e829a8c234dbd356abb76b5d81edcb709 Mon Sep 17 00:00:00 2001 From: adminhelper <201401339hs@gmail.com> Date: Fri, 7 Nov 2025 16:59:24 +0900 Subject: [PATCH 45/85] =?UTF-8?q?docs=20:=201round=20=EB=AC=B8=EC=84=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/{ => 1round}/1round.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/{ => 1round}/1round.md (100%) diff --git a/docs/1round.md b/docs/1round/1round.md similarity index 100% rename from docs/1round.md rename to docs/1round/1round.md From 4faa67e71c2e45c6d3d2c09ef61ceecbe46a2177 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A0=95=EB=B0=B0?= Date: Sat, 8 Nov 2025 12:43:45 +0900 Subject: [PATCH 46/85] =?UTF-8?q?round1:=20=EB=A6=AC=EB=B7=B0=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/loopers/domain/point/Point.java | 2 + .../domain/user/UserJpaRepository.java | 7 + .../com/loopers/domain/user/UserService.java | 2 +- .../interfaces/api/point/PointV1ApiSpec.java | 2 +- .../api/point/PointV1Controller.java | 3 + .../point/PointServiceIntegrationTest.java | 2 +- .../loopers/domain/user/UserModelTest.java | 170 ++++-------------- 7 files changed, 48 insertions(+), 140 deletions(-) diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java b/apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java index d27ebbe22..12c0c00c8 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java @@ -8,9 +8,11 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; +import lombok.Getter; @Entity @Table(name = "point") +@Getter public class Point extends BaseEntity { @ManyToOne @JoinColumn(referencedColumnName = "id", nullable = false, updatable = false) diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserJpaRepository.java index 7f6fb8222..d44b8e3db 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserJpaRepository.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserJpaRepository.java @@ -1,6 +1,9 @@ package com.loopers.domain.user; +import jakarta.persistence.LockModeType; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Lock; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; import java.util.Optional; @@ -9,5 +12,9 @@ public interface UserJpaRepository extends JpaRepository { Optional findByUserId(String userId); + @Lock(LockModeType.PESSIMISTIC_WRITE) + @Query("SELECT u FROM User u WHERE u.userId = :userId") + Optional findByUserIdForUpdate(String userId); + boolean existsUserByUserId(String userId); } diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java index be907604a..dc628535d 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java @@ -31,7 +31,7 @@ public Optional findByUserId(String userId) { // find by user id with lock for update @Transactional public Optional findByUserIdForUpdate(String userId) { - return userJpaRepository.findByUserId(userId); + return userJpaRepository.findByUserIdForUpdate(userId); } @Transactional(readOnly = true) public Optional getCurrentPoint(String userId) { diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1ApiSpec.java index bfb935ad2..fb18c23d6 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1ApiSpec.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1ApiSpec.java @@ -5,7 +5,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.tags.Tag; -@Tag(name = "Point V1 API", description = "์‚ฌ์šฉ์ž API ์ž…๋‹ˆ๋‹ค.") +@Tag(name = "Point V1 API", description = "ใ…ใ…—์ธํŠธ API ์ž…๋‹ˆ๋‹ค.") public interface PointV1ApiSpec { // /points diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Controller.java index d2fb3ce63..f92e19cd1 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Controller.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Controller.java @@ -31,6 +31,9 @@ public ApiResponse getUserPoints(@RequestHeader(value public ApiResponse chargeUserPoints( @RequestHeader(value = "X-USER-ID", required = false) String userId, @RequestBody PointV1Dto.PointChargeRequest request) { + if (StringUtils.isBlank(userId)) { + throw new CoreException(ErrorType.BAD_REQUEST); + } Long chargedPoint = pointFacade.chargePoints(userId, request.amount()); PointV1Dto.PointResponse response = PointV1Dto.PointResponse.from(chargedPoint); return ApiResponse.success(response); diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java index a2097aef9..49dd27c15 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java @@ -1,11 +1,11 @@ package com.loopers.domain.point; import com.loopers.support.error.CoreException; -import jakarta.transaction.Transactional; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; import static org.junit.jupiter.api.Assertions.assertThrows; diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserModelTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserModelTest.java index 45adade80..03b394446 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserModelTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserModelTest.java @@ -4,6 +4,8 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -17,7 +19,6 @@ class Create { private final String validBirthday = "1993-03-13"; private final String validGender = "male"; - // ID ๊ฐ€ ์˜๋ฌธ ๋ฐ ์ˆซ์ž 10์ž ์ด๋‚ด ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. @DisplayName("ID ๊ฐ€ ์˜๋ฌธ ๋ฐ ์ˆซ์ž 10์ž ์ด๋‚ด ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. - null ์ธ ๊ฒฝ์šฐ") @Test @@ -34,26 +35,15 @@ void throwsException_whenIdIsInvalidFormat_Null() { assertThat(result.getMessage()).isEqualTo("ID๋Š” ์˜๋ฌธ ๋ฐ ์ˆซ์ž 10์ž ์ด๋‚ด์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค."); } - @DisplayName("ID ๊ฐ€ ์˜๋ฌธ ๋ฐ ์ˆซ์ž 10์ž ์ด๋‚ด ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. - ์˜๋ฌธ ๋ฐ ์ˆซ์ž๊ฐ€ ์•„๋‹Œ ๊ฒฝ์šฐ") - @Test - void throwsException_whenIdIsInvalidFormat_NotAlphanumeric() { - // arrange - String invalidId = "user!@#"; - - // act - CoreException result = assertThrows(CoreException.class, () -> { - User.create(invalidId, validEmail, validBirthday, validGender); - }); - - // assert - assertThat(result.getMessage()).isEqualTo("ID๋Š” ์˜๋ฌธ ๋ฐ ์ˆซ์ž 10์ž ์ด๋‚ด์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค."); - } - - @DisplayName("ID ๊ฐ€ ์˜๋ฌธ ๋ฐ ์ˆซ์ž 10์ž ์ด๋‚ด ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. - ์˜๋ฌธ ๋ฐ ์ˆซ์ž 10์ž ์ดˆ๊ณผ์ธ ๊ฒฝ์šฐ") - @Test - void throwsException_whenIdIsInvalidFormat_TooLong() { - // arrange - String invalidId = "user1234567"; + @DisplayName("ID ๊ฐ€ ์˜๋ฌธ ๋ฐ ์ˆซ์ž 10์ž ์ด๋‚ด ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. - null์•„๋‹Œ ์—ฌ๋Ÿฌ ์ž˜๋ชป๋œ ํ˜•์‹๋“ค") + @ParameterizedTest + @ValueSource(strings = { + "", // ๋นˆ ๋ฌธ์ž์—ด + "user!@#", // ์˜๋ฌธ ๋ฐ ์ˆซ์ž๊ฐ€ ์•„๋‹Œ ๊ฒฝ์šฐ + "user1234567" // ์˜๋ฌธ ๋ฐ ์ˆซ์ž 10์ž ์ดˆ๊ณผ์ธ ๊ฒฝ์šฐ + }) + void throwsException_whenIdIsInvalidFormat(String invalidId) { + // arrange: invalidId parameter // act CoreException result = assertThrows(CoreException.class, () -> { @@ -85,57 +75,18 @@ void throwsException_whenEmailIsInvalidFormat_Null() { assertThat(result.getMessage()).isEqualTo("์ด๋ฉ”์ผ ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); } - @DisplayName("์ด๋ฉ”์ผ์ด xx@yy.zz ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. - @๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ") - @Test - void throwsException_whenEmailIsInvalidFormat_MissingAtSymbol() { - // arrange - String invalidEmail = "userexample.com"; - - // act - CoreException result = assertThrows(CoreException.class, () -> { - User.create(validId, invalidEmail, validBirthday, validGender); - }); - - // assert - assertThat(result.getMessage()).isEqualTo("์ด๋ฉ”์ผ ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); - } - - @DisplayName("์ด๋ฉ”์ผ์ด xx@yy.zz ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. - ๋„๋ฉ”์ธ ๋ถ€๋ถ„์ด ์—†๋Š” ๊ฒฝ์šฐ") - @Test - void throwsException_whenEmailIsInvalidFormat_MissingDomain() { - // arrange - String invalidEmail = "user@.com"; - - // act - CoreException result = assertThrows(CoreException.class, () -> { - User.create(validId, invalidEmail, validBirthday, validGender); - }); - - // assert - assertThat(result.getMessage()).isEqualTo("์ด๋ฉ”์ผ ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); - } - - @DisplayName("์ด๋ฉ”์ผ์ด xx@yy.zz ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. - ์ตœ์ƒ์œ„ ๋„๋ฉ”์ธ์ด ์—†๋Š” ๊ฒฝ์šฐ") - @Test - void throwsException_whenEmailIsInvalidFormat_MissingTopLevelDomain() { - // arrange - String invalidEmail = "user@example"; - - // act - CoreException result = assertThrows(CoreException.class, () -> { - User.create(validId, invalidEmail, validBirthday, validGender); - }); - - // assert - assertThat(result.getMessage()).isEqualTo("์ด๋ฉ”์ผ ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); - } - - @DisplayName("์ด๋ฉ”์ผ์ด xx@yy.zz ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. - @.๋งŒ ์žˆ๋Š” ๊ฒฝ์šฐ") - @Test - void throwsException_whenEmailIsInvalidFormat_OnlyAtAndDot() { - // arrange - String invalidEmail = "@."; + @DisplayName("์ด๋ฉ”์ผ์ด xx@yy.zz ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. - null ์ด ์•„๋‹Œ ์—ฌ๋Ÿฌ ์ž˜๋ชป๋œ ํ˜•์‹๋“ค") + @ParameterizedTest + @ValueSource(strings = { + "", // ๋นˆ ๋ฌธ์ž์—ด + "userexample.com", // @๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ + "user@.com", // ๋„๋ฉ”์ธ ๋ถ€๋ถ„์ด ์—†๋Š” ๊ฒฝ์šฐ + "user@example", // ์ตœ์ƒ์œ„ ๋„๋ฉ”์ธ์ด ์—†๋Š” ๊ฒฝ์šฐ + "@." // @.๋งŒ ์žˆ๋Š” ๊ฒฝ์šฐ + }) + void throwsException_whenEmailIsInvalidFormat(String invalidEmail) { + // arrange: invalidEmail parameter // act CoreException result = assertThrows(CoreException.class, () -> { User.create(validId, invalidEmail, validBirthday, validGender); @@ -165,72 +116,17 @@ void throwsException_whenBirthdayIsInvalidFormat_Null() { assertThat(result.getMessage()).isEqualTo("์ƒ๋…„์›”์ผ ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); } - @DisplayName("์ƒ๋…„์›”์ผ์ด yyyy-MM-dd ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. - ์ž˜๋ชป๋œ ํ˜•์‹์ธ ๊ฒฝ์šฐ 13-03-1993") - @Test - void throwsException_whenBirthdayIsInvalidFormat() { - // arrange - String invalidBirthday = "13-03-1993"; - - // act - CoreException result = assertThrows(CoreException.class, () -> { - User.create(validId, validEmail, invalidBirthday, validGender); - }); - - // assert - assertThat(result.getMessage()).isEqualTo("์ƒ๋…„์›”์ผ ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); - } - - @DisplayName("์ƒ๋…„์›”์ผ์ด yyyy-MM-dd ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. - ์ž˜๋ชป๋œ ํ˜•์‹์ธ ๊ฒฝ์šฐ 1993/03/13") - @Test - void throwsException_whenBirthdayIsInvalidFormat_Slashes() { - // arrange - String invalidBirthday = "1993/03/13"; - - // act - CoreException result = assertThrows(CoreException.class, () -> { - User.create(validId, validEmail, invalidBirthday, validGender); - }); - - // assert - assertThat(result.getMessage()).isEqualTo("์ƒ๋…„์›”์ผ ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); - } - - @DisplayName("์ƒ๋…„์›”์ผ์ด yyyy-MM-dd ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. - ์ž˜๋ชป๋œ ํ˜•์‹์ธ ๊ฒฝ์šฐ 19930313") - @Test - void throwsException_whenBirthdayIsInvalidFormat_NoSeparators() { - // arrange - String invalidBirthday = "19930313"; - - // act - CoreException result = assertThrows(CoreException.class, () -> { - User.create(validId, validEmail, invalidBirthday, validGender); - }); - - // assert - assertThat(result.getMessage()).isEqualTo("์ƒ๋…„์›”์ผ ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); - } - - @DisplayName("์ƒ๋…„์›”์ผ์ด yyyy-MM-dd ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. - 930313") - @Test - void throwsException_whenBirthdayIsInvalidFormat_ShortDate() { - // arrange - String invalidBirthday = "930313"; - - // act - CoreException result = assertThrows(CoreException.class, () -> { - User.create(validId, validEmail, invalidBirthday, validGender); - }); - - // assert - assertThat(result.getMessage()).isEqualTo("์ƒ๋…„์›”์ผ ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); - } - - @DisplayName("์ƒ๋…„์›”์ผ์ด yyyy-MM-dd ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. - ๋นˆ ๋ฌธ์ž์—ด") - @Test - void throwsException_whenBirthdayIsInvalidFormat_EmptyString() { - // arrange - String invalidBirthday = ""; - + @DisplayName("์ƒ๋…„์›”์ผ์ด yyyy-MM-dd ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. - null์ด ์•„๋‹Œ ์—ฌ๋Ÿฌ ์ž˜๋ชป๋œ ํ˜•์‹๋“ค") + @ParameterizedTest + @ValueSource(strings = { + "13-03-1993", // ์ž˜๋ชป๋œ ํ˜•์‹ + "1993/03/13", // ์ž˜๋ชป๋œ ํ˜•์‹ + "19930313", // ์ž˜๋ชป๋œ ํ˜•์‹ + "930313", // ์ž˜๋ชป๋œ ํ˜•์‹ + "" // ๋นˆ ๋ฌธ์ž์—ด + }) + void throwsException_whenBirthdayIsInvalidFormat(String invalidBirthday) { + // arrange: invalidBirthday parameter // act CoreException result = assertThrows(CoreException.class, () -> { User.create(validId, validEmail, invalidBirthday, validGender); From 86a8205b09436cae10c4bcd958aaf3b6828723db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A0=95=EB=B0=B0?= Date: Tue, 11 Nov 2025 19:02:31 +0900 Subject: [PATCH 47/85] =?UTF-8?q?round1:=20=EB=A6=AC=EB=B7=B0=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ํฌ์ธํŠธ 1:1๋กœ ๊ด€๊ณ„ ์ˆ˜์ • ๋„๋ฉ”์ธ ๋ถ„๋ฆฌ(User.currentPoint ์ œ๊ฑฐ) --- .../application/point/PointFacade.java | 16 ++++-- .../loopers/application/user/UserFacade.java | 5 ++ .../java/com/loopers/domain/point/Point.java | 21 ++++---- .../domain/point/PointJpaRepository.java | 3 ++ .../loopers/domain/point/PointService.java | 25 +++++---- .../java/com/loopers/domain/user/User.java | 3 -- .../com/loopers/domain/user/UserService.java | 5 +- .../api/point/PointV1Controller.java | 2 +- .../loopers/domain/point/PointModelTest.java | 11 +--- .../point/PointServiceIntegrationTest.java | 52 ++++++++++++++++++- .../user/UserServiceIntegrationTest.java | 39 -------------- 11 files changed, 101 insertions(+), 81 deletions(-) diff --git a/apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java index 1a9af650f..126a528de 100644 --- a/apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java @@ -1,24 +1,32 @@ package com.loopers.application.point; import com.loopers.domain.point.PointService; +import com.loopers.domain.user.User; import com.loopers.domain.user.UserService; import com.loopers.support.error.CoreException; import com.loopers.support.error.ErrorType; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; @RequiredArgsConstructor @Component public class PointFacade { - private final UserService userService; private final PointService pointService; + private final UserService userService; + @Transactional(readOnly = true) public Long getCurrentPoint(String userId) { - return userService.getCurrentPoint(userId) + User user = userService.findByUserId(userId).orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); + + return pointService.getCurrentPoint(user.getId()) .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); } - public Long chargePoints(String userId, int amount) { - return pointService.chargePoint(userId, amount); + @Transactional + public Long chargePoint(String userId, int amount) { + User user = userService.findByUserIdForUpdate(userId).orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); + + return pointService.chargePoint(user.getId(), amount); } } diff --git a/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java index 848eed169..030d014b6 100644 --- a/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java @@ -1,11 +1,13 @@ package com.loopers.application.user; +import com.loopers.domain.point.PointService; import com.loopers.domain.user.User; import com.loopers.domain.user.UserService; import com.loopers.support.error.CoreException; import com.loopers.support.error.ErrorType; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; import java.util.Optional; @@ -13,9 +15,12 @@ @Component public class UserFacade { private final UserService userService; + private final PointService pointService; + @Transactional public UserInfo registerUser(String userId, String email, String birthday, String gender) { User registeredUser = userService.registerUser(userId, email, birthday, gender); + pointService.createPoint(registeredUser.getId()); return UserInfo.from(registeredUser); } diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java b/apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java index 12c0c00c8..8673786b4 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java @@ -1,12 +1,10 @@ package com.loopers.domain.point; import com.loopers.domain.BaseEntity; -import com.loopers.domain.user.User; import com.loopers.support.error.CoreException; import com.loopers.support.error.ErrorType; +import jakarta.persistence.Column; import jakarta.persistence.Entity; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; import lombok.Getter; @@ -14,24 +12,27 @@ @Table(name = "point") @Getter public class Point extends BaseEntity { - @ManyToOne - @JoinColumn(referencedColumnName = "id", nullable = false, updatable = false) - private User user; + @Column(name = "user_id", nullable = false, updatable = false, unique = true) + private Long userId; private Long amount; protected Point() { } - private Point(User user, Long amount) { - this.user = user; + private Point(Long userId, Long amount) { + this.userId = userId; this.amount = amount; } - public static Point create(User user, int amount) { + public static Point create(Long userId) { + return new Point(userId, 0L); + } + + public void charge(int amount) { if (amount <= 0) { throw new CoreException(ErrorType.BAD_REQUEST, "์ถฉ์ „ ํฌ์ธํŠธ๋Š” 0๋ณด๋‹ค ์ปค์•ผ ํ•ฉ๋‹ˆ๋‹ค."); } - return new Point(user, (long) amount); + this.amount += amount; } } diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/point/PointJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/point/PointJpaRepository.java index 2c1e365a2..1a8ea9b67 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/point/PointJpaRepository.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/point/PointJpaRepository.java @@ -3,7 +3,10 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import java.util.Optional; + @Repository public interface PointJpaRepository extends JpaRepository { + Optional findByUserId(Long userId); } diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java b/apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java index ce0960041..723bf078f 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java @@ -1,28 +1,35 @@ package com.loopers.domain.point; -import com.loopers.domain.user.User; -import com.loopers.domain.user.UserService; import com.loopers.support.error.CoreException; import com.loopers.support.error.ErrorType; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.Optional; + @RequiredArgsConstructor @Service public class PointService { private final PointJpaRepository pointJpaRepository; - private final UserService userService; @Transactional - public Long chargePoint(String userId, int amount) { - User user = userService.findByUserIdForUpdate(userId).orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); - - Point point = Point.create(user, amount); + public void createPoint(Long userId) { + Point point = Point.create(userId); + pointJpaRepository.save(point); + } - user.setCurrentPoint(user.getCurrentPoint() + amount); + @Transactional + public Long chargePoint(Long userId, int amount) { + Point point = pointJpaRepository.findByUserId(userId) + .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "ํฌ์ธํŠธ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); + point.charge(amount); pointJpaRepository.save(point); + return point.getAmount(); + } - return user.getCurrentPoint(); + @Transactional(readOnly = true) + public Optional getCurrentPoint(Long userId) { + return pointJpaRepository.findByUserId(userId).map(Point::getAmount); } } diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java index 908a5efac..eefd20d78 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java @@ -7,7 +7,6 @@ import jakarta.persistence.Entity; import jakarta.persistence.Table; import lombok.Getter; -import lombok.Setter; import org.apache.commons.lang3.StringUtils; @Entity @@ -22,8 +21,6 @@ protected User() { private String email; private String birthday; private String gender; - @Setter - private Long currentPoint = 0L; private User(String userId, String email, String birthday, String gender) { this.userId = userId; diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java index dc628535d..c4e9d6fd5 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java @@ -33,8 +33,5 @@ public Optional findByUserId(String userId) { public Optional findByUserIdForUpdate(String userId) { return userJpaRepository.findByUserIdForUpdate(userId); } - @Transactional(readOnly = true) - public Optional getCurrentPoint(String userId) { - return findByUserId(userId).map(User::getCurrentPoint); - } + } diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Controller.java index f92e19cd1..285e70a04 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Controller.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Controller.java @@ -34,7 +34,7 @@ public ApiResponse chargeUserPoints( if (StringUtils.isBlank(userId)) { throw new CoreException(ErrorType.BAD_REQUEST); } - Long chargedPoint = pointFacade.chargePoints(userId, request.amount()); + Long chargedPoint = pointFacade.chargePoint(userId, request.amount()); PointV1Dto.PointResponse response = PointV1Dto.PointResponse.from(chargedPoint); return ApiResponse.success(response); } diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/point/PointModelTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/point/PointModelTest.java index 6eb3f5f72..8e702e629 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/point/PointModelTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/point/PointModelTest.java @@ -1,6 +1,5 @@ package com.loopers.domain.point; -import com.loopers.domain.user.User; import com.loopers.support.error.CoreException; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -20,15 +19,9 @@ class Create { @ValueSource(ints = {0, -10, -100}) void throwsException_whenPointIsZeroOrNegative(int invalidPoint) { // arrange - String validId = "user123"; - String validEmail = "xx@yy.zz"; - String validBirthday = "1993-03-13"; - String validGender = "male"; - - User user = User.create(validId, validEmail, validBirthday, validGender); - + Point point = Point.create(0L); // act - CoreException result = assertThrows(CoreException.class, () -> Point.create(user, invalidPoint)); + CoreException result = assertThrows(CoreException.class, () -> point.charge(invalidPoint)); // assert assertThat(result.getMessage()).isEqualTo("์ถฉ์ „ ํฌ์ธํŠธ๋Š” 0๋ณด๋‹ค ์ปค์•ผ ํ•ฉ๋‹ˆ๋‹ค."); diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java index 49dd27c15..b22645d4f 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java @@ -1,19 +1,68 @@ package com.loopers.domain.point; +import com.loopers.application.point.PointFacade; +import com.loopers.domain.user.User; +import com.loopers.domain.user.UserService; import com.loopers.support.error.CoreException; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.transaction.annotation.Transactional; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; @SpringBootTest @Transactional public class PointServiceIntegrationTest { + @Autowired + private PointFacade pointFacade; @Autowired private PointService pointService; + @Autowired + private UserService userService; + + @BeforeEach + void setUp() { + String validId = "user123"; + String validEmail = "xx@yy.zz"; + String validBirthday = "1993-03-13"; + String validGender = "male"; + // ์œ ์ € ๋“ฑ๋ก + User registeredUser = userService.registerUser(validId, validEmail, validBirthday, validGender); + pointService.createPoint(registeredUser.getId()); + } + + @DisplayName("ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•  ๊ฒฝ์šฐ, ๋ณด์œ  ํฌ์ธํŠธ๊ฐ€ ๋ฐ˜ํ™˜๋œ๋‹ค.") + @Test + void returnsUserPoints_whenUserExists() { + // arrange: setUp() ๋ฉ”์„œ๋“œ์—์„œ ์ด๋ฏธ ์œ ์ € ๋“ฑ๋ก + String existingUserId = "user123"; + Long userId = userService.findByUserId(existingUserId).get().getId(); + + // act + Optional currentPoint = pointService.getCurrentPoint(userId); + + // assert + assertThat(currentPoint.orElse(null)).isEqualTo(0L); + } + + @DisplayName("ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ, null ์ด ๋ฐ˜ํ™˜๋œ๋‹ค.") + @Test + void returnsNullPoints_whenUserDoesNotExist() { + // arrange: setUp() ๋ฉ”์„œ๋“œ์—์„œ ๋“ฑ๋ก๋˜์ง€ ์•Š์€ ์œ ์ € ID ์‚ฌ์šฉ + Long nonExistingUserId = -1L; + + // act + Optional currentPoint = pointService.getCurrentPoint(nonExistingUserId); + + // assert + assertThat(currentPoint).isNotPresent(); + } //์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ์ € ID ๋กœ ์ถฉ์ „์„ ์‹œ๋„ํ•œ ๊ฒฝ์šฐ, ์‹คํŒจํ•œ๋‹ค. @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ์ € ID ๋กœ ์ถฉ์ „์„ ์‹œ๋„ํ•œ ๊ฒฝ์šฐ, ์‹คํŒจํ•œ๋‹ค.") @@ -24,7 +73,6 @@ void throwsExceptionWhenChargePointWithNonExistingUserId() { int chargeAmount = 1000; // act & assert - assertThrows(CoreException.class, () -> pointService.chargePoint(nonExistingUserId, chargeAmount)); + assertThrows(CoreException.class, () -> pointFacade.chargePoint(nonExistingUserId, chargeAmount)); } - } diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java index b7698e76c..299efaf2c 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java @@ -105,43 +105,4 @@ void returnsNull_whenUserDoesNotExist() { // assert assertThat(foundUser).isNotPresent(); } - - @DisplayName("ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•  ๊ฒฝ์šฐ, ๋ณด์œ  ํฌ์ธํŠธ๊ฐ€ ๋ฐ˜ํ™˜๋œ๋‹ค.") - @Test - void returnsUserPoints_whenUserExists() { - // arrange - String validId = "user123"; - String validEmail = "xx@yy.zz"; - String validBirthday = "1993-03-13"; - String validGender = "male"; - // ์œ ์ € ๋“ฑ๋ก - userService.registerUser(validId, validEmail, validBirthday, validGender); - String existingUserId = "user123"; - - // act - Optional currentPoint = userService.getCurrentPoint(existingUserId); - - // assert - assertThat(currentPoint).isPresent(); - assertThat(currentPoint.get()).isEqualTo(0L); - } - - @DisplayName("ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ, null ์ด ๋ฐ˜ํ™˜๋œ๋‹ค.") - @Test - void returnsNullPoints_whenUserDoesNotExist() { - // arrange - String validId = "user123"; - String validEmail = "xx@yy.zz"; - String validBirthday = "1993-03-13"; - String validGender = "male"; - // ์œ ์ € ๋“ฑ๋ก - userService.registerUser(validId, validEmail, validBirthday, validGender); - String nonExistingId = "nonexist"; - - // act - Optional currentPoint = userService.getCurrentPoint(nonExistingId); - - // assert - assertThat(currentPoint).isNotPresent(); - } } From d06a0d0f5830cc99a98236ef419a37c2355cc5d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A0=95=EB=B0=B0?= Date: Wed, 12 Nov 2025 02:27:25 +0900 Subject: [PATCH 48/85] =?UTF-8?q?round1:=20=EB=A6=AC=EB=B7=B0=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9=20-=20JPA=20repo=20=EA=B5=AC=ED=98=84=EC=B2=B4=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/loopers/domain/point/Point.java | 2 +- .../loopers/domain/point/PointRepository.java | 9 +++++ .../loopers/domain/point/PointService.java | 10 +++--- .../java/com/loopers/domain/user/User.java | 2 +- .../loopers/domain/user/UserRepository.java | 13 +++++++ .../com/loopers/domain/user/UserService.java | 10 +++--- .../point/PointJpaRepository.java | 5 ++- .../point/PointRepositoryImpl.java | 24 +++++++++++++ .../user/UserJpaRepository.java | 5 ++- .../user/UserRepositoryImpl.java | 34 +++++++++++++++++++ .../user/UserServiceIntegrationTest.java | 2 +- 11 files changed, 97 insertions(+), 19 deletions(-) create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/point/PointRepository.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java rename apps/commerce-api/src/main/java/com/loopers/{domain => infrastructure}/point/PointJpaRepository.java (69%) create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointRepositoryImpl.java rename apps/commerce-api/src/main/java/com/loopers/{domain => infrastructure}/user/UserJpaRepository.java (85%) create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java b/apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java index 8673786b4..dbf1bcd61 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java @@ -9,7 +9,7 @@ import lombok.Getter; @Entity -@Table(name = "point") +@Table(name = "tb_point") @Getter public class Point extends BaseEntity { @Column(name = "user_id", nullable = false, updatable = false, unique = true) diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/point/PointRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/point/PointRepository.java new file mode 100644 index 000000000..07b90479c --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/point/PointRepository.java @@ -0,0 +1,9 @@ +package com.loopers.domain.point; + +import java.util.Optional; + +public interface PointRepository { + Optional findByUserId(Long userId); + + void save(Point point); +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java b/apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java index 723bf078f..f557f79e8 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java @@ -11,25 +11,25 @@ @RequiredArgsConstructor @Service public class PointService { - private final PointJpaRepository pointJpaRepository; + private final PointRepository pointRepository; @Transactional public void createPoint(Long userId) { Point point = Point.create(userId); - pointJpaRepository.save(point); + pointRepository.save(point); } @Transactional public Long chargePoint(Long userId, int amount) { - Point point = pointJpaRepository.findByUserId(userId) + Point point = pointRepository.findByUserId(userId) .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "ํฌ์ธํŠธ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); point.charge(amount); - pointJpaRepository.save(point); + pointRepository.save(point); return point.getAmount(); } @Transactional(readOnly = true) public Optional getCurrentPoint(Long userId) { - return pointJpaRepository.findByUserId(userId).map(Point::getAmount); + return pointRepository.findByUserId(userId).map(Point::getAmount); } } diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java index eefd20d78..bd8bc6adf 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java @@ -10,7 +10,7 @@ import org.apache.commons.lang3.StringUtils; @Entity -@Table(name = "user") +@Table(name = "tb_user") @Getter public class User extends BaseEntity { protected User() { diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java new file mode 100644 index 000000000..90f701fbd --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java @@ -0,0 +1,13 @@ +package com.loopers.domain.user; + +import java.util.Optional; + +public interface UserRepository { + Optional findByUserId(String userId); + + Optional findByUserIdForUpdate(String userId); + + boolean existsUserByUserId(String userId); + + User save(User user); +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java index c4e9d6fd5..8b1129b45 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java @@ -11,27 +11,27 @@ @RequiredArgsConstructor @Service public class UserService { - private final UserJpaRepository userJpaRepository; + private final UserRepository userRepository; @Transactional public User registerUser(String userId, String email, String birthday, String gender) { // ์ด๋ฏธ ๋“ฑ๋ก๋œ userId ์ธ ๊ฒฝ์šฐ, ์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚จ๋‹ค. - if (userJpaRepository.existsUserByUserId(userId)) { + if (userRepository.existsUserByUserId(userId)) { throw new CoreException(ErrorType.CONFLICT, "์ด๋ฏธ ์กด์žฌํ•˜๋Š” ์‚ฌ์šฉ์ž ID ์ž…๋‹ˆ๋‹ค."); } User user = User.create(userId, email, birthday, gender); - return userJpaRepository.save(user); + return userRepository.save(user); } @Transactional(readOnly = true) public Optional findByUserId(String userId) { - return userJpaRepository.findByUserId(userId); + return userRepository.findByUserId(userId); } // find by user id with lock for update @Transactional public Optional findByUserIdForUpdate(String userId) { - return userJpaRepository.findByUserIdForUpdate(userId); + return userRepository.findByUserIdForUpdate(userId); } } diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/point/PointJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointJpaRepository.java similarity index 69% rename from apps/commerce-api/src/main/java/com/loopers/domain/point/PointJpaRepository.java rename to apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointJpaRepository.java index 1a8ea9b67..74320f6a2 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/point/PointJpaRepository.java +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointJpaRepository.java @@ -1,11 +1,10 @@ -package com.loopers.domain.point; +package com.loopers.infrastructure.point; +import com.loopers.domain.point.Point; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; import java.util.Optional; -@Repository public interface PointJpaRepository extends JpaRepository { Optional findByUserId(Long userId); diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointRepositoryImpl.java new file mode 100644 index 000000000..052de7762 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointRepositoryImpl.java @@ -0,0 +1,24 @@ +package com.loopers.infrastructure.point; + +import com.loopers.domain.point.Point; +import com.loopers.domain.point.PointRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +@RequiredArgsConstructor +@Component +public class PointRepositoryImpl implements PointRepository { + private final PointJpaRepository pointJpaRepository; + + @Override + public Optional findByUserId(Long userId) { + return pointJpaRepository.findByUserId(userId); + } + + @Override + public void save(Point point) { + pointJpaRepository.save(point); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserJpaRepository.java similarity index 85% rename from apps/commerce-api/src/main/java/com/loopers/domain/user/UserJpaRepository.java rename to apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserJpaRepository.java index d44b8e3db..0f298e023 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserJpaRepository.java +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserJpaRepository.java @@ -1,14 +1,13 @@ -package com.loopers.domain.user; +package com.loopers.infrastructure.user; +import com.loopers.domain.user.User; import jakarta.persistence.LockModeType; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Lock; import org.springframework.data.jpa.repository.Query; -import org.springframework.stereotype.Repository; import java.util.Optional; -@Repository public interface UserJpaRepository extends JpaRepository { Optional findByUserId(String userId); diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java new file mode 100644 index 000000000..2018487e8 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java @@ -0,0 +1,34 @@ +package com.loopers.infrastructure.user; + +import com.loopers.domain.user.User; +import com.loopers.domain.user.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +@RequiredArgsConstructor +@Component +public class UserRepositoryImpl implements UserRepository { + private final UserJpaRepository userJpaRepository; + + @Override + public Optional findByUserId(String userId) { + return userJpaRepository.findByUserId(userId); + } + + @Override + public Optional findByUserIdForUpdate(String userId) { + return userJpaRepository.findByUserIdForUpdate(userId); + } + + @Override + public boolean existsUserByUserId(String userId) { + return userJpaRepository.existsUserByUserId(userId); + } + + @Override + public User save(User user) { + return userJpaRepository.save(user); + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java index 299efaf2c..0bd755cdf 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java @@ -22,7 +22,7 @@ public class UserServiceIntegrationTest { private UserService userService; @MockitoSpyBean - private UserJpaRepository spyUserRepository; + private UserRepository spyUserRepository; @DisplayName("ํšŒ์› ๊ฐ€์ž…์‹œ User ์ €์žฅ์ด ์ˆ˜ํ–‰๋œ๋‹ค.") @Test From 5042c22dc6520c7bbb96a2c626346c6f16751e0a Mon Sep 17 00:00:00 2001 From: adminhelper <201401339hs@gmail.com> Date: Fri, 14 Nov 2025 16:45:18 +0900 Subject: [PATCH 49/85] =?UTF-8?q?docs=20:=202round=20=EB=AC=B8=EC=84=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=2003-class-diagram.md=2004-erd.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/2round/03-class-diagram.md | 19 +++++++-------- docs/2round/04-erd.md | 41 +++++++++++++++------------------ 2 files changed, 28 insertions(+), 32 deletions(-) diff --git a/docs/2round/03-class-diagram.md b/docs/2round/03-class-diagram.md index 19a4758e6..45421dc8b 100644 --- a/docs/2round/03-class-diagram.md +++ b/docs/2round/03-class-diagram.md @@ -14,14 +14,13 @@ class User { class Point { Long id - Long userId - Long amount + String userId + Long balance } class Brand { Long id String name - String description } class Product { @@ -29,7 +28,8 @@ class Product { Long brandId String name Long price - String status + Long likeCount; + Long stock } class Stock { @@ -47,18 +47,19 @@ class Like { class Order { Long id String userId - String status Long totalPrice + OrderStatus status LocalDateTime createdAt List orderItems } class OrderItem { Long id - Long orderId + Order order Long productId - int quantity - Long priceSnapshot + String productName + Long quantity + Long price } class Payment { @@ -71,7 +72,7 @@ class Payment { %% ๊ด€๊ณ„ ์„ค์ • User --> Point -Brand --> Product +Brand --> Product Product --> Stock Product --> Like User --> Like diff --git a/docs/2round/04-erd.md b/docs/2round/04-erd.md index afc7cb8be..6389b2202 100644 --- a/docs/2round/04-erd.md +++ b/docs/2round/04-erd.md @@ -1,4 +1,5 @@ -# ์—”ํ‹ฐํ‹ฐ ๋‹ค์ด์–ด๊ทธ๋žจ +# erd + ```mermaid erDiagram USER { @@ -12,13 +13,12 @@ erDiagram POINT { bigint id PK varchar user_id FK - bigint amount + bigint balance } BRAND { bigint id PK varchar name - varchar description } PRODUCT { @@ -26,12 +26,8 @@ erDiagram bigint brand_id FK varchar name bigint price - varchar status - } - - STOCK { - bigint product_id PK, FK - int quantity + bigint like_count + bigint stock } LIKE { @@ -44,8 +40,8 @@ erDiagram ORDERS { bigint id PK varchar user_id FK + bigint total_amount varchar status - bigint total_price datetime created_at } @@ -53,8 +49,9 @@ erDiagram bigint id PK bigint order_id FK bigint product_id FK - int quantity - bigint price_snapshot + varchar product_name + bigint quantity + bigint price } PAYMENT { @@ -65,15 +62,13 @@ erDiagram datetime created_at } - %% ๊ด€๊ณ„ ์„ค์ • (ํ•œ๊ธ€ ๋ฒ„์ „) - USER ||--|| POINT : "" - BRAND ||--o{ PRODUCT : "" - PRODUCT ||--|| STOCK : "" - PRODUCT ||--o{ LIKE : "" - USER ||--o{ LIKE : "" - USER ||--o{ ORDERS : "" - ORDERS ||--o{ ORDER_ITEM : "" - ORDERS ||--|| PAYMENT : "" - ORDER_ITEM }o--|| PRODUCT : "" - + %% ๊ด€๊ณ„ (cardinality) + USER ||--|| POINT : "1:1" + BRAND ||--o{ PRODUCT : "1:N" + PRODUCT ||--o{ LIKE : "1:N" + USER ||--o{ LIKE : "1:N" + USER ||--o{ ORDERS : "1:N" + ORDERS ||--o{ ORDER_ITEM : "1:N" + ORDER_ITEM }o--|| PRODUCT : "N:1" + ORDERS ||--|| PAYMENT : "1:1" ``` \ No newline at end of file From 4fda674eb233af0586f8757a8fc2d68d46e2cb42 Mon Sep 17 00:00:00 2001 From: adminhelper <201401339hs@gmail.com> Date: Fri, 14 Nov 2025 16:45:30 +0900 Subject: [PATCH 50/85] =?UTF-8?q?docs=20:=203round=20=EB=AC=B8=EC=84=9C?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/3round/3round.md | 60 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 docs/3round/3round.md diff --git a/docs/3round/3round.md b/docs/3round/3round.md new file mode 100644 index 000000000..61df49f68 --- /dev/null +++ b/docs/3round/3round.md @@ -0,0 +1,60 @@ +# ๐Ÿ“ Round 3 Quests + +--- + +## ๐Ÿ’ป Implementation Quest + +> *** ๋„๋ฉ”์ธ ๋ชจ๋ธ๋ง**์„ ํ†ตํ•ด Product, Brand, Like, Order ๊ธฐ๋Šฅ์˜ ํ•ต์‹ฌ ๊ฐœ๋…์„ **Entity, Value Object, Domain Service ๋“ฑ ์ ํ•ฉํ•œ** **์ฝ”๋“œ**๋กœ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค. +* ๋ ˆ์ด์–ด๋“œ ์•„ํ‚คํ…์ฒ˜ + DIP ๋ฅผ ์ ์šฉํ•ด ์œ ์—ฐํ•˜๊ณ  ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅํ•œ ๊ตฌ์กฐ๋กœ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค. +* **Application Layer๋ฅผ ๊ฒฝ๋Ÿ‰ ์ˆ˜์ค€**์œผ๋กœ ๊ตฌ์„ฑํ•˜์—ฌ, ๋„๋ฉ”์ธ ๊ฐ์ฒด๋ฅผ ์กฐํ•ฉํ•˜๋Š” ํ๋ฆ„์„ ์‹ค์ œ ๊ตฌํ˜„ํ•ด๋ด…๋‹ˆ๋‹ค. +* **๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑ**ํ•˜์—ฌ ๋„๋ฉ”์ธ ๋กœ์ง์˜ ์ •ํ•ฉ์„ฑ๊ณผ ๊ทœ์น™์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. +> + +### ๐Ÿ“‹ ๊ณผ์ œ ์ •๋ณด + +- ์ƒํ’ˆ, ๋ธŒ๋žœ๋“œ, ์ข‹์•„์š”, ์ฃผ๋ฌธ ๊ธฐ๋Šฅ์˜ **๋„๋ฉ”์ธ ๋ชจ๋ธ ๋ฐ ๋„๋ฉ”์ธ ์„œ๋น„์Šค**๋ฅผ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค. +- ๋„๋ฉ”์ธ ๊ฐ„ ํ˜‘๋ ฅ ํ๋ฆ„์„ ์„ค๊ณ„ํ•˜๊ณ , ํ•„์š”ํ•œ ๋กœ์ง์„ **๋„๋ฉ”์ธ ์„œ๋น„์Šค**๋กœ ๋ถ„๋ฆฌํ•ฉ๋‹ˆ๋‹ค. +- Application Layer์—์„œ ๋„๋ฉ”์ธ ๊ฐ์ฒด๋ฅผ ์กฐํ•ฉํ•˜๋Š” ํ๋ฆ„์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค. + (์˜ˆ: `ProductFacade.getProductDetail(productId)` โ†’ `Product + Brand + Like ์กฐํ•ฉ`) +- Repository Interface ์™€ ๊ตฌํ˜„์ฒด๋Š” ๋ถ„๋ฆฌํ•˜๊ณ , ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅ์„ฑ์„ ๊ณ ๋ คํ•œ ๊ตฌ์กฐ๋ฅผ ์„ค๊ณ„ํ•ฉ๋‹ˆ๋‹ค. +- ๋ชจ๋“  ํ•ต์‹ฌ ๋„๋ฉ”์ธ ๋กœ์ง์— ๋Œ€ํ•ด ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•˜๊ณ , ์˜ˆ์™ธ/๊ฒฝ๊ณ„ ์ผ€์ด์Šค๋„ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. + +### ๐Ÿท Product / Brand ๋„๋ฉ”์ธ + +## โœ… Checklist + +- [x] ์ƒํ’ˆ ์ •๋ณด ๊ฐ์ฒด๋Š” ๋ธŒ๋žœ๋“œ ์ •๋ณด, ์ข‹์•„์š” ์ˆ˜๋ฅผ ํฌํ•จํ•œ๋‹ค. +- [ ] ์ƒํ’ˆ์˜ ์ •๋ ฌ ์กฐ๊ฑด(`latest`, `price_asc`, `likes_desc`) ์„ ๊ณ ๋ คํ•œ ์กฐํšŒ ๊ธฐ๋Šฅ์„ ์„ค๊ณ„ํ–ˆ๋‹ค +- [x] ์ƒํ’ˆ์€ ์žฌ๊ณ ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๊ณ , ์ฃผ๋ฌธ ์‹œ ์ฐจ๊ฐํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•œ๋‹ค +- [x] ์žฌ๊ณ ๋Š” ๊ฐ์†Œ๋งŒ ๊ฐ€๋Šฅํ•˜๋ฉฐ ์Œ์ˆ˜ ๋ฐฉ์ง€๋Š” ๋„๋ฉ”์ธ ๋ ˆ๋ฒจ์—์„œ ์ฒ˜๋ฆฌ๋œ๋‹ค + +### ๐Ÿ‘ Like ๋„๋ฉ”์ธ + +- [x] ์ข‹์•„์š”๋Š” ์œ ์ €์™€ ์ƒํ’ˆ ๊ฐ„์˜ ๊ด€๊ณ„๋กœ ๋ณ„๋„ ๋„๋ฉ”์ธ์œผ๋กœ ๋ถ„๋ฆฌํ–ˆ๋‹ค +- [x] ์ค‘๋ณต ์ข‹์•„์š” ๋ฐฉ์ง€๋ฅผ ์œ„ํ•œ ๋ฉฑ๋“ฑ์„ฑ ์ฒ˜๋ฆฌ๊ฐ€ ๊ตฌํ˜„๋˜์—ˆ๋‹ค +- [x] ์ƒํ’ˆ์˜ ์ข‹์•„์š” ์ˆ˜๋Š” ์ƒํ’ˆ ์ƒ์„ธ/๋ชฉ๋ก ์กฐํšŒ์—์„œ ํ•จ๊ป˜ ์ œ๊ณต๋œ๋‹ค +- [ ] ๋‹จ์œ„ ํ…Œ์ŠคํŠธ์—์„œ ์ข‹์•„์š” ๋“ฑ๋ก/์ทจ์†Œ/์ค‘๋ณต ๋ฐฉ์ง€ ํ๋ฆ„์„ ๊ฒ€์ฆํ–ˆ๋‹ค + +### ๐Ÿ›’ Order ๋„๋ฉ”์ธ + +- [ ] ์ฃผ๋ฌธ์€ ์—ฌ๋Ÿฌ ์ƒํ’ˆ์„ ํฌํ•จํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ๊ฐ ์ƒํ’ˆ์˜ ์ˆ˜๋Ÿ‰์„ ๋ช…์‹œํ•œ๋‹ค +- [ ] ์ฃผ๋ฌธ ์‹œ ์ƒํ’ˆ์˜ ์žฌ๊ณ  ์ฐจ๊ฐ, ์œ ์ € ํฌ์ธํŠธ ์ฐจ๊ฐ ๋“ฑ์„ ์ˆ˜ํ–‰ํ•œ๋‹ค +- [ ] ์žฌ๊ณ  ๋ถ€์กฑ, ํฌ์ธํŠธ ๋ถ€์กฑ ๋“ฑ ์˜ˆ์™ธ ํ๋ฆ„์„ ๊ณ ๋ คํ•ด ์„ค๊ณ„๋˜์—ˆ๋‹ค +- [ ] ๋‹จ์œ„ ํ…Œ์ŠคํŠธ์—์„œ ์ •์ƒ ์ฃผ๋ฌธ / ์˜ˆ์™ธ ์ฃผ๋ฌธ ํ๋ฆ„์„ ๋ชจ๋‘ ๊ฒ€์ฆํ–ˆ๋‹ค + +### ๐Ÿงฉ ๋„๋ฉ”์ธ ์„œ๋น„์Šค + +- [ ] ๋„๋ฉ”์ธ ๊ฐ„ ํ˜‘๋ ฅ ๋กœ์ง์€ Domain Service์— ์œ„์น˜์‹œ์ผฐ๋‹ค +- [ ] ์ƒํ’ˆ ์ƒ์„ธ ์กฐํšŒ ์‹œ Product + Brand ์ •๋ณด ์กฐํ•ฉ์€ ๋„๋ฉ”์ธ ์„œ๋น„์Šค์—์„œ ์ฒ˜๋ฆฌํ–ˆ๋‹ค +- [ ] ๋ณตํ•ฉ ์œ ์Šค์ผ€์ด์Šค๋Š” Application Layer์— ์กด์žฌํ•˜๊ณ , ๋„๋ฉ”์ธ ๋กœ์ง์€ ์œ„์ž„๋˜์—ˆ๋‹ค +- [ ] ๋„๋ฉ”์ธ ์„œ๋น„์Šค๋Š” ์ƒํƒœ ์—†์ด, ๋„๋ฉ”์ธ ๊ฐ์ฒด์˜ ํ˜‘๋ ฅ ์ค‘์‹ฌ์œผ๋กœ ์„ค๊ณ„๋˜์—ˆ๋‹ค + +### **๐Ÿงฑ ์†Œํ”„ํŠธ์›จ์–ด ์•„ํ‚คํ…์ฒ˜ & ์„ค๊ณ„** + +- [ ] ์ „์ฒด ํ”„๋กœ์ ํŠธ์˜ ๊ตฌ์„ฑ์€ ์•„๋ž˜ ์•„ํ‚คํ…์ฒ˜๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๊ตฌ์„ฑ๋˜์—ˆ๋‹ค + - Application โ†’ **Domain** โ† Infrastructure +- [ ] Application Layer๋Š” ๋„๋ฉ”์ธ ๊ฐ์ฒด๋ฅผ ์กฐํ•ฉํ•ด ํ๋ฆ„์„ orchestration ํ–ˆ๋‹ค +- [ ] ํ•ต์‹ฌ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์€ Entity, VO, Domain Service ์— ์œ„์น˜ํ•œ๋‹ค +- [ ] Repository Interface๋Š” Domain Layer ์— ์ •์˜๋˜๊ณ , ๊ตฌํ˜„์ฒด๋Š” Infra์— ์œ„์น˜ํ•œ๋‹ค +- [ ] ํŒจํ‚ค์ง€๋Š” ๊ณ„์ธต + ๋„๋ฉ”์ธ ๊ธฐ์ค€์œผ๋กœ ๊ตฌ์„ฑ๋˜์—ˆ๋‹ค (`/domain/order`, `/application/like` ๋“ฑ) +- [ ] ํ…Œ์ŠคํŠธ๋Š” ์™ธ๋ถ€ ์˜์กด์„ฑ์„ ๋ถ„๋ฆฌํ•˜๊ณ , Fake/Stub ๋“ฑ์„ ์‚ฌ์šฉํ•ด ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๊ฐ€ ๊ฐ€๋Šฅํ•˜๊ฒŒ ๊ตฌ์„ฑ๋˜์—ˆ๋‹ค \ No newline at end of file From 25b423ec65fcf24fed8e99f405e2091fcf603e77 Mon Sep 17 00:00:00 2001 From: adminhelper <201401339hs@gmail.com> Date: Fri, 14 Nov 2025 16:49:20 +0900 Subject: [PATCH 51/85] =?UTF-8?q?feat(brand):=20=EB=B8=8C=EB=9E=9C?= =?UTF-8?q?=EB=93=9C=20=EB=8F=84=EB=A9=94=EC=9D=B8=20=EB=AA=A8=EB=8D=B8=20?= =?UTF-8?q?=EB=B0=8F=20=EB=8F=84=EB=A9=94=EC=9D=B8=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Brand ์—”ํ‹ฐํ‹ฐ ์ƒ์„ฑ ๋ฐ ๋„๋ฉ”์ธ ๊ฒ€์ฆ ๋กœ์ง ์ถ”๊ฐ€ - BrandRepository ์ธํ„ฐํŽ˜์ด์Šค ์ •์˜ - Brand ๋„๋ฉ”์ธ ์„œ๋น„์Šค ์„ค๊ณ„ - Brand ๋‹จ์œ„/ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ (์ƒ์„ฑ/๊ฒ€์ฆ) --- .../java/com/loopers/domain/brand/Brand.java | 47 +++++++++++++++++++ .../loopers/domain/brand/BrandRepository.java | 20 ++++++++ .../loopers/domain/brand/BrandService.java | 36 ++++++++++++++ .../brand/BrandJpaRepository.java | 21 +++++++++ .../brand/BrandRepositoryImpl.java | 36 ++++++++++++++ .../com/loopers/domain/brand/BrandTest.java | 42 +++++++++++++++++ 6 files changed, 202 insertions(+) create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/brand/Brand.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandRepository.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandService.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandJpaRepository.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandRepositoryImpl.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandTest.java diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/brand/Brand.java b/apps/commerce-api/src/main/java/com/loopers/domain/brand/Brand.java new file mode 100644 index 000000000..bacd46b25 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/brand/Brand.java @@ -0,0 +1,47 @@ +package com.loopers.domain.brand; + +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import jakarta.persistence.*; +import lombok.Getter; + +/** + * packageName : com.loopers.domain.brand + * fileName : Brand + * author : byeonsungmun + * date : 2025. 11. 11. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 11. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +@Entity +@Table(name = "brand") +@Getter +public class Brand { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String name; + + protected Brand() {} + + private Brand(String name) { + this.name = requireValidName(name); + } + + public static Brand create(String name) { + return new Brand(name); + } + + + private String requireValidName(String name) { + if (name == null || name.isEmpty()) { + throw new CoreException(ErrorType.BAD_REQUEST, "์ƒํ’ˆ๋ช… ๋น„์–ด ์žˆ์„์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + return name.trim(); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandRepository.java new file mode 100644 index 000000000..c558b23fc --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandRepository.java @@ -0,0 +1,20 @@ +package com.loopers.domain.brand; + +import java.util.Optional; + +/** + * packageName : com.loopers.domain.brand + * fileName : BrandRepository + * author : byeonsungmun + * date : 2025. 11. 12. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 12. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +public interface BrandRepository { + Optional findById(Long id); + + void save(Brand brand); +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandService.java b/apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandService.java new file mode 100644 index 000000000..6aa724710 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandService.java @@ -0,0 +1,36 @@ +package com.loopers.domain.brand; + +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +/** + * packageName : com.loopers.domain.brand + * fileName : BrandService + * author : byeonsungmun + * date : 2025. 11. 12. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 12. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +@Component +@RequiredArgsConstructor +public class BrandService { + + private final BrandRepository brandRepository; + + @Transactional + public void save(Brand brand) { + brandRepository.save(brand); + } + + @Transactional(readOnly = true) + public Brand getBrand(Long id) { + return brandRepository.findById(id) + .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "๋ธŒ๋žœ๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandJpaRepository.java new file mode 100644 index 000000000..111990a22 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandJpaRepository.java @@ -0,0 +1,21 @@ +package com.loopers.infrastructure.brand; + +import com.loopers.domain.brand.Brand; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +/** + * packageName : com.loopers.infrastructure.brand + * fileName : BrandJpaRepository + * author : byeonsungmun + * date : 2025. 11. 12. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 12. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +public interface BrandJpaRepository extends JpaRepository { + Optional findById(Long id); +} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandRepositoryImpl.java new file mode 100644 index 000000000..f23e6e5d9 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandRepositoryImpl.java @@ -0,0 +1,36 @@ +package com.loopers.infrastructure.brand; + +import com.loopers.domain.brand.Brand; +import com.loopers.domain.brand.BrandRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +/** + * packageName : com.loopers.infrastructure.brand + * fileName : BrandRepositroyImpl + * author : byeonsungmun + * date : 2025. 11. 12. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 12. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +@RequiredArgsConstructor +@Component +public class BrandRepositoryImpl implements BrandRepository { + + private final BrandJpaRepository jpaRepository; + + @Override + public Optional findById(Long id) { + return jpaRepository.findById(id); + } + + @Override + public void save(Brand brand) { + jpaRepository.save(brand); + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandTest.java new file mode 100644 index 000000000..9541c11f4 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandTest.java @@ -0,0 +1,42 @@ +package com.loopers.domain.brand; + +import com.loopers.support.error.CoreException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * packageName : com.loopers.domain.brand + * fileName : BrandTest + * author : byeonsungmun + * date : 2025. 11. 14. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 14. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +class BrandTest { + + @DisplayName("Brand ๋‹จ์œ„ ํ…Œ์ŠคํŠธ") + @Nested + class CreateBrandTest { + + @Test + @DisplayName("๋ธŒ๋žœ๋“œ ์ƒ์„ฑ ์„ฑ๊ณต") + void createBrandSuccess() { + Brand brand = Brand.create("Nike"); + assertThat(brand.getName()).isEqualTo("Nike"); + } + + @Test + @DisplayName("๋ธŒ๋žœ๋“œ ์ด๋ฆ„์ด ์—†์œผ๋ฉด ์˜ˆ์™ธ") + void createBrandFail() { + assertThatThrownBy(() -> Brand.create("")) + .isInstanceOf(CoreException.class); + } + } +} From 04ff3450f9994f9f4fd0906a9922d0e7136e867a Mon Sep 17 00:00:00 2001 From: adminhelper <201401339hs@gmail.com> Date: Fri, 14 Nov 2025 16:51:05 +0900 Subject: [PATCH 52/85] =?UTF-8?q?feat(product):=20=EC=83=81=ED=92=88=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=20=EB=AA=A8=EB=8D=B8=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20=EB=B0=8F=20=EC=9E=AC=EA=B3=A0/=EC=A0=95=EB=A0=AC?= =?UTF-8?q?=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Product ์—”ํ‹ฐํ‹ฐ ๊ตฌํ˜„ (์žฌ๊ณ , ์ข‹์•„์š” ์ˆ˜ ํฌํ•จ) - decreaseStock, increaseLikeCount ๋“ฑ ๋„๋ฉ”์ธ ๋กœ์ง ์ถ”๊ฐ€ - ProductRepository ์ธํ„ฐํŽ˜์ด์Šค ์ •์˜ - Product ๋‹จ์œ„/ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ --- .../product/ProductDetailInfo.java | 32 +++++ .../application/product/ProductFacade.java | 47 +++++++ .../application/product/ProductInfo.java | 33 +++++ .../com/loopers/domain/product/Product.java | 117 ++++++++++++++++++ .../loopers/domain/product/ProductDetail.java | 45 +++++++ .../domain/product/ProductDomainService.java | 39 ++++++ .../domain/product/ProductRepository.java | 29 +++++ .../domain/product/ProductService.java | 39 ++++++ .../product/ProductJpaRepository.java | 19 +++ .../product/ProductRepositoryImpl.java | 55 ++++++++ .../ProductServiceIntegrationTest.java | 43 +++++++ .../loopers/domain/product/ProductTest.java | 95 ++++++++++++++ 12 files changed, 593 insertions(+) create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/product/ProductDetailInfo.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/product/ProductInfo.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/product/ProductDetail.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/product/ProductDomainService.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/product/ProductRepository.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/product/ProductServiceIntegrationTest.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/product/ProductTest.java diff --git a/apps/commerce-api/src/main/java/com/loopers/application/product/ProductDetailInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductDetailInfo.java new file mode 100644 index 000000000..2a9ecee27 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductDetailInfo.java @@ -0,0 +1,32 @@ +package com.loopers.application.product; + +import com.loopers.domain.product.ProductDetail; + +/** + * packageName : com.loopers.application.product + * fileName : ProductDetail + * author : byeonsungmun + * date : 2025. 11. 13. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 13. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +public record ProductDetailInfo( + Long id, + String name, + String brandName, + Long price, + Long likeCount +) { + public static ProductDetailInfo from(ProductDetail productDetail) { + return new ProductDetailInfo( + productDetail.getId(), + productDetail.getName(), + productDetail.getBrandName(), + productDetail.getPrice(), + productDetail.getLikeCount() + ); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java new file mode 100644 index 000000000..7b83360e7 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java @@ -0,0 +1,47 @@ +package com.loopers.application.product; + +import com.loopers.domain.brand.Brand; +import com.loopers.domain.brand.BrandService; +import com.loopers.domain.like.LikeService; +import com.loopers.domain.product.ProductDetail; +import com.loopers.domain.product.ProductDomainService; +import com.loopers.domain.product.ProductService; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Component; + +/** + * packageName : com.loopers.application.product + * fileName : ProdcutFacade + * author : byeonsungmun + * date : 2025. 11. 10. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 10. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +@Component +@RequiredArgsConstructor +public class ProductFacade { + + private final ProductService productService; + private final BrandService brandService; + private final LikeService likeService; + private final ProductDomainService productDomainService; + + public Page getProducts(Pageable pageable) { + return productService.getProducts(pageable) + .map(product -> { + Brand brand = brandService.getBrand(product.getId()); + long likeCount = likeService.countByProductId(product.getId()); + return ProductInfo.of(product, brand, likeCount); + }); + } + + public ProductDetailInfo getProduct(Long id) { + ProductDetail productDetail = productDomainService.getProductDetail(id); + return ProductDetailInfo.from(productDetail); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/product/ProductInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductInfo.java new file mode 100644 index 000000000..8bcd93dd8 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductInfo.java @@ -0,0 +1,33 @@ +package com.loopers.application.product; + +import com.loopers.domain.brand.Brand; +import com.loopers.domain.product.Product; + +/** + * packageName : com.loopers.application.product + * fileName : ProductInfo + * author : byeonsungmun + * date : 2025. 11. 10. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 10. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +public record ProductInfo( + Long id, + String name, + String brandName, + Long price, + Long likeCount +) { + public static ProductInfo of(Product product, Brand brand, Long likeCount) { + return new ProductInfo( + product.getId(), + product.getName(), + brand.getName(), + product.getPrice(), + likeCount + ); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java new file mode 100644 index 000000000..cade20580 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java @@ -0,0 +1,117 @@ +package com.loopers.domain.product; + +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import jakarta.persistence.*; +import lombok.Getter; + +/** + * packageName : com.loopers.domain.product + * fileName : Product + * author : byeonsungmun + * date : 2025. 11. 10. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 10. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ + +@Entity +@Table(name = "product") +@Getter +public class Product { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "ref_brand_id", nullable = false) + private Long brandId; + + @Column(nullable = false) + private String name; + + @Column(nullable = false) + private Long price; + + @Column + private Long likeCount; + + @Column(nullable = false) + private Long stock; + + protected Product() {} + + private Product(Long brandId, String name, Long price, Long likeCount, Long stock) { + this.brandId = requireValidBrandId(brandId); + this.name = requireValidName(name); + this.price = requireValidPrice(price); + this.likeCount = requireValidLikeCount(likeCount); + this.stock = requireValidStock(stock); + } + + public static Product create(Long brandId, String name, Long price, Long stock) { + return new Product( + brandId, + name, + price, + 0L, + stock + ); + } + + private Long requireValidBrandId(Long brandId) { + if (brandId == null || brandId <= 0) { + throw new CoreException(ErrorType.BAD_REQUEST, "๋ธŒ๋žœ๋“œ ID๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."); + } + + return brandId; + } + + private String requireValidName(String name) { + if (name == null || name.isEmpty()) { + throw new CoreException(ErrorType.BAD_REQUEST, "์ƒํ’ˆ๋ช…์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."); + } + return name; + } + + private Long requireValidPrice(Long price) { + if (price == null || price < 0) { + throw new CoreException(ErrorType.BAD_REQUEST, "์ƒํ’ˆ ๊ฐ€๊ฒฉ์€ 0์› ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + } + return price; + } + + public Long requireValidLikeCount(Long likeCount) { + if (likeCount == null || likeCount < 0) { + throw new CoreException(ErrorType.BAD_REQUEST, "์ข‹์•„์š” ๊ฐœ์ˆ˜๋Š” 0๊ฐœ ๋ฏธ๋งŒ์œผ๋กœ ์„ค์ •ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + return likeCount; + } + + private Long requireValidStock(Long stock) { + if (stock < 0) { + throw new CoreException(ErrorType.BAD_REQUEST, "์ƒํ’ˆ ์žฌ๊ณ ๋Š” 0 ๋ฏธ๋งŒ์œผ๋กœ ์„ค์ •ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + return stock; + } + + public void increaseLikeCount() { + this.likeCount++; + } + + public void decreaseLikeCount() { + if (this.likeCount > 0) this.likeCount--; + } + + public void decreaseStock(Long quantity) { + if (quantity <= 0) { + throw new CoreException(ErrorType.BAD_REQUEST, "์ฐจ๊ฐ ์ˆ˜๋Ÿ‰์€ 0๋ณด๋‹ค ์ปค์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + } + if (this.stock - quantity < 0) { + throw new CoreException(ErrorType.BAD_REQUEST, "์žฌ๊ณ ๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค."); + } + this.stock -= quantity; + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductDetail.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductDetail.java new file mode 100644 index 000000000..808bff196 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductDetail.java @@ -0,0 +1,45 @@ +package com.loopers.domain.product; + +import com.loopers.domain.brand.Brand; +import lombok.Getter; + +/** + * packageName : com.loopers.domain.product + * fileName : ProductDetail + * author : byeonsungmun + * date : 2025. 11. 13. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 13. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +@Getter +public class ProductDetail { + + private Long id; + private String name; + private String brandName; + private Long price; + private Long likeCount; + + protected ProductDetail() {} + + private ProductDetail(Long id, String name, String brandName, Long price, Long likeCount) { + this.id = id; + this.name = name; + this.brandName = brandName; + this.price = price; + this.likeCount = likeCount; + } + + public static ProductDetail of(Product product, Brand brand, Long likeCount) { + return new ProductDetail( + product.getId(), + product.getName(), + brand.getName(), + product.getPrice(), + likeCount + ); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductDomainService.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductDomainService.java new file mode 100644 index 000000000..f86edfddd --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductDomainService.java @@ -0,0 +1,39 @@ +package com.loopers.domain.product; + +import com.loopers.domain.brand.Brand; +import com.loopers.domain.brand.BrandRepository; +import com.loopers.domain.like.LikeRepository; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +/** + * packageName : com.loopers.domain.product + * fileName : ProductDetailService + * author : byeonsungmun + * date : 2025. 11. 13. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 13. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +@Component +@RequiredArgsConstructor +public class ProductDomainService { + + private final ProductRepository productRepository; + private final BrandRepository brandRepository; + private final LikeRepository likeRepository; + + public ProductDetail getProductDetail(Long id) { + Product product = productRepository.findById(id) + .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "์ƒํ’ˆ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); + Brand brand = brandRepository.findById(product.getBrandId()) + .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "๋ธŒ๋žœ๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค")); + long likeCount = likeRepository.countByProductId(id); + + return ProductDetail.of(product, brand, likeCount); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductRepository.java new file mode 100644 index 000000000..dadda62a0 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductRepository.java @@ -0,0 +1,29 @@ +package com.loopers.domain.product; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.Optional; + +/** + * packageName : com.loopers.domain.product + * fileName : ProductRepositroy + * author : byeonsungmun + * date : 2025. 11. 12. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 12. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +public interface ProductRepository { + Page findAll(Pageable pageable); + + Optional findById(Long id); + + void incrementLikeCount(Long productId); + + void decrementLikeCount(Long productId); + + Product save(Product product); +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java new file mode 100644 index 000000000..a9c03fc80 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java @@ -0,0 +1,39 @@ +package com.loopers.domain.product; + +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +/** + * packageName : com.loopers.domain.product + * fileName : ProductService + * author : byeonsungmun + * date : 2025. 11. 12. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 12. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ + +@Component +@RequiredArgsConstructor +public class ProductService { + + private final ProductRepository productRepository; + + @Transactional(readOnly = true) + public Page getProducts(Pageable pageable) { + return productRepository.findAll(pageable); + } + + public Product getProduct(Long productId) { + return productRepository.findById(productId) + .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "ํ•ด๋‹น ์ƒํ’ˆ์ด ์—†์Šต๋‹ˆ๋‹ค")); + } +} + diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.java new file mode 100644 index 000000000..5ceaae067 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.java @@ -0,0 +1,19 @@ +package com.loopers.infrastructure.product; + +import com.loopers.domain.product.Product; +import org.springframework.data.jpa.repository.JpaRepository; + +/** + * packageName : com.loopers.infrastructure.product + * fileName : ProductJpaRepository + * author : byeonsungmun + * date : 2025. 11. 13. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 13. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +public interface ProductJpaRepository extends JpaRepository { + +} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java new file mode 100644 index 000000000..ba0feb19c --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java @@ -0,0 +1,55 @@ +package com.loopers.infrastructure.product; + +import com.loopers.domain.product.Product; +import com.loopers.domain.product.ProductRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +/** + * packageName : com.loopers.infrastructure.product + * fileName : ProductRepositoryImpl + * author : byeonsungmun + * date : 2025. 11. 13. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 13. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +@Component +@RequiredArgsConstructor +public class ProductRepositoryImpl implements ProductRepository { + + private final ProductJpaRepository productJpaRepository; + + @Override + public Page findAll(Pageable pageable) { + return productJpaRepository.findAll(pageable); + } + + @Override + public Optional findById(Long id) { + return productJpaRepository.findById(id); + } + + @Override + public void incrementLikeCount(Long productId) { + Product product = productJpaRepository.findById(productId).get(); + product.increaseLikeCount(); + } + + @Override + public void decrementLikeCount(Long productId) { + Product product = productJpaRepository.findById(productId).get(); + product.decreaseLikeCount(); + } + + @Override + public Product save(Product product) { + return productJpaRepository.save(product); + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductServiceIntegrationTest.java new file mode 100644 index 000000000..8ad61a194 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductServiceIntegrationTest.java @@ -0,0 +1,43 @@ +package com.loopers.domain.product; + +import com.loopers.utils.DatabaseCleanUp; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +/** + * packageName : com.loopers.domain.product + * fileName : ProductServiceIntegrationTest + * author : byeonsungmun + * date : 2025. 11. 13. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 13. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ + +@SpringBootTest +public class ProductServiceIntegrationTest { + + @Autowired + private ProductService productService; + + @Autowired + private DatabaseCleanUp databaseCleanUp; + + @AfterEach + void tearDown() { + databaseCleanUp.truncateAllTables(); + } + + @Nested + @DisplayName("์ƒํ’ˆ ๋ชฉ๋ก ์กฐํšŒ ํ…Œ์ŠคํŠธ") + class ProductListTests { + + Product product; + + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductTest.java new file mode 100644 index 000000000..c2c6fdd9b --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductTest.java @@ -0,0 +1,95 @@ +package com.loopers.domain.product; + +import com.loopers.support.error.CoreException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * packageName : com.loopers.domain.product + * fileName : ProductTest + * author : byeonsungmun + * date : 2025. 11. 10. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 10. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +class ProductTest { + @DisplayName("Product ์ข‹์•„์š” ์ˆ˜ ์ฆ๊ฐ ํ…Œ์ŠคํŠธ") + @Nested + class LikeCountChange { + + @DisplayName("์ข‹์•„์š” ์ˆ˜๋ฅผ ์ฆ๊ฐ€์‹œํ‚จ๋‹ค.") + @Test + void increaseLikeCount_incrementsLikeCount() { + // given + Product product = Product.create(11L, "๋‚˜์ดํ‚ค ์—์–ด2", 30000L, 10L); + + // when + product.increaseLikeCount(); + + // then + assertEquals(1L, product.getLikeCount()); + } + + @DisplayName("์ข‹์•„์š” ์ˆ˜๋ฅผ ๊ฐ์†Œ์‹œํ‚จ๋‹ค. 0 ๋ฏธ๋งŒ์œผ๋กœ๋Š” ๊ฐ์†Œํ•˜์ง€ ์•Š๋Š”๋‹ค.") + @Test + void decreaseLikeCount_decrementsLikeCountButNotBelowZero() { + // given + Product product = Product.create(11L, "๋‚˜์ดํ‚ค ์—์–ด2", 30000L, 1L); + + // when + product.decreaseLikeCount(); + + // then + assertEquals(0L, product.getLikeCount()); + + // when decrease again + product.decreaseLikeCount(); + + // then likeCount should not go below 0 + assertEquals(0L, product.getLikeCount()); + } + } + + @DisplayName("Product ์žฌ๊ณ  ์ฐจ๊ฐ ํ…Œ์ŠคํŠธ") + @Nested + class Stock { + + @DisplayName("์žฌ๊ณ ๋ฅผ ์ •์ƒ ์ฐจ๊ฐํ•œ๋‹ค.") + @Test + void decreaseStock_successfullyDecreasesStock() { + // given + Product product = Product.create(1L, "๋‚˜์ดํ‚ค ์—์–ด2", 30000L, 10L); + + // when + product.decreaseStock(3L); + + // then + assertEquals(7, product.getStock()); + } + + @DisplayName("์ฐจ๊ฐ ์ˆ˜๋Ÿ‰์ด 0 ์ดํ•˜์ด๋ฉด ์˜ˆ์™ธ ๋ฐœ์ƒ") + @Test + void decreaseStock_withInvalidQuantity_throwsException() { + Product product = Product.create(1L, "๋‚˜์ดํ‚ค ์—์–ด2", 30000L, 10L); + + assertThrows(CoreException.class, () -> product.decreaseStock(0L)); + assertThrows(CoreException.class, () -> product.decreaseStock(-1L)); + } + + @DisplayName("์žฌ๊ณ ๋ณด๋‹ค ํฐ ์ˆ˜๋Ÿ‰ ์ฐจ๊ฐ ์‹œ ์˜ˆ์™ธ ๋ฐœ์ƒ") + @Test + void decreaseStock_withInsufficientStock_throwsException() { + Product product = Product.create(1L, "๋‚˜์ดํ‚ค ์—์–ด2", 30000L, 10L); + + assertThrows(CoreException.class, () -> product.decreaseStock(11L)); + } + } +} + From 7e0ac82a96f46a3b49837b5133167fae240e4e94 Mon Sep 17 00:00:00 2001 From: adminhelper <201401339hs@gmail.com> Date: Fri, 14 Nov 2025 16:51:42 +0900 Subject: [PATCH 53/85] =?UTF-8?q?feat(like):=20=EC=A2=8B=EC=95=84=EC=9A=94?= =?UTF-8?q?=20=EB=8F=84=EB=A9=94=EC=9D=B8=20=EA=B5=AC=ED=98=84=20=EB=B0=8F?= =?UTF-8?q?=20=EB=A9=B1=EB=93=B1=EC=84=B1=20=EC=B2=98=EB=A6=AC=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 - Like ์—”ํ‹ฐํ‹ฐ ๊ตฌํ˜„ - ์ข‹์•„์š” ๋“ฑ๋ก/์ทจ์†Œ ๋กœ์ง ๋ฐ ์ค‘๋ณต ๋ฐฉ์ง€ (๋ฉฑ๋“ฑ์„ฑ) ๊ตฌํ˜„ - Product.likeCount ์ฆ๊ฐ€/๊ฐ์†Œ ์—ฐ๋™ - LikeRepository ์ธํ„ฐํŽ˜์ด์Šค ์ •์˜ - Like ๋‹จ์œ„ /ํ†ต ํ•ฉ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ --- .../loopers/application/like/LikeFacade.java | 34 ++++ .../java/com/loopers/domain/like/Like.java | 63 +++++++ .../loopers/domain/like/LikeRepository.java | 25 +++ .../com/loopers/domain/like/LikeService.java | 49 ++++++ .../like/LikeJpaRepository.java | 23 +++ .../like/LikeRepositoryImpl.java | 46 ++++++ .../like/LikeServiceIntegrationTest.java | 155 ++++++++++++++++++ .../com/loopers/domain/like/LikeTest.java | 91 ++++++++++ 8 files changed, 486 insertions(+) create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/like/LikeFacade.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/like/Like.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/like/LikeRepository.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/like/LikeService.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeJpaRepository.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeRepositoryImpl.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/like/LikeServiceIntegrationTest.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/like/LikeTest.java diff --git a/apps/commerce-api/src/main/java/com/loopers/application/like/LikeFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/like/LikeFacade.java new file mode 100644 index 000000000..5d885672e --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/like/LikeFacade.java @@ -0,0 +1,34 @@ +package com.loopers.application.like; + +import com.loopers.domain.like.LikeService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +/** + * packageName : com.loopers.application.like + * fileName : LikeFacade + * author : byeonsungmun + * date : 2025. 11. 14. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 14. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +@Component +@RequiredArgsConstructor +@Transactional +public class LikeFacade { + + private final LikeService likeService; + + public void createLike(String userId, Long productId) { + likeService.like(userId, productId); + } + + public void deleteLike(String userId, Long productId) { + likeService.unlike(userId, productId); + } +} + diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/like/Like.java b/apps/commerce-api/src/main/java/com/loopers/domain/like/Like.java new file mode 100644 index 000000000..4430b496a --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/like/Like.java @@ -0,0 +1,63 @@ +package com.loopers.domain.like; + +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import jakarta.persistence.*; +import lombok.Getter; + +import java.time.LocalDateTime; + +/** + * packageName : com.loopers.domain.like + * fileName : Like + * author : byeonsungmun + * date : 2025. 11. 11. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 11. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +@Entity +@Table(name = "product_like") +@Getter +public class Like { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "ref_user_id", nullable = false) + private String userId; + + @Column(name = "ref_product_id", nullable = false) + private Long productId; + + @Column(nullable = false) + private LocalDateTime createdAt; + + protected Like() {} + + private Like(String userId, Long productId) { + this.userId = requireValidUserId(userId); + this.productId = requireValidProductId(productId); + this.createdAt = LocalDateTime.now(); + } + + public static Like create(String userId, Long productId) { + return new Like(userId, productId); + } + + private String requireValidUserId(String userId) { + if (userId == null || userId.isEmpty()) { + throw new CoreException(ErrorType.BAD_REQUEST, "์‚ฌ์šฉ์ž ID๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."); + } + return userId; + } + + private Long requireValidProductId(Long productId) { + if (productId == null || productId <= 0) { + throw new CoreException(ErrorType.BAD_REQUEST, "์ƒํ’ˆ ID๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."); + } + return productId; + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/like/LikeRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/like/LikeRepository.java new file mode 100644 index 000000000..945b10235 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/like/LikeRepository.java @@ -0,0 +1,25 @@ +package com.loopers.domain.like; + +import java.util.Optional; + +/** + * packageName : com.loopers.domain.like + * fileName : LikeRepository + * author : byeonsungmun + * date : 2025. 11. 12. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 12. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +public interface LikeRepository { + + Optional findByUserIdAndProductId(String userId, Long productId); + + void save(Like like); + + void delete(Like like); + + long countByProductId(Long productId); +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/like/LikeService.java b/apps/commerce-api/src/main/java/com/loopers/domain/like/LikeService.java new file mode 100644 index 000000000..41ae90b6a --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/like/LikeService.java @@ -0,0 +1,49 @@ +package com.loopers.domain.like; + +import com.loopers.domain.product.ProductRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +/** + * packageName : com.loopers.application.like + * fileName : LikeService + * author : byeonsungmun + * date : 2025. 11. 12. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 12. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +@Component +@RequiredArgsConstructor +public class LikeService { + + private final LikeRepository likeRepository; + private final ProductRepository productRepository; + + @Transactional + public void like(String userId, Long productId) { + if (likeRepository.findByUserIdAndProductId(userId, productId).isPresent()) return; + + Like like = Like.create(userId, productId); + likeRepository.save(like); + productRepository.incrementLikeCount(productId); + } + + @Transactional + public void unlike(String userId, Long productId) { + likeRepository.findByUserIdAndProductId(userId, productId) + .ifPresent(like -> { + likeRepository.delete(like); + productRepository.decrementLikeCount(productId); + }); + } + + @Transactional(readOnly = true) + public long countByProductId(Long productId) { + return likeRepository.countByProductId(productId); + } + +} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeJpaRepository.java new file mode 100644 index 000000000..865a30db7 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeJpaRepository.java @@ -0,0 +1,23 @@ +package com.loopers.infrastructure.like; + +import com.loopers.domain.like.Like; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +/** + * packageName : com.loopers.infrastructure.like + * fileName : LikeJpaRepository + * author : byeonsungmun + * date : 2025. 11. 12. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 12. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +public interface LikeJpaRepository extends JpaRepository { + Optional findByUserIdAndProductId(String userId, Long productId); + + long countByProductId(Long productId); +} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeRepositoryImpl.java new file mode 100644 index 000000000..e037b6efb --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeRepositoryImpl.java @@ -0,0 +1,46 @@ +package com.loopers.infrastructure.like; + +import com.loopers.domain.like.Like; +import com.loopers.domain.like.LikeRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +/** + * packageName : com.loopers.infrastructure.like + * fileName : LikeRepositoryImpl + * author : byeonsungmun + * date : 2025. 11. 12. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 12. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +@Component +@RequiredArgsConstructor +public class LikeRepositoryImpl implements LikeRepository { + + private final LikeJpaRepository likeJpaRepository; + + @Override + public Optional findByUserIdAndProductId(String userId, Long productId) { + return likeJpaRepository.findByUserIdAndProductId(userId, productId); + } + + @Override + public void save(Like like) { + likeJpaRepository.save(like); + } + + @Override + public void delete(Like like) { + likeJpaRepository.delete(like); + } + + @Override + public long countByProductId(Long productId) { + return likeJpaRepository.countByProductId(productId); + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeServiceIntegrationTest.java new file mode 100644 index 000000000..0be07a6fb --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeServiceIntegrationTest.java @@ -0,0 +1,155 @@ +package com.loopers.domain.like; + +import com.loopers.domain.product.Product; +import com.loopers.domain.product.ProductRepository; +import com.loopers.domain.user.User; +import com.loopers.domain.user.UserRepository; +import com.loopers.utils.DatabaseCleanUp; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; + +import static org.assertj.core.api.Assertions.*; + +/** + * packageName : com.loopers.domain.like + * fileName : LikeServiceIntegrationTest + * author : byeonsungmun + * date : 2025. 11. 14. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 14. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +@SpringBootTest +class LikeServiceIntegrationTest { + + @Autowired + private LikeService likeService; + + @Autowired + private LikeRepository likeRepository; + + @Autowired + private ProductRepository productRepository; + + @Autowired + private UserRepository userRepository; + + @Autowired + private DatabaseCleanUp cleanUp; + + @AfterEach + void tearDown() { + cleanUp.truncateAllTables(); + } + + @Nested + @DisplayName("์ข‹์•„์š” ๊ธฐ๋Šฅ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ") + class LikeTests { + + @Test + @DisplayName("์ข‹์•„์š” ์ƒ์„ฑ ์„ฑ๊ณต โ†’ ์ข‹์•„์š” ์ €์žฅ + ์ƒํ’ˆ์˜ likeCount ์ฆ๊ฐ€") + @Transactional + void likeSuccess() { + // given + User user = userRepository.save(new User("user1", "u1@mail.com", "1990-01-01", "MALE")); + Product product = productRepository.save(Product.create(1L, "์ƒํ’ˆA", 1000L, 10L)); + + // when + likeService.like(user.getUserId(), product.getId()); + + // then + Like saved = likeRepository.findByUserIdAndProductId("user1", 1L).orElse(null); + assertThat(saved).isNotNull(); + + Product updated = productRepository.findById(1L).get(); + assertThat(updated.getLikeCount()).isEqualTo(1L); + } + + @Test + @DisplayName("์ค‘๋ณต ์ข‹์•„์š” ์‹œ likeCount ์ฆ๊ฐ€ ์•ˆ ํ•˜๊ณ  ์ €์žฅ๋„ ์•ˆ ๋จ") + @Transactional + void duplicateLike() { + // given + userRepository.save(new User("user1", "u1@mail.com", "1990-01-01", "MALE")); + productRepository.save(Product.create(1L, "์ƒํ’ˆA", 1000L, 10L)); + + likeService.like("user1", 1L); + + // when + likeService.like("user1", 1L); // ์ค‘๋ณต ํ˜ธ์ถœ + + // then + long likeCount = likeRepository.countByProductId(1L); + assertThat(likeCount).isEqualTo(1L); + + Product updated = productRepository.findById(1L).get(); + assertThat(updated.getLikeCount()).isEqualTo(1L); // ์ฆ๊ฐ€ X + } + + @Test + @DisplayName("์ข‹์•„์š” ์ทจ์†Œ ์„ฑ๊ณต โ†’ like ์‚ญ์ œ + ์ƒํ’ˆ์˜ likeCount ๊ฐ์†Œ") + @Transactional + void unlikeSuccess() { + // given + userRepository.save(new User("user1", "u1@mail.com", "1990-01-01", "MALE")); + productRepository.save(Product.create(1L, "์ƒํ’ˆA", 1000L, 10L)); + + likeService.like("user1", 1L); + + // when + likeService.unlike("user1", 1L); + + // then + Like like = likeRepository.findByUserIdAndProductId("user1", 1L).orElse(null); + assertThat(like).isNull(); + + Product updated = productRepository.findById(1L).get(); + assertThat(updated.getLikeCount()).isEqualTo(0L); + } + + @Test + @DisplayName("์—†๋Š” ์ข‹์•„์š” ์ทจ์†Œ ์‹œ likeCount ๊ฐ์†Œ ์•ˆ ํ•จ") + @Transactional + void unlikeNonExisting() { + // given + userRepository.save(new User("user1", "u1@mail.com", "1990-01-01", "MALE")); + Product product = Product.create(1L, "์ƒํ’ˆA", 1000L, 10L); + product.increaseLikeCount(); + product.increaseLikeCount(); + product.increaseLikeCount(); + product.increaseLikeCount(); + product.increaseLikeCount(); + + productRepository.save(product); + // when โ€” ํ˜ธ์ถœ์€ ํ•ด๋„ + likeService.unlike("user1", 1L); + + // then โ€” ๋ณ€ํ™” ์—†์Œ + Product updated = productRepository.findById(1L).get(); + assertThat(updated.getLikeCount()).isEqualTo(5L); + } + + @Test + @DisplayName("countByProductId ์ •์ƒ ์กฐํšŒ") + @Transactional + void countTest() { + // given + userRepository.save(new User("user1", "u1@mail.com", "1990-01-01", "MALE")); + userRepository.save(new User("user2", "u2@mail.com", "1991-01-01", "MALE")); + productRepository.save(Product.create(1L, "์ƒํ’ˆA", 1000L, 10L)); + + likeService.like("user1", 1L); + likeService.like("user2", 1L); + + // when + long count = likeService.countByProductId(1L); + + // then + assertThat(count).isEqualTo(2L); + } + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeTest.java new file mode 100644 index 000000000..d5b8bd851 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeTest.java @@ -0,0 +1,91 @@ +package com.loopers.domain.like; + +import com.loopers.support.error.CoreException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * packageName : com.loopers.domain.like + * fileName : LikeTest + * author : byeonsungmun + * date : 2025. 11. 13. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 13. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +class LikeTest { + + + @DisplayName("์ •์ƒ์ ์œผ๋กœ Like ์—”ํ‹ฐํ‹ฐ๋ฅผ ์ƒ์„ฑ์ˆ˜ ํ•  ์žˆ๋‹ค") + @Nested + class LikeCreate { + + @DisplayName("Like์ƒ์„ฑ์ž๋ฅผ ์ƒ์„ฑํ•œ๋‹ค") + @Test + void createLike_success() { + // given + String userId = "user-001"; + Long productId = 100L; + + // when + Like like = Like.create(userId, productId); + + // then + assertThat(like.getUserId()).isEqualTo(userId); + assertThat(like.getProductId()).isEqualTo(productId); + assertThat(like.getCreatedAt()).isBeforeOrEqualTo(LocalDateTime.now()); + } + + @Test + @DisplayName("userId๊ฐ€ null์ด๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค") + void createLike_invalidUserId_null() { + // given + String userId = null; + Long productId = 100L; + + // when & then + assertThrows(CoreException.class, () -> Like.create(userId, productId)); + } + + @Test + @DisplayName("userId๊ฐ€ ๋นˆ ๋ฌธ์ž์—ด์ด๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค") + void createLike_invalidUserId_empty() { + // given + String userId = ""; + Long productId = 100L; + + // when & then + assertThrows(CoreException.class, () -> Like.create(userId, productId)); + } + + @Test + @DisplayName("productId๊ฐ€ null์ด๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค") + void createLike_invalidProductId_null() { + // given + String userId = "user-001"; + Long productId = null; + + // when & then + assertThrows(CoreException.class, () -> Like.create(userId, productId)); + } + + @Test + @DisplayName("productId๊ฐ€ 0 ์ดํ•˜์ด๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค") + void createLike_invalidProductId_zeroOrNegative() { + // given + String userId = "user-001"; + Long productId = -1L; + + // when & then + assertThrows(CoreException.class, () -> Like.create(userId, productId)); + } + } +} From 1087ea25cc8da37b4a565b7131ab9418aabd157f Mon Sep 17 00:00:00 2001 From: adminhelper <201401339hs@gmail.com> Date: Fri, 14 Nov 2025 16:59:26 +0900 Subject: [PATCH 54/85] =?UTF-8?q?feat(order,=20point):=20=EC=A3=BC?= =?UTF-8?q?=EB=AC=B8=20=EC=83=9D=EC=84=B1=20=EC=9C=A0=EC=8A=A4=EC=BC=80?= =?UTF-8?q?=EC=9D=B4=EC=8A=A4=20=EB=B0=8F=20=ED=8F=AC=EC=9D=B8=ED=8A=B8=20?= =?UTF-8?q?=EC=B0=A8=EA=B0=90=20=EB=8F=84=EB=A9=94=EC=9D=B8=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Order / OrderItem ์—”ํ‹ฐํ‹ฐ ๊ตฌํ˜„ - ์ƒํ’ˆ ์žฌ๊ณ  ์ฐจ๊ฐ, ํฌ์ธํŠธ ์ฐจ๊ฐ ๋„๋ฉ”์ธ ๋กœ์ง ์ถ”๊ฐ€ - Order ๋„๋ฉ”์ธ ์„œ๋น„์Šค + Facade ์กฐํ•ฉ ๊ตฌํ˜„ - OrderRepository, PointRepository ์ธํ„ฐํŽ˜์ด์Šค ์ •์˜ - ์ •์ƒ ์ฃผ๋ฌธ/์žฌ๊ณ ๋ถ€์กฑ ํฌ์ธํŠธ/๋ถ€์กฑ ๋‹จ์œ„ / ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ --- .../application/order/CreateOrderCommand.java | 19 ++ .../application/order/OrderFacade.java | 83 +++++++++ .../loopers/application/order/OrderInfo.java | 42 +++++ .../application/order/OrderItemCommand.java | 17 ++ .../application/order/OrderItemInfo.java | 32 ++++ .../loopers/application/point/PointInfo.java | 2 +- .../java/com/loopers/domain/order/Order.java | 85 +++++++++ .../com/loopers/domain/order/OrderItem.java | 91 ++++++++++ .../loopers/domain/order/OrderRepository.java | 21 +++ .../loopers/domain/order/OrderService.java | 28 +++ .../com/loopers/domain/order/OrderStatus.java | 42 +++++ .../java/com/loopers/domain/point/Point.java | 31 +++- .../loopers/domain/point/PointService.java | 17 ++ .../order/OrderJpaRepository.java | 18 ++ .../order/OrderRepositoryImpl.java | 36 ++++ .../point/PointRepositoryImpl.java | 3 +- .../order/OrderServiceIntegrationTest.java | 170 ++++++++++++++++++ .../com/loopers/domain/order/OrderTest.java | 122 +++++++++++++ .../point/PointServiceIntegrationTest.java | 19 +- .../com/loopers/domain/point/PointTest.java | 111 ++++++++++-- .../api/point/PointV1ControllerTest.java | 4 +- 21 files changed, 969 insertions(+), 24 deletions(-) create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/order/CreateOrderCommand.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/order/OrderInfo.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/order/OrderItemCommand.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/order/OrderItemInfo.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/order/Order.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItem.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/order/OrderRepository.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/order/OrderStatus.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderJpaRepository.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderRepositoryImpl.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/order/OrderServiceIntegrationTest.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/order/OrderTest.java diff --git a/apps/commerce-api/src/main/java/com/loopers/application/order/CreateOrderCommand.java b/apps/commerce-api/src/main/java/com/loopers/application/order/CreateOrderCommand.java new file mode 100644 index 000000000..683e39cdd --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/order/CreateOrderCommand.java @@ -0,0 +1,19 @@ +package com.loopers.application.order; + +import java.util.List; + +/** + * packageName : com.loopers.application.order + * fileName : CreateOrderCommand + * author : byeonsungmun + * date : 2025. 11. 14. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 14. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +public record CreateOrderCommand( + String userId, + List items +) {} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java new file mode 100644 index 000000000..9e06282b4 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java @@ -0,0 +1,83 @@ +package com.loopers.application.order; + +import com.loopers.domain.order.Order; +import com.loopers.domain.order.OrderItem; +import com.loopers.domain.order.OrderService; +import com.loopers.domain.order.OrderStatus; +import com.loopers.domain.point.Point; +import com.loopers.domain.point.PointService; +import com.loopers.domain.product.Product; +import com.loopers.domain.product.ProductService; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +/** + * packageName : com.loopers.application.order + * fileName : OrderFacade + * author : byeonsungmun + * date : 2025. 11. 13. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 13. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ + +@Slf4j +@Component +@RequiredArgsConstructor +public class OrderFacade { + + private final OrderService orderService; + private final ProductService productService; + private final PointService pointService; + + @Transactional + public OrderInfo createOrder(CreateOrderCommand command) { + + if (command == null || command.items() == null || command.items().isEmpty()) { + throw new CoreException(ErrorType.NOT_FOUND, "์ƒํ’ˆ ์ •๋ณด๊ฐ€ ๋น„์–ด์žˆ์Šต๋‹ˆ๋‹ค"); + } + + Order order = Order.create(command.userId()); + + for (OrderItemCommand itemCommand : command.items()) { + + //์ƒํ’ˆ๊ฐ€์ ธ์˜ค๊ณ  + Product product = productService.getProduct(itemCommand.productId()); + + // ์žฌ๊ณ ๊ฐ์†Œ + product.decreaseStock(itemCommand.quantity()); + + // OrderItem์ƒ์„ฑ + OrderItem orderItem = OrderItem.create( + product.getId(), + product.getName(), + itemCommand.quantity(), + product.getPrice()); + + order.addOrderItem(orderItem); + orderItem.setOrder(order); + } + + //์ด ๊ฐ€๊ฒฉ๊ตฌํ•˜๊ณ  + long totalAmount = order.getOrderItems().stream() + .mapToLong(OrderItem::getAmount) + .sum(); + + order.updateTotalAmount(totalAmount); + + Point point = pointService.findPointByUserId(command.userId()); + point.use(totalAmount); + + //์ €์žฅ + Order saved = orderService.createOrder(order); + saved.updateStatus(OrderStatus.COMPLETE); + + return OrderInfo.from(saved); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderInfo.java new file mode 100644 index 000000000..70028c27c --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderInfo.java @@ -0,0 +1,42 @@ +package com.loopers.application.order; + +import com.loopers.domain.order.Order; +import com.loopers.domain.order.OrderStatus; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * packageName : com.loopers.application.order + * fileName : OrderInfo + * author : byeonsungmun + * date : 2025. 11. 14. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 14. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +public record OrderInfo( + Long orderId, + String userId, + Long totalAmount, + OrderStatus status, + LocalDateTime createdAt, + List items +) { + public static OrderInfo from(Order order) { + List itemInfos = order.getOrderItems().stream() + .map(OrderItemInfo::from) + .toList(); + + return new OrderInfo( + order.getId(), + order.getUserId(), + order.getTotalAmount(), + order.getStatus(), + order.getCreatedAt(), + itemInfos + ); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderItemCommand.java b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderItemCommand.java new file mode 100644 index 000000000..1ac46862f --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderItemCommand.java @@ -0,0 +1,17 @@ +package com.loopers.application.order; + +/** + * packageName : com.loopers.application.order + * fileName : OrderItemCommand + * author : byeonsungmun + * date : 2025. 11. 14. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 14. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +public record OrderItemCommand( + Long productId, + Long quantity +) {} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderItemInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderItemInfo.java new file mode 100644 index 000000000..b3f2359c6 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderItemInfo.java @@ -0,0 +1,32 @@ +package com.loopers.application.order; + +import com.loopers.domain.order.OrderItem; + +/** + * packageName : com.loopers.application.order + * fileName : OrderInfo + * author : byeonsungmun + * date : 2025. 11. 13. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 13. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +public record OrderItemInfo( + Long productId, + String productName, + Long quantity, + Long price, + Long amount +) { + public static OrderItemInfo from(OrderItem item) { + return new OrderItemInfo( + item.getProductId(), + item.getProductName(), + item.getQuantity(), + item.getPrice(), + item.getAmount() + ); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/point/PointInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/point/PointInfo.java index 2c357dc7e..65497297b 100644 --- a/apps/commerce-api/src/main/java/com/loopers/application/point/PointInfo.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/point/PointInfo.java @@ -6,7 +6,7 @@ public record PointInfo(String userId, Long amount) { public static PointInfo from(Point info) { return new PointInfo( info.getUserId(), - info.getAmount() + info.getBalance() ); } diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/Order.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/Order.java new file mode 100644 index 000000000..9d7b9d3f2 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/order/Order.java @@ -0,0 +1,85 @@ +package com.loopers.domain.order; + +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import jakarta.persistence.*; +import lombok.Getter; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +/** + * packageName : com.loopers.domain.order + * fileName : Order + * author : byeonsungmun + * date : 2025. 11. 11. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 11. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +@Entity +@Table(name = "orders") +@Getter +public class Order { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "ref_user_id", nullable = false) + private String userId; + + @Column(nullable = false) + private Long totalAmount; + + @Enumerated(EnumType.STRING) + private OrderStatus status; + + @Column(nullable = false) + private LocalDateTime createdAt; + + @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true) + private List orderItems = new ArrayList<>(); + + protected Order() {} + + private Order(String userId, OrderStatus status) { + this.userId = requiredValidUserId(userId); + this.totalAmount = 0L; + this.status = requiredValidStatus(status); + this.createdAt = LocalDateTime.now(); + } + + public static Order create(String userId) { + return new Order(userId, OrderStatus.PENDING); + } + + public void addOrderItem(OrderItem orderItem) { + this.orderItems.add(orderItem); + } + + private OrderStatus requiredValidStatus(OrderStatus status) { + if (status == null) { + throw new CoreException(ErrorType.BAD_REQUEST, "์ฃผ๋ฌธ ์ƒํƒœ๋Š” ํ•„์ˆ˜ ์ž…๋‹ˆ๋‹ค."); + } + return status; + } + + private String requiredValidUserId(String userId) { + if (userId == null || userId.isEmpty()) { + throw new CoreException(ErrorType.BAD_REQUEST, "์‚ฌ์šฉ์ž ID๋Š” ํ•„์ˆ˜ ์ž…๋‹ˆ๋‹ค."); + } + return userId; + } + + public void updateTotalAmount(long totalAmount) { + this.totalAmount = totalAmount; + } + + public void updateStatus(OrderStatus status) { + this.status = status; + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItem.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItem.java new file mode 100644 index 000000000..dce97a44a --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItem.java @@ -0,0 +1,91 @@ +package com.loopers.domain.order; + +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + +/** + * packageName : com.loopers.domain.order + * fileName : OrderItem + * author : byeonsungmun + * date : 2025. 11. 11. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 11. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ + +@Entity +@Table(name = "order_item") +@Getter +public class OrderItem { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Setter + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "order_id", nullable = false) + private Order order; + + @Column(name = "ref_product_id", nullable = false) + private Long productId; + + @Column(name = "ref_product_name", nullable = false) + private String productName; + + @Column(nullable = false) + private Long quantity; + + @Column(nullable = false) + private Long price; + + protected OrderItem() {} + + private OrderItem(Long productId, String productName, Long quantity, Long price) { + this.productId = requiredValidProductId(productId); + this.productName = requiredValidProductName(productName); + this.quantity = requiredQuantity(quantity); + this.price = requiredPrice(price); + } + + public static OrderItem create(Long productId, String productName, Long quantity, Long price) { + return new OrderItem(productId, productName, quantity, price); + } + + public Long getAmount() { + return quantity * price; + } + + private Long requiredValidProductId(Long productId) { + if (productId == null || productId <= 0) { + throw new CoreException(ErrorType.BAD_REQUEST, "์ƒํ’ˆ ID๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."); + } + return productId; + } + + private String requiredValidProductName(String productName) { + if (productName == null || productName.isEmpty()) { + throw new CoreException(ErrorType.BAD_REQUEST, "์ƒํ’ˆ๋ช…์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."); + } + return productName; + } + + private Long requiredQuantity(Long quantity) { + if (quantity == null || quantity <= 0) { + throw new CoreException(ErrorType.BAD_REQUEST, "์ˆ˜๋Ÿ‰์€ 1๊ฐœ ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + } + return quantity; + } + + private Long requiredPrice(Long price) { + if (price == null || price < 0) { + throw new CoreException(ErrorType.BAD_REQUEST, "๊ฐ€๊ฒฉ์€ 0์› ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + } + return price; + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderRepository.java new file mode 100644 index 000000000..c80262041 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderRepository.java @@ -0,0 +1,21 @@ +package com.loopers.domain.order; + +import java.util.Optional; + +/** + * packageName : com.loopers.domain.order + * fileName : OrderRepository + * author : byeonsungmun + * date : 2025. 11. 13. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 13. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +public interface OrderRepository { + + Order save(Order order); + + Optional findById(Long orderId); +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java new file mode 100644 index 000000000..a66be03d3 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java @@ -0,0 +1,28 @@ +package com.loopers.domain.order; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +/** + * packageName : com.loopers.domain.order + * fileName : OrderService + * author : byeonsungmun + * date : 2025. 11. 13. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 13. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +@Component +@RequiredArgsConstructor +public class OrderService { + + private final OrderRepository orderRepository; + + @Transactional + public Order createOrder(Order order) { + return orderRepository.save(order); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderStatus.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderStatus.java new file mode 100644 index 000000000..14ea592ef --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderStatus.java @@ -0,0 +1,42 @@ +package com.loopers.domain.order; + +/** + * packageName : com.loopers.domain.order + * fileName : OrderStatus + * author : byeonsungmun + * date : 2025. 11. 11. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 11. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +public enum OrderStatus { + + COMPLETE("๊ฒฐ์ œ์„ฑ๊ณต"), + CANCEL("๊ฒฐ์ œ์ทจ์†Œ"), + FAIL("๊ฒฐ์ œ์‹คํŒจ"), + PENDING("๊ฒฐ์ œ์ค‘"); + + private final String description; + + OrderStatus(String description) { + this.description = description; + } + + public boolean isCompleted() { + return this == COMPLETE; + } + + public boolean isPending() { + return this == PENDING; + } + + public boolean isCanceled() { + return this == CANCEL; + } + + public String description() { + return description; + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java b/apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java index b8f453f14..ea0c18c9d 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java @@ -3,8 +3,7 @@ import com.loopers.domain.BaseEntity; import com.loopers.support.error.CoreException; import com.loopers.support.error.ErrorType; -import jakarta.persistence.Entity; -import jakarta.persistence.Table; +import jakarta.persistence.*; import lombok.Getter; @Entity @@ -12,15 +11,23 @@ @Getter public class Point extends BaseEntity { + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Id + private Long id; + private String userId; - private Long amount; + private Long balance; protected Point() {} - public Point(String userId, Long amount) { + private Point(String userId, Long balance) { this.userId = requireValidUserId(userId); - this.amount = amount; + this.balance = balance; + } + + public static Point create(String userId, Long balance) { + return new Point(userId, balance); } String requireValidUserId(String userId) { @@ -34,7 +41,17 @@ public void charge(Long chargeAmount) { if (chargeAmount == null || chargeAmount <= 0) { throw new CoreException(ErrorType.BAD_REQUEST, "0์› ์ดํ•˜๋กœ ํฌ์ธํŠธ๋ฅผ ์ถฉ์ „ ํ• ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); } - this.amount += chargeAmount; - new Point(this.userId, this.amount); + this.balance += chargeAmount; + new Point(this.userId, this.balance); + } + + public void use(Long useAmount) { + if (useAmount == null || useAmount <= 0) { + throw new CoreException(ErrorType.BAD_REQUEST, "0์› ์ดํ•˜๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + if (this.balance < useAmount) { + throw new CoreException(ErrorType.BAD_REQUEST, "ํฌ์ธํŠธ๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค."); + } + this.balance -= useAmount; } } diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java b/apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java index dfc0788c6..1a6293f91 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java @@ -23,4 +23,21 @@ public Point chargePoint(String userId, Long chargeAmount) { point.charge(chargeAmount); return pointRepository.save(point); } + + @Transactional + public Point usePoint(String userId, Long useAmount) { + Point point = pointRepository.findByUserId(userId) + .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "ํฌ์ธํŠธ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); + + if (useAmount == null || useAmount <= 0) { + throw new CoreException(ErrorType.NOT_FOUND, "์ฐจ๊ฐํ•  ํฌ์ธํŠธ๋Š” 1 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + } + + if (point.getBalance() < useAmount) { + throw new CoreException(ErrorType.NOT_FOUND, "ํฌ์ธํŠธ๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค."); + } + + point.use(useAmount); + return pointRepository.save(point); + } } diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderJpaRepository.java new file mode 100644 index 000000000..39cfb136d --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderJpaRepository.java @@ -0,0 +1,18 @@ +package com.loopers.infrastructure.order; + +import com.loopers.domain.order.Order; +import org.springframework.data.jpa.repository.JpaRepository; + +/** + * packageName : com.loopers.infrastructure.order + * fileName : OrderJpaRepository + * author : byeonsungmun + * date : 2025. 11. 14. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 14. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +public interface OrderJpaRepository extends JpaRepository { +} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderRepositoryImpl.java new file mode 100644 index 000000000..f8c7b5b68 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderRepositoryImpl.java @@ -0,0 +1,36 @@ +package com.loopers.infrastructure.order; + +import com.loopers.domain.order.Order; +import com.loopers.domain.order.OrderRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +/** + * packageName : com.loopers.infrastructure.order + * fileName : OrderRepositroyImpl + * author : byeonsungmun + * date : 2025. 11. 14. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 14. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +@Component +@RequiredArgsConstructor +public class OrderRepositoryImpl implements OrderRepository { + + private final OrderJpaRepository orderJpaRepository; + + @Override + public Order save(Order order) { + return orderJpaRepository.save(order); + } + + @Override + public Optional findById(Long orderId) { + return orderJpaRepository.findById(orderId); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointRepositoryImpl.java index 1663b1d4f..530191b66 100644 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointRepositoryImpl.java +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointRepositoryImpl.java @@ -20,7 +20,6 @@ public Optional findByUserId(String userId) { @Override public Point save(Point point) { - pointJpaRepository.save(point); - return point; + return pointJpaRepository.save(point); } } diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderServiceIntegrationTest.java new file mode 100644 index 000000000..149e71540 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderServiceIntegrationTest.java @@ -0,0 +1,170 @@ +package com.loopers.domain.order; + +import com.loopers.application.order.CreateOrderCommand; +import com.loopers.application.order.OrderFacade; +import com.loopers.application.order.OrderInfo; +import com.loopers.application.order.OrderItemCommand; +import com.loopers.domain.point.Point; +import com.loopers.domain.point.PointRepository; +import com.loopers.domain.product.Product; +import com.loopers.domain.product.ProductRepository; +import com.loopers.utils.DatabaseCleanUp; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * packageName : com.loopers.domain.order + * fileName : OrderServiceIntegrationTest + * author : byeonsungmun + * date : 2025. 11. 14. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 14. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +@SpringBootTest +public class OrderServiceIntegrationTest { + + @Autowired + private OrderFacade orderFacade; + + @Autowired + private ProductRepository productRepository; + + @Autowired + private PointRepository pointRepository; + + @Autowired + private OrderRepository orderRepository; + + @Autowired + private DatabaseCleanUp databaseCleanUp; + + @AfterEach + void tearDown() { + databaseCleanUp.truncateAllTables(); + } + + @Nested + @DisplayName("์ฃผ๋ฌธ ์ƒ์„ฑ ์„ฑ๊ณต") + class OrderCreateSuccess { + + @Test + @Transactional + void createOrder_success() { + + // given + Product p1 = productRepository.save(Product.create(1L, "์•„๋ฉ”๋ฆฌ์นด๋…ธ", 3000L, 100L)); + Product p2 = productRepository.save(Product.create(1L, "๋ผ๋–ผ", 4000L, 200L)); + + pointRepository.save(Point.create("user1", 20000L)); + + CreateOrderCommand command = new CreateOrderCommand( + "user1", + List.of( + new OrderItemCommand(p1.getId(), 2L), // 6000์› + new OrderItemCommand(p2.getId(), 1L) // 4000์› + ) + ); + + // when + OrderInfo info = orderFacade.createOrder(command); + + // then + Order saved = orderRepository.findById(info.orderId()).orElseThrow(); + + assertThat(saved.getStatus()).isEqualTo(OrderStatus.COMPLETE); + assertThat(saved.getTotalAmount()).isEqualTo(10000L); + assertThat(saved.getOrderItems()).hasSize(2); + + // ์žฌ๊ณ  ๊ฐ์†Œ ํ™•์ธ + Product updated1 = productRepository.findById(p1.getId()).get(); + Product updated2 = productRepository.findById(p2.getId()).get(); + assertThat(updated1.getStock()).isEqualTo(98); + assertThat(updated2.getStock()).isEqualTo(199); + + // ํฌ์ธํŠธ ๊ฐ์†Œ ํ™•์ธ + Point point = pointRepository.findByUserId("user1").get(); + assertThat(point.getBalance()).isEqualTo(10000L); // 20000 - 10000 + + } + } + + @Nested + @DisplayName("์ฃผ๋ฌธ ์‹คํŒจ ์ผ€์ด์Šค") + class OrderCreateFail { + + @Test + @Transactional + @DisplayName("์žฌ๊ณ  ๋ถ€์กฑ์œผ๋กœ ์‹คํŒจ") + void insufficientStock_fail() { + Product item = productRepository.save(Product.create(1L, "์ƒํ’ˆ", 1000L, 1L)); + pointRepository.save(Point.create("user1", 5000L)); + + CreateOrderCommand command = new CreateOrderCommand( + "user1", + List.of(new OrderItemCommand(item.getId(), 5L)) + ); + + assertThatThrownBy(() -> orderFacade.createOrder(command)) + .isInstanceOf(RuntimeException.class); // ๋„ˆ์˜ ๋„๋ฉ”์ธ ์˜ˆ์™ธ ํƒ€์ž… ๋งž์ถฐ๋„ ๋จ + } + + @Test + @Transactional + @DisplayName("ํฌ์ธํŠธ ๋ถ€์กฑ์œผ๋กœ ์‹คํŒจ") + void insufficientPoint_fail() { + Product item = productRepository.save(Product.create(1L, "์ƒํ’ˆ", 1000L, 10L)); + pointRepository.save(Point.create("user1", 2000L)); // ๋ถ€์กฑ + + CreateOrderCommand command = new CreateOrderCommand( + "user1", + List.of(new OrderItemCommand(item.getId(), 5L)) // ์ด 5000์› + ); + + assertThatThrownBy(() -> orderFacade.createOrder(command)) + .hasMessageContaining("ํฌ์ธํŠธ"); // ๋ฉ”์‹œ์ง€ ๋งž์ถ”๋ฉด ๋” ์ •ํ™•ํ•˜๊ฒŒ ๊ฐ€๋Šฅ + } + + @Test + @Transactional + @DisplayName("์—†๋Š” ์ƒํ’ˆ ์ฃผ๋ฌธ ์‹œ ์‹คํŒจ") + void noProduct_fail() { + pointRepository.save(Point.create("user1", 10000L)); + + CreateOrderCommand command = new CreateOrderCommand( + "user1", + List.of(new OrderItemCommand(999L, 1L)) + ); + + assertThatThrownBy(() -> orderFacade.createOrder(command)) + .isInstanceOf(RuntimeException.class); + } + + @Test + @Transactional + @DisplayName("์œ ์ € ํฌ์ธํŠธ ์ •๋ณด ์—†์œผ๋ฉด ์‹คํŒจ") + void noUserPoint_fail() { + Product item = productRepository.save(Product.create(1L, "์ƒํ’ˆ", 1000L, 10L)); + + CreateOrderCommand command = new CreateOrderCommand( + "user1", + List.of(new OrderItemCommand(item.getId(), 1L)) + ); + + assertThatThrownBy(() -> orderFacade.createOrder(command)) + .isInstanceOf(RuntimeException.class); + } + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderTest.java new file mode 100644 index 000000000..60ed16ecc --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderTest.java @@ -0,0 +1,122 @@ +package com.loopers.domain.order; + +import com.loopers.support.error.CoreException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * packageName : com.loopers.domain.order + * fileName : OrderTest + * author : byeonsungmun + * date : 2025. 11. 14. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 14. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +class OrderTest { + + @Nested + @DisplayName("Order ์ƒ์„ฑ ํ…Œ์ŠคํŠธ") + class CreateOrderTest { + + @Test + @DisplayName("์ฃผ๋ฌธ ์ƒ์„ฑ ์„ฑ๊ณต") + void createOrderSuccess() { + // when + Order order = Order.create("user123"); + + // then + assertThat(order.getUserId()).isEqualTo("user123"); + assertThat(order.getStatus()).isEqualTo(OrderStatus.PENDING); + assertThat(order.getTotalAmount()).isEqualTo(0L); + assertThat(order.getCreatedAt()).isNotNull(); + assertThat(order.getOrderItems()).isEmpty(); + } + + @Test + @DisplayName("userId๊ฐ€ null์ด๋ฉด ์ƒ์„ฑ ์‹คํŒจ") + void createOrderFailUserIdNull() { + assertThatThrownBy(() -> Order.create(null)) + .isInstanceOf(CoreException.class) + .hasMessageContaining("์‚ฌ์šฉ์ž ID๋Š” ํ•„์ˆ˜"); + } + + @Test + @DisplayName("userId๊ฐ€ ๊ณต๋ฐฑ์ด๋ฉด ์ƒ์„ฑ ์‹คํŒจ") + void createOrderFailUserIdBlank() { + assertThatThrownBy(() -> Order.create("")) + .isInstanceOf(CoreException.class) + .hasMessageContaining("์‚ฌ์šฉ์ž ID๋Š” ํ•„์ˆ˜"); + } + } + + @Nested + @DisplayName("Order ์ƒํƒœ ์—…๋ฐ์ดํŠธ ํ…Œ์ŠคํŠธ") + class UpdateStatusTest { + + @Test + @DisplayName("์ฃผ๋ฌธ ์ƒํƒœ ์—…๋ฐ์ดํŠธ ์„ฑ๊ณต") + void updateStatusSuccess() { + // given + Order order = Order.create("user123"); + + // when + order.updateStatus(OrderStatus.COMPLETE); + + // then + assertThat(order.getStatus()).isEqualTo(OrderStatus.COMPLETE); + } + } + + @Nested + @DisplayName("์ด์•ก ์—…๋ฐ์ดํŠธ ํ…Œ์ŠคํŠธ") + class UpdateAmountTest { + + @Test + @DisplayName("์ด์•ก ์—…๋ฐ์ดํŠธ ์„ฑ๊ณต") + void updateTotalAmountSuccess() { + // given + Order order = Order.create("user123"); + + // when + order.updateTotalAmount(5000L); + + // then + assertThat(order.getTotalAmount()).isEqualTo(5000L); + } + } + + @Nested + @DisplayName("OrderItem ์ถ”๊ฐ€ ํ…Œ์ŠคํŠธ") + class AddOrderItemTest { + + @Test + @DisplayName("OrderItem ์ถ”๊ฐ€ ์„ฑ๊ณต") + void addOrderItemSuccess() { + // given + Order order = Order.create("user123"); + + OrderItem item = OrderItem.create( + 1L, + "์ƒํ’ˆ๋ช…", + 2L, + 1000L + ); + + // when + order.addOrderItem(item); + item.setOrder(order); + + // then + assertThat(order.getOrderItems()).hasSize(1); + assertThat(order.getOrderItems().getFirst().getProductName()).isEqualTo("์ƒํ’ˆ๋ช…"); + assertThat(item.getOrder()).isEqualTo(order); + } + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java index 882bca673..b623bc9c7 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java @@ -49,14 +49,14 @@ void returnPointInfo_whenValidIdIsProvided() { String gender = "MALE"; userRepository.save(new User(id, email, birth, gender)); - pointRepository.save(new Point(id, 0L)); + pointRepository.save(Point.create(id, 0L)); //when Point result = pointService.findPointByUserId(id); //then assertThat(result.getUserId()).isEqualTo(id); - assertThat(result.getAmount()).isEqualTo(0L); + assertThat(result.getBalance()).isEqualTo(0L); } @DisplayName("ํšŒ์›์ด ์กด์žฌ ํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ, null ์ด ๋ฐ˜ํ™˜๋œ๋‹ค.") @@ -89,5 +89,20 @@ void throwsChargeAmountFailException_whenUserIDIsNotProvided() { //then assertThat(exception.getErrorType()).isEqualTo(ErrorType.NOT_FOUND); } + + @Test + @DisplayName("ํšŒ์›์ด ์กด์žฌํ•˜๋ฉด ํฌ์ธํŠธ ์ถฉ์ „ ์„ฑ๊ณต") + void chargeSuccess() { + // given + String userId = "user2"; + userRepository.save(new User(userId, "yh45g@loopers.com", "1994-12-05", "MALE")); + pointRepository.save(Point.create(userId, 1000L)); + + // when + Point updated = pointService.chargePoint(userId, 500L); + + // then + assertThat(updated.getBalance()).isEqualTo(1500L); + } } } diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/point/PointTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/point/PointTest.java index 81bedab7d..f33fb2821 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/point/PointTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/point/PointTest.java @@ -5,21 +5,112 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; class PointTest { - @DisplayName("Point ๋‹จ์œ„ ํ…Œ์ŠคํŠธ") + + @Nested + @DisplayName("ํฌ์ธํŠธ ์ƒ์„ฑ ํ…Œ์ŠคํŠธ") + class CreatePointTest { + + @Test + @DisplayName("ํฌ์ธํŠธ ์ƒ์„ฑ ์„ฑ๊ณต") + void createPointSuccess() { + // when + Point point = Point.create("user123", 100L); + + // then + assertThat(point.getUserId()).isEqualTo("user123"); + assertThat(point.getBalance()).isEqualTo(100L); + } + + @Test + @DisplayName("userId๊ฐ€ null์ด๋ฉด ์ƒ์„ฑ ์‹คํŒจ") + void createPointFailUserIdNull() { + assertThatThrownBy(() -> Point.create(null, 100L)) + .isInstanceOf(CoreException.class) + .hasMessageContaining("์‚ฌ์šฉ์ž ID๊ฐ€ ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + + @Test + @DisplayName("userId๊ฐ€ ๊ณต๋ฐฑ์ด๋ฉด ์ƒ์„ฑ ์‹คํŒจ") + void createPointFailUserIdBlank() { + assertThatThrownBy(() -> Point.create("", 100L)) + .isInstanceOf(CoreException.class) + .hasMessageContaining("์‚ฌ์šฉ์ž ID๊ฐ€ ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + } + + @Nested + @DisplayName("ํฌ์ธํŠธ ์ถฉ์ „ ํ…Œ์ŠคํŠธ") + class ChargePointTest { + + @Test + @DisplayName("ํฌ์ธํŠธ ์ถฉ์ „ ์„ฑ๊ณต") + void chargeSuccess() { + // given + Point point = Point.create("user123", 100L); + + // when + point.charge(50L); + + // then + assertThat(point.getBalance()).isEqualTo(150L); + } + + @Test + @DisplayName("ํฌ์ธํŠธ ์ถฉ์ „ ์‹คํŒจ - 0 ์ดํ•˜ ์ž…๋ ฅ") + void chargeFailZeroOrNegative() { + Point point = Point.create("user123", 100L); + + assertThatThrownBy(() -> point.charge(0L)) + .isInstanceOf(CoreException.class) + .hasMessageContaining("์ถฉ์ „"); + + assertThatThrownBy(() -> point.charge(-10L)) + .isInstanceOf(CoreException.class); + } + } + @Nested - class Charge { - @DisplayName("0 ์ดํ•˜์˜ ์ •์ˆ˜๋กœ ํฌ์ธํŠธ๋ฅผ ์ถฉ์ „ ์‹œ ์‹คํŒจํ•œ๋‹ค.") + @DisplayName("ํฌ์ธํŠธ ์‚ฌ์šฉ ํ…Œ์ŠคํŠธ") + class UsePointTest { + + @Test + @DisplayName("ํฌ์ธํŠธ ์‚ฌ์šฉ ์„ฑ๊ณต") + void useSuccess() { + // given + Point point = Point.create("user123", 100L); + + // when + point.use(40L); + + // then + assertThat(point.getBalance()).isEqualTo(60L); + } + + @Test + @DisplayName("ํฌ์ธํŠธ ์‚ฌ์šฉ ์‹คํŒจ - 0 ์ดํ•˜ ์ž…๋ ฅ") + void useFailZeroOrNegative() { + Point point = Point.create("user123", 100L); + + assertThatThrownBy(() -> point.use(0L)) + .isInstanceOf(CoreException.class) + .hasMessageContaining("์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + + assertThatThrownBy(() -> point.use(-10L)) + .isInstanceOf(CoreException.class); + } + @Test - void throwsChargeAmountFailException_whenZeroAmountOrNegative() { - //given - Point point = new Point("yh45g", 0L); + @DisplayName("ํฌ์ธํŠธ ์‚ฌ์šฉ ์‹คํŒจ - ์ž”์•ก ๋ถ€์กฑ") + void useFailNotEnough() { + Point point = Point.create("user123", 50L); - //when&then - assertThrows(CoreException.class, () -> - point.charge(0L)); + assertThatThrownBy(() -> point.use(100L)) + .isInstanceOf(CoreException.class) + .hasMessageContaining("ํฌ์ธํŠธ๊ฐ€ ๋ถ€์กฑ"); } } diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/point/PointV1ControllerTest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/point/PointV1ControllerTest.java index b725fd807..7d7a2c18c 100644 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/point/PointV1ControllerTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/point/PointV1ControllerTest.java @@ -58,7 +58,7 @@ void returnPoint_whenValidUserIdIsProvided() { Long amount = 1000L; userRepository.save(new User(id, email, birth, gender)); - pointRepository.save(new Point(id, amount)); + pointRepository.save(Point.create(id, amount)); HttpHeaders headers = new HttpHeaders(); headers.add("X-USER-ID", id); @@ -112,7 +112,7 @@ void returnsTotalPoint_whenChargeUserPoint() { String gender = "MALE"; userRepository.save(new User(id, email, birth, gender)); - pointRepository.save(new Point(id, 0L)); + pointRepository.save(Point.create(id, 0L)); PointV1Dto.ChargePointRequest request = new PointV1Dto.ChargePointRequest(id, 1000L); From 3e2e0f447b5130cdd4f5db670811f828b544e1e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A0=95=EB=B0=B0?= Date: Fri, 14 Nov 2025 08:53:14 +0900 Subject: [PATCH 55/85] =?UTF-8?q?round3:=20Product,=20Brand,=20Like,=20Ord?= =?UTF-8?q?er=20=EB=8F=84=EB=A9=94=EC=9D=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../like/product/LikeProductFacade.java | 83 ++ .../like/product/LikeProductInfo.java | 11 + .../application/order/OrderFacade.java | 84 ++ .../loopers/application/order/OrderInfo.java | 21 + .../application/order/OrderItemInfo.java | 27 + .../application/product/ProductFacade.java | 72 ++ .../application/product/ProductInfo.java | 11 + .../java/com/loopers/domain/brand/Brand.java | 30 + .../loopers/domain/brand/BrandRepository.java | 10 + .../loopers/domain/brand/BrandService.java | 26 + .../com/loopers/domain/common/vo/Money.java | 17 + .../com/loopers/domain/common/vo/Price.java | 26 + .../domain/like/product/LikeProduct.java | 37 + .../like/product/LikeProductRepository.java | 16 + .../like/product/LikeProductService.java | 31 + .../metrics/product/ProductMetrics.java | 33 + .../product/ProductMetricsRepository.java | 11 + .../product/ProductMetricsService.java | 27 + .../java/com/loopers/domain/order/Order.java | 44 + .../com/loopers/domain/order/OrderItem.java | 50 ++ .../com/loopers/domain/order/OrderItem_b.java | 14 + .../loopers/domain/order/OrderRepository.java | 14 + .../loopers/domain/order/OrderService.java | 32 + .../java/com/loopers/domain/point/Point.java | 16 +- .../loopers/domain/point/PointService.java | 9 + .../com/loopers/domain/product/Product.java | 46 + .../domain/product/ProductRepository.java | 18 + .../domain/product/ProductService.java | 56 ++ .../com/loopers/domain/supply/Supply.java | 43 + .../domain/supply/SupplyRepository.java | 15 + .../loopers/domain/supply/SupplyService.java | 40 + .../com/loopers/domain/supply/vo/Stock.java | 40 + .../brand/BrandJpaRepository.java | 8 + .../brand/BrandRepositoryImpl.java | 25 + .../like/LikeProductJpaRepository.java | 17 + .../like/LikeProductRepositoryImpl.java | 37 + .../product/ProductMetricsJpaRepository.java | 10 + .../product/ProductMetricsRepositoryImpl.java | 25 + .../order/OrderJpaRepository.java | 16 + .../order/OrderRepositoryImpl.java | 31 + .../product/ProductJpaRepository.java | 7 + .../product/ProductRepositoryImpl.java | 38 + .../supply/SupplyJpaRepository.java | 21 + .../supply/SupplyRepositoryImpl.java | 36 + .../like/product/LikeProductV1ApiSpec.java | 49 ++ .../like/product/LikeProductV1Controller.java | 59 ++ .../api/like/product/LikeProductV1Dto.java | 48 + .../interfaces/api/order/OrderV1ApiSpec.java | 54 ++ .../api/order/OrderV1Controller.java | 60 ++ .../interfaces/api/order/OrderV1Dto.java | 73 ++ .../api/point/PointV1Controller.java | 2 +- .../api/product/ProductV1ApiSpec.java | 40 + .../api/product/ProductV1Controller.java | 49 ++ .../interfaces/api/product/ProductV1Dto.java | 46 + .../interfaces/api/user/UserV1Controller.java | 10 +- .../order/OrderFacadeIntegrationTest.java | 340 +++++++ .../com/loopers/domain/brand/BrandTest.java | 127 +++ .../loopers/domain/common/vo/PriceTest.java | 62 ++ .../LikeProductServiceIntegrationTest.java | 232 +++++ .../domain/like/product/LikeProductTest.java | 202 +++++ .../loopers/domain/order/OrderItemTest.java | 276 ++++++ .../order/OrderServiceIntegrationTest.java | 145 +++ .../com/loopers/domain/order/OrderTest.java | 176 ++++ .../ProductServiceIntegrationTest.java | 362 ++++++++ .../com/loopers/domain/supply/SupplyTest.java | 130 +++ .../loopers/domain/supply/vo/StockTest.java | 183 ++++ .../api/LikeProductV1ApiE2ETest.java | 485 ++++++++++ .../interfaces/api/OrderV1ApiE2ETest.java | 828 ++++++++++++++++++ .../interfaces/api/PointV1ApiE2ETest.java | 176 +++- .../interfaces/api/ProductV1ApiE2ETest.java | 265 ++++++ .../interfaces/api/UserV1ApiE2ETest.java | 72 +- 71 files changed, 5780 insertions(+), 52 deletions(-) create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/like/product/LikeProductFacade.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/like/product/LikeProductInfo.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/order/OrderInfo.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/order/OrderItemInfo.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/product/ProductInfo.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/brand/Brand.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandRepository.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandService.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/common/vo/Money.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/common/vo/Price.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/like/product/LikeProduct.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/like/product/LikeProductRepository.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/like/product/LikeProductService.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/metrics/product/ProductMetrics.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/metrics/product/ProductMetricsRepository.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/metrics/product/ProductMetricsService.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/order/Order.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItem.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItem_b.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/order/OrderRepository.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/product/ProductRepository.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/supply/Supply.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/supply/SupplyRepository.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/supply/SupplyService.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/supply/vo/Stock.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandJpaRepository.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandRepositoryImpl.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeProductJpaRepository.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeProductRepositoryImpl.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/metrics/product/ProductMetricsJpaRepository.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/metrics/product/ProductMetricsRepositoryImpl.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderJpaRepository.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderRepositoryImpl.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/supply/SupplyJpaRepository.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/supply/SupplyRepositoryImpl.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/product/LikeProductV1ApiSpec.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/product/LikeProductV1Controller.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/product/LikeProductV1Dto.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1ApiSpec.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Controller.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Dto.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1ApiSpec.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Dto.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/application/order/OrderFacadeIntegrationTest.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandTest.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/common/vo/PriceTest.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/like/product/LikeProductServiceIntegrationTest.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/like/product/LikeProductTest.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/order/OrderItemTest.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/order/OrderServiceIntegrationTest.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/order/OrderTest.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/product/ProductServiceIntegrationTest.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/supply/SupplyTest.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/supply/vo/StockTest.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/interfaces/api/LikeProductV1ApiE2ETest.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/interfaces/api/OrderV1ApiE2ETest.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/interfaces/api/ProductV1ApiE2ETest.java diff --git a/apps/commerce-api/src/main/java/com/loopers/application/like/product/LikeProductFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/like/product/LikeProductFacade.java new file mode 100644 index 000000000..c70fa18fa --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/like/product/LikeProductFacade.java @@ -0,0 +1,83 @@ +package com.loopers.application.like.product; + +import com.loopers.domain.brand.Brand; +import com.loopers.domain.brand.BrandService; +import com.loopers.domain.like.product.LikeProduct; +import com.loopers.domain.like.product.LikeProductService; +import com.loopers.domain.metrics.product.ProductMetrics; +import com.loopers.domain.metrics.product.ProductMetricsService; +import com.loopers.domain.product.Product; +import com.loopers.domain.product.ProductService; +import com.loopers.domain.supply.Supply; +import com.loopers.domain.supply.SupplyService; +import com.loopers.domain.user.User; +import com.loopers.domain.user.UserService; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +@RequiredArgsConstructor +@Component +public class LikeProductFacade { + private final LikeProductService likeProductService; + private final UserService userService; + private final ProductService productService; + private final ProductMetricsService productMetricsService; + private final BrandService brandService; + private final SupplyService supplyService; + + public void likeProduct(String userId, Long productId) { + User user = userService.findByUserId(userId).orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); + if (!productService.existsById(productId)) { + throw new CoreException(ErrorType.NOT_FOUND, "์ƒํ’ˆ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + likeProductService.likeProduct(user.getId(), productId); + } + + public void unlikeProduct(String userId, Long productId) { + User user = userService.findByUserId(userId).orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); + if (!productService.existsById(productId)) { + throw new CoreException(ErrorType.NOT_FOUND, "์ƒํ’ˆ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + likeProductService.unlikeProduct(user.getId(), productId); + } + + public Page getLikedProducts(String userId, Pageable pageable) { + User user = userService.findByUserId(userId).orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); + Page likedProducts = likeProductService.getLikedProducts(user.getId(), pageable); + + List productIds = likedProducts.map(LikeProduct::getProductId).toList(); + Map productMap = productService.getProductMapByIds(productIds); + + Set brandIds = productMap.values().stream().map(Product::getBrandId).collect(Collectors.toSet()); + + Map metricsMap = productMetricsService.getMetricsMapByProductIds(productIds); + Map supplyMap = supplyService.getSupplyMapByProductIds(productIds); + Map brandMap = brandService.getBrandMapByBrandIds(brandIds); + + return likedProducts.map(likeProduct -> { + Product product = productMap.get(likeProduct.getProductId()); + ProductMetrics metrics = metricsMap.get(product.getId()); + Brand brand = brandMap.get(product.getBrandId()); + Supply supply = supplyMap.get(product.getId()); + + return new LikeProductInfo( + product.getId(), + product.getName(), + brand.getName(), + product.getPrice().amount(), + metrics.getLikeCount(), + supply.getStock().quantity() + ); + }); + + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/like/product/LikeProductInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/like/product/LikeProductInfo.java new file mode 100644 index 000000000..ce8b4928c --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/like/product/LikeProductInfo.java @@ -0,0 +1,11 @@ +package com.loopers.application.like.product; + +public record LikeProductInfo( + Long id, + String name, + String brand, + int price, + int likes, + int stock +) { +} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java new file mode 100644 index 000000000..e4725ada1 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java @@ -0,0 +1,84 @@ +package com.loopers.application.order; + +import com.loopers.domain.order.Order; +import com.loopers.domain.order.OrderItem; +import com.loopers.domain.order.OrderService; +import com.loopers.domain.point.PointService; +import com.loopers.domain.product.Product; +import com.loopers.domain.product.ProductService; +import com.loopers.domain.supply.SupplyService; +import com.loopers.domain.user.User; +import com.loopers.domain.user.UserService; +import com.loopers.interfaces.api.order.OrderV1Dto; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@RequiredArgsConstructor +@Component +public class OrderFacade { + private final UserService userService; + private final OrderService orderService; + private final ProductService productService; + private final PointService pointService; + private final SupplyService supplyService; + + public OrderInfo getOrderInfo(String userId, Long orderId) { + User user = userService.findByUserId(userId).orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); + Order order = orderService.getOrderByIdAndUserId(orderId, user.getId()); + + return OrderInfo.from(order); + } + + @Transactional(readOnly = true) + public Page getOrderList(String userId, Pageable pageable) { + User user = userService.findByUserId(userId).orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); + Page orders = orderService.getOrdersByUserId(user.getId(), pageable); + return orders.map(OrderInfo::from); + } + + @Transactional + public OrderInfo createOrder(String userId, OrderV1Dto.OrderRequest request) { + User user = userService.findByUserId(userId).orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); + + // request์—์„œ productId - quantity ๋งต ์ƒ์„ฑ + Map productQuantityMap = request.items().stream() + .collect(Collectors.toMap( + OrderV1Dto.OrderRequest.OrderItemRequest::productId, + OrderV1Dto.OrderRequest.OrderItemRequest::quantity + )); + + Map productMap = productService.getProductMapByIds(productQuantityMap.keySet()); + + request.items().forEach(item -> { + supplyService.checkAndDecreaseStock(item.productId(), item.quantity()); + }); + + Integer totalAmount = productService.calculateTotalAmount(productQuantityMap); + + pointService.checkAndDeductPoint(user.getId(), totalAmount); + + List orderItems = request.items() + .stream() + .map(item -> OrderItem.create( + item.productId(), + productMap.get(item.productId()).getName(), + item.quantity(), + productMap.get(item.productId()).getPrice() + )) + .toList(); + Order order = Order.create(user.getId(), orderItems); + + orderService.save(order); + + return OrderInfo.from(order); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderInfo.java new file mode 100644 index 000000000..c75047e66 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderInfo.java @@ -0,0 +1,21 @@ +package com.loopers.application.order; + +import com.loopers.domain.order.Order; + +import java.util.List; + +public record OrderInfo( + Long orderId, + Long userId, + Integer totalPrice, + List items +) { + public static OrderInfo from(Order order) { + return new OrderInfo( + order.getId(), + order.getUserId(), + order.getTotalPrice().amount(), + OrderItemInfo.fromList(order.getOrderItems()) + ); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderItemInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderItemInfo.java new file mode 100644 index 000000000..99c53a78e --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderItemInfo.java @@ -0,0 +1,27 @@ +package com.loopers.application.order; + +import com.loopers.domain.order.OrderItem; + +import java.util.List; + +public record OrderItemInfo( + Long productId, + String productName, + Integer quantity, + Integer totalPrice +) { + public static OrderItemInfo from(OrderItem orderItem) { + return new OrderItemInfo( + orderItem.getProductId(), + orderItem.getProductName(), + orderItem.getQuantity(), + orderItem.getTotalPrice() + ); + } + + public static List fromList(List items) { + return items.stream() + .map(OrderItemInfo::from) + .toList(); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java new file mode 100644 index 000000000..e5feac116 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java @@ -0,0 +1,72 @@ +package com.loopers.application.product; + +import com.loopers.domain.brand.Brand; +import com.loopers.domain.brand.BrandService; +import com.loopers.domain.metrics.product.ProductMetrics; +import com.loopers.domain.metrics.product.ProductMetricsService; +import com.loopers.domain.product.Product; +import com.loopers.domain.product.ProductService; +import com.loopers.domain.supply.Supply; +import com.loopers.domain.supply.SupplyService; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +@RequiredArgsConstructor +@Component +public class ProductFacade { + private final ProductService productService; + private final ProductMetricsService productMetricsService; + private final BrandService brandService; + private final SupplyService supplyService; + + @Transactional(readOnly = true) + public Page getProductList(Pageable pageable) { + Page products = productService.getProducts(pageable); + + List productIds = products.map(Product::getId).toList(); + Set brandIds = products.map(Product::getBrandId).toSet(); + + Map metricsMap = productMetricsService.getMetricsMapByProductIds(productIds); + Map supplyMap = supplyService.getSupplyMapByProductIds(productIds); + Map brandMap = brandService.getBrandMapByBrandIds(brandIds); + + return products.map(product -> { + ProductMetrics metrics = metricsMap.get(product.getId()); + Brand brand = brandMap.get(product.getBrandId()); + Supply supply = supplyMap.get(product.getId()); + + return new ProductInfo( + product.getId(), + product.getName(), + brand.getName(), + product.getPrice().amount(), + metrics.getLikeCount(), + supply.getStock().quantity() + ); + }); + } + + @Transactional(readOnly = true) + public ProductInfo getProductDetail(Long productId) { + Product product = productService.getProductById(productId); + ProductMetrics metrics = productMetricsService.getMetricsByProductId(productId); + Brand brand = brandService.getBrandById(product.getBrandId()); + Supply supply = supplyService.getSupplyByProductId(productId); + + return new ProductInfo( + productId, + product.getName(), + brand.getName(), + product.getPrice().amount(), + metrics.getLikeCount(), + supply.getStock().quantity() + ); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/product/ProductInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductInfo.java new file mode 100644 index 000000000..b5286ed99 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductInfo.java @@ -0,0 +1,11 @@ +package com.loopers.application.product; + +public record ProductInfo( + Long id, + String name, + String brand, + int price, + int likes, + int stock +) { +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/brand/Brand.java b/apps/commerce-api/src/main/java/com/loopers/domain/brand/Brand.java new file mode 100644 index 000000000..a55ccbd33 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/brand/Brand.java @@ -0,0 +1,30 @@ + package com.loopers.domain.brand; + +import com.loopers.domain.BaseEntity; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import lombok.Getter; +import org.apache.commons.lang3.StringUtils; + +@Entity +@Table(name = "tb_brand") +@Getter +public class Brand extends BaseEntity { + private String name; + + protected Brand() { + } + + private Brand(String name) { + this.name = name; + } + + public static Brand create(String name) { + if (StringUtils.isBlank(name)) { + throw new CoreException(ErrorType.BAD_REQUEST, "๋ธŒ๋žœ๋“œ ์ด๋ฆ„์€ ํ•„์ˆ˜์ด๋ฉฐ 1์ž ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + } + return new Brand(name); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandRepository.java new file mode 100644 index 000000000..c51be9399 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandRepository.java @@ -0,0 +1,10 @@ +package com.loopers.domain.brand; + +import java.util.Collection; +import java.util.Optional; + +public interface BrandRepository { + Optional findById(Long id); + + Collection findAllByIdIn(Collection ids); +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandService.java b/apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandService.java new file mode 100644 index 000000000..3fba0f915 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandService.java @@ -0,0 +1,26 @@ +package com.loopers.domain.brand; + +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.Collection; +import java.util.Map; + +@RequiredArgsConstructor +@Component +public class BrandService { + private final BrandRepository brandRepository; + + public Brand getBrandById(Long brandId) { + return brandRepository.findById(brandId) + .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "๋ธŒ๋žœ๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); + } + + public Map getBrandMapByBrandIds(Collection brandIds) { + return brandRepository.findAllByIdIn(brandIds) + .stream() + .collect(java.util.stream.Collectors.toMap(Brand::getId, brand -> brand)); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/common/vo/Money.java b/apps/commerce-api/src/main/java/com/loopers/domain/common/vo/Money.java new file mode 100644 index 000000000..8f3460701 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/common/vo/Money.java @@ -0,0 +1,17 @@ +//package com.loopers.domain.common.vo; +// +//import com.loopers.support.error.CoreException; +//import com.loopers.support.error.ErrorType; +// +//public record Money(int amount) { +// public Money add(Money other) { +// return new Money(this.amount + other.amount); +// } +// +// public Money subtract(Money other) { +// // defensive programming: prevent negative money amounts +// if (this.amount < other.amount) { +// throw new CoreException(ErrorType.BAD_REQUEST, ) +// } +// } +//} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/common/vo/Price.java b/apps/commerce-api/src/main/java/com/loopers/domain/common/vo/Price.java new file mode 100644 index 000000000..58eee0043 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/common/vo/Price.java @@ -0,0 +1,26 @@ +package com.loopers.domain.common.vo; + +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import jakarta.persistence.AttributeConverter; + +public record Price(int amount) { + public Price { + if (amount < 0) { + throw new CoreException(ErrorType.BAD_REQUEST, "๊ฐ€๊ฒฉ์€ ์Œ์ˆ˜๊ฐ€ ๋  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + } + + public static class Converter implements AttributeConverter { + + @Override + public Integer convertToDatabaseColumn(Price attribute) { + return attribute.amount(); + } + + @Override + public Price convertToEntityAttribute(Integer dbData) { + return new Price(dbData); + } + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/like/product/LikeProduct.java b/apps/commerce-api/src/main/java/com/loopers/domain/like/product/LikeProduct.java new file mode 100644 index 000000000..ee8d09c49 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/like/product/LikeProduct.java @@ -0,0 +1,37 @@ +package com.loopers.domain.like.product; + +import com.loopers.domain.BaseEntity; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import lombok.Getter; + +@Entity +@Table(name = "tb_like_product") +@Getter +public class LikeProduct extends BaseEntity { + @Column(name = "user_id", nullable = false, updatable = false) + private Long userId; + @Column(name = "product_id", nullable = false, updatable = false) + private Long productId; + + protected LikeProduct() { + } + + private LikeProduct(Long userId, Long productId) { + this.userId = userId; + this.productId = productId; + } + + public static LikeProduct create(Long userId, Long productId) { + if (userId == null || userId <= 0) { + throw new CoreException(ErrorType.BAD_REQUEST, "์‚ฌ์šฉ์ž ID๋Š” 1 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + } + if (productId == null || productId <= 0) { + throw new CoreException(ErrorType.BAD_REQUEST, "์ƒํ’ˆ ID๋Š” 1 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + } + return new LikeProduct(userId, productId); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/like/product/LikeProductRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/like/product/LikeProductRepository.java new file mode 100644 index 000000000..525176f10 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/like/product/LikeProductRepository.java @@ -0,0 +1,16 @@ +package com.loopers.domain.like.product; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.Optional; + +public interface LikeProductRepository { + boolean existsByUserIdAndProductId(Long userId, Long productId); + + Optional findByUserIdAndProductId(Long userId, Long productId); + + void save(LikeProduct likeProduct); + + Page getLikeProductsByUserIdAndDeletedAtIsNull(Long userId, Pageable pageable); +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/like/product/LikeProductService.java b/apps/commerce-api/src/main/java/com/loopers/domain/like/product/LikeProductService.java new file mode 100644 index 000000000..f9cacef65 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/like/product/LikeProductService.java @@ -0,0 +1,31 @@ +package com.loopers.domain.like.product; + +import com.loopers.domain.BaseEntity; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +public class LikeProductService { + private final LikeProductRepository likeProductRepository; + + public void likeProduct(Long userId, Long productId) { + likeProductRepository.findByUserIdAndProductId(userId, productId) + .ifPresentOrElse(BaseEntity::restore, () -> { + LikeProduct likeProduct = LikeProduct.create(userId, productId); + likeProductRepository.save(likeProduct); + }); + } + + public void unlikeProduct(Long userId, Long productId) { + likeProductRepository.findByUserIdAndProductId(userId, productId) + .ifPresent(BaseEntity::delete); + } + + public Page getLikedProducts(Long userId, Pageable pageable) { + return likeProductRepository.getLikeProductsByUserIdAndDeletedAtIsNull(userId, PageRequest.of(pageable.getPageNumber(), pageable.getPageSize())); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/metrics/product/ProductMetrics.java b/apps/commerce-api/src/main/java/com/loopers/domain/metrics/product/ProductMetrics.java new file mode 100644 index 000000000..d815fd878 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/metrics/product/ProductMetrics.java @@ -0,0 +1,33 @@ +package com.loopers.domain.metrics.product; + +import com.loopers.domain.BaseEntity; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import lombok.Getter; + +@Entity +@Table(name = "tb_product_metrics") +@Getter +public class ProductMetrics extends BaseEntity { + // ํ˜„์žฌ๋Š” ์ƒํ’ˆ์˜ ์ข‹์•„์š” ์ˆ˜๋งŒ ๊ด€๋ฆฌํ•˜์ง€๋งŒ, ์ถ”ํ›„์— ๋‹ค๋ฅธ ๋ฉ”ํŠธ๋ฆญ๋“ค๋„ ์ถ”๊ฐ€๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. + private Long productId; + private Integer likeCount; + + protected ProductMetrics() { + } + + public static ProductMetrics create(Long productId, Integer likeCount) { + if (productId == null || productId <= 0) { + throw new CoreException(ErrorType.BAD_REQUEST, "์ƒํ’ˆ ID๋Š” 1 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + } + if (likeCount == null || likeCount < 0) { + throw new CoreException(ErrorType.BAD_REQUEST, "์ข‹์•„์š” ์ˆ˜๋Š” 0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + } + ProductMetrics metrics = new ProductMetrics(); + metrics.productId = productId; + metrics.likeCount = likeCount; + return metrics; + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/metrics/product/ProductMetricsRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/metrics/product/ProductMetricsRepository.java new file mode 100644 index 000000000..64b8d5c75 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/metrics/product/ProductMetricsRepository.java @@ -0,0 +1,11 @@ +package com.loopers.domain.metrics.product; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +public interface ProductMetricsRepository { + Optional findByProductId(Long productId); + + Collection findByProductIds(Collection productIds); +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/metrics/product/ProductMetricsService.java b/apps/commerce-api/src/main/java/com/loopers/domain/metrics/product/ProductMetricsService.java new file mode 100644 index 000000000..4975f177c --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/metrics/product/ProductMetricsService.java @@ -0,0 +1,27 @@ +package com.loopers.domain.metrics.product; + +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.Collection; +import java.util.Map; +import java.util.stream.Collectors; + +@RequiredArgsConstructor +@Component +public class ProductMetricsService { + private final ProductMetricsRepository productMetricsRepository; + + public ProductMetrics getMetricsByProductId(Long productId) { + return productMetricsRepository.findByProductId(productId) + .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "ํ•ด๋‹น ์ƒํ’ˆ์˜ ๋ฉ”ํŠธ๋ฆญ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); + } + + public Map getMetricsMapByProductIds(Collection productIds) { + return productMetricsRepository.findByProductIds(productIds) + .stream() + .collect(Collectors.toMap(ProductMetrics::getProductId, metrics -> metrics)); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/Order.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/Order.java new file mode 100644 index 000000000..c9af5dba3 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/order/Order.java @@ -0,0 +1,44 @@ +package com.loopers.domain.order; + +import com.loopers.domain.BaseEntity; +import com.loopers.domain.common.vo.Price; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import jakarta.persistence.*; +import lombok.Getter; + +import java.util.List; + +@Entity +@Table(name = "tb_order") +@Getter +public class Order extends BaseEntity { + private Long userId; + @ElementCollection + @CollectionTable( + name = "tb_order_item", + joinColumns = @JoinColumn(name = "order_id") + ) + private List orderItems; + @Convert(converter = Price.Converter.class) + private Price totalPrice; + + protected Order() { + } + + private Order(Long userId, List orderItems) { + this.userId = userId; + this.orderItems = orderItems; + this.totalPrice = new Price(orderItems.stream().map(OrderItem::getTotalPrice).reduce(Math::addExact).get()); + } + + public static Order create(Long userId, List orderItems) { + if (userId == null || userId <= 0) { + throw new CoreException(ErrorType.BAD_REQUEST, "์‚ฌ์šฉ์ž ID๋Š” 1 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + } + if (orderItems == null || orderItems.isEmpty()) { + throw new CoreException(ErrorType.BAD_REQUEST, "์ฃผ๋ฌธ ํ•ญ๋ชฉ์€ ์ตœ์†Œ 1๊ฐœ ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + } + return new Order(userId, orderItems); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItem.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItem.java new file mode 100644 index 000000000..e4a4eaa3f --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItem.java @@ -0,0 +1,50 @@ +package com.loopers.domain.order; + +import com.loopers.domain.common.vo.Price; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import jakarta.persistence.Convert; +import jakarta.persistence.Embeddable; +import lombok.Getter; +import org.apache.commons.lang3.StringUtils; + +@Embeddable +@Getter +public class OrderItem { + private Long productId; + private String productName; + private Integer quantity; + @Convert(converter = Price.Converter.class) + private Price price; + + public Integer getTotalPrice() { + return this.price.amount() * this.quantity; + } + + protected OrderItem() { + } + + private OrderItem(Long productId, String productName, Integer quantity, Price price) { + this.productId = productId; + this.productName = productName; + this.quantity = quantity; + this.price = price; + } + + public static OrderItem create(Long productId, String productName, Integer quantity, Price price) { + if (productId == null || productId <= 0) { + throw new CoreException(ErrorType.BAD_REQUEST, "์ƒํ’ˆ ID๋Š” 1 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + } + if (StringUtils.isBlank(productName)) { + throw new CoreException(ErrorType.BAD_REQUEST, "์ƒํ’ˆ๋ช…์€ ํ•„์ˆ˜์ด๋ฉฐ ๊ณต๋ฐฑ์ผ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + if (quantity == null || quantity <= 0) { + throw new CoreException(ErrorType.BAD_REQUEST, "์ฃผ๋ฌธ ์ˆ˜๋Ÿ‰์€ 1 ์ด์ƒ์˜ ์ž์—ฐ์ˆ˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + } + if (price == null) { + throw new CoreException(ErrorType.BAD_REQUEST, "๊ฐ€๊ฒฉ์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."); + } + + return new OrderItem(productId, productName, quantity, price); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItem_b.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItem_b.java new file mode 100644 index 000000000..ca3d76b1f --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItem_b.java @@ -0,0 +1,14 @@ +//package com.loopers.domain.order; +// +//import com.loopers.domain.common.vo.Price; +// +//public record OrderItem( +// Long productId, +// String productName, +// Integer quantity, +// Price price +//) { +// public Integer getTotalPrice() { +// return this.price.amount() * this.quantity; +// } +//} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderRepository.java new file mode 100644 index 000000000..0118aa719 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderRepository.java @@ -0,0 +1,14 @@ +package com.loopers.domain.order; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.Optional; + +public interface OrderRepository{ + Optional findByIdAndUserId(Long id, Long userId); + + Order save(Order order); + + Page findByUserIdAndDeletedAtIsNull(Long userId, Pageable pageable); +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java new file mode 100644 index 000000000..7628720f4 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java @@ -0,0 +1,32 @@ +package com.loopers.domain.order; + +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class OrderService { + private final OrderRepository orderRepository; + + public Order save(Order order) { + return orderRepository.save(order); + } + + public Order getOrderByIdAndUserId(Long orderId, Long userId) { + return orderRepository.findByIdAndUserId(orderId, userId) + .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "์ฃผ๋ฌธ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); + } + + public Page getOrdersByUserId(Long userId, Pageable pageable) { + int page = pageable.getPageNumber(); + int size = pageable.getPageSize(); + Sort sort = Sort.by(Sort.Direction.DESC, "createdAt"); + return orderRepository.findByUserIdAndDeletedAtIsNull(userId, PageRequest.of(page, size, sort)); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java b/apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java index dbf1bcd61..ebe3d964b 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java @@ -28,11 +28,21 @@ public static Point create(Long userId) { return new Point(userId, 0L); } - public void charge(int amount) { - if (amount <= 0) { + public void charge(int otherAmount) { + if (otherAmount <= 0) { throw new CoreException(ErrorType.BAD_REQUEST, "์ถฉ์ „ ํฌ์ธํŠธ๋Š” 0๋ณด๋‹ค ์ปค์•ผ ํ•ฉ๋‹ˆ๋‹ค."); } - this.amount += amount; + this.amount += otherAmount; + } + + public void deduct(int otherAmount) { + if (otherAmount <= 0) { + throw new CoreException(ErrorType.BAD_REQUEST, "์ฐจ๊ฐํ•  ํฌ์ธํŠธ๋Š” 0๋ณด๋‹ค ์ปค์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + } + if (this.amount < otherAmount) { + throw new CoreException(ErrorType.BAD_REQUEST, "ํฌ์ธํŠธ๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค."); + } + this.amount -= otherAmount; } } diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java b/apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java index f557f79e8..2ea51a376 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java @@ -32,4 +32,13 @@ public Long chargePoint(Long userId, int amount) { public Optional getCurrentPoint(Long userId) { return pointRepository.findByUserId(userId).map(Point::getAmount); } + + @Transactional + public void checkAndDeductPoint(Long userId, Integer totalAmount) { + Point point = pointRepository.findByUserId(userId).orElseThrow( + () -> new CoreException(ErrorType.NOT_FOUND, "ํฌ์ธํŠธ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.") + ); + point.deduct(totalAmount); + pointRepository.save(point); + } } diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java new file mode 100644 index 000000000..250516420 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java @@ -0,0 +1,46 @@ +package com.loopers.domain.product; + +import com.loopers.domain.BaseEntity; +import com.loopers.domain.common.vo.Price; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import lombok.Getter; +import org.apache.commons.lang3.StringUtils; + +@Entity +@Table(name = "tb_product") +@Getter +public class Product extends BaseEntity { + protected Product() { + } + + private String name; + @Column(name = "brand_id", nullable = false, updatable = false) + private Long brandId; + @Convert(converter = Price.Converter.class) + private Price price; + + public static Product create(String name, Long brandId, Price price) { + if (StringUtils.isBlank(name)) { + throw new CoreException(ErrorType.BAD_REQUEST, "์ƒํ’ˆ๋ช…์€ ํ•„์ˆ˜์ด๋ฉฐ ๊ณต๋ฐฑ์ผ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + if (brandId == null || brandId <= 0) { + throw new CoreException(ErrorType.BAD_REQUEST, "๋ธŒ๋žœ๋“œ ID๋Š” 1 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + } + if (price == null) { + throw new CoreException(ErrorType.BAD_REQUEST, "๊ฐ€๊ฒฉ์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."); + } + if (price.amount() < 0) { + throw new CoreException(ErrorType.BAD_REQUEST, "๊ฐ€๊ฒฉ์€ ์Œ์ˆ˜๊ฐ€ ๋  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + Product product = new Product(); + product.name = name; + product.brandId = brandId; + product.price = price; + return product; + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductRepository.java new file mode 100644 index 000000000..beb141147 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductRepository.java @@ -0,0 +1,18 @@ +package com.loopers.domain.product; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +public interface ProductRepository { + Optional findById(Long productId); + + Page findAll(Pageable pageable); + + List findAllByIdIn(Collection ids); + + boolean existsById(Long productId); +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java new file mode 100644 index 000000000..38fe96076 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java @@ -0,0 +1,56 @@ +package com.loopers.domain.product; + +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Component; + +import java.util.Collection; +import java.util.Map; +import java.util.stream.Collectors; + +@RequiredArgsConstructor +@Component +public class ProductService { + private final ProductRepository productRepository; + + public Product getProductById(Long productId) { + return productRepository.findById(productId) + .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "์ƒํ’ˆ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); + } + + public Map getProductMapByIds(Collection productIds) { + return productRepository.findAllByIdIn(productIds) + .stream() + .collect(Collectors.toMap(Product::getId, product -> product)); + } + + public Page getProducts(Pageable pageable) { + int page = pageable.getPageNumber(); + int size = pageable.getPageSize(); + String sortStr = pageable.getSort().toString().split(":")[0]; + Sort sort = Sort.by(Sort.Direction.DESC, "createdAt"); + if (StringUtils.startsWith(sortStr, "price_asc")) { + sort = Sort.by(Sort.Direction.ASC, "price"); + } else if (StringUtils.equals(sortStr, "like_desc")) { + sort = Sort.by(Sort.Direction.DESC, "like_count"); + } + return productRepository.findAll(PageRequest.of(page, size, sort)); + } + + public Integer calculateTotalAmount(Map items) { + return productRepository.findAllByIdIn(items.keySet()) + .stream() + .mapToInt(product -> product.getPrice().amount() * items.get(product.getId())) + .sum(); + } + + public boolean existsById(Long productId) { + return productRepository.existsById(productId); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/supply/Supply.java b/apps/commerce-api/src/main/java/com/loopers/domain/supply/Supply.java new file mode 100644 index 000000000..834bd4628 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/supply/Supply.java @@ -0,0 +1,43 @@ +package com.loopers.domain.supply; + +import com.loopers.domain.BaseEntity; +import com.loopers.domain.supply.vo.Stock; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import jakarta.persistence.Convert; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.Setter; + +@Entity +@Table(name = "tb_supply") +@Getter +public class Supply extends BaseEntity { + private Long productId; + @Setter + @Convert(converter = Stock.Converter.class) + private Stock stock; + // think: ์ธ๋‹น ๊ตฌ๋งค์ œํ•œ? + + protected Supply() { + } + + public static Supply create(Long productId, Stock stock) { + if (productId == null || productId <= 0) { + throw new CoreException(ErrorType.BAD_REQUEST, "์ƒํ’ˆ ID๋Š” 1 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + } + if (stock == null) { + throw new CoreException(ErrorType.BAD_REQUEST, "์žฌ๊ณ ๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."); + } + Supply supply = new Supply(); + supply.productId = productId; + supply.stock = stock; + return supply; + } + + // decreaseStock, increaseStock + public void decreaseStock(int quantity) { + this.stock = this.stock.decrease(quantity); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/supply/SupplyRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/supply/SupplyRepository.java new file mode 100644 index 000000000..7e7cb09ca --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/supply/SupplyRepository.java @@ -0,0 +1,15 @@ +package com.loopers.domain.supply; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +public interface SupplyRepository { + Optional findByProductId(Long productId); + + List findAllByProductIdIn(Collection productIds); + + Optional findByProductIdForUpdate(Long productId); + + Supply save(Supply supply); +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/supply/SupplyService.java b/apps/commerce-api/src/main/java/com/loopers/domain/supply/SupplyService.java new file mode 100644 index 000000000..b8de2c1df --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/supply/SupplyService.java @@ -0,0 +1,40 @@ +package com.loopers.domain.supply; + +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Collection; +import java.util.Map; +import java.util.stream.Collectors; + +@RequiredArgsConstructor +@Component +public class SupplyService { + private final SupplyRepository supplyRepository; + + public Supply getSupplyByProductId(Long productId) { + return supplyRepository.findByProductId(productId) + .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "ํ•ด๋‹น ์ƒํ’ˆ์˜ ์žฌ๊ณ  ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); + } + + public Map getSupplyMapByProductIds(Collection productIds) { + return supplyRepository.findAllByProductIdIn(productIds) + .stream() + .collect(Collectors.toMap(Supply::getProductId, supply -> supply)); + } + + @Transactional + public void checkAndDecreaseStock(Long productId, Integer quantity) { + Supply supply = supplyRepository.findByProductIdForUpdate(productId) + .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "ํ•ด๋‹น ์ƒํ’ˆ์˜ ์žฌ๊ณ  ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); + supply.decreaseStock(quantity); + supplyRepository.save(supply); + } + + public Supply saveSupply(Supply supply) { + return supplyRepository.save(supply); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/supply/vo/Stock.java b/apps/commerce-api/src/main/java/com/loopers/domain/supply/vo/Stock.java new file mode 100644 index 000000000..b76e5f1a1 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/supply/vo/Stock.java @@ -0,0 +1,40 @@ +package com.loopers.domain.supply.vo; + +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import jakarta.persistence.AttributeConverter; + +public record Stock(int quantity) { + public Stock { + if (quantity < 0) { + throw new CoreException(ErrorType.BAD_REQUEST, "์žฌ๊ณ ๋Š” ์Œ์ˆ˜๊ฐ€ ๋  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + } + + public boolean isOutOfStock() { + return this.quantity <= 0; + } + + public Stock decrease(int orderQuantity) { + if (orderQuantity <= 0) { + throw new CoreException(ErrorType.BAD_REQUEST, "์ฃผ๋ฌธ ์ˆ˜๋Ÿ‰์€ 0๋ณด๋‹ค ์ปค์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + } + if (orderQuantity > this.quantity) { + throw new CoreException(ErrorType.BAD_REQUEST, "์žฌ๊ณ ๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค."); + } + return new Stock(this.quantity - orderQuantity); + } + + public static class Converter implements AttributeConverter { + + @Override + public Integer convertToDatabaseColumn(Stock attribute) { + return attribute.quantity(); + } + + @Override + public Stock convertToEntityAttribute(Integer dbData) { + return new Stock(dbData); + } + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandJpaRepository.java new file mode 100644 index 000000000..aa99ac6ca --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandJpaRepository.java @@ -0,0 +1,8 @@ +package com.loopers.infrastructure.brand; + +import com.loopers.domain.brand.Brand; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface BrandJpaRepository extends JpaRepository { + +} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandRepositoryImpl.java new file mode 100644 index 000000000..69b7cbb79 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandRepositoryImpl.java @@ -0,0 +1,25 @@ +package com.loopers.infrastructure.brand; + +import com.loopers.domain.brand.Brand; +import com.loopers.domain.brand.BrandRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.Collection; +import java.util.Optional; + +@RequiredArgsConstructor +@Component +public class BrandRepositoryImpl implements BrandRepository { + private final BrandJpaRepository brandJpaRepository; + + @Override + public Optional findById(Long id) { + return brandJpaRepository.findById(id); + } + + @Override + public Collection findAllByIdIn(Collection ids) { + return brandJpaRepository.findAllById(ids); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeProductJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeProductJpaRepository.java new file mode 100644 index 000000000..6c247ec44 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeProductJpaRepository.java @@ -0,0 +1,17 @@ +package com.loopers.infrastructure.like; + +import com.loopers.domain.like.product.LikeProduct; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.time.ZonedDateTime; +import java.util.Optional; + +public interface LikeProductJpaRepository extends JpaRepository { + Optional findByUserIdAndProductId(Long userId, Long productId); + + boolean existsByUserIdAndProductId(Long userId, Long productId); + + Page getLikeProductsByUserIdAndDeletedAtIsNull(Long userId, Pageable pageable); +} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeProductRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeProductRepositoryImpl.java new file mode 100644 index 000000000..8827a431f --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeProductRepositoryImpl.java @@ -0,0 +1,37 @@ +package com.loopers.infrastructure.like; + +import com.loopers.domain.like.product.LikeProduct; +import com.loopers.domain.like.product.LikeProductRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +@RequiredArgsConstructor +@Component +public class LikeProductRepositoryImpl implements LikeProductRepository { + private final LikeProductJpaRepository likeProductJpaRepository; + + @Override + public boolean existsByUserIdAndProductId(Long userId, Long productId) { + return likeProductJpaRepository.existsByUserIdAndProductId(userId, productId); + } + + @Override + public Optional findByUserIdAndProductId(Long userId, Long productId) { + return likeProductJpaRepository.findByUserIdAndProductId(userId, productId); + } + + @Override + public void save(LikeProduct likeProduct) { + likeProductJpaRepository.save(likeProduct); + } + + @Override + public Page getLikeProductsByUserIdAndDeletedAtIsNull(Long userId, Pageable pageable) { + return likeProductJpaRepository.getLikeProductsByUserIdAndDeletedAtIsNull(userId, pageable); + } + +} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/metrics/product/ProductMetricsJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/metrics/product/ProductMetricsJpaRepository.java new file mode 100644 index 000000000..42bde9788 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/metrics/product/ProductMetricsJpaRepository.java @@ -0,0 +1,10 @@ +package com.loopers.infrastructure.metrics.product; + +import com.loopers.domain.metrics.product.ProductMetrics; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface ProductMetricsJpaRepository extends JpaRepository { + Optional findByProductId(Long productId); +} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/metrics/product/ProductMetricsRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/metrics/product/ProductMetricsRepositoryImpl.java new file mode 100644 index 000000000..db76a1d92 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/metrics/product/ProductMetricsRepositoryImpl.java @@ -0,0 +1,25 @@ +package com.loopers.infrastructure.metrics.product; + +import com.loopers.domain.metrics.product.ProductMetrics; +import com.loopers.domain.metrics.product.ProductMetricsRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.Collection; +import java.util.Optional; + +@RequiredArgsConstructor +@Component +public class ProductMetricsRepositoryImpl implements ProductMetricsRepository { + private final ProductMetricsJpaRepository jpaRepository; + + @Override + public Optional findByProductId(Long productId) { + return jpaRepository.findByProductId(productId); + } + + @Override + public Collection findByProductIds(Collection productIds) { + return jpaRepository.findAllById(productIds); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderJpaRepository.java new file mode 100644 index 000000000..c3337045e --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderJpaRepository.java @@ -0,0 +1,16 @@ +package com.loopers.infrastructure.order; + +import com.loopers.domain.order.Order; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface OrderJpaRepository extends JpaRepository { + + Optional findByIdAndUserIdAndDeletedAtIsNull(Long id, Long userId); + + Page findByUserIdAndDeletedAtIsNull(Long userId, Pageable pageable); + +} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderRepositoryImpl.java new file mode 100644 index 000000000..67a7462fe --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderRepositoryImpl.java @@ -0,0 +1,31 @@ +package com.loopers.infrastructure.order; + +import com.loopers.domain.order.Order; +import com.loopers.domain.order.OrderRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +@RequiredArgsConstructor +@Component +public class OrderRepositoryImpl implements OrderRepository { + private final OrderJpaRepository orderJpaRepository; + + @Override + public Optional findByIdAndUserId(Long id, Long userId) { + return orderJpaRepository.findByIdAndUserIdAndDeletedAtIsNull(id, userId); + } + + @Override + public Order save(Order order) { + return orderJpaRepository.save(order); + } + + @Override + public Page findByUserIdAndDeletedAtIsNull(Long userId, Pageable pageable) { + return orderJpaRepository.findByUserIdAndDeletedAtIsNull(userId, pageable); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.java new file mode 100644 index 000000000..0375b7ca7 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.java @@ -0,0 +1,7 @@ +package com.loopers.infrastructure.product; + +import com.loopers.domain.product.Product; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ProductJpaRepository extends JpaRepository { +} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java new file mode 100644 index 000000000..b2b1115b9 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java @@ -0,0 +1,38 @@ +package com.loopers.infrastructure.product; + +import com.loopers.domain.product.Product; +import com.loopers.domain.product.ProductRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Component; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +@RequiredArgsConstructor +@Component +public class ProductRepositoryImpl implements ProductRepository { + private final ProductJpaRepository productJpaRepository; + + @Override + public Optional findById(Long productId) { + return productJpaRepository.findById(productId); + } + + @Override + public Page findAll(Pageable pageable) { + return productJpaRepository.findAll(pageable); + } + + @Override + public List findAllByIdIn(Collection ids) { + return productJpaRepository.findAllById(ids); + } + + @Override + public boolean existsById(Long productId) { + return productJpaRepository.existsById(productId); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/supply/SupplyJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/supply/SupplyJpaRepository.java new file mode 100644 index 000000000..ec66a450d --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/supply/SupplyJpaRepository.java @@ -0,0 +1,21 @@ +package com.loopers.infrastructure.supply; + +import com.loopers.domain.supply.Supply; +import jakarta.persistence.LockModeType; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Lock; +import org.springframework.data.jpa.repository.Query; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +public interface SupplyJpaRepository extends JpaRepository { + Optional findByProductId(Long productId); + + List findAllByProductIdIn(Collection productIds); + + @Lock(LockModeType.PESSIMISTIC_WRITE) + @Query("SELECT s FROM Supply s WHERE s.productId = :productId") + Optional findByProductIdForUpdate(Long productId); +} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/supply/SupplyRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/supply/SupplyRepositoryImpl.java new file mode 100644 index 000000000..92b13a07b --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/supply/SupplyRepositoryImpl.java @@ -0,0 +1,36 @@ +package com.loopers.infrastructure.supply; + +import com.loopers.domain.supply.Supply; +import com.loopers.domain.supply.SupplyRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +@RequiredArgsConstructor +@Component +public class SupplyRepositoryImpl implements SupplyRepository { + private final SupplyJpaRepository supplyJpaRepository; + + @Override + public Optional findByProductId(Long productId) { + return supplyJpaRepository.findByProductId(productId); + } + + @Override + public List findAllByProductIdIn(Collection productIds) { + return supplyJpaRepository.findAllByProductIdIn(productIds); + } + + @Override + public Optional findByProductIdForUpdate(Long productId) { + return supplyJpaRepository.findByProductIdForUpdate(productId); + } + + @Override + public Supply save(Supply supply) { + return supplyJpaRepository.save(supply); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/product/LikeProductV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/product/LikeProductV1ApiSpec.java new file mode 100644 index 000000000..716f9b735 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/product/LikeProductV1ApiSpec.java @@ -0,0 +1,49 @@ +package com.loopers.interfaces.api.like.product; + +import com.loopers.interfaces.api.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.data.domain.Pageable; +import org.springframework.web.bind.annotation.RequestHeader; + +@Tag(name = "Like Product V1 API", description = "์ƒํ’ˆ ์ข‹์•„์š” API ์ž…๋‹ˆ๋‹ค.") +public interface LikeProductV1ApiSpec { + // /api/v1/like/products/{productId} - POST + @Operation( + method = "POST", + summary = "์ƒํ’ˆ ์ข‹์•„์š” ์ถ”๊ฐ€", + description = "ํšŒ์›์ด ํŠน์ • ์ƒํ’ˆ์— ์ข‹์•„์š”๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค." + ) + ApiResponse likeProduct( + @RequestHeader(value = "X-USER-ID", required = false) String userId, + Long productId + ); + + // /api/v1/like/products/{productId} - DELETE + @Operation( + method = "DELETE", + summary = "์ƒํ’ˆ ์ข‹์•„์š” ์ทจ์†Œ", + description = "ํšŒ์›์ด ํŠน์ • ์ƒํ’ˆ์— ๋Œ€ํ•œ ์ข‹์•„์š”๋ฅผ ์ทจ์†Œํ•ฉ๋‹ˆ๋‹ค." + ) + ApiResponse unlikeProduct( + @RequestHeader(value = "X-USER-ID", required = false) String userId, + Long productId + ); + + // /api/v1/like/products - GET + @Operation( + method = "GET", + summary = "ํšŒ์›์ด ์ข‹์•„์š”ํ•œ ์ƒํ’ˆ ๋ชฉ๋ก ์กฐํšŒ", + description = "ํšŒ์›์ด ์ข‹์•„์š”ํ•œ ์ƒํ’ˆ๋“ค์˜ ๋ชฉ๋ก์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค." + ) + ApiResponse getLikedProducts( + @RequestHeader(value = "X-USER-ID", required = false) String userId, + @Schema( + name = "ํŽ˜์ด์ง€ ์ •๋ณด", + description = "ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ, ํŽ˜์ด์ง€ ํฌ๊ธฐ, ์ •๋ ฌ ์ •๋ณด๋ฅผ ํฌํ•จํ•œ ํŽ˜์ด์ง€ ์ •๋ณด" + + "\n- ๊ธฐ๋ณธ๊ฐ’: page=0, size=20" + ) + Pageable pageable + ); +} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/product/LikeProductV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/product/LikeProductV1Controller.java new file mode 100644 index 000000000..f49e584d1 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/product/LikeProductV1Controller.java @@ -0,0 +1,59 @@ +package com.loopers.interfaces.api.like.product; + +import com.loopers.application.like.product.LikeProductFacade; +import com.loopers.application.like.product.LikeProductInfo; +import com.loopers.interfaces.api.ApiResponse; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; +import org.springframework.web.bind.annotation.*; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/v1/like/products") +public class LikeProductV1Controller implements LikeProductV1ApiSpec { + private final LikeProductFacade likeProductFacade; + + @RequestMapping(method = RequestMethod.POST, path = "/{productId}") + @Override + public ApiResponse likeProduct( + @RequestHeader(value = "X-USER-ID", required = false) String userId, + @PathVariable Long productId) { + if (StringUtils.isBlank(userId)) { + throw new CoreException(ErrorType.BAD_REQUEST); + } + likeProductFacade.likeProduct(userId, productId); + return ApiResponse.success(null); + } + + @RequestMapping(method = RequestMethod.DELETE, path = "/{productId}") + @Override + public ApiResponse unlikeProduct( + @RequestHeader(value = "X-USER-ID", required = false) String userId, + @PathVariable Long productId + ) { + if (StringUtils.isBlank(userId)) { + throw new CoreException(ErrorType.BAD_REQUEST); + } + likeProductFacade.unlikeProduct(userId, productId); + return ApiResponse.success(null); + } + + @RequestMapping(method = RequestMethod.GET) + @Override + public ApiResponse getLikedProducts( + @RequestHeader(value = "X-USER-ID", required = false) String userId, + @PageableDefault(size = 20) Pageable pageable + ) { + if (StringUtils.isBlank(userId)) { + throw new CoreException(ErrorType.BAD_REQUEST); + } + Page likedProducts = likeProductFacade.getLikedProducts(userId, pageable); + LikeProductV1Dto.ProductsResponse response = LikeProductV1Dto.ProductsResponse.from(likedProducts); + return ApiResponse.success(response); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/product/LikeProductV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/product/LikeProductV1Dto.java new file mode 100644 index 000000000..81c084b56 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/product/LikeProductV1Dto.java @@ -0,0 +1,48 @@ +package com.loopers.interfaces.api.like.product; + +import com.loopers.application.like.product.LikeProductInfo; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Sort; + +import java.util.List; + +public class LikeProductV1Dto { + public record ProductResponse( + Long id, + String name, + String brand, + int price, + int likes, + int stock + ) { + public static LikeProductV1Dto.ProductResponse from(LikeProductInfo info) { + return new LikeProductV1Dto.ProductResponse( + info.id(), + info.name(), + info.brand(), + info.price(), + info.likes(), + info.stock() + ); + } + } + + public record ProductsResponse( + List content, + int totalPages, + long totalElements, + int number, + int size + + ) { + public static LikeProductV1Dto.ProductsResponse from(Page page) { + return new LikeProductV1Dto.ProductsResponse( + page.map(LikeProductV1Dto.ProductResponse::from).getContent(), + page.getTotalPages(), + page.getTotalElements(), + page.getNumber(), + page.getSize() + ); + } + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1ApiSpec.java new file mode 100644 index 000000000..57197cb68 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1ApiSpec.java @@ -0,0 +1,54 @@ +package com.loopers.interfaces.api.order; + +import com.loopers.interfaces.api.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; +import org.springframework.web.bind.annotation.RequestHeader; + +@Tag(name = "Order V1 API", description = "์ฃผ๋ฌธ API ์ž…๋‹ˆ๋‹ค.") +public interface OrderV1ApiSpec { + // /api/v1/orders - POST + @Operation( + method = "POST", + summary = "์ฃผ๋ฌธ ์ƒ์„ฑ", + description = "์ƒˆ๋กœ์šด ์ฃผ๋ฌธ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค." + ) + ApiResponse createOrder( + @RequestHeader(value = "X-USER-ID", required = false) String userId, + @Schema( + name = "์ฃผ๋ฌธ ์š”์ฒญ ์ •๋ณด", + description = "์ฃผ๋ฌธ ์ƒ์„ฑ์— ํ•„์š”ํ•œ ์ •๋ณด" + ) + OrderV1Dto.OrderRequest request + ); + + // /api/v1/orders - GET + @Operation( + method = "GET", + summary = "์ฃผ๋ฌธ ๋ชฉ๋ก ์กฐํšŒ", + description = "ํšŒ์›์˜ ์ฃผ๋ฌธ ๋ชฉ๋ก์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค." + ) + ApiResponse getOrderList( + @RequestHeader(value = "X-USER-ID", required = false) String userId, + @PageableDefault(size = 20) Pageable pageable + ); + + // /api/v1/orders/{orderId} - GET + @Operation( + method = "GET", + summary = "์ฃผ๋ฌธ ์ƒ์„ธ ์กฐํšŒ", + description = "ํŠน์ • ์ฃผ๋ฌธ์˜ ์ƒ์„ธ ์ •๋ณด๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค." + ) + ApiResponse getOrderDetail( + @RequestHeader(value = "X-USER-ID", required = false) String userId, + @Schema( + name = "์ฃผ๋ฌธ ID", + description = "์กฐํšŒํ•  ์ฃผ๋ฌธ์˜ ID" + ) + Long orderId + ); +} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Controller.java new file mode 100644 index 000000000..ae8f75ae3 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Controller.java @@ -0,0 +1,60 @@ +package com.loopers.interfaces.api.order; + +import com.loopers.application.order.OrderFacade; +import com.loopers.application.order.OrderInfo; +import com.loopers.interfaces.api.ApiResponse; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; +import org.springframework.web.bind.annotation.*; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/v1/orders") +public class OrderV1Controller implements OrderV1ApiSpec { + private final OrderFacade orderFacade; + + @RequestMapping(method = RequestMethod.POST) + @Override + public ApiResponse createOrder( + @RequestHeader(value = "X-USER-ID", required = false) String userId, + @RequestBody OrderV1Dto.OrderRequest request + ) { + if (StringUtils.isBlank(userId)) { + throw new CoreException(ErrorType.BAD_REQUEST); + } + OrderInfo orderInfo = orderFacade.createOrder(userId, request); + OrderV1Dto.OrderResponse response = OrderV1Dto.OrderResponse.from(orderInfo); + return ApiResponse.success(response); + } + + @RequestMapping(method = RequestMethod.GET) + @Override + public ApiResponse getOrderList( + @RequestHeader(value = "X-USER-ID", required = false) String userId, + @PageableDefault(size = 20) Pageable pageable) { + if (StringUtils.isBlank(userId)) { + throw new CoreException(ErrorType.BAD_REQUEST); + } + Page orderInfos = orderFacade.getOrderList(userId, pageable); + OrderV1Dto.OrderPageResponse response = OrderV1Dto.OrderPageResponse.from(orderInfos); + return ApiResponse.success(response); + } + + @RequestMapping(method = RequestMethod.GET, path = "/{orderId}") + @Override + public ApiResponse getOrderDetail( + @RequestHeader(value = "X-USER-ID", required = false) String userId, + @PathVariable Long orderId) { + if (StringUtils.isBlank(userId)) { + throw new CoreException(ErrorType.BAD_REQUEST); + } + OrderInfo orderInfo = orderFacade.getOrderInfo(userId, orderId); + OrderV1Dto.OrderResponse response = OrderV1Dto.OrderResponse.from(orderInfo); + return ApiResponse.success(response); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Dto.java new file mode 100644 index 000000000..01b97d12e --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Dto.java @@ -0,0 +1,73 @@ +package com.loopers.interfaces.api.order; + +import com.loopers.application.order.OrderInfo; +import com.loopers.application.order.OrderItemInfo; +import org.springframework.data.domain.Page; + +import java.util.List; + +public class OrderV1Dto { + public record OrderRequest( + List items + ) { + public record OrderItemRequest( + Long productId, + Integer quantity + ) { + } + } + + public record OrderResponse( + Long orderId, + List items, + Integer totalPrice + ) { + public static OrderResponse from(OrderInfo info) { + return new OrderResponse( + info.orderId(), + OrderItem.fromList(info.items()), + info.totalPrice() + ); + } + } + + public record OrderItem( + Long productId, + String productName, + Integer quantity, + Integer totalPrice + ) { + public static OrderItem from(OrderItemInfo info) { + return new OrderItem( + info.productId(), + info.productName(), + info.quantity(), + info.totalPrice() + ); + } + + public static List fromList(List infos) { + return infos.stream() + .map(OrderItem::from) + .toList(); + } + } + + public record OrderPageResponse( + List content, + int totalPages, + long totalElements, + int number, + int size + ) { + public static OrderPageResponse from(Page page) { + return new OrderPageResponse( + page.map(OrderResponse::from).getContent(), + page.getTotalPages(), + page.getTotalElements(), + page.getNumber(), + page.getSize() + ); + } + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Controller.java index 285e70a04..c656f69d3 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Controller.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Controller.java @@ -26,7 +26,7 @@ public ApiResponse getUserPoints(@RequestHeader(value return ApiResponse.success(response); } - @RequestMapping(method = RequestMethod.POST) + @RequestMapping(method = RequestMethod.POST, path = "/charge") @Override public ApiResponse chargeUserPoints( @RequestHeader(value = "X-USER-ID", required = false) String userId, diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1ApiSpec.java new file mode 100644 index 000000000..5217803a1 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1ApiSpec.java @@ -0,0 +1,40 @@ +package com.loopers.interfaces.api.product; + +import com.loopers.interfaces.api.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.data.domain.Pageable; + +@Tag(name = "Product V1 API", description = "์ƒํ’ˆ API ์ž…๋‹ˆ๋‹ค.") +public interface ProductV1ApiSpec { + // /api/v1/products - GET + @Operation( + method = "GET", + summary = "์ƒํ’ˆ ๋ชฉ๋ก ์กฐํšŒ", + description = "์ƒํ’ˆ ๋ชฉ๋ก์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค." + ) + ApiResponse getProductList( + @Schema( + name = "ํŽ˜์ด์ง€ ์ •๋ณด", + description = "ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ, ํŽ˜์ด์ง€ ํฌ๊ธฐ, ์ •๋ ฌ ์ •๋ณด๋ฅผ ํฌํ•จํ•œ ํŽ˜์ด์ง€ ์ •๋ณด" + + "\n- sort ์˜ต์…˜: latest (์ตœ์‹ ์ˆœ), price_asc (๊ฐ€๊ฒฉ ์˜ค๋ฆ„์ฐจ์ˆœ), like_desc (์ข‹์•„์š” ๋‚ด๋ฆผ์ฐจ์ˆœ)" + + "\n- ๊ธฐ๋ณธ๊ฐ’: page=0, size=20, sort=latest" + ) + Pageable pageable + ); + + // /api/v1/products/{productId} - GET + @Operation( + method = "GET", + summary = "์ƒํ’ˆ ์ƒ์„ธ ์กฐํšŒ", + description = "์ƒํ’ˆ ์ƒ์„ธ ์ •๋ณด๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค." + ) + ApiResponse getProductDetail( + @Schema( + name = "์ƒํ’ˆ ID", + description = "์กฐํšŒํ•  ์ƒํ’ˆ์˜ ID" + ) + Long productId + ); +} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java new file mode 100644 index 000000000..9963fca1c --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java @@ -0,0 +1,49 @@ +package com.loopers.interfaces.api.product; + +import com.loopers.application.product.ProductFacade; +import com.loopers.application.product.ProductInfo; +import com.loopers.interfaces.api.ApiResponse; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.PageableDefault; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/v1/products") +public class ProductV1Controller implements ProductV1ApiSpec { + private final ProductFacade productFacade; + + @RequestMapping(method = RequestMethod.GET) + @Override + public ApiResponse getProductList(@PageableDefault(size = 20) Pageable pageable) { + int page = pageable.getPageNumber(); + int size = pageable.getPageSize(); + String sortStr = pageable.getSort().toString().split(":")[0]; + Sort sort = Sort.by(Sort.Direction.DESC, "createdAt"); + if (StringUtils.equals(sortStr, "price_asc")) { + sort = Sort.by(Sort.Direction.ASC, "price"); + } else if (StringUtils.equals(sortStr, "like_desc")) { + sort = Sort.by(Sort.Direction.DESC, "like_count"); + } + + Page products = productFacade.getProductList(PageRequest.of(page, size, sort)); + ProductV1Dto.ProductsPageResponse response = ProductV1Dto.ProductsPageResponse.from(products); + return ApiResponse.success(response); + } + + @RequestMapping(method = RequestMethod.GET, path = "/{productId}") + @Override + public ApiResponse getProductDetail(@PathVariable Long productId) { + ProductInfo info = productFacade.getProductDetail(productId); + ProductV1Dto.ProductResponse response = ProductV1Dto.ProductResponse.from(info); + return ApiResponse.success(response); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Dto.java new file mode 100644 index 000000000..29f957cf5 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Dto.java @@ -0,0 +1,46 @@ +package com.loopers.interfaces.api.product; + +import com.loopers.application.product.ProductInfo; +import org.springframework.data.domain.Page; + +import java.util.List; + +public class ProductV1Dto { + public record ProductResponse( + Long id, + String name, + String brand, + int price, + int likes, + int stock + ) { + public static ProductResponse from(ProductInfo info) { + return new ProductResponse( + info.id(), + info.name(), + info.brand(), + info.price(), + info.likes(), + info.stock() + ); + } + } + + public record ProductsPageResponse( + List content, + int totalPages, + long totalElements, + int number, + int size + ) { + public static ProductsPageResponse from(Page page) { + return new ProductsPageResponse( + page.map(ProductResponse::from).getContent(), + page.getTotalPages(), + page.getTotalElements(), + page.getNumber(), + page.getSize() + ); + } + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java index 059faf73f..e61ec5290 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java @@ -3,7 +3,10 @@ import com.loopers.application.user.UserFacade; import com.loopers.application.user.UserInfo; import com.loopers.interfaces.api.ApiResponse; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; import org.springframework.web.bind.annotation.*; @RequiredArgsConstructor @@ -25,9 +28,12 @@ public ApiResponse registerUser(@RequestBody UserV1Dto.U return ApiResponse.success(response); } - @RequestMapping(method = RequestMethod.GET, path = "/{userId}") + @RequestMapping(method = RequestMethod.GET, path = "/me") @Override - public ApiResponse getUserInfo(@PathVariable String userId) { + public ApiResponse getUserInfo(@RequestHeader(value = "X-USER-ID", required = false) String userId) { + if (StringUtils.isBlank(userId)) { + throw new CoreException(ErrorType.BAD_REQUEST); + } UserInfo info = userFacade.getUserInfo(userId); UserV1Dto.UserResponse response = UserV1Dto.UserResponse.from(info); return ApiResponse.success(response); diff --git a/apps/commerce-api/src/test/java/com/loopers/application/order/OrderFacadeIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/application/order/OrderFacadeIntegrationTest.java new file mode 100644 index 000000000..7adbb731d --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/application/order/OrderFacadeIntegrationTest.java @@ -0,0 +1,340 @@ +package com.loopers.application.order; + +import com.loopers.domain.brand.Brand; +import com.loopers.domain.common.vo.Price; +import com.loopers.domain.metrics.product.ProductMetrics; +import com.loopers.domain.point.Point; +import com.loopers.domain.product.Product; +import com.loopers.domain.supply.Supply; +import com.loopers.domain.supply.vo.Stock; +import com.loopers.domain.user.User; +import com.loopers.infrastructure.brand.BrandJpaRepository; +import com.loopers.infrastructure.metrics.product.ProductMetricsJpaRepository; +import com.loopers.infrastructure.point.PointJpaRepository; +import com.loopers.infrastructure.product.ProductJpaRepository; +import com.loopers.infrastructure.supply.SupplyJpaRepository; +import com.loopers.infrastructure.user.UserJpaRepository; +import com.loopers.interfaces.api.order.OrderV1Dto; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import com.loopers.utils.DatabaseCleanUp; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@SpringBootTest +@Transactional +@DisplayName("์ฃผ๋ฌธ Facade(OrderFacade) ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ") +public class OrderFacadeIntegrationTest { + + @Autowired + private OrderFacade orderFacade; + + @Autowired + private UserJpaRepository userJpaRepository; + + @Autowired + private PointJpaRepository pointJpaRepository; + + @Autowired + private BrandJpaRepository brandJpaRepository; + + @Autowired + private ProductJpaRepository productJpaRepository; + + @Autowired + private ProductMetricsJpaRepository productMetricsJpaRepository; + + @Autowired + private SupplyJpaRepository supplyJpaRepository; + + @Autowired + private DatabaseCleanUp databaseCleanUp; + + private String userId; + private Long userEntityId; + private Long brandId; + private Long productId1; + private Long productId2; + private Long productId3; + + @AfterEach + void tearDown() { + databaseCleanUp.truncateAllTables(); + } + + @BeforeEach + void setup() { + // User ๋“ฑ๋ก + User user = User.create("user123", "test@test.com", "1993-03-13", "male"); + User savedUser = userJpaRepository.save(user); + userId = savedUser.getUserId(); + userEntityId = savedUser.getId(); + + // Point ๋“ฑ๋ก ๋ฐ ์ถฉ์ „ + Point point = Point.create(userEntityId); + point.charge(100000); + pointJpaRepository.save(point); + + // Brand ๋“ฑ๋ก + Brand brand = Brand.create("Nike"); + Brand savedBrand = brandJpaRepository.save(brand); + brandId = savedBrand.getId(); + + // Product ๋“ฑ๋ก + Product product1 = Product.create("์ƒํ’ˆ1", brandId, new Price(10000)); + Product savedProduct1 = productJpaRepository.save(product1); + productId1 = savedProduct1.getId(); + ProductMetrics metrics1 = ProductMetrics.create(productId1, 0); + productMetricsJpaRepository.save(metrics1); + + Product product2 = Product.create("์ƒํ’ˆ2", brandId, new Price(20000)); + Product savedProduct2 = productJpaRepository.save(product2); + productId2 = savedProduct2.getId(); + ProductMetrics metrics2 = ProductMetrics.create(productId2, 0); + productMetricsJpaRepository.save(metrics2); + + Product product3 = Product.create("์ƒํ’ˆ3", brandId, new Price(15000)); + Product savedProduct3 = productJpaRepository.save(product3); + productId3 = savedProduct3.getId(); + ProductMetrics metrics3 = ProductMetrics.create(productId3, 0); + productMetricsJpaRepository.save(metrics3); + + // Supply ๋“ฑ๋ก (์žฌ๊ณ  ์„ค์ •) + Supply supply1 = Supply.create(productId1, new Stock(100)); + supplyJpaRepository.save(supply1); + + Supply supply2 = Supply.create(productId2, new Stock(50)); + supplyJpaRepository.save(supply2); + + Supply supply3 = Supply.create(productId3, new Stock(30)); + supplyJpaRepository.save(supply3); + } + + @DisplayName("์ฃผ๋ฌธ ์ƒ์„ฑ ์‹œ, ") + @Nested + class CreateOrder { + @DisplayName("์ •์ƒ์ ์ธ ์ฃผ๋ฌธ์„ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค. (Happy Path)") + @Test + void should_createOrder_when_validRequest() { + // arrange + OrderV1Dto.OrderRequest request = new OrderV1Dto.OrderRequest( + List.of( + new OrderV1Dto.OrderRequest.OrderItemRequest(productId1, 2), + new OrderV1Dto.OrderRequest.OrderItemRequest(productId2, 1) + ) + ); + + // act + OrderInfo orderInfo = orderFacade.createOrder(userId, request); + + // assert + assertThat(orderInfo).isNotNull(); + assertThat(orderInfo.orderId()).isNotNull(); + assertThat(orderInfo.items()).hasSize(2); + assertThat(orderInfo.totalPrice()).isEqualTo(40000); + } + + @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ƒํ’ˆ ID๊ฐ€ ํฌํ•จ๋œ ๊ฒฝ์šฐ, ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") + @Test + void should_throwException_when_productIdDoesNotExist() { + // arrange + Long nonExistentProductId = 99999L; + OrderV1Dto.OrderRequest request = new OrderV1Dto.OrderRequest( + List.of( + new OrderV1Dto.OrderRequest.OrderItemRequest(nonExistentProductId, 1) + ) + ); + + // act & assert + // Note: ํ˜„์žฌ ๊ตฌํ˜„์—์„œ๋Š” productMap.get()์ด null์„ ๋ฐ˜ํ™˜ํ•˜์—ฌ NullPointerException์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Œ + // ๋˜๋Š” SupplyService.checkAndDecreaseStock์—์„œ NOT_FOUND ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Œ + assertThrows(Exception.class, () -> orderFacade.createOrder(userId, request)); + } + + @DisplayName("๋‹จ์ผ ์ƒํ’ˆ ์žฌ๊ณ  ๋ถ€์กฑ ์‹œ, ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") + @Test + void should_throwException_when_singleProductStockInsufficient() { + // arrange + OrderV1Dto.OrderRequest request = new OrderV1Dto.OrderRequest( + List.of( + new OrderV1Dto.OrderRequest.OrderItemRequest(productId1, 99999) + ) + ); + + // act & assert + CoreException exception = assertThrows(CoreException.class, () -> orderFacade.createOrder(userId, request)); + assertThat(exception.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); + assertThat(exception.getMessage()).contains("์žฌ๊ณ ๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค"); + } + + @DisplayName("์—ฌ๋Ÿฌ ์ƒํ’ˆ ์ค‘ ์ผ๋ถ€๋งŒ ์žฌ๊ณ  ๋ถ€์กฑ ์‹œ, ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") + @Test + void should_throwException_when_partialStockInsufficient() { + // arrange + // productId1: ์žฌ๊ณ  100, productId2: ์žฌ๊ณ  50 + // productId1์€ ์ถฉ๋ถ„ํ•˜์ง€๋งŒ productId2๋Š” ๋ถ€์กฑ + OrderV1Dto.OrderRequest request = new OrderV1Dto.OrderRequest( + List.of( + new OrderV1Dto.OrderRequest.OrderItemRequest(productId1, 10), // ์žฌ๊ณ  ์ถฉ๋ถ„ + new OrderV1Dto.OrderRequest.OrderItemRequest(productId2, 99999) // ์žฌ๊ณ  ๋ถ€์กฑ + ) + ); + + // act & assert + CoreException exception = assertThrows(CoreException.class, () -> orderFacade.createOrder(userId, request)); + assertThat(exception.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); + assertThat(exception.getMessage()).contains("์žฌ๊ณ ๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค"); + // Note: ํ˜„์žฌ ๊ตฌํ˜„์—์„œ๋Š” ์ฒซ ๋ฒˆ์งธ ์žฌ๊ณ  ๋ถ€์กฑ ์ƒํ’ˆ์—์„œ๋งŒ ์˜ˆ์™ธ ๋ฐœ์ƒ + // ๊ฐœ์„  ํ›„์—๋Š” ๋ชจ๋“  ๋ถ€์กฑํ•œ ์ƒํ’ˆ์„ ํ•œ ๋ฒˆ์— ์•Œ๋ ค์ค„ ์ˆ˜ ์žˆ์Œ + } + + @DisplayName("์—ฌ๋Ÿฌ ์ƒํ’ˆ ๋ชจ๋‘ ์žฌ๊ณ  ๋ถ€์กฑ ์‹œ, ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") + @Test + void should_throwException_when_allProductsStockInsufficient() { + // arrange + OrderV1Dto.OrderRequest request = new OrderV1Dto.OrderRequest( + List.of( + new OrderV1Dto.OrderRequest.OrderItemRequest(productId1, 99999), + new OrderV1Dto.OrderRequest.OrderItemRequest(productId2, 99999) + ) + ); + + // act & assert + CoreException exception = assertThrows(CoreException.class, () -> orderFacade.createOrder(userId, request)); + assertThat(exception.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); + assertThat(exception.getMessage()).contains("์žฌ๊ณ ๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค"); + // Note: ํ˜„์žฌ ๊ตฌํ˜„์—์„œ๋Š” ์ฒซ ๋ฒˆ์งธ ์žฌ๊ณ  ๋ถ€์กฑ ์ƒํ’ˆ์—์„œ๋งŒ ์˜ˆ์™ธ ๋ฐœ์ƒ + } + + @DisplayName("Supply ์—”ํ‹ฐํ‹ฐ๊ฐ€ ์—†๋Š” ์ƒํ’ˆ ์ฃผ๋ฌธ ์‹œ, ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") + @Test + void should_throwException_when_supplyDoesNotExist() { + // arrange + // Supply๊ฐ€ ์—†๋Š” ์ƒํ’ˆ ์ƒ์„ฑ + Product productWithoutSupply = Product.create("์žฌ๊ณ ์—†๋Š”์ƒํ’ˆ", brandId, new Price(10000)); + Product savedProduct = productJpaRepository.save(productWithoutSupply); + ProductMetrics metrics = ProductMetrics.create(savedProduct.getId(), 0); + productMetricsJpaRepository.save(metrics); + // Supply๋Š” ์ƒ์„ฑํ•˜์ง€ ์•Š์Œ + + OrderV1Dto.OrderRequest request = new OrderV1Dto.OrderRequest( + List.of( + new OrderV1Dto.OrderRequest.OrderItemRequest(savedProduct.getId(), 1) + ) + ); + + // act & assert + CoreException exception = assertThrows(CoreException.class, () -> orderFacade.createOrder(userId, request)); + assertThat(exception.getErrorType()).isEqualTo(ErrorType.NOT_FOUND); + assertThat(exception.getMessage()).contains("์žฌ๊ณ  ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"); + } + + @DisplayName("ํฌ์ธํŠธ ๋ถ€์กฑ ์‹œ, ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") + @Test + void should_throwException_when_pointInsufficient() { + // arrange + // ํฌ์ธํŠธ๋ฅผ ๋ชจ๋‘ ์‚ฌ์šฉ + OrderV1Dto.OrderRequest firstOrder = new OrderV1Dto.OrderRequest( + List.of( + new OrderV1Dto.OrderRequest.OrderItemRequest(productId1, 10) + ) + ); + orderFacade.createOrder(userId, firstOrder); + + // ํฌ์ธํŠธ ๋ถ€์กฑํ•œ ์ฃผ๋ฌธ ์‹œ๋„ + OrderV1Dto.OrderRequest request = new OrderV1Dto.OrderRequest( + List.of( + new OrderV1Dto.OrderRequest.OrderItemRequest(productId2, 1) // 20000์› ํ•„์š” (๋ถ€์กฑ) + ) + ); + + // act & assert + CoreException exception = assertThrows(CoreException.class, () -> orderFacade.createOrder(userId, request)); + assertThat(exception.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); + // Note: ์žฌ๊ณ  ๋ถ€์กฑ ์˜ˆ์™ธ๊ฐ€ ๋จผ์ € ๋ฐœ์ƒํ•  ์ˆ˜๋„ ์žˆ์œผ๋ฏ€๋กœ, ํฌ์ธํŠธ ๋ถ€์กฑ ๋ฉ”์‹œ์ง€๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธ + // ๋˜๋Š” ์žฌ๊ณ  ๋ถ€์กฑ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Œ (99999๋Š” ์žฌ๊ณ  ๋ถ€์กฑ) + } + + @DisplayName("ํฌ์ธํŠธ๊ฐ€ ์ •ํ™•ํžˆ ์ผ์น˜ํ•  ๊ฒฝ์šฐ, ์ฃผ๋ฌธ์ด ์„ฑ๊ณตํ•œ๋‹ค. (Edge Case)") + @Test + void should_createOrder_when_pointExactlyMatches() { + // arrange + // ํฌ์ธํŠธ๋ฅผ ๊ฑฐ์˜ ๋ชจ๋‘ ์‚ฌ์šฉ + OrderV1Dto.OrderRequest firstOrder = new OrderV1Dto.OrderRequest( + List.of( + new OrderV1Dto.OrderRequest.OrderItemRequest(productId1, 9) // 90000์› ์‚ฌ์šฉ + ) + ); + orderFacade.createOrder(userId, firstOrder); + // ๋‚จ์€ ํฌ์ธํŠธ: 10000์› + + // ์ •ํ™•ํžˆ ์ผ์น˜ํ•˜๋Š” ์ฃผ๋ฌธ + OrderV1Dto.OrderRequest request = new OrderV1Dto.OrderRequest( + List.of( + new OrderV1Dto.OrderRequest.OrderItemRequest(productId1, 1) // ์ •ํ™•ํžˆ 10000์› + ) + ); + + // act + OrderInfo orderInfo = orderFacade.createOrder(userId, request); + + // assert + assertThat(orderInfo).isNotNull(); + assertThat(orderInfo.totalPrice()).isEqualTo(10000); + } + + @DisplayName("์ค‘๋ณต ์ƒํ’ˆ์ด ํฌํ•จ๋œ ๊ฒฝ์šฐ, IllegalStateException์ด ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") + @Test + void should_throwException_when_duplicateProducts() { + // arrange + // ๊ฐ™์€ ์ƒํ’ˆ์„ ์—ฌ๋Ÿฌ ๋ฒˆ ์ฃผ๋ฌธ ํ•ญ๋ชฉ์— ํฌํ•จ + // Note: Collectors.toMap()์€ ์ค‘๋ณต ํ‚ค๊ฐ€ ์žˆ์œผ๋ฉด IllegalStateException์„ ๋ฐœ์ƒ์‹œํ‚ด + OrderV1Dto.OrderRequest request = new OrderV1Dto.OrderRequest( + List.of( + new OrderV1Dto.OrderRequest.OrderItemRequest(productId1, 2), + new OrderV1Dto.OrderRequest.OrderItemRequest(productId1, 3) // ์ค‘๋ณต + ) + ); + + // act & assert + // Note: Collectors.toMap()์—์„œ ์ค‘๋ณต ํ‚ค๋กœ ์ธํ•ด IllegalStateException ๋ฐœ์ƒ + assertThrows(IllegalStateException.class, () -> orderFacade.createOrder(userId, request)); + } + + // Note: ํŠธ๋žœ์žญ์…˜ ๋กค๋ฐฑ ๊ฒ€์ฆ์€ E2E ํ…Œ์ŠคํŠธ์—์„œ ์ˆ˜ํ–‰ํ•˜๋Š” ๊ฒƒ์ด ๋” ์ ์ ˆํ•จ + // ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ๋Š” @Transactional๋กœ ์ธํ•ด ๋กค๋ฐฑ์ด ์ œ๋Œ€๋กœ ๊ฒ€์ฆ๋˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Œ + + @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์‚ฌ์šฉ์ž๋กœ ์ฃผ๋ฌธ ์‹œ๋„ ์‹œ, ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") + @Test + void should_throwException_when_userDoesNotExist() { + // arrange + String nonExistentUserId = "nonexist"; + OrderV1Dto.OrderRequest request = new OrderV1Dto.OrderRequest( + List.of( + new OrderV1Dto.OrderRequest.OrderItemRequest(productId1, 1) + ) + ); + + // act & assert + CoreException exception = assertThrows(CoreException.class, () -> orderFacade.createOrder(nonExistentUserId, request)); + assertThat(exception.getErrorType()).isEqualTo(ErrorType.NOT_FOUND); + assertThat(exception.getMessage()).contains("์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"); + } + + // Note: Point ์—”ํ‹ฐํ‹ฐ๊ฐ€ ์—†๋Š” ์‚ฌ์šฉ์ž ํ…Œ์ŠคํŠธ๋Š” E2E ํ…Œ์ŠคํŠธ์—์„œ ์ˆ˜ํ–‰ํ•˜๋Š” ๊ฒƒ์ด ๋” ์ ์ ˆํ•จ + // ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ๋Š” @Transactional๋กœ ์ธํ•ด ๋กค๋ฐฑ์ด ์ œ๋Œ€๋กœ ๊ฒ€์ฆ๋˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Œ + // E2E ํ…Œ์ŠคํŠธ์—์„œ ์‹ค์ œ HTTP ์š”์ฒญ์„ ํ†ตํ•ด ํŠธ๋žœ์žญ์…˜ ๋กค๋ฐฑ์„ ๊ฒ€์ฆํ•  ์ˆ˜ ์žˆ์Œ + } +} + diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandTest.java new file mode 100644 index 000000000..3b6dd2093 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandTest.java @@ -0,0 +1,127 @@ +package com.loopers.domain.brand; + +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@DisplayName("๋ธŒ๋žœ๋“œ(Brand) Entity ํ…Œ์ŠคํŠธ") +public class BrandTest { + + @DisplayName("๋ธŒ๋žœ๋“œ๋ฅผ ์ƒ์„ฑํ•  ๋•Œ, ") + @Nested + class Create { + @DisplayName("์ •์ƒ์ ์ธ ์ด๋ฆ„์œผ๋กœ ๋ธŒ๋žœ๋“œ๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค. (Happy Path)") + @Test + void should_createBrand_when_validName() { + // arrange + String brandName = "Nike"; + + // act + Brand brand = Brand.create(brandName); + + // assert + assertThat(brand.getName()).isEqualTo("Nike"); + } + + @DisplayName("๋นˆ ๋ฌธ์ž์—ด๋กœ ๋ธŒ๋žœ๋“œ๋ฅผ ์ƒ์„ฑํ•  ๊ฒฝ์šฐ, ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") + @Test + void should_throwException_when_emptyName() { + // arrange + String brandName = ""; + + // act & assert + CoreException exception = assertThrows(CoreException.class, () -> Brand.create(brandName)); + assertThat(exception.getMessage()).isEqualTo("๋ธŒ๋žœ๋“œ ์ด๋ฆ„์€ ํ•„์ˆ˜์ด๋ฉฐ 1์ž ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + assertThat(exception.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); + } + + @DisplayName("null๋กœ ๋ธŒ๋žœ๋“œ๋ฅผ ์ƒ์„ฑํ•  ๊ฒฝ์šฐ, ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") + @Test + void should_throwException_when_nullName() { + // arrange + String brandName = null; + + // act & assert + CoreException exception = assertThrows(CoreException.class, () -> Brand.create(brandName)); + assertThat(exception.getMessage()).isEqualTo("๋ธŒ๋žœ๋“œ ์ด๋ฆ„์€ ํ•„์ˆ˜์ด๋ฉฐ 1์ž ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + assertThat(exception.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); + } + + @DisplayName("๊ณต๋ฐฑ๋งŒ ์žˆ๋Š” ๋ฌธ์ž์—ด๋กœ ๋ธŒ๋žœ๋“œ๋ฅผ ์ƒ์„ฑํ•  ๊ฒฝ์šฐ, ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") + @Test + void should_throwException_when_blankName() { + // arrange + String brandName = " "; + + // act & assert + CoreException exception = assertThrows(CoreException.class, () -> Brand.create(brandName)); + assertThat(exception.getMessage()).isEqualTo("๋ธŒ๋žœ๋“œ ์ด๋ฆ„์€ ํ•„์ˆ˜์ด๋ฉฐ 1์ž ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + assertThat(exception.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); + } + + @DisplayName("๊ธด ์ด๋ฆ„์œผ๋กœ๋„ ๋ธŒ๋žœ๋“œ๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค. (Edge Case)") + @Test + void should_createBrand_when_longName() { + // arrange + String brandName = "A".repeat(1000); + + // act + Brand brand = Brand.create(brandName); + + // assert + assertThat(brand.getName()).isEqualTo("A".repeat(1000)); + } + } + + @DisplayName("๋ธŒ๋žœ๋“œ ์กฐํšŒ๋ฅผ ํ•  ๋•Œ, ") + @Nested + class Retrieve { + @DisplayName("์ƒ์„ฑํ•œ ๋ธŒ๋žœ๋“œ์˜ ์ด๋ฆ„์„ ์กฐํšŒํ•  ์ˆ˜ ์žˆ๋‹ค. (Happy Path)") + @Test + void should_retrieveName_when_brandCreated() { + // arrange + Brand brand = Brand.create("Adidas"); + + // act + String name = brand.getName(); + + // assert + assertThat(name).isEqualTo("Adidas"); + } + } + + @DisplayName("๋ธŒ๋žœ๋“œ ๋™๋“ฑ์„ฑ์„ ํ™•์ธํ•  ๋•Œ, ") + @Nested + class Equality { + @DisplayName("๊ฐ™์€ ์ด๋ฆ„์„ ๊ฐ€์ง„ ๋ธŒ๋žœ๋“œ๋Š” ์„œ๋กœ ๋‹ค๋ฅธ ์ธ์Šคํ„ด์Šค์ด๋‹ค. (Edge Case)") + @Test + void should_beDifferentInstances_when_sameName() { + // arrange + String brandName = "Puma"; + Brand brand1 = Brand.create(brandName); + Brand brand2 = Brand.create(brandName); + + // act & assert + assertThat(brand1).isNotSameAs(brand2); + assertThat(brand1).isNotEqualTo(brand2); + } + + @DisplayName("๋‹ค๋ฅธ ์ด๋ฆ„์„ ๊ฐ€์ง„ ๋ธŒ๋žœ๋“œ๋Š” ์„œ๋กœ ๋‹ค๋ฅธ ์ธ์Šคํ„ด์Šค์ด๋‹ค. (Happy Path)") + @Test + void should_beDifferentInstances_when_differentNames() { + // arrange + Brand brand1 = Brand.create("Nike"); + Brand brand2 = Brand.create("Adidas"); + + // act & assert + assertThat(brand1).isNotSameAs(brand2); + assertThat(brand1).isNotEqualTo(brand2); + assertThat(brand1.getName()).isNotEqualTo(brand2.getName()); + } + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/common/vo/PriceTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/common/vo/PriceTest.java new file mode 100644 index 000000000..f4427c64b --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/common/vo/PriceTest.java @@ -0,0 +1,62 @@ +package com.loopers.domain.common.vo; + +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@DisplayName("๊ฐ€๊ฒฉ(Price) Value Object ํ…Œ์ŠคํŠธ") +public class PriceTest { + + @DisplayName("๊ฐ€๊ฒฉ์„ ์ƒ์„ฑํ•  ๋•Œ, ") + @Nested + class Create { + @DisplayName("์ •์ƒ์ ์ธ ๊ฐ€๊ฒฉ์œผ๋กœ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค. (Happy Path)") + @Test + void should_createPrice_when_validAmount() { + // arrange + int amount = 10000; + + // act + Price price = new Price(amount); + + // assert + assertThat(price.amount()).isEqualTo(10000); + } + + @DisplayName("๊ฐ€๊ฒฉ์ด 0์ด์–ด๋„ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค. (Edge Case)") + @Test + void should_createPrice_when_amountIsZero() { + // arrange + int amount = 0; + + // act + Price price = new Price(amount); + + // assert + assertThat(price.amount()).isEqualTo(0); + } + + @DisplayName("์Œ์ˆ˜ ๊ฐ€๊ฒฉ์œผ๋กœ ์ƒ์„ฑํ•˜๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") + @ParameterizedTest + @ValueSource(ints = {-1, -10, -100, -1000, -10000}) + void should_throwException_when_amountIsNegative(int invalidAmount) { + // arrange: invalidAmount parameter + + // act + CoreException exception = assertThrows(CoreException.class, () -> { + new Price(invalidAmount); + }); + + // assert + assertThat(exception.getMessage()).isEqualTo("๊ฐ€๊ฒฉ์€ ์Œ์ˆ˜๊ฐ€ ๋  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + assertThat(exception.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); + } + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/like/product/LikeProductServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/like/product/LikeProductServiceIntegrationTest.java new file mode 100644 index 000000000..d339870ce --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/like/product/LikeProductServiceIntegrationTest.java @@ -0,0 +1,232 @@ +package com.loopers.domain.like.product; + +import com.loopers.domain.product.ProductService; +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; + +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@SpringBootTest +@Transactional +@DisplayName("์ƒํ’ˆ ์ข‹์•„์š” ์„œ๋น„์Šค(LikeProductService) ํ…Œ์ŠคํŠธ") +public class LikeProductServiceIntegrationTest { + + @MockitoSpyBean + private LikeProductRepository spyLikeProductRepository; + + @MockitoSpyBean + private ProductService spyProductService; + + @Autowired + private LikeProductService likeProductService; + + @DisplayName("์ข‹์•„์š”๋ฅผ ๋“ฑ๋กํ•  ๋•Œ, ") + @Nested + class LikeProductTest { + @DisplayName("์ฒ˜์Œ ์ข‹์•„์š”๋ฅผ ๋“ฑ๋กํ•˜๋ฉด ์ƒˆ๋กœ์šด ์ข‹์•„์š”๊ฐ€ ์ƒ์„ฑ๋œ๋‹ค. (Happy Path)") + @Test + void should_createLikeProduct_when_firstLike() { + // arrange + Long userId = 1L; + Long productId = 100L; + when(spyLikeProductRepository.findByUserIdAndProductId(userId, productId)) + .thenReturn(Optional.empty()); + + // act + likeProductService.likeProduct(userId, productId); + + // assert + verify(spyLikeProductRepository).findByUserIdAndProductId(1L, 100L); + ArgumentCaptor captor = ArgumentCaptor.forClass(LikeProduct.class); + verify(spyLikeProductRepository, times(1)).save(captor.capture()); + LikeProduct savedLike = captor.getValue(); + assertThat(savedLike.getUserId()).isEqualTo(1L); + assertThat(savedLike.getProductId()).isEqualTo(100L); + } + + @DisplayName("์ด๋ฏธ ์‚ญ์ œ๋œ ์ข‹์•„์š”๋ฅผ ๋‹ค์‹œ ๋“ฑ๋กํ•˜๋ฉด ๋ณต์›๋œ๋‹ค. (Idempotency)") + @Test + void should_restoreLikeProduct_when_alreadyDeleted() { + // arrange + Long userId = 1L; + Long productId = 100L; + LikeProduct deletedLike = LikeProduct.create(userId, productId); + deletedLike.delete(); // ์‚ญ์ œ ์ƒํƒœ๋กœ ๋งŒ๋“ค๊ธฐ + when(spyLikeProductRepository.findByUserIdAndProductId(userId, productId)) + .thenReturn(Optional.of(deletedLike)); + + // act + likeProductService.likeProduct(userId, productId); + + // assert + verify(spyLikeProductRepository).findByUserIdAndProductId(1L, 100L); + verify(spyLikeProductRepository, never()).save(any()); + // restore๊ฐ€ ํ˜ธ์ถœ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ (deletedAt์ด null์ด ๋˜์–ด์•ผ ํ•จ) + assertThat(deletedLike.getDeletedAt()).isNull(); + } + + @DisplayName("๊ฐ™์€ ์‚ฌ์šฉ์ž๊ฐ€ ๊ฐ™์€ ์ƒํ’ˆ์— ์—ฌ๋Ÿฌ ๋ฒˆ ์ข‹์•„์š”๋ฅผ ๋“ฑ๋กํ•ด๋„ ํ•œ ๋ฒˆ๋งŒ ์ €์žฅ๋œ๋‹ค. (Idempotency)") + @Test + void should_notCreateDuplicate_when_likeMultipleTimes() { + // arrange + Long userId = 1L; + Long productId = 100L; + LikeProduct existingLike = LikeProduct.create(userId, productId); + when(spyLikeProductRepository.findByUserIdAndProductId(userId, productId)) + .thenReturn(Optional.of(existingLike)); + + // act + likeProductService.likeProduct(userId, productId); + likeProductService.likeProduct(userId, productId); + + // assert + verify(spyLikeProductRepository, times(2)).findByUserIdAndProductId(1L, 100L); + verify(spyLikeProductRepository, never()).save(any()); + } + } + + + @DisplayName("์ข‹์•„์š”๋ฅผ ์ทจ์†Œํ•  ๋•Œ, ") + @Nested + class UnlikeProduct { + @DisplayName("์กด์žฌํ•˜๋Š” ์ข‹์•„์š”๋ฅผ ์ทจ์†Œํ•˜๋ฉด ์‚ญ์ œ๋œ๋‹ค. (Happy Path)") + @Test + void should_deleteLikeProduct_when_likeExists() { + // arrange + Long userId = 1L; + Long productId = 100L; + LikeProduct likeProduct = LikeProduct.create(userId, productId); + when(spyLikeProductRepository.findByUserIdAndProductId(userId, productId)) + .thenReturn(Optional.of(likeProduct)); + + // act + likeProductService.unlikeProduct(userId, productId); + + // assert + verify(spyLikeProductRepository).findByUserIdAndProductId(1L, 100L); + assertThat(likeProduct.getDeletedAt()).isNotNull(); + } + + @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ข‹์•„์š”๋ฅผ ์ทจ์†Œํ•ด๋„ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š๋Š”๋‹ค. (Edge Case)") + @Test + void should_notThrowException_when_likeNotFound() { + // arrange + Long userId = 1L; + Long productId = 100L; + when(spyLikeProductRepository.findByUserIdAndProductId(userId, productId)) + .thenReturn(Optional.empty()); + + // act & assert + likeProductService.unlikeProduct(userId, productId); + verify(spyLikeProductRepository).findByUserIdAndProductId(1L, 100L); + // ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š์•„์•ผ ํ•จ + } + + @DisplayName("์ด๋ฏธ ์‚ญ์ œ๋œ ์ข‹์•„์š”๋ฅผ ๋‹ค์‹œ ์ทจ์†Œํ•ด๋„ ๋ฉฑ๋“ฑํ•˜๊ฒŒ ๋™์ž‘ํ•œ๋‹ค. (Idempotency)") + @Test + void should_beIdempotent_when_unlikeDeletedLike() { + // arrange + Long userId = 1L; + Long productId = 100L; + LikeProduct deletedLike = LikeProduct.create(userId, productId); + deletedLike.delete(); + when(spyLikeProductRepository.findByUserIdAndProductId(userId, productId)) + .thenReturn(Optional.of(deletedLike)); + + // act + likeProductService.unlikeProduct(userId, productId); + + // assert + verify(spyLikeProductRepository).findByUserIdAndProductId(1L, 100L); + // delete๋Š” ๋ฉฑ๋“ฑํ•˜๊ฒŒ ๋™์ž‘ํ•˜๋ฏ€๋กœ deletedAt์ด ๊ทธ๋Œ€๋กœ ์œ ์ง€๋˜์–ด์•ผ ํ•จ + assertThat(deletedLike.getDeletedAt()).isNotNull(); + } + } + + @DisplayName("์ข‹์•„์š” ํ† ๊ธ€์„ ํ•  ๋•Œ, ") + @Nested + class ToggleLike { + @DisplayName("์ข‹์•„์š”๊ฐ€ ์—†์œผ๋ฉด ๋“ฑ๋กํ•˜๊ณ , ์žˆ์œผ๋ฉด ์ทจ์†Œํ•œ๋‹ค. (Toggle)") + @Test + void should_toggleLike_when_likeAndUnlike() { + // arrange + Long userId = 1L; + Long productId = 100L; + when(spyLikeProductRepository.findByUserIdAndProductId(userId, productId)) + .thenReturn(Optional.empty()) + .thenReturn(Optional.of(LikeProduct.create(userId, productId))); + + // act - ์ฒซ ๋ฒˆ์งธ ํ˜ธ์ถœ: ์ข‹์•„์š” ๋“ฑ๋ก + likeProductService.likeProduct(userId, productId); + // ๋‘ ๋ฒˆ์งธ ํ˜ธ์ถœ: ์ข‹์•„์š” ์ทจ์†Œ + likeProductService.unlikeProduct(userId, productId); + + // assert + verify(spyLikeProductRepository, times(2)).findByUserIdAndProductId(1L, 100L); + verify(spyLikeProductRepository, times(1)).save(any(LikeProduct.class)); + } + } + + @DisplayName("์ข‹์•„์š”ํ•œ ์ƒํ’ˆ ๋ชฉ๋ก์„ ์กฐํšŒํ•  ๋•Œ, ") + @Nested + class GetLikedProducts { + @DisplayName("์ข‹์•„์š”ํ•œ ์ƒํ’ˆ์ด ์žˆ์œผ๋ฉด ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. (Happy Path)") + @Test + void should_returnLikedProducts_when_likesExist() { + // arrange + Long userId = 1L; + Pageable pageable = PageRequest.of(0, 20); + List likedProducts = List.of( + LikeProduct.create(userId, 100L), + LikeProduct.create(userId, 200L) + ); + Page productPage = new PageImpl<>(likedProducts, pageable, 2); + when(spyLikeProductRepository.getLikeProductsByUserIdAndDeletedAtIsNull(userId, pageable)) + .thenReturn(productPage); + + // act + Page result = likeProductService.getLikedProducts(userId, pageable); + + // assert + verify(spyLikeProductRepository).getLikeProductsByUserIdAndDeletedAtIsNull(1L, Pageable.ofSize(20)); + assertThat(result).isNotNull(); + assertThat(result.getContent()).hasSize(2); + assertThat(result.getTotalElements()).isEqualTo(2); + } + + @DisplayName("์ข‹์•„์š”ํ•œ ์ƒํ’ˆ์ด ์—†์œผ๋ฉด ๋นˆ ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. (Edge Case)") + @Test + void should_returnEmptyList_when_noLikes() { + // arrange + Long userId = 1L; + Pageable pageable = PageRequest.of(0, 20); + Page emptyPage = new PageImpl<>(List.of(), pageable, 0); + when(spyLikeProductRepository.getLikeProductsByUserIdAndDeletedAtIsNull(userId, pageable)) + .thenReturn(emptyPage); + + // act + Page result = likeProductService.getLikedProducts(userId, pageable); + + // assert + verify(spyLikeProductRepository).getLikeProductsByUserIdAndDeletedAtIsNull(1L, Pageable.ofSize(20)); + assertThat(result).isNotNull(); + assertThat(result.getContent()).isEmpty(); + assertThat(result.getTotalElements()).isEqualTo(0); + } + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/like/product/LikeProductTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/like/product/LikeProductTest.java new file mode 100644 index 000000000..4b072ff62 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/like/product/LikeProductTest.java @@ -0,0 +1,202 @@ +package com.loopers.domain.like.product; + +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@DisplayName("์ƒํ’ˆ ์ข‹์•„์š”(LikeProduct) Entity ํ…Œ์ŠคํŠธ") +public class LikeProductTest { + + @DisplayName("์ข‹์•„์š”๋ฅผ ์ƒ์„ฑํ•  ๋•Œ, ") + @Nested + class Create { + @DisplayName("์ •์ƒ์ ์ธ userId์™€ productId๋กœ ์ข‹์•„์š”๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค. (Happy Path)") + @Test + void should_createLikeProduct_when_validUserIdAndProductId() { + // arrange + Long userId = 1L; + Long productId = 100L; + + // act + LikeProduct likeProduct = LikeProduct.create(userId, productId); + + // assert + assertThat(likeProduct.getUserId()).isEqualTo(1L); + assertThat(likeProduct.getProductId()).isEqualTo(100L); + } + + @DisplayName("userId๊ฐ€ 0์ธ ๊ฒฝ์šฐ, ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") + @Test + void should_throwException_when_userIdIsZero() { + // arrange + Long userId = 0L; + Long productId = 100L; + + // act & assert + CoreException exception = assertThrows(CoreException.class, () -> LikeProduct.create(userId, productId)); + assertThat(exception.getMessage()).isEqualTo("์‚ฌ์šฉ์ž ID๋Š” 1 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + assertThat(exception.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); + } + + @DisplayName("productId๊ฐ€ 0์ธ ๊ฒฝ์šฐ, ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") + @Test + void should_throwException_when_productIdIsZero() { + // arrange + Long userId = 1L; + Long productId = 0L; + + // act & assert + CoreException exception = assertThrows(CoreException.class, () -> LikeProduct.create(userId, productId)); + assertThat(exception.getMessage()).isEqualTo("์ƒํ’ˆ ID๋Š” 1 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + assertThat(exception.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); + } + + @DisplayName("userId์™€ productId๊ฐ€ ๋ชจ๋‘ 0์ธ ๊ฒฝ์šฐ, ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") + @Test + void should_throwException_when_bothIdsAreZero() { + // arrange + Long userId = 0L; + Long productId = 0L; + + // act & assert + CoreException exception = assertThrows(CoreException.class, () -> LikeProduct.create(userId, productId)); + assertThat(exception.getMessage()).isEqualTo("์‚ฌ์šฉ์ž ID๋Š” 1 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + assertThat(exception.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); + } + + @DisplayName("์Œ์ˆ˜ userId์ธ ๊ฒฝ์šฐ, ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") + @Test + void should_throwException_when_userIdIsNegative() { + // arrange + Long userId = -1L; + Long productId = 100L; + + // act & assert + CoreException exception = assertThrows(CoreException.class, () -> LikeProduct.create(userId, productId)); + assertThat(exception.getMessage()).isEqualTo("์‚ฌ์šฉ์ž ID๋Š” 1 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + assertThat(exception.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); + } + + @DisplayName("์Œ์ˆ˜ productId์ธ ๊ฒฝ์šฐ, ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") + @Test + void should_throwException_when_productIdIsNegative() { + // arrange + Long userId = 1L; + Long productId = -1L; + + // act & assert + CoreException exception = assertThrows(CoreException.class, () -> LikeProduct.create(userId, productId)); + assertThat(exception.getMessage()).isEqualTo("์ƒํ’ˆ ID๋Š” 1 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + assertThat(exception.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); + } + + @DisplayName("null userId์ธ ๊ฒฝ์šฐ, ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") + @Test + void should_throwException_when_userIdIsNull() { + // arrange + Long userId = null; + Long productId = 100L; + + // act & assert + CoreException exception = assertThrows(CoreException.class, () -> LikeProduct.create(userId, productId)); + assertThat(exception.getMessage()).isEqualTo("์‚ฌ์šฉ์ž ID๋Š” 1 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + assertThat(exception.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); + } + + @DisplayName("null productId์ธ ๊ฒฝ์šฐ, ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") + @Test + void should_throwException_when_productIdIsNull() { + // arrange + Long userId = 1L; + Long productId = null; + + // act & assert + CoreException exception = assertThrows(CoreException.class, () -> LikeProduct.create(userId, productId)); + assertThat(exception.getMessage()).isEqualTo("์ƒํ’ˆ ID๋Š” 1 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + assertThat(exception.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); + } + } + + @DisplayName("์ข‹์•„์š” ์กฐํšŒ๋ฅผ ํ•  ๋•Œ, ") + @Nested + class Retrieve { + @DisplayName("์ƒ์„ฑํ•œ ์ข‹์•„์š”์˜ userId๋ฅผ ์กฐํšŒํ•  ์ˆ˜ ์žˆ๋‹ค. (Happy Path)") + @Test + void should_retrieveUserId_when_likeProductCreated() { + // arrange + Long userId = 1L; + Long productId = 100L; + LikeProduct likeProduct = LikeProduct.create(userId, productId); + + // act + Long retrievedUserId = likeProduct.getUserId(); + + // assert + assertThat(retrievedUserId).isEqualTo(1L); + } + + @DisplayName("์ƒ์„ฑํ•œ ์ข‹์•„์š”์˜ productId๋ฅผ ์กฐํšŒํ•  ์ˆ˜ ์žˆ๋‹ค. (Happy Path)") + @Test + void should_retrieveProductId_when_likeProductCreated() { + // arrange + Long userId = 1L; + Long productId = 100L; + LikeProduct likeProduct = LikeProduct.create(userId, productId); + + // act + Long retrievedProductId = likeProduct.getProductId(); + + // assert + assertThat(retrievedProductId).isEqualTo(100L); + } + } + + @DisplayName("์ข‹์•„์š” ๋™๋“ฑ์„ฑ์„ ํ™•์ธํ•  ๋•Œ, ") + @Nested + class Equality { + @DisplayName("๊ฐ™์€ userId์™€ productId๋ฅผ ๊ฐ€์ง„ ์ข‹์•„์š”๋Š” ์„œ๋กœ ๋‹ค๋ฅธ ์ธ์Šคํ„ด์Šค์ด๋‹ค. (Edge Case)") + @Test + void should_beDifferentInstances_when_sameUserIdAndProductId() { + // arrange + Long userId = 1L; + Long productId = 100L; + LikeProduct likeProduct1 = LikeProduct.create(userId, productId); + LikeProduct likeProduct2 = LikeProduct.create(userId, productId); + + // act & assert + assertThat(likeProduct1).isNotSameAs(likeProduct2); + assertThat(likeProduct1).isNotEqualTo(likeProduct2); + } + + @DisplayName("๋‹ค๋ฅธ userId๋ฅผ ๊ฐ€์ง„ ์ข‹์•„์š”๋Š” ์„œ๋กœ ๋‹ค๋ฅธ ์ธ์Šคํ„ด์Šค์ด๋‹ค. (Happy Path)") + @Test + void should_beDifferentInstances_when_differentUserId() { + // arrange + LikeProduct likeProduct1 = LikeProduct.create(1L, 100L); + LikeProduct likeProduct2 = LikeProduct.create(2L, 100L); + + // act & assert + assertThat(likeProduct1).isNotSameAs(likeProduct2); + assertThat(likeProduct1).isNotEqualTo(likeProduct2); + assertThat(likeProduct1.getUserId()).isNotEqualTo(likeProduct2.getUserId()); + } + + @DisplayName("๋‹ค๋ฅธ productId๋ฅผ ๊ฐ€์ง„ ์ข‹์•„์š”๋Š” ์„œ๋กœ ๋‹ค๋ฅธ ์ธ์Šคํ„ด์Šค์ด๋‹ค. (Happy Path)") + @Test + void should_beDifferentInstances_when_differentProductId() { + // arrange + LikeProduct likeProduct1 = LikeProduct.create(1L, 100L); + LikeProduct likeProduct2 = LikeProduct.create(1L, 200L); + + // act & assert + assertThat(likeProduct1).isNotSameAs(likeProduct2); + assertThat(likeProduct1).isNotEqualTo(likeProduct2); + assertThat(likeProduct1.getProductId()).isNotEqualTo(likeProduct2.getProductId()); + } + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderItemTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderItemTest.java new file mode 100644 index 000000000..35a5056a8 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderItemTest.java @@ -0,0 +1,276 @@ +package com.loopers.domain.order; + +import com.loopers.domain.common.vo.Price; +import com.loopers.support.error.CoreException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@DisplayName("์ฃผ๋ฌธ ํ•ญ๋ชฉ(OrderItem) Value Object ํ…Œ์ŠคํŠธ") +public class OrderItemTest { + + @DisplayName("์ฃผ๋ฌธ ํ•ญ๋ชฉ์„ ์ƒ์„ฑํ•  ๋•Œ, ") + @Nested + class Create { + @DisplayName("์ •์ƒ์ ์ธ ๊ฐ’์œผ๋กœ ์ฃผ๋ฌธ ํ•ญ๋ชฉ์„ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค. (Happy Path)") + @Test + void should_createOrderItem_when_validValues() { + // arrange + Long productId = 1L; + String productName = "์ƒํ’ˆ๋ช…"; + Integer quantity = 2; + Price price = new Price(10000); + + // act + OrderItem orderItem = OrderItem.create(productId, productName, quantity, price); + + // assert + assertThat(orderItem.getProductId()).isEqualTo(1L); + assertThat(orderItem.getProductName()).isEqualTo("์ƒํ’ˆ๋ช…"); + assertThat(orderItem.getQuantity()).isEqualTo(2); + assertThat(orderItem.getPrice().amount()).isEqualTo(10000); + } + + @DisplayName("์ˆ˜๋Ÿ‰์ด 0์ด๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") + @Test + void should_throwException_when_quantityIsZero() { + // arrange + Long productId = 1L; + String productName = "์ƒํ’ˆ๋ช…"; + Integer quantity = 0; + Price price = new Price(10000); + + // act + CoreException exception = assertThrows(CoreException.class, () -> { + OrderItem.create(productId, productName, quantity, price); + }); + + // assert + assertThat(exception.getMessage()).isEqualTo("์ฃผ๋ฌธ ์ˆ˜๋Ÿ‰์€ 1 ์ด์ƒ์˜ ์ž์—ฐ์ˆ˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + } + + @DisplayName("์ˆ˜๋Ÿ‰์ด ์Œ์ˆ˜์ด๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") + @Test + void should_throwException_when_quantityIsNegative() { + // arrange + Long productId = 1L; + String productName = "์ƒํ’ˆ๋ช…"; + Integer quantity = -1; + Price price = new Price(10000); + + // act + CoreException exception = assertThrows(CoreException.class, () -> { + OrderItem.create(productId, productName, quantity, price); + }); + + // assert + assertThat(exception.getMessage()).isEqualTo("์ฃผ๋ฌธ ์ˆ˜๋Ÿ‰์€ 1 ์ด์ƒ์˜ ์ž์—ฐ์ˆ˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + } + + @DisplayName("๊ฐ€๊ฒฉ์ด 0์ธ ์ฃผ๋ฌธ ํ•ญ๋ชฉ์„ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค. (Edge Case)") + @Test + void should_createOrderItem_when_priceIsZero() { + // arrange + Long productId = 1L; + String productName = "์ƒํ’ˆ๋ช…"; + Integer quantity = 1; + Price price = new Price(0); + + // act + OrderItem orderItem = OrderItem.create(productId, productName, quantity, price); + + // assert + assertThat(orderItem.getPrice().amount()).isEqualTo(0); + } + + @DisplayName("productName์ด null์ด๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") + @Test + void should_throwException_when_productNameIsNull() { + // arrange + Long productId = 1L; + String productName = null; + Integer quantity = 1; + Price price = new Price(10000); + + // act + CoreException exception = assertThrows(CoreException.class, () -> { + OrderItem.create(productId, productName, quantity, price); + }); + + // assert + assertThat(exception.getMessage()).isEqualTo("์ƒํ’ˆ๋ช…์€ ํ•„์ˆ˜์ด๋ฉฐ ๊ณต๋ฐฑ์ผ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + + @DisplayName("productName์ด ๋นˆ ๋ฌธ์ž์—ด์ด๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") + @Test + void should_throwException_when_productNameIsEmpty() { + // arrange + Long productId = 1L; + String productName = ""; + Integer quantity = 1; + Price price = new Price(10000); + + // act + CoreException exception = assertThrows(CoreException.class, () -> { + OrderItem.create(productId, productName, quantity, price); + }); + + // assert + assertThat(exception.getMessage()).isEqualTo("์ƒํ’ˆ๋ช…์€ ํ•„์ˆ˜์ด๋ฉฐ ๊ณต๋ฐฑ์ผ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + + @DisplayName("productName์ด ๊ณต๋ฐฑ๋งŒ ์žˆ์œผ๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") + @Test + void should_throwException_when_productNameIsBlank() { + // arrange + Long productId = 1L; + String productName = " "; + Integer quantity = 1; + Price price = new Price(10000); + + // act + CoreException exception = assertThrows(CoreException.class, () -> { + OrderItem.create(productId, productName, quantity, price); + }); + + // assert + assertThat(exception.getMessage()).isEqualTo("์ƒํ’ˆ๋ช…์€ ํ•„์ˆ˜์ด๋ฉฐ ๊ณต๋ฐฑ์ผ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + + @DisplayName("productId๊ฐ€ null์ด๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") + @Test + void should_throwException_when_productIdIsNull() { + // arrange + Long productId = null; + String productName = "์ƒํ’ˆ๋ช…"; + Integer quantity = 1; + Price price = new Price(10000); + + // act + CoreException exception = assertThrows(CoreException.class, () -> { + OrderItem.create(productId, productName, quantity, price); + }); + + // assert + assertThat(exception.getMessage()).isEqualTo("์ƒํ’ˆ ID๋Š” 1 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + } + + @DisplayName("productId๊ฐ€ 0์ด๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") + @Test + void should_throwException_when_productIdIsZero() { + // arrange + Long productId = 0L; + String productName = "์ƒํ’ˆ๋ช…"; + Integer quantity = 1; + Price price = new Price(10000); + + // act + CoreException exception = assertThrows(CoreException.class, () -> { + OrderItem.create(productId, productName, quantity, price); + }); + + // assert + assertThat(exception.getMessage()).isEqualTo("์ƒํ’ˆ ID๋Š” 1 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + } + + @DisplayName("productId๊ฐ€ ์Œ์ˆ˜์ด๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") + @Test + void should_throwException_when_productIdIsNegative() { + // arrange + Long productId = -1L; + String productName = "์ƒํ’ˆ๋ช…"; + Integer quantity = 1; + Price price = new Price(10000); + + // act + CoreException exception = assertThrows(CoreException.class, () -> { + OrderItem.create(productId, productName, quantity, price); + }); + + // assert + assertThat(exception.getMessage()).isEqualTo("์ƒํ’ˆ ID๋Š” 1 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + } + + @DisplayName("quantity๊ฐ€ null์ด๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") + @Test + void should_throwException_when_quantityIsNull() { + // arrange + Long productId = 1L; + String productName = "์ƒํ’ˆ๋ช…"; + Integer quantity = null; + Price price = new Price(10000); + + // act + CoreException exception = assertThrows(CoreException.class, () -> { + OrderItem.create(productId, productName, quantity, price); + }); + + // assert + assertThat(exception.getMessage()).isEqualTo("์ฃผ๋ฌธ ์ˆ˜๋Ÿ‰์€ 1 ์ด์ƒ์˜ ์ž์—ฐ์ˆ˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + } + + @DisplayName("price๊ฐ€ null์ด๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") + @Test + void should_throwException_when_priceIsNull() { + // arrange + Long productId = 1L; + String productName = "์ƒํ’ˆ๋ช…"; + Integer quantity = 1; + Price price = null; + + // act + CoreException exception = assertThrows(CoreException.class, () -> { + OrderItem.create(productId, productName, quantity, price); + }); + + // assert + assertThat(exception.getMessage()).isEqualTo("๊ฐ€๊ฒฉ์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."); + } + } + + @DisplayName("์ฃผ๋ฌธ ํ•ญ๋ชฉ์˜ ์ด ๊ฐ€๊ฒฉ์„ ๊ณ„์‚ฐํ•  ๋•Œ, ") + @Nested + class GetTotalPrice { + @DisplayName("์ •์ƒ์ ์ธ ์ˆ˜๋Ÿ‰๊ณผ ๊ฐ€๊ฒฉ์œผ๋กœ ์ด ๊ฐ€๊ฒฉ์„ ๊ณ„์‚ฐํ•  ์ˆ˜ ์žˆ๋‹ค. (Happy Path)") + @Test + void should_calculateTotalPrice_when_validQuantityAndPrice() { + // arrange + OrderItem orderItem = OrderItem.create(1L, "์ƒํ’ˆ๋ช…", 3, new Price(10000)); + + // act + Integer totalPrice = orderItem.getTotalPrice(); + + // assert + assertThat(totalPrice).isEqualTo(30000); + } + + @DisplayName("์ˆ˜๋Ÿ‰์ด 1์ด๋ฉด ๊ฐ€๊ฒฉ๊ณผ ๋™์ผํ•˜๋‹ค. (Edge Case)") + @Test + void should_returnPrice_when_quantityIsOne() { + // arrange + OrderItem orderItem = OrderItem.create(1L, "์ƒํ’ˆ๋ช…", 1, new Price(10000)); + + // act + Integer totalPrice = orderItem.getTotalPrice(); + + // assert + assertThat(totalPrice).isEqualTo(10000); + } + + @DisplayName("๊ฐ€๊ฒฉ์ด 0์ด๋ฉด ์ด ๊ฐ€๊ฒฉ์ด 0์ด๋‹ค. (Edge Case)") + @Test + void should_returnZero_when_priceIsZero() { + // arrange + OrderItem orderItem = OrderItem.create(1L, "์ƒํ’ˆ๋ช…", 3, new Price(0)); + + // act + Integer totalPrice = orderItem.getTotalPrice(); + + // assert + assertThat(totalPrice).isEqualTo(0); + } + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderServiceIntegrationTest.java new file mode 100644 index 000000000..1e524bdf0 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderServiceIntegrationTest.java @@ -0,0 +1,145 @@ +package com.loopers.domain.order; + +import com.loopers.domain.common.vo.Price; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; + +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@SpringBootTest +@Transactional +@DisplayName("์ฃผ๋ฌธ ์„œ๋น„์Šค(OrderService) ํ…Œ์ŠคํŠธ") +public class OrderServiceIntegrationTest { + + @MockitoSpyBean + private OrderRepository spyOrderRepository; + + @Autowired + private OrderService orderService; + + @DisplayName("์ฃผ๋ฌธ์„ ์ €์žฅํ•  ๋•Œ, ") + @Nested + class SaveOrder { + @DisplayName("์ •์ƒ์ ์ธ ์ฃผ๋ฌธ์„ ์ €์žฅํ•˜๋ฉด ์ฃผ๋ฌธ์ด ์ €์žฅ๋œ๋‹ค. (Happy Path)") + @Test + void should_saveOrder_when_validOrder() { + // arrange + Long userId = 1L; + List orderItems = List.of( + OrderItem.create(1L, "์ƒํ’ˆ1", 2, new Price(10000)), + OrderItem.create(2L, "์ƒํ’ˆ2", 1, new Price(20000)) + ); + Order order = Order.create(userId, orderItems); +// when(spyOrderRepository.save(any(Order.class))).thenReturn(order); + + // act + Order result = orderService.save(order); + + // assert + verify(spyOrderRepository).save(any(Order.class)); + assertThat(result).isNotNull(); + assertThat(result.getUserId()).isEqualTo(1L); + assertThat(result.getOrderItems()).hasSize(2); + } + + @DisplayName("๋‹จ์ผ ์ฃผ๋ฌธ ํ•ญ๋ชฉ์„ ๊ฐ€์ง„ ์ฃผ๋ฌธ์„ ์ €์žฅํ•  ์ˆ˜ ์žˆ๋‹ค. (Edge Case)") + @Test + void should_saveOrder_when_singleOrderItem() { + // arrange + Long userId = 1L; + List orderItems = List.of( + OrderItem.create(1L, "์ƒํ’ˆ1", 1, new Price(10000)) + ); + Order order = Order.create(userId, orderItems); +// when(spyOrderRepository.save(any(Order.class))).thenReturn(order); + + // act + Order result = orderService.save(order); + + // assert + verify(spyOrderRepository).save(any(Order.class)); + assertThat(result).isNotNull(); + assertThat(result.getOrderItems()).hasSize(1); + } + } + + @DisplayName("์ฃผ๋ฌธ ID์™€ ์‚ฌ์šฉ์ž ID๋กœ ์ฃผ๋ฌธ์„ ์กฐํšŒํ•  ๋•Œ, ") + @Nested + class GetOrderByIdAndUserId { + @DisplayName("์กด์žฌํ•˜๋Š” ์ฃผ๋ฌธ ID์™€ ์‚ฌ์šฉ์ž ID๋กœ ์กฐํšŒํ•˜๋ฉด ์ฃผ๋ฌธ์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. (Happy Path)") + @Test + void should_returnOrder_when_orderExists() { + // arrange + Long orderId = 1L; + Long userId = 1L; + List orderItems = List.of( + OrderItem.create(1L, "์ƒํ’ˆ1", 2, new Price(10000)) + ); + Order order = Order.create(userId, orderItems); + when(spyOrderRepository.findByIdAndUserId(orderId, userId)).thenReturn(Optional.of(order)); + + // act + Order result = orderService.getOrderByIdAndUserId(orderId, userId); + + // assert + verify(spyOrderRepository).findByIdAndUserId(1L, 1L); + assertThat(result).isNotNull(); + assertThat(result.getUserId()).isEqualTo(1L); + assertThat(result.getOrderItems()).hasSize(1); + } + + @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ฃผ๋ฌธ ID๋กœ ์กฐํšŒํ•˜๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") + @Test + void should_throwException_when_orderNotFound() { + // arrange + Long orderId = 999L; + Long userId = 1L; + when(spyOrderRepository.findByIdAndUserId(orderId, userId)).thenReturn(Optional.empty()); + + // act + CoreException exception = assertThrows(CoreException.class, () -> { + orderService.getOrderByIdAndUserId(orderId, userId); + }); + + // assert + verify(spyOrderRepository).findByIdAndUserId(999L, 1L); + assertThat(exception.getMessage()).isEqualTo("์ฃผ๋ฌธ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + assertThat(exception.getErrorType()).isEqualTo(ErrorType.NOT_FOUND); + } + + @DisplayName("๋‹ค๋ฅธ ์‚ฌ์šฉ์ž์˜ ์ฃผ๋ฌธ ID๋กœ ์กฐํšŒํ•˜๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") + @Test + void should_throwException_when_orderBelongsToDifferentUser() { + // arrange + Long orderId = 1L; + Long userId = 1L; + Long differentUserId = 2L; + when(spyOrderRepository.findByIdAndUserId(orderId, differentUserId)).thenReturn(Optional.empty()); + + // act + CoreException exception = assertThrows(CoreException.class, () -> { + orderService.getOrderByIdAndUserId(orderId, differentUserId); + }); + + // assert + verify(spyOrderRepository).findByIdAndUserId(1L, 2L); + assertThat(exception.getMessage()).isEqualTo("์ฃผ๋ฌธ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + assertThat(exception.getErrorType()).isEqualTo(ErrorType.NOT_FOUND); + } + } +} + diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderTest.java new file mode 100644 index 000000000..b4521bb1b --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderTest.java @@ -0,0 +1,176 @@ +package com.loopers.domain.order; + +import com.loopers.domain.common.vo.Price; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@DisplayName("์ฃผ๋ฌธ(Order) Entity ํ…Œ์ŠคํŠธ") +public class OrderTest { + + @DisplayName("์ฃผ๋ฌธ์„ ์ƒ์„ฑํ•  ๋•Œ, ") + @Nested + class Create { + @DisplayName("์ •์ƒ์ ์ธ userId์™€ ์ฃผ๋ฌธ ํ•ญ๋ชฉ ๋ฆฌ์ŠคํŠธ๋กœ ์ฃผ๋ฌธ์„ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค. (Happy Path)") + @Test + void should_createOrder_when_validUserIdAndOrderItems() { + // arrange + Long userId = 1L; + List orderItems = List.of( + OrderItem.create(1L, "์ƒํ’ˆ1", 2, new Price(10000)), + OrderItem.create(2L, "์ƒํ’ˆ2", 1, new Price(20000)) + ); + + // act + Order order = Order.create(userId, orderItems); + + // assert + assertThat(order.getUserId()).isEqualTo(1L); + assertThat(order.getOrderItems()).hasSize(2); + assertThat(order.getOrderItems().get(0).getProductId()).isEqualTo(1L); + assertThat(order.getOrderItems().get(1).getProductId()).isEqualTo(2L); + } + + @DisplayName("๋‹จ์ผ ์ฃผ๋ฌธ ํ•ญ๋ชฉ์œผ๋กœ ์ฃผ๋ฌธ์„ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค. (Edge Case)") + @Test + void should_createOrder_when_singleOrderItem() { + // arrange + Long userId = 1L; + List orderItems = List.of( + OrderItem.create(1L, "์ƒํ’ˆ1", 1, new Price(10000)) + ); + + // act + Order order = Order.create(userId, orderItems); + + // assert + assertThat(order.getUserId()).isEqualTo(1L); + assertThat(order.getOrderItems()).hasSize(1); + } + + @DisplayName("๋นˆ ์ฃผ๋ฌธ ํ•ญ๋ชฉ ๋ฆฌ์ŠคํŠธ๋กœ ์ฃผ๋ฌธ์„ ์ƒ์„ฑํ•  ๊ฒฝ์šฐ, ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") + @Test + void should_throwException_when_emptyOrderItems() { + // arrange + Long userId = 1L; + List orderItems = new ArrayList<>(); + + // act & assert + CoreException exception = assertThrows(CoreException.class, () -> Order.create(userId, orderItems)); + assertThat(exception.getMessage()).isEqualTo("์ฃผ๋ฌธ ํ•ญ๋ชฉ์€ ์ตœ์†Œ 1๊ฐœ ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + assertThat(exception.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); + } + + @DisplayName("null ์ฃผ๋ฌธ ํ•ญ๋ชฉ ๋ฆฌ์ŠคํŠธ๋กœ ์ฃผ๋ฌธ์„ ์ƒ์„ฑํ•  ๊ฒฝ์šฐ, ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") + @Test + void should_throwException_when_nullOrderItems() { + // arrange + Long userId = 1L; + List orderItems = null; + + // act & assert + CoreException exception = assertThrows(CoreException.class, () -> Order.create(userId, orderItems)); + assertThat(exception.getMessage()).isEqualTo("์ฃผ๋ฌธ ํ•ญ๋ชฉ์€ ์ตœ์†Œ 1๊ฐœ ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + assertThat(exception.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); + } + + @DisplayName("null userId๋กœ ์ฃผ๋ฌธ์„ ์ƒ์„ฑํ•  ๊ฒฝ์šฐ, ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") + @Test + void should_throwException_when_nullUserId() { + // arrange + Long userId = null; + List orderItems = List.of( + OrderItem.create(1L, "์ƒํ’ˆ1", 1, new Price(10000)) + ); + + // act & assert + CoreException exception = assertThrows(CoreException.class, () -> Order.create(userId, orderItems)); + assertThat(exception.getMessage()).isEqualTo("์‚ฌ์šฉ์ž ID๋Š” 1 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + assertThat(exception.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); + } + + @DisplayName("0 ์ดํ•˜์˜ userId๋กœ ์ฃผ๋ฌธ์„ ์ƒ์„ฑํ•  ๊ฒฝ์šฐ, ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") + @Test + void should_throwException_when_invalidUserId() { + // arrange + Long userId = 0L; + List orderItems = List.of( + OrderItem.create(1L, "์ƒํ’ˆ1", 1, new Price(10000)) + ); + + // act & assert + CoreException exception = assertThrows(CoreException.class, () -> Order.create(userId, orderItems)); + assertThat(exception.getMessage()).isEqualTo("์‚ฌ์šฉ์ž ID๋Š” 1 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + assertThat(exception.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); + } + + @DisplayName("์—ฌ๋Ÿฌ ์ฃผ๋ฌธ ํ•ญ๋ชฉ์œผ๋กœ ์ฃผ๋ฌธ์„ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค. (Edge Case)") + @Test + void should_createOrder_when_multipleOrderItems() { + // arrange + Long userId = 1L; + List orderItems = List.of( + OrderItem.create(1L, "์ƒํ’ˆ1", 2, new Price(10000)), + OrderItem.create(2L, "์ƒํ’ˆ2", 1, new Price(20000)), + OrderItem.create(3L, "์ƒํ’ˆ3", 3, new Price(15000)) + ); + + // act + Order order = Order.create(userId, orderItems); + + // assert + assertThat(order.getUserId()).isEqualTo(1L); + assertThat(order.getOrderItems()).hasSize(3); + } + + } + + @DisplayName("์ฃผ๋ฌธ ์กฐํšŒ๋ฅผ ํ•  ๋•Œ, ") + @Nested + class Retrieve { + @DisplayName("์ƒ์„ฑํ•œ ์ฃผ๋ฌธ์˜ userId๋ฅผ ์กฐํšŒํ•  ์ˆ˜ ์žˆ๋‹ค. (Happy Path)") + @Test + void should_retrieveUserId_when_orderCreated() { + // arrange + Long userId = 1L; + List orderItems = List.of( + OrderItem.create(1L, "์ƒํ’ˆ1", 1, new Price(10000)) + ); + Order order = Order.create(userId, orderItems); + + // act + Long retrievedUserId = order.getUserId(); + + // assert + assertThat(retrievedUserId).isEqualTo(1L); + } + + @DisplayName("์ƒ์„ฑํ•œ ์ฃผ๋ฌธ์˜ ์ฃผ๋ฌธ ํ•ญ๋ชฉ ๋ฆฌ์ŠคํŠธ๋ฅผ ์กฐํšŒํ•  ์ˆ˜ ์žˆ๋‹ค. (Happy Path)") + @Test + void should_retrieveOrderItems_when_orderCreated() { + // arrange + Long userId = 1L; + List orderItems = List.of( + OrderItem.create(1L, "์ƒํ’ˆ1", 2, new Price(10000)), + OrderItem.create(2L, "์ƒํ’ˆ2", 1, new Price(20000)) + ); + Order order = Order.create(userId, orderItems); + + // act + List retrievedOrderItems = order.getOrderItems(); + + // assert + assertThat(retrievedOrderItems).hasSize(2); + assertThat(retrievedOrderItems.get(0).getProductId()).isEqualTo(1L); + assertThat(retrievedOrderItems.get(1).getProductId()).isEqualTo(2L); + } + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductServiceIntegrationTest.java new file mode 100644 index 000000000..f7ccc360f --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductServiceIntegrationTest.java @@ -0,0 +1,362 @@ +package com.loopers.domain.product; + +import com.loopers.domain.brand.Brand; +import com.loopers.domain.common.vo.Price; +import com.loopers.domain.metrics.product.ProductMetrics; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import com.loopers.utils.DatabaseCleanUp; +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; + +import java.util.*; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@SpringBootTest +@Transactional +@DisplayName("์ƒํ’ˆ ์„œ๋น„์Šค(ProductService) ํ…Œ์ŠคํŠธ") +public class ProductServiceIntegrationTest { + + @MockitoSpyBean + private ProductRepository spyProductRepository; + + @Autowired + private ProductService productService; + + @Autowired + private com.loopers.infrastructure.brand.BrandJpaRepository brandJpaRepository; + + @Autowired + private com.loopers.infrastructure.product.ProductJpaRepository productJpaRepository; + + @Autowired + private com.loopers.infrastructure.metrics.product.ProductMetricsJpaRepository productMetricsJpaRepository; + + @Autowired + private DatabaseCleanUp databaseCleanUp; + + private Long brandId; + private Long productId1; + private Long productId2; + private Long productId3; + + @BeforeEach + void setup() { + // Brand ๋“ฑ๋ก + Brand brand = Brand.create("Nike"); + Brand savedBrand = brandJpaRepository.save(brand); + brandId = savedBrand.getId(); + + // Product ๋“ฑ๋ก + Product product1 = Product.create("์ƒํ’ˆ1", brandId, new Price(10000)); + Product savedProduct1 = productJpaRepository.save(product1); + productId1 = savedProduct1.getId(); + // ProductMetrics ๋“ฑ๋ก + ProductMetrics metrics1 = ProductMetrics.create(productId1, 4); + productMetricsJpaRepository.save(metrics1); + + Product product2 = Product.create("์ƒํ’ˆ2", brandId, new Price(20000)); + Product savedProduct2 = productJpaRepository.save(product2); + productId2 = savedProduct2.getId(); + // ProductMetrics ๋“ฑ๋ก + ProductMetrics metrics2 = ProductMetrics.create(productId2, 0); + productMetricsJpaRepository.save(metrics2); + + Product product3 = Product.create("์ƒํ’ˆ3", brandId, new Price(15000)); + Product savedProduct3 = productJpaRepository.save(product3); + productId3 = savedProduct3.getId(); + // ProductMetrics ๋“ฑ๋ก + ProductMetrics metrics3 = ProductMetrics.create(productId3, 3); + productMetricsJpaRepository.save(metrics3); + } + + @AfterEach + void tearDown() { + databaseCleanUp.truncateAllTables(); + } + + @DisplayName("์ƒํ’ˆ ID๋กœ ์ƒํ’ˆ์„ ์กฐํšŒํ•  ๋•Œ, ") + @Nested + class GetProductById { + @DisplayName("์กด์žฌํ•˜๋Š” ์ƒํ’ˆ ID๋กœ ์กฐํšŒํ•˜๋ฉด ์ƒํ’ˆ์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. (Happy Path)") + @Test + void should_returnProduct_when_productExists() { + // arrange + Long productId = 1L; + Product product = createProduct(productId, "์ƒํ’ˆ๋ช…", 1L, 10000); + when(spyProductRepository.findById(productId)).thenReturn(Optional.of(product)); + + // act + Product result = productService.getProductById(productId); + + // assert + verify(spyProductRepository).findById(1L); + assertThat(result).isNotNull(); + assertThat(result.getId()).isEqualTo(1L); + assertThat(result.getName()).isEqualTo("์ƒํ’ˆ๋ช…"); + } + + @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ƒํ’ˆ ID๋กœ ์กฐํšŒํ•˜๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") + @Test + void should_throwException_when_productNotFound() { + // arrange + Long productId = 999L; + when(spyProductRepository.findById(productId)).thenReturn(Optional.empty()); + + // act + CoreException exception = assertThrows(CoreException.class, () -> { + productService.getProductById(productId); + }); + + // assert + verify(spyProductRepository).findById(999L); + assertThat(exception.getMessage()).isEqualTo("์ƒํ’ˆ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + assertThat(exception.getErrorType()).isEqualTo(ErrorType.NOT_FOUND); + } + } + + @DisplayName("์—ฌ๋Ÿฌ ์ƒํ’ˆ ID๋กœ ์ƒํ’ˆ ๋งต์„ ์กฐํšŒํ•  ๋•Œ, ") + @Nested + class GetProductMapByIds { + @DisplayName("์กด์žฌํ•˜๋Š” ์ƒํ’ˆ ID๋“ค๋กœ ์กฐํšŒํ•˜๋ฉด ์ƒํ’ˆ ๋งต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. (Happy Path)") + @Test + void should_returnProductMap_when_productsExist() { + // arrange + List productIds = List.of(1L, 2L, 3L); + List products = List.of( + createProduct(1L, "์ƒํ’ˆ1", 1L, 10000), + createProduct(2L, "์ƒํ’ˆ2", 1L, 20000), + createProduct(3L, "์ƒํ’ˆ3", 2L, 15000) + ); + when(spyProductRepository.findAllByIdIn(productIds)).thenReturn(products); + + // act + Map result = productService.getProductMapByIds(productIds); + + // assert + verify(spyProductRepository).findAllByIdIn(any(Collection.class)); + assertThat(result).hasSize(3); + assertThat(result.get(1L).getName()).isEqualTo("์ƒํ’ˆ1"); + assertThat(result.get(2L).getName()).isEqualTo("์ƒํ’ˆ2"); + assertThat(result.get(3L).getName()).isEqualTo("์ƒํ’ˆ3"); + } + + @DisplayName("๋นˆ ID ๋ฆฌ์ŠคํŠธ๋กœ ์กฐํšŒํ•˜๋ฉด ๋นˆ ๋งต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. (Edge Case)") + @Test + void should_returnEmptyMap_when_emptyIdList() { + // arrange + List productIds = Collections.emptyList(); + when(spyProductRepository.findAllByIdIn(productIds)).thenReturn(Collections.emptyList()); + + // act + Map result = productService.getProductMapByIds(productIds); + + // assert + verify(spyProductRepository).findAllByIdIn(any(Collection.class)); + assertThat(result).isEmpty(); + } + + @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ƒํ’ˆ ID๋“ค๋กœ ์กฐํšŒํ•˜๋ฉด ๋นˆ ๋งต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. (Edge Case)") + @Test + void should_returnEmptyMap_when_productsNotFound() { + // arrange + List productIds = List.of(999L, 1000L); + when(spyProductRepository.findAllByIdIn(productIds)).thenReturn(Collections.emptyList()); + + // act + Map result = productService.getProductMapByIds(productIds); + + // assert + verify(spyProductRepository).findAllByIdIn(any(Collection.class)); + assertThat(result).isEmpty(); + } + } + + @DisplayName("์ƒํ’ˆ ๋ชฉ๋ก์„ ์กฐํšŒํ•  ๋•Œ, ") + @Nested + class GetProducts { + @DisplayName("๊ธฐ๋ณธ ํŽ˜์ด์ง€๋„ค์ด์…˜์œผ๋กœ ์กฐํšŒํ•˜๋ฉด ์ƒํ’ˆ ํŽ˜์ด์ง€๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค. (Happy Path)") + @Test + void should_returnProductPage_when_defaultPageable() { + // arrange + Pageable pageable = PageRequest.of(0, 20); + + // act + Page result = productService.getProducts(pageable); + + // assert + verify(spyProductRepository).findAll(any(Pageable.class)); + assertThat(result).isNotNull(); + assertThat(result.getContent()).hasSize(3); + assertThat(result.getTotalElements()).isEqualTo(3); + } + + @DisplayName("์ตœ์‹ ์ˆœ ์ •๋ ฌ๋กœ ์กฐํšŒํ•˜๋ฉด ์ƒํ’ˆ ํŽ˜์ด์ง€๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค. (Happy Path)") + @Test + void should_returnProductPage_when_sortedByLatest() { + // arrange + Sort sort = Sort.by("latest"); + Pageable pageable = PageRequest.of(0, 20, sort); + + // act + Page result = productService.getProducts(pageable); + + // assert + verify(spyProductRepository).findAll(any(Pageable.class)); + assertThat(result).isNotNull(); + assertThat(result.getContent()).hasSize(3); + // ์ตœ์‹ ์ˆœ ์ •๋ ฌ ๊ฒ€์ฆ createdAt ๊ธฐ์ค€ ๋‚ด๋ฆผ์ฐจ์ˆœ + assertThat(result.getContent().get(0).getCreatedAt()).isAfterOrEqualTo(result.getContent().get(1).getCreatedAt()); + assertThat(result.getContent().get(1).getCreatedAt()).isAfterOrEqualTo(result.getContent().get(2).getCreatedAt()); + } + + @DisplayName("๊ฐ€๊ฒฉ ์˜ค๋ฆ„์ฐจ์ˆœ ์ •๋ ฌ๋กœ ์กฐํšŒํ•˜๋ฉด ์ƒํ’ˆ ํŽ˜์ด์ง€๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค. (Happy Path)") + @Test + void should_returnProductPage_when_sortedByPriceAsc() { + // arrange + Sort sort = Sort.by("price_asc"); + Pageable pageable = PageRequest.of(0, 20, sort); + + // act + Page result = productService.getProducts(pageable); + + // assert + verify(spyProductRepository).findAll(any(Pageable.class)); + assertThat(result).isNotNull(); + assertThat(result.getContent()).hasSize(3); + // ๊ฐ€๊ฒฉ ์˜ค๋ฆ„์ฐจ์ˆœ ์ •๋ ฌ ๊ฒ€์ฆ + assertThat(result.getContent().get(0).getPrice().amount()).isLessThanOrEqualTo(result.getContent().get(1).getPrice().amount()); + assertThat(result.getContent().get(1).getPrice().amount()).isLessThanOrEqualTo(result.getContent().get(2).getPrice().amount()); + } + + @DisplayName("์ข‹์•„์š” ๋‚ด๋ฆผ์ฐจ์ˆœ ์ •๋ ฌ๋กœ ์กฐํšŒํ•˜๋ฉด ์ƒํ’ˆ ํŽ˜์ด์ง€๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค. (Happy Path)") + @Test + void should_returnProductPage_when_sortedByLikesDesc() { + // arrange + Sort sort = Sort.by("likes_desc"); + Pageable pageable = PageRequest.of(0, 20, sort); + + // act + Page result = productService.getProducts(pageable); + + // assert + verify(spyProductRepository).findAll(any(Pageable.class)); + assertThat(result).isNotNull(); + assertThat(result.getContent()).hasSize(3); + // ์ข‹์•„์š” ๋‚ด๋ฆผ์ฐจ์ˆœ ์ •๋ ฌ ๊ฒ€์ฆ + assertThat(result.getContent().get(0).getId()).isGreaterThanOrEqualTo(result.getContent().get(1).getId()); + assertThat(result.getContent().get(1).getId()).isGreaterThanOrEqualTo(result.getContent().get(2).getId()); + } + } + + @DisplayName("์ด ๊ฐ€๊ฒฉ์„ ๊ณ„์‚ฐํ•  ๋•Œ, ") + @Nested + class CalculateTotalAmount { + @DisplayName("์ •์ƒ์ ์ธ ์ƒํ’ˆ๊ณผ ์ˆ˜๋Ÿ‰์œผ๋กœ ์ด ๊ฐ€๊ฒฉ์„ ๊ณ„์‚ฐํ•  ์ˆ˜ ์žˆ๋‹ค. (Happy Path)") + @Test + void should_calculateTotalAmount_when_validProductsAndQuantities() { + // arrange + Map items = Map.of( + 1L, 2, + 2L, 3 + ); + List products = List.of( + createProduct(1L, "์ƒํ’ˆ1", 1L, 10000), + createProduct(2L, "์ƒํ’ˆ2", 1L, 20000) + ); + when(spyProductRepository.findAllByIdIn(items.keySet())).thenReturn(products); + + // act + Integer result = productService.calculateTotalAmount(items); + + // assert + verify(spyProductRepository).findAllByIdIn(any(Collection.class)); + assertThat(result).isEqualTo(80000); + } + + @DisplayName("๋‹จ์ผ ์ƒํ’ˆ์œผ๋กœ ์ด ๊ฐ€๊ฒฉ์„ ๊ณ„์‚ฐํ•  ์ˆ˜ ์žˆ๋‹ค. (Edge Case)") + @Test + void should_calculateTotalAmount_when_singleProduct() { + // arrange + Map items = Map.of(1L, 5); + List products = List.of(createProduct(1L, "์ƒํ’ˆ1", 1L, 10000)); + when(spyProductRepository.findAllByIdIn(items.keySet())).thenReturn(products); + + // act + Integer result = productService.calculateTotalAmount(items); + + // assert + verify(spyProductRepository).findAllByIdIn(any(Collection.class)); + assertThat(result).isEqualTo(50000); + } + + @DisplayName("์ˆ˜๋Ÿ‰์ด 1์ธ ์ƒํ’ˆ๋“ค๋กœ ์ด ๊ฐ€๊ฒฉ์„ ๊ณ„์‚ฐํ•  ์ˆ˜ ์žˆ๋‹ค. (Edge Case)") + @Test + void should_calculateTotalAmount_when_quantityIsOne() { + // arrange + Map items = Map.of( + 1L, 1, + 2L, 1 + ); + List products = List.of( + createProduct(1L, "์ƒํ’ˆ1", 1L, 10000), + createProduct(2L, "์ƒํ’ˆ2", 1L, 20000) + ); + when(spyProductRepository.findAllByIdIn(items.keySet())).thenReturn(products); + + // act + Integer result = productService.calculateTotalAmount(items); + + // assert + verify(spyProductRepository).findAllByIdIn(any(Collection.class)); + assertThat(result).isEqualTo(30000); + } + + @DisplayName("๊ฐ€๊ฒฉ์ด 0์ธ ์ƒํ’ˆ์ด ํฌํ•จ๋˜์–ด๋„ ์ด ๊ฐ€๊ฒฉ์„ ๊ณ„์‚ฐํ•  ์ˆ˜ ์žˆ๋‹ค. (Edge Case)") + @Test + void should_calculateTotalAmount_when_priceIsZero() { + // arrange + Map items = Map.of( + 1L, 2, + 2L, 1 + ); + List products = List.of( + createProduct(1L, "์ƒํ’ˆ1", 1L, 0), + createProduct(2L, "์ƒํ’ˆ2", 1L, 20000) + ); + when(spyProductRepository.findAllByIdIn(items.keySet())).thenReturn(products); + + // act + Integer result = productService.calculateTotalAmount(items); + + // assert + verify(spyProductRepository).findAllByIdIn(any(Collection.class)); + assertThat(result).isEqualTo(20000); + } + } + + private Product createProduct(Long id, String name, Long brandId, int priceAmount) { + Product product = Product.create(name, brandId, new Price(priceAmount)); + // ํ…Œ์ŠคํŠธ์šฉ์œผ๋กœ id ์„ค์ • (๋ฆฌํ”Œ๋ ‰์…˜ ์‚ฌ์šฉ) + try { + java.lang.reflect.Field idField = Product.class.getSuperclass().getDeclaredField("id"); + idField.setAccessible(true); + idField.set(product, id); + } catch (Exception e) { + throw new RuntimeException("Failed to set Product id", e); + } + return product; + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/supply/SupplyTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/supply/SupplyTest.java new file mode 100644 index 000000000..6a27fdd5a --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/supply/SupplyTest.java @@ -0,0 +1,130 @@ +package com.loopers.domain.supply; + +import com.loopers.domain.supply.vo.Stock; +import com.loopers.support.error.CoreException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@DisplayName("์žฌ๊ณ  ๊ณต๊ธ‰(Supply) Entity ํ…Œ์ŠคํŠธ") +public class SupplyTest { + + @DisplayName("์žฌ๊ณ  ์ฐจ๊ฐ์„ ํ•  ๋•Œ, ") + @Nested + class DecreaseStock { + @DisplayName("์ •์ƒ์ ์ธ ์ˆ˜๋Ÿ‰์œผ๋กœ ์ฐจ๊ฐํ•˜๋ฉด ์žฌ๊ณ ๊ฐ€ ๊ฐ์†Œํ•œ๋‹ค. (Happy Path)") + @Test + void should_decreaseStock_when_validQuantity() { + // arrange + Supply supply = createSupply(10); + int orderQuantity = 3; + + // act + supply.decreaseStock(orderQuantity); + + // assert + assertThat(supply.getStock().quantity()).isEqualTo(7); + } + + @DisplayName("์žฌ๊ณ ๊ฐ€ ์ฐจ๊ฐ ์ˆ˜๋Ÿ‰๊ณผ ์ •ํ™•ํžˆ ๊ฐ™์œผ๋ฉด ์žฌ๊ณ ๊ฐ€ 0์ด ๋œ๋‹ค. (Edge Case)") + @Test + void should_setStockToZero_when_stockEqualsOrderQuantity() { + // arrange + Supply supply = createSupply(5); + int orderQuantity = 5; + + // act + supply.decreaseStock(orderQuantity); + + // assert + assertThat(supply.getStock().quantity()).isEqualTo(0); + } + + @DisplayName("์žฌ๊ณ ๊ฐ€ 1๊ฐœ์ผ ๋•Œ 1๊ฐœ ์ฐจ๊ฐํ•˜๋ฉด ์žฌ๊ณ ๊ฐ€ 0์ด ๋œ๋‹ค. (Edge Case)") + @Test + void should_setStockToZero_when_stockIsOneAndDecreaseOne() { + // arrange + Supply supply = createSupply(1); + int orderQuantity = 1; + + // act + supply.decreaseStock(orderQuantity); + + // assert + assertThat(supply.getStock().quantity()).isEqualTo(0); + } + + @DisplayName("0 ์ดํ•˜์˜ ์ˆ˜๋Ÿ‰์œผ๋กœ ์ฐจ๊ฐํ•˜๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") + @ParameterizedTest + @ValueSource(ints = {0, -1, -10}) + void should_throwException_when_orderQuantityIsZeroOrNegative(int invalidQuantity) { + // arrange + Supply supply = createSupply(10); + + // act + CoreException exception = assertThrows(CoreException.class, () -> { + supply.decreaseStock(invalidQuantity); + }); + + // assert + assertThat(exception.getMessage()).isEqualTo("์ฃผ๋ฌธ ์ˆ˜๋Ÿ‰์€ 0๋ณด๋‹ค ์ปค์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + } + + @DisplayName("์žฌ๊ณ ๋ณด๋‹ค ๋งŽ์€ ์ˆ˜๋Ÿ‰์œผ๋กœ ์ฐจ๊ฐํ•˜๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") + @Test + void should_throwException_when_orderQuantityExceedsStock() { + // arrange + Supply supply = createSupply(5); + int orderQuantity = 10; + + // act + CoreException exception = assertThrows(CoreException.class, () -> { + supply.decreaseStock(orderQuantity); + }); + + // assert + assertThat(exception.getMessage()).isEqualTo("์žฌ๊ณ ๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค."); + } + + @DisplayName("์žฌ๊ณ ๊ฐ€ 0์ผ ๋•Œ ์ฐจ๊ฐํ•˜๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") + @Test + void should_throwException_when_stockIsZero() { + // arrange + Supply supply = createSupply(0); + int orderQuantity = 1; + + // act + CoreException exception = assertThrows(CoreException.class, () -> { + supply.decreaseStock(orderQuantity); + }); + + // assert + assertThat(exception.getMessage()).isEqualTo("์žฌ๊ณ ๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค."); + } + + @DisplayName("์—ฌ๋Ÿฌ ๋ฒˆ ์ฐจ๊ฐํ•˜๋ฉด ์žฌ๊ณ ๊ฐ€ ๋ˆ„์  ๊ฐ์†Œํ•œ๋‹ค. (Edge Case)") + @Test + void should_accumulateDecrease_when_decreaseMultipleTimes() { + // arrange + Supply supply = createSupply(10); + + // act + supply.decreaseStock(2); + supply.decreaseStock(3); + supply.decreaseStock(1); + + // assert + assertThat(supply.getStock().quantity()).isEqualTo(4); + } + } + + private Supply createSupply(int stockQuantity) { + // ํ…Œ์ŠคํŠธ์šฉ์œผ๋กœ ๋”๋ฏธ productId ์‚ฌ์šฉ + return Supply.create(1L, new Stock(stockQuantity)); + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/supply/vo/StockTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/supply/vo/StockTest.java new file mode 100644 index 000000000..81bf89a91 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/supply/vo/StockTest.java @@ -0,0 +1,183 @@ +package com.loopers.domain.supply.vo; + +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@DisplayName("์žฌ๊ณ (Stock) Value Object ํ…Œ์ŠคํŠธ") +public class StockTest { + + @DisplayName("์žฌ๊ณ ๋ฅผ ์ƒ์„ฑํ•  ๋•Œ, ") + @Nested + class Create { + @DisplayName("์ •์ƒ์ ์ธ ์žฌ๊ณ  ์ˆ˜๋Ÿ‰์œผ๋กœ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค. (Happy Path)") + @Test + void should_createStock_when_validQuantity() { + // arrange + int quantity = 10; + + // act + Stock stock = new Stock(quantity); + + // assert + assertThat(stock.quantity()).isEqualTo(10); + } + + @DisplayName("์žฌ๊ณ ๊ฐ€ 0์ด์–ด๋„ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค. (Edge Case)") + @Test + void should_createStock_when_quantityIsZero() { + // arrange + int quantity = 0; + + // act + Stock stock = new Stock(quantity); + + // assert + assertThat(stock.quantity()).isEqualTo(0); + } + + @DisplayName("์Œ์ˆ˜ ์žฌ๊ณ ๋กœ ์ƒ์„ฑํ•  ๊ฒฝ์šฐ, ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") + @Test + void should_throwException_when_quantityIsNegative() { + // arrange + int quantity = -1; + + // act & assert + CoreException exception = assertThrows(CoreException.class, () -> new Stock(quantity)); + assertThat(exception.getMessage()).isEqualTo("์žฌ๊ณ ๋Š” ์Œ์ˆ˜๊ฐ€ ๋  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + assertThat(exception.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); + } + } + + @DisplayName("์žฌ๊ณ  ์ฐจ๊ฐ์„ ํ•  ๋•Œ, ") + @Nested + class Decrease { + @DisplayName("์ •์ƒ์ ์ธ ์ˆ˜๋Ÿ‰์œผ๋กœ ์ฐจ๊ฐํ•˜๋ฉด ์žฌ๊ณ ๊ฐ€ ๊ฐ์†Œํ•œ๋‹ค. (Happy Path)") + @Test + void should_decreaseStock_when_validQuantity() { + // arrange + Stock stock = new Stock(10); + int orderQuantity = 3; + + // act + Stock result = stock.decrease(orderQuantity); + + // assert + assertThat(result.quantity()).isEqualTo(7); + } + + @DisplayName("์žฌ๊ณ ๊ฐ€ ์ฐจ๊ฐ ์ˆ˜๋Ÿ‰๊ณผ ์ •ํ™•ํžˆ ๊ฐ™์œผ๋ฉด ์žฌ๊ณ ๊ฐ€ 0์ด ๋œ๋‹ค. (Edge Case)") + @Test + void should_setStockToZero_when_stockEqualsOrderQuantity() { + // arrange + Stock stock = new Stock(5); + int orderQuantity = 5; + + // act + Stock result = stock.decrease(orderQuantity); + + // assert + assertThat(result.quantity()).isEqualTo(0); + } + + @DisplayName("์žฌ๊ณ ๊ฐ€ 1๊ฐœ์ผ ๋•Œ 1๊ฐœ ์ฐจ๊ฐํ•˜๋ฉด ์žฌ๊ณ ๊ฐ€ 0์ด ๋œ๋‹ค. (Edge Case)") + @Test + void should_setStockToZero_when_stockIsOneAndDecreaseOne() { + // arrange + Stock stock = new Stock(1); + int orderQuantity = 1; + + // act + Stock result = stock.decrease(orderQuantity); + + // assert + assertThat(result.quantity()).isEqualTo(0); + } + + @DisplayName("0 ์ดํ•˜์˜ ์ˆ˜๋Ÿ‰์œผ๋กœ ์ฐจ๊ฐํ•˜๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") + @ParameterizedTest + @ValueSource(ints = {0, -1, -10}) + void should_throwException_when_orderQuantityIsZeroOrNegative(int invalidQuantity) { + // arrange + Stock stock = new Stock(10); + + // act + CoreException exception = assertThrows(CoreException.class, () -> { + stock.decrease(invalidQuantity); + }); + + // assert + assertThat(exception.getMessage()).isEqualTo("์ฃผ๋ฌธ ์ˆ˜๋Ÿ‰์€ 0๋ณด๋‹ค ์ปค์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + } + + @DisplayName("์žฌ๊ณ ๋ณด๋‹ค ๋งŽ์€ ์ˆ˜๋Ÿ‰์œผ๋กœ ์ฐจ๊ฐํ•˜๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") + @Test + void should_throwException_when_orderQuantityExceedsStock() { + // arrange + Stock stock = new Stock(5); + int orderQuantity = 10; + + // act + CoreException exception = assertThrows(CoreException.class, () -> { + stock.decrease(orderQuantity); + }); + + // assert + assertThat(exception.getMessage()).isEqualTo("์žฌ๊ณ ๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค."); + } + + @DisplayName("์žฌ๊ณ ๊ฐ€ 0์ผ ๋•Œ ์ฐจ๊ฐํ•˜๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") + @Test + void should_throwException_when_stockIsZero() { + // arrange + Stock stock = new Stock(0); + int orderQuantity = 1; + + // act + CoreException exception = assertThrows(CoreException.class, () -> { + stock.decrease(orderQuantity); + }); + + // assert + assertThat(exception.getMessage()).isEqualTo("์žฌ๊ณ ๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค."); + } + } + + @DisplayName("์žฌ๊ณ  ๋ถ€์กฑ ํ™•์ธ์„ ํ•  ๋•Œ, ") + @Nested + class IsOutOfStock { + @DisplayName("์žฌ๊ณ ๊ฐ€ 0์ด๋ฉด true๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค. (Edge Case)") + @Test + void should_returnTrue_when_stockIsZero() { + // arrange + Stock stock = new Stock(0); + + // act + boolean result = stock.isOutOfStock(); + + // assert + assertThat(result).isTrue(); + } + + + @DisplayName("์žฌ๊ณ ๊ฐ€ 1 ์ด์ƒ์ด๋ฉด false๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค. (Happy Path)") + @Test + void should_returnFalse_when_stockIsPositive() { + // arrange + Stock stock = new Stock(10); + + // act + boolean result = stock.isOutOfStock(); + + // assert + assertThat(result).isFalse(); + } + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/LikeProductV1ApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/LikeProductV1ApiE2ETest.java new file mode 100644 index 000000000..de6c6d615 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/LikeProductV1ApiE2ETest.java @@ -0,0 +1,485 @@ +package com.loopers.interfaces.api; + +import com.loopers.domain.brand.Brand; +import com.loopers.domain.common.vo.Price; +import com.loopers.domain.metrics.product.ProductMetrics; +import com.loopers.domain.product.Product; +import com.loopers.domain.supply.Supply; +import com.loopers.domain.supply.SupplyService; +import com.loopers.domain.supply.vo.Stock; +import com.loopers.infrastructure.brand.BrandJpaRepository; +import com.loopers.infrastructure.metrics.product.ProductMetricsJpaRepository; +import com.loopers.infrastructure.product.ProductJpaRepository; +import com.loopers.interfaces.api.like.product.LikeProductV1Dto; +import com.loopers.interfaces.api.user.UserV1Dto; +import com.loopers.utils.DatabaseCleanUp; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.transaction.annotation.Transactional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class LikeProductV1ApiE2ETest { + + private final String ENDPOINT_USER = "/api/v1/users"; + private final String ENDPOINT_LIKE_PRODUCTS = "/api/v1/like/products"; + + private final TestRestTemplate testRestTemplate; + private final DatabaseCleanUp databaseCleanUp; + private final BrandJpaRepository brandJpaRepository; + private final ProductJpaRepository productJpaRepository; + private final ProductMetricsJpaRepository productMetricsJpaRepository; + private final SupplyService supplyService; + + @Autowired + public LikeProductV1ApiE2ETest( + TestRestTemplate testRestTemplate, + DatabaseCleanUp databaseCleanUp, + BrandJpaRepository brandJpaRepository, + ProductJpaRepository productJpaRepository, + ProductMetricsJpaRepository productMetricsJpaRepository, + SupplyService supplyService + ) { + this.testRestTemplate = testRestTemplate; + this.databaseCleanUp = databaseCleanUp; + this.brandJpaRepository = brandJpaRepository; + this.productJpaRepository = productJpaRepository; + this.productMetricsJpaRepository = productMetricsJpaRepository; + this.supplyService = supplyService; + } + + @AfterEach + void tearDown() { + databaseCleanUp.truncateAllTables(); + } + + private final String validUserId = "user123"; + private final String validEmail = "xx@yy.zz"; + private final String validBirthday = "1993-03-13"; + private final String validGender = "male"; + + private Long brandId; + private Long productId1; + private Long productId2; + + @BeforeEach + @Transactional + void setupUserAndProducts() { + // User ๋“ฑ๋ก + UserV1Dto.UserRegisterRequest request = new UserV1Dto.UserRegisterRequest( + validUserId, + validEmail, + validBirthday, + validGender + ); + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + testRestTemplate.exchange(ENDPOINT_USER, HttpMethod.POST, new HttpEntity<>(request), responseType); + + // Brand ๋“ฑ๋ก + Brand brand = Brand.create("Nike"); + Brand savedBrand = brandJpaRepository.save(brand); + brandId = savedBrand.getId(); + + // Product ๋“ฑ๋ก + Product product1 = createProduct("์ƒํ’ˆ1", brandId, 10000); + Product savedProduct1 = productJpaRepository.save(product1); + productId1 = savedProduct1.getId(); + // ProductMetrics ๋“ฑ๋ก + ProductMetrics metrics1 = ProductMetrics.create(productId1, 0); + productMetricsJpaRepository.save(metrics1); + // Supply ๋“ฑ๋ก + Supply supply1 = Supply.create(productId1, new Stock(100)); + supplyService.saveSupply(supply1); + + Product product2 = createProduct("์ƒํ’ˆ2", brandId, 20000); + Product savedProduct2 = productJpaRepository.save(product2); + productId2 = savedProduct2.getId(); + // ProductMetrics ๋“ฑ๋ก + ProductMetrics metrics2 = ProductMetrics.create(productId2, 0); + productMetricsJpaRepository.save(metrics2); + // Supply ๋“ฑ๋ก + Supply supply2 = Supply.create(productId2, new Stock(200)); + supplyService.saveSupply(supply2); + } + + + private Product createProduct(String name, Long brandId, int priceAmount) { + return Product.create(name, brandId, new Price(priceAmount)); + } + + private HttpHeaders createHeaders() { + HttpHeaders headers = new HttpHeaders(); + headers.add("X-USER-ID", validUserId); + return headers; + } + + @DisplayName("POST /api/v1/like/products/{productId}") + @Nested + class PostLikeProduct { + @DisplayName("๋กœ๊ทธ์ธํ•œ ์œ ์ €๊ฐ€ ์ƒํ’ˆ ์ข‹์•„์š” ๋“ฑ๋ก์— ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, `200 OK` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnOk_whenLikeProductSuccess() { + // arrange + String url = ENDPOINT_LIKE_PRODUCTS + "/" + productId1; + HttpHeaders headers = createHeaders(); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + ResponseEntity> response = testRestTemplate.exchange( + url, HttpMethod.POST, new HttpEntity<>(null, headers), responseType); + + // assert + assertTrue(response.getStatusCode().is2xxSuccessful()); + } + + @DisplayName("๊ฐ™์€ ์ƒํ’ˆ์— ์—ฌ๋Ÿฌ ๋ฒˆ ์ข‹์•„์š”๋ฅผ ๋“ฑ๋กํ•ด๋„ ๋ฉฑ๋“ฑํ•˜๊ฒŒ ๋™์ž‘ํ•œ๋‹ค.") + @Test + void beIdempotent_whenLikeProductMultipleTimes() { + // arrange + String url = ENDPOINT_LIKE_PRODUCTS + "/" + productId1; + HttpHeaders headers = createHeaders(); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + ResponseEntity> response1 = testRestTemplate.exchange( + url, HttpMethod.POST, new HttpEntity<>(null, headers), responseType); + ResponseEntity> response2 = testRestTemplate.exchange( + url, HttpMethod.POST, new HttpEntity<>(null, headers), responseType); + + // assert + assertTrue(response1.getStatusCode().is2xxSuccessful()); + assertTrue(response2.getStatusCode().is2xxSuccessful()); + } + + @DisplayName("๋น„๋กœ๊ทธ์ธ ์œ ์ €๊ฐ€ ์ข‹์•„์š” ๋“ฑ๋ก์„ ์‹œ๋„ํ•  ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnBadRequest_whenXUserIdHeaderIsMissing() { + // arrange + String url = ENDPOINT_LIKE_PRODUCTS + "/" + productId1; + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + ResponseEntity> response = testRestTemplate.exchange( + url, HttpMethod.POST, new HttpEntity<>(null, null), responseType); + + // assert + assertThat(response.getStatusCode().value()).isEqualTo(400); + } + + @DisplayName("X-USER-ID ํ—ค๋”๊ฐ€ ๋นˆ ๋ฌธ์ž์—ด์ผ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnBadRequest_whenXUserIdHeaderIsEmpty() { + // arrange + String url = ENDPOINT_LIKE_PRODUCTS + "/" + productId1; + HttpHeaders headers = new HttpHeaders(); + headers.add("X-USER-ID", ""); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + ResponseEntity> response = testRestTemplate.exchange( + url, HttpMethod.POST, new HttpEntity<>(null, headers), responseType); + + // assert + assertThat(response.getStatusCode().value()).isEqualTo(400); + } + + @DisplayName("X-USER-ID ํ—ค๋”๊ฐ€ ๊ณต๋ฐฑ๋งŒ ์žˆ์„ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnBadRequest_whenXUserIdHeaderIsBlank() { + // arrange + String url = ENDPOINT_LIKE_PRODUCTS + "/" + productId1; + HttpHeaders headers = new HttpHeaders(); + headers.add("X-USER-ID", " "); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + ResponseEntity> response = testRestTemplate.exchange( + url, HttpMethod.POST, new HttpEntity<>(null, headers), responseType); + + // assert + assertThat(response.getStatusCode().value()).isEqualTo(400); + } + + @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ์ € ID๋กœ ์ข‹์•„์š” ๋“ฑ๋ก์„ ์‹œ๋„ํ•  ๊ฒฝ์šฐ, `404 Not Found` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnNotFound_whenUserIdDoesNotExist() { + // arrange + String url = ENDPOINT_LIKE_PRODUCTS + "/" + productId1; + HttpHeaders headers = new HttpHeaders(); + headers.add("X-USER-ID", "nonexist"); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + ResponseEntity> response = testRestTemplate.exchange( + url, HttpMethod.POST, new HttpEntity<>(null, headers), responseType); + + // assert + // Note: ํ˜„์žฌ ์‹คํŒจํ•  ์ˆ˜ ์žˆ์ง€๋งŒ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๋Š” ์ž‘์„ฑํ•จ + assertThat(response.getStatusCode().value() == 400 || response.getStatusCode().value() == 404).isTrue(); + } + + @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ƒํ’ˆ ID๋กœ ์ข‹์•„์š” ๋“ฑ๋ก์„ ์‹œ๋„ํ•  ๊ฒฝ์šฐ, `404 Not Found` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnNotFound_whenProductIdDoesNotExist() { + // arrange + Long nonExistentProductId = 99999L; + String url = ENDPOINT_LIKE_PRODUCTS + "/" + nonExistentProductId; + HttpHeaders headers = createHeaders(); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + ResponseEntity> response = testRestTemplate.exchange( + url, HttpMethod.POST, new HttpEntity<>(null, headers), responseType); + + // assert + // Note: ํ˜„์žฌ ์‹คํŒจํ•  ์ˆ˜ ์žˆ์ง€๋งŒ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๋Š” ์ž‘์„ฑํ•จ + assertThat(response.getStatusCode().value() == 400 || response.getStatusCode().value() == 404).isTrue(); + } + } + + @DisplayName("DELETE /api/v1/like/products/{productId}") + @Nested + class DeleteLikeProduct { + @DisplayName("๋กœ๊ทธ์ธํ•œ ์œ ์ €๊ฐ€ ์ƒํ’ˆ ์ข‹์•„์š” ์ทจ์†Œ์— ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, `200 OK` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnOk_whenUnlikeProductSuccess() { + // arrange + String url = ENDPOINT_LIKE_PRODUCTS + "/" + productId1; + HttpHeaders headers = createHeaders(); + // ๋จผ์ € ์ข‹์•„์š” ๋“ฑ๋ก + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + testRestTemplate.exchange(url, HttpMethod.POST, new HttpEntity<>(null, headers), responseType); + + // act + ResponseEntity> response = testRestTemplate.exchange( + url, HttpMethod.DELETE, new HttpEntity<>(null, headers), responseType); + + // assert + assertTrue(response.getStatusCode().is2xxSuccessful()); + } + + @DisplayName("์ข‹์•„์š”ํ•˜์ง€ ์•Š์€ ์ƒํ’ˆ์„ ์ทจ์†Œํ•ด๋„ ๋ฉฑ๋“ฑํ•˜๊ฒŒ ๋™์ž‘ํ•œ๋‹ค.") + @Test + void beIdempotent_whenUnlikeProductNotLiked() { + // arrange + String url = ENDPOINT_LIKE_PRODUCTS + "/" + productId1; + HttpHeaders headers = createHeaders(); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + ResponseEntity> response = testRestTemplate.exchange( + url, HttpMethod.DELETE, new HttpEntity<>(null, headers), responseType); + + // assert + assertTrue(response.getStatusCode().is2xxSuccessful()); + } + + @DisplayName("๋น„๋กœ๊ทธ์ธ ์œ ์ €๊ฐ€ ์ข‹์•„์š” ์ทจ์†Œ๋ฅผ ์‹œ๋„ํ•  ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnBadRequest_whenXUserIdHeaderIsMissing() { + // arrange + String url = ENDPOINT_LIKE_PRODUCTS + "/" + productId1; + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + ResponseEntity> response = testRestTemplate.exchange( + url, HttpMethod.DELETE, new HttpEntity<>(null, null), responseType); + + // assert + assertThat(response.getStatusCode().value()).isEqualTo(400); + } + + @DisplayName("X-USER-ID ํ—ค๋”๊ฐ€ ๋นˆ ๋ฌธ์ž์—ด์ผ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnBadRequest_whenXUserIdHeaderIsEmpty() { + // arrange + String url = ENDPOINT_LIKE_PRODUCTS + "/" + productId1; + HttpHeaders headers = new HttpHeaders(); + headers.add("X-USER-ID", ""); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + ResponseEntity> response = testRestTemplate.exchange( + url, HttpMethod.DELETE, new HttpEntity<>(null, headers), responseType); + + // assert + assertThat(response.getStatusCode().value()).isEqualTo(400); + } + + @DisplayName("X-USER-ID ํ—ค๋”๊ฐ€ ๊ณต๋ฐฑ๋งŒ ์žˆ์„ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnBadRequest_whenXUserIdHeaderIsBlank() { + // arrange + String url = ENDPOINT_LIKE_PRODUCTS + "/" + productId1; + HttpHeaders headers = new HttpHeaders(); + headers.add("X-USER-ID", " "); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + ResponseEntity> response = testRestTemplate.exchange( + url, HttpMethod.DELETE, new HttpEntity<>(null, headers), responseType); + + // assert + assertThat(response.getStatusCode().value()).isEqualTo(400); + } + + @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ์ € ID๋กœ ์ข‹์•„์š” ์ทจ์†Œ๋ฅผ ์‹œ๋„ํ•  ๊ฒฝ์šฐ, `404 Not Found` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnNotFound_whenUserIdDoesNotExist() { + // arrange + String url = ENDPOINT_LIKE_PRODUCTS + "/" + productId1; + HttpHeaders headers = new HttpHeaders(); + headers.add("X-USER-ID", "nonexist"); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + ResponseEntity> response = testRestTemplate.exchange( + url, HttpMethod.DELETE, new HttpEntity<>(null, headers), responseType); + + // assert + // Note: ํ˜„์žฌ ์‹คํŒจํ•  ์ˆ˜ ์žˆ์ง€๋งŒ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๋Š” ์ž‘์„ฑํ•จ + assertThat(response.getStatusCode().value() == 400 || response.getStatusCode().value() == 404).isTrue(); + } + } + + @DisplayName("GET /api/v1/like/products") + @Nested + class GetLikedProducts { + @DisplayName("๋กœ๊ทธ์ธํ•œ ์œ ์ €๊ฐ€ ์ข‹์•„์š”ํ•œ ์ƒํ’ˆ ๋ชฉ๋ก ์กฐํšŒ์— ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ 200 OK ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + @Test + void returnLikedProducts_whenGetLikedProductsSuccess() { + // arrange + HttpHeaders headers = createHeaders(); + // ์ข‹์•„์š” ๋“ฑ๋ก + ParameterizedTypeReference> likeResponseType = new ParameterizedTypeReference<>() { + }; + testRestTemplate.exchange(ENDPOINT_LIKE_PRODUCTS + "/" + productId1, HttpMethod.POST, new HttpEntity<>(null, headers), likeResponseType); + testRestTemplate.exchange(ENDPOINT_LIKE_PRODUCTS + "/" + productId2, HttpMethod.POST, new HttpEntity<>(null, headers), likeResponseType); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + ResponseEntity> response = testRestTemplate.exchange( + ENDPOINT_LIKE_PRODUCTS, HttpMethod.GET, new HttpEntity<>(null, headers), responseType); + + // assert + assertTrue(response.getStatusCode().is2xxSuccessful()); + } + + @DisplayName("ํŽ˜์ด์ง€๋„ค์ด์…˜ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ์กฐํšŒํ•  ๊ฒฝ์šฐ, ํ•ด๋‹น ํŽ˜์ด์ง€์˜ ์ข‹์•„์š”ํ•œ ์ƒํ’ˆ ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnLikedProducts_whenWithPagination() { + // arrange + String url = ENDPOINT_LIKE_PRODUCTS + "?page=0&size=10"; + HttpHeaders headers = createHeaders(); + // ์ข‹์•„์š” ๋“ฑ๋ก + ParameterizedTypeReference> likeResponseType = new ParameterizedTypeReference<>() { + }; + testRestTemplate.exchange(ENDPOINT_LIKE_PRODUCTS + "/" + productId1, HttpMethod.POST, new HttpEntity<>(null, headers), likeResponseType); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + ResponseEntity> response = testRestTemplate.exchange( + url, HttpMethod.GET, new HttpEntity<>(null, headers), responseType); + + // assert + assertAll( + () -> assertTrue(response.getStatusCode().is2xxSuccessful()), + () -> assertThat(response.getBody()).isNotNull(), + () -> assertThat(response.getBody().data()).isNotNull() + ); + } + + @DisplayName("๋น„๋กœ๊ทธ์ธ ์œ ์ €๊ฐ€ ์ข‹์•„์š”ํ•œ ์ƒํ’ˆ ๋ชฉ๋ก ์กฐํšŒ๋ฅผ ์‹œ๋„ํ•  ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnBadRequest_whenXUserIdHeaderIsMissing() { + // arrange + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + ResponseEntity> response = testRestTemplate.exchange( + ENDPOINT_LIKE_PRODUCTS, HttpMethod.GET, new HttpEntity<>(null, null), responseType); + + // assert + assertThat(response.getStatusCode().value()).isEqualTo(400); + } + + @DisplayName("X-USER-ID ํ—ค๋”๊ฐ€ ๋นˆ ๋ฌธ์ž์—ด์ผ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnBadRequest_whenXUserIdHeaderIsEmpty() { + // arrange + HttpHeaders headers = new HttpHeaders(); + headers.add("X-USER-ID", ""); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + ResponseEntity> response = testRestTemplate.exchange( + ENDPOINT_LIKE_PRODUCTS, HttpMethod.GET, new HttpEntity<>(null, headers), responseType); + + // assert + assertThat(response.getStatusCode().value()).isEqualTo(400); + } + + @DisplayName("X-USER-ID ํ—ค๋”๊ฐ€ ๊ณต๋ฐฑ๋งŒ ์žˆ์„ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnBadRequest_whenXUserIdHeaderIsBlank() { + // arrange + HttpHeaders headers = new HttpHeaders(); + headers.add("X-USER-ID", " "); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + ResponseEntity> response = testRestTemplate.exchange( + ENDPOINT_LIKE_PRODUCTS, HttpMethod.GET, new HttpEntity<>(null, headers), responseType); + + // assert + assertThat(response.getStatusCode().value()).isEqualTo(400); + } + + @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ์ € ID๋กœ ์ข‹์•„์š”ํ•œ ์ƒํ’ˆ ๋ชฉ๋ก ์กฐํšŒ๋ฅผ ์‹œ๋„ํ•  ๊ฒฝ์šฐ, `404 Not Found` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnNotFound_whenUserIdDoesNotExist() { + // arrange + HttpHeaders headers = new HttpHeaders(); + headers.add("X-USER-ID", "nonexist"); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + ResponseEntity> response = testRestTemplate.exchange( + ENDPOINT_LIKE_PRODUCTS, HttpMethod.GET, new HttpEntity<>(null, headers), responseType); + + // assert + // Note: ํ˜„์žฌ ์‹คํŒจํ•  ์ˆ˜ ์žˆ์ง€๋งŒ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๋Š” ์ž‘์„ฑํ•จ + assertThat(response.getStatusCode().value() == 400 || response.getStatusCode().value() == 404).isTrue(); + } + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/OrderV1ApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/OrderV1ApiE2ETest.java new file mode 100644 index 000000000..cbfaf772d --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/OrderV1ApiE2ETest.java @@ -0,0 +1,828 @@ +package com.loopers.interfaces.api; + +import com.loopers.domain.brand.Brand; +import com.loopers.domain.common.vo.Price; +import com.loopers.domain.metrics.product.ProductMetrics; +import com.loopers.domain.product.Product; +import com.loopers.domain.supply.Supply; +import com.loopers.domain.supply.vo.Stock; +import com.loopers.infrastructure.brand.BrandJpaRepository; +import com.loopers.infrastructure.metrics.product.ProductMetricsJpaRepository; +import com.loopers.infrastructure.product.ProductJpaRepository; +import com.loopers.infrastructure.supply.SupplyJpaRepository; +import com.loopers.interfaces.api.order.OrderV1Dto; +import com.loopers.interfaces.api.point.PointV1Dto; +import com.loopers.interfaces.api.user.UserV1Dto; +import com.loopers.utils.DatabaseCleanUp; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class OrderV1ApiE2ETest { + + private final String ENDPOINT_USER = "/api/v1/users"; + private final String ENDPOINT_POINT = "/api/v1/points"; + private final String ENDPOINT_ORDERS = "/api/v1/orders"; + + private final TestRestTemplate testRestTemplate; + private final DatabaseCleanUp databaseCleanUp; + private final BrandJpaRepository brandJpaRepository; + private final ProductJpaRepository productJpaRepository; + private final SupplyJpaRepository supplyJpaRepository; + private final ProductMetricsJpaRepository productMetricsJpaRepository; + + @Autowired + public OrderV1ApiE2ETest( + TestRestTemplate testRestTemplate, + DatabaseCleanUp databaseCleanUp, + BrandJpaRepository brandJpaRepository, + ProductJpaRepository productJpaRepository, + SupplyJpaRepository supplyJpaRepository, + ProductMetricsJpaRepository productMetricsJpaRepository + ) { + this.testRestTemplate = testRestTemplate; + this.databaseCleanUp = databaseCleanUp; + this.brandJpaRepository = brandJpaRepository; + this.productJpaRepository = productJpaRepository; + this.supplyJpaRepository = supplyJpaRepository; + this.productMetricsJpaRepository = productMetricsJpaRepository; + } + + @AfterEach + void tearDown() { + databaseCleanUp.truncateAllTables(); + } + + private final String validUserId = "user123"; + private final String validEmail = "xx@yy.zz"; + private final String validBirthday = "1993-03-13"; + private final String validGender = "male"; + + private Long brandId; + private Long productId1; + private Long productId2; + private Long productId3; + + @BeforeEach + void setupUserAndProducts() { + // User ๋“ฑ๋ก + UserV1Dto.UserRegisterRequest userRequest = new UserV1Dto.UserRegisterRequest( + validUserId, + validEmail, + validBirthday, + validGender + ); + ParameterizedTypeReference> userResponseType = new ParameterizedTypeReference<>() { + }; + testRestTemplate.exchange(ENDPOINT_USER, HttpMethod.POST, new HttpEntity<>(userRequest), userResponseType); + + // ํฌ์ธํŠธ ์ถฉ์ „ + HttpHeaders headers = createHeaders(); + PointV1Dto.PointChargeRequest pointRequest = new PointV1Dto.PointChargeRequest(100000); + ParameterizedTypeReference> pointResponseType = new ParameterizedTypeReference<>() { + }; + testRestTemplate.exchange(ENDPOINT_POINT + "/charge", HttpMethod.POST, new HttpEntity<>(pointRequest, headers), pointResponseType); + + // Brand ๋“ฑ๋ก + Brand brand = Brand.create("Nike"); + Brand savedBrand = brandJpaRepository.save(brand); + brandId = savedBrand.getId(); + + // Product ๋“ฑ๋ก + Product product1 = createProduct("์ƒํ’ˆ1", brandId, 10000); + Product savedProduct1 = productJpaRepository.save(product1); + productId1 = savedProduct1.getId(); + // ProductMetrics ๋“ฑ๋ก + ProductMetrics metrics1 = ProductMetrics.create(productId1, 0); + productMetricsJpaRepository.save(metrics1); + + Product product2 = createProduct("์ƒํ’ˆ2", brandId, 20000); + Product savedProduct2 = productJpaRepository.save(product2); + productId2 = savedProduct2.getId(); + // ProductMetrics ๋“ฑ๋ก + ProductMetrics metrics2 = ProductMetrics.create(productId2, 0); + productMetricsJpaRepository.save(metrics2); + + Product product3 = createProduct("์ƒํ’ˆ3", brandId, 15000); + Product savedProduct3 = productJpaRepository.save(product3); + productId3 = savedProduct3.getId(); + // ProductMetrics ๋“ฑ๋ก + ProductMetrics metrics3 = ProductMetrics.create(productId3, 0); + productMetricsJpaRepository.save(metrics3); + + // Supply ๋“ฑ๋ก (์žฌ๊ณ  ์„ค์ •) + Supply supply1 = createSupply(productId1, 100); + supplyJpaRepository.save(supply1); + + Supply supply2 = createSupply(productId2, 50); + supplyJpaRepository.save(supply2); + + Supply supply3 = createSupply(productId3, 30); + supplyJpaRepository.save(supply3); + } + + private Product createProduct(String name, Long brandId, int priceAmount) { + return Product.create(name, brandId, new Price(priceAmount)); + } + + private Supply createSupply(Long productId, int stockQuantity) { + return Supply.create(productId, new Stock(stockQuantity)); + } + + private HttpHeaders createHeaders() { + HttpHeaders headers = new HttpHeaders(); + headers.add("X-USER-ID", validUserId); + return headers; + } + + @DisplayName("POST /api/v1/orders") + @Nested + class PostOrder { + @DisplayName("๋กœ๊ทธ์ธํ•œ ์œ ์ €๊ฐ€ ์ฃผ๋ฌธ ์ƒ์„ฑ์— ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ์ƒ์„ฑ๋œ ์ฃผ๋ฌธ ์ •๋ณด๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnOrderInfo_whenCreateOrderSuccess() { + // arrange + OrderV1Dto.OrderRequest request = new OrderV1Dto.OrderRequest( + List.of( + new OrderV1Dto.OrderRequest.OrderItemRequest(productId1, 2), + new OrderV1Dto.OrderRequest.OrderItemRequest(productId2, 1) + ) + ); + HttpHeaders headers = createHeaders(); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + ResponseEntity> response = testRestTemplate.exchange( + ENDPOINT_ORDERS, HttpMethod.POST, new HttpEntity<>(request, headers), responseType); + + // assert + assertAll( + () -> assertTrue(response.getStatusCode().is2xxSuccessful()), + () -> assertThat(response.getBody()).isNotNull(), + () -> assertThat(response.getBody().data()).isNotNull(), + () -> assertThat(response.getBody().data().orderId()).isNotNull(), + () -> assertThat(response.getBody().data().items()).hasSize(2), + () -> assertThat(response.getBody().data().totalPrice()).isEqualTo(40000) + ); + } + + @DisplayName("์žฌ๊ณ ๊ฐ€ ๋ถ€์กฑํ•œ ์ƒํ’ˆ์„ ์ฃผ๋ฌธํ•  ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnBadRequest_whenStockInsufficient() { + // arrange + OrderV1Dto.OrderRequest request = new OrderV1Dto.OrderRequest( + List.of( + new OrderV1Dto.OrderRequest.OrderItemRequest(productId1, 99999) + ) + ); + HttpHeaders headers = createHeaders(); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + ResponseEntity> response = testRestTemplate.exchange( + ENDPOINT_ORDERS, HttpMethod.POST, new HttpEntity<>(request, headers), responseType); + + // assert + assertThat(response.getStatusCode().value() == 400).isTrue(); + } + + @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ƒํ’ˆ ID๋กœ ์ฃผ๋ฌธํ•  ๊ฒฝ์šฐ, `404 Not Found` ๋˜๋Š” `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnNotFoundOrBadRequest_whenProductIdDoesNotExist() { + // arrange + Long nonExistentProductId = 99999L; + OrderV1Dto.OrderRequest request = new OrderV1Dto.OrderRequest( + List.of( + new OrderV1Dto.OrderRequest.OrderItemRequest(nonExistentProductId, 1) + ) + ); + HttpHeaders headers = createHeaders(); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + ResponseEntity> response = testRestTemplate.exchange( + ENDPOINT_ORDERS, HttpMethod.POST, new HttpEntity<>(request, headers), responseType); + + // assert + // Note: OrderFacade์—์„œ getProductMapByIds๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด ์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ƒํ’ˆ์€ ๋งต์— ํฌํ•จ๋˜์ง€ ์•Š์Œ + // ์ดํ›„ OrderItem.create์—์„œ productMap.get()์ด null์„ ๋ฐ˜ํ™˜ํ•˜๋ฉด NullPointerException์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Œ + // ๋˜๋Š” SupplyService.checkAndDecreaseStock์—์„œ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Œ + assertThat(response.getStatusCode().value() == 400 || response.getStatusCode().value() == 404 || response.getStatusCode().value() == 500).isTrue(); + } + + @DisplayName("ํฌ์ธํŠธ๊ฐ€ ๋ถ€์กฑํ•œ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnBadRequest_whenPointInsufficient() { + // arrange + // ํฌ์ธํŠธ๋ฅผ ๋ชจ๋‘ ์‚ฌ์šฉ + HttpHeaders headers = createHeaders(); + OrderV1Dto.OrderRequest firstOrder = new OrderV1Dto.OrderRequest( + List.of( + new OrderV1Dto.OrderRequest.OrderItemRequest(productId1, 10) + ) + ); + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + testRestTemplate.exchange(ENDPOINT_ORDERS, HttpMethod.POST, new HttpEntity<>(firstOrder, headers), responseType); + + // ํฌ์ธํŠธ ๋ถ€์กฑํ•œ ์ฃผ๋ฌธ ์‹œ๋„ + OrderV1Dto.OrderRequest request = new OrderV1Dto.OrderRequest( + List.of( + new OrderV1Dto.OrderRequest.OrderItemRequest(productId2, 99999) + ) + ); + + // act + ResponseEntity> response = testRestTemplate.exchange( + ENDPOINT_ORDERS, HttpMethod.POST, new HttpEntity<>(request, headers), responseType); + + // assert + assertThat(response.getStatusCode().value() == 400).isTrue(); + } + + @DisplayName("๋น„๋กœ๊ทธ์ธ ์œ ์ €๊ฐ€ ์ฃผ๋ฌธ ์ƒ์„ฑ์„ ์‹œ๋„ํ•  ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnBadRequest_whenXUserIdHeaderIsMissing() { + // arrange + OrderV1Dto.OrderRequest request = new OrderV1Dto.OrderRequest( + List.of( + new OrderV1Dto.OrderRequest.OrderItemRequest(productId1, 1) + ) + ); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + ResponseEntity> response = testRestTemplate.exchange( + ENDPOINT_ORDERS, HttpMethod.POST, new HttpEntity<>(request, null), responseType); + + // assert + assertThat(response.getStatusCode().value()).isEqualTo(400); + } + + @DisplayName("X-USER-ID ํ—ค๋”๊ฐ€ ๋นˆ ๋ฌธ์ž์—ด์ผ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnBadRequest_whenXUserIdHeaderIsEmpty() { + // arrange + OrderV1Dto.OrderRequest request = new OrderV1Dto.OrderRequest( + List.of( + new OrderV1Dto.OrderRequest.OrderItemRequest(productId1, 1) + ) + ); + HttpHeaders headers = new HttpHeaders(); + headers.add("X-USER-ID", ""); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + ResponseEntity> response = testRestTemplate.exchange( + ENDPOINT_ORDERS, HttpMethod.POST, new HttpEntity<>(request, headers), responseType); + + // assert + assertThat(response.getStatusCode().value()).isEqualTo(400); + } + + @DisplayName("X-USER-ID ํ—ค๋”๊ฐ€ ๊ณต๋ฐฑ๋งŒ ์žˆ์„ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnBadRequest_whenXUserIdHeaderIsBlank() { + // arrange + OrderV1Dto.OrderRequest request = new OrderV1Dto.OrderRequest( + List.of( + new OrderV1Dto.OrderRequest.OrderItemRequest(productId1, 1) + ) + ); + HttpHeaders headers = new HttpHeaders(); + headers.add("X-USER-ID", " "); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + ResponseEntity> response = testRestTemplate.exchange( + ENDPOINT_ORDERS, HttpMethod.POST, new HttpEntity<>(request, headers), responseType); + + // assert + assertThat(response.getStatusCode().value()).isEqualTo(400); + } + + @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ์ € ID๋กœ ์ฃผ๋ฌธ ์ƒ์„ฑ์„ ์‹œ๋„ํ•  ๊ฒฝ์šฐ, `404 Not Found` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnNotFound_whenUserIdDoesNotExist() { + // arrange + OrderV1Dto.OrderRequest request = new OrderV1Dto.OrderRequest( + List.of( + new OrderV1Dto.OrderRequest.OrderItemRequest(productId1, 1) + ) + ); + HttpHeaders headers = new HttpHeaders(); + headers.add("X-USER-ID", "nonexist"); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + ResponseEntity> response = testRestTemplate.exchange( + ENDPOINT_ORDERS, HttpMethod.POST, new HttpEntity<>(request, headers), responseType); + + // assert + assertThat(response.getStatusCode().value() == 404).isTrue(); + } + + @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ƒํ’ˆ ID๋กœ ์ฃผ๋ฌธ ์ƒ์„ฑ์„ ์‹œ๋„ํ•  ๊ฒฝ์šฐ, `404 Not Found` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnNotFound_whenProductIdDoesNotExist() { + // arrange + Long nonExistentProductId = 99999L; + OrderV1Dto.OrderRequest request = new OrderV1Dto.OrderRequest( + List.of( + new OrderV1Dto.OrderRequest.OrderItemRequest(nonExistentProductId, 1) + ) + ); + HttpHeaders headers = createHeaders(); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + ResponseEntity> response = testRestTemplate.exchange( + ENDPOINT_ORDERS, HttpMethod.POST, new HttpEntity<>(request, headers), responseType); + + // assert + assertThat(response.getStatusCode().value() == 404).isTrue(); + } + + @DisplayName("์—ฌ๋Ÿฌ ์ƒํ’ˆ ์ค‘ ์ผ๋ถ€๋งŒ ์žฌ๊ณ  ๋ถ€์กฑ ์‹œ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnBadRequest_whenPartialStockInsufficient() { + // arrange + // productId1: ์žฌ๊ณ  100, productId2: ์žฌ๊ณ  50 + // productId1์€ ์ถฉ๋ถ„ํ•˜์ง€๋งŒ productId2๋Š” ๋ถ€์กฑ + OrderV1Dto.OrderRequest request = new OrderV1Dto.OrderRequest( + List.of( + new OrderV1Dto.OrderRequest.OrderItemRequest(productId1, 10), // ์žฌ๊ณ  ์ถฉ๋ถ„ + new OrderV1Dto.OrderRequest.OrderItemRequest(productId2, 99999) // ์žฌ๊ณ  ๋ถ€์กฑ + ) + ); + HttpHeaders headers = createHeaders(); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + ResponseEntity> response = testRestTemplate.exchange( + ENDPOINT_ORDERS, HttpMethod.POST, new HttpEntity<>(request, headers), responseType); + + // assert + assertThat(response.getStatusCode().value()).isEqualTo(400); + // Note: ํ˜„์žฌ ๊ตฌํ˜„์—์„œ๋Š” ์ฒซ ๋ฒˆ์งธ ์žฌ๊ณ  ๋ถ€์กฑ ์ƒํ’ˆ์—์„œ๋งŒ ์˜ˆ์™ธ ๋ฐœ์ƒ + // ๊ฐœ์„  ํ›„์—๋Š” ๋ชจ๋“  ๋ถ€์กฑํ•œ ์ƒํ’ˆ์„ ํ•œ ๋ฒˆ์— ์•Œ๋ ค์ค„ ์ˆ˜ ์žˆ์Œ + } + + @DisplayName("์—ฌ๋Ÿฌ ์ƒํ’ˆ ๋ชจ๋‘ ์žฌ๊ณ  ๋ถ€์กฑ ์‹œ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnBadRequest_whenAllProductsStockInsufficient() { + // arrange + OrderV1Dto.OrderRequest request = new OrderV1Dto.OrderRequest( + List.of( + new OrderV1Dto.OrderRequest.OrderItemRequest(productId1, 99999), + new OrderV1Dto.OrderRequest.OrderItemRequest(productId2, 99999) + ) + ); + HttpHeaders headers = createHeaders(); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + ResponseEntity> response = testRestTemplate.exchange( + ENDPOINT_ORDERS, HttpMethod.POST, new HttpEntity<>(request, headers), responseType); + + // assert + assertThat(response.getStatusCode().value()).isEqualTo(400); + // Note: ํ˜„์žฌ ๊ตฌํ˜„์—์„œ๋Š” ์ฒซ ๋ฒˆ์งธ ์žฌ๊ณ  ๋ถ€์กฑ ์ƒํ’ˆ์—์„œ๋งŒ ์˜ˆ์™ธ ๋ฐœ์ƒ + } + + @DisplayName("ํฌ์ธํŠธ๊ฐ€ ์ •ํ™•ํžˆ ์ผ์น˜ํ•  ๊ฒฝ์šฐ, ์ฃผ๋ฌธ์ด ์„ฑ๊ณตํ•œ๋‹ค.") + @Test + void returnOrderInfo_whenPointExactlyMatches() { + // arrange + // ํฌ์ธํŠธ๋ฅผ ๊ฑฐ์˜ ๋ชจ๋‘ ์‚ฌ์šฉ + HttpHeaders headers = createHeaders(); + OrderV1Dto.OrderRequest firstOrder = new OrderV1Dto.OrderRequest( + List.of( + new OrderV1Dto.OrderRequest.OrderItemRequest(productId1, 9) // 90000์› ์‚ฌ์šฉ + ) + ); + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + testRestTemplate.exchange(ENDPOINT_ORDERS, HttpMethod.POST, new HttpEntity<>(firstOrder, headers), responseType); + // ๋‚จ์€ ํฌ์ธํŠธ: 10000์› + + // ์ •ํ™•ํžˆ ์ผ์น˜ํ•˜๋Š” ์ฃผ๋ฌธ + OrderV1Dto.OrderRequest request = new OrderV1Dto.OrderRequest( + List.of( + new OrderV1Dto.OrderRequest.OrderItemRequest(productId1, 1) // ์ •ํ™•ํžˆ 10000์› + ) + ); + + // act + ResponseEntity> response = testRestTemplate.exchange( + ENDPOINT_ORDERS, HttpMethod.POST, new HttpEntity<>(request, headers), responseType); + + // assert + assertAll( + () -> assertTrue(response.getStatusCode().is2xxSuccessful()), + () -> assertThat(response.getBody()).isNotNull(), + () -> assertThat(response.getBody().data()).isNotNull(), + () -> assertThat(response.getBody().data().totalPrice()).isEqualTo(10000) + ); + } + + @DisplayName("์ค‘๋ณต ์ƒํ’ˆ์ด ํฌํ•จ๋œ ๊ฒฝ์šฐ, `500 Internal Server Error` ๋˜๋Š” `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnError_whenDuplicateProducts() { + // arrange + // ๊ฐ™์€ ์ƒํ’ˆ์„ ์—ฌ๋Ÿฌ ๋ฒˆ ์ฃผ๋ฌธ ํ•ญ๋ชฉ์— ํฌํ•จ + // Note: Collectors.toMap()์€ ์ค‘๋ณต ํ‚ค๊ฐ€ ์žˆ์œผ๋ฉด IllegalStateException์„ ๋ฐœ์ƒ์‹œํ‚ด + OrderV1Dto.OrderRequest request = new OrderV1Dto.OrderRequest( + List.of( + new OrderV1Dto.OrderRequest.OrderItemRequest(productId1, 2), + new OrderV1Dto.OrderRequest.OrderItemRequest(productId1, 3) // ์ค‘๋ณต + ) + ); + HttpHeaders headers = createHeaders(); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + ResponseEntity> response = testRestTemplate.exchange( + ENDPOINT_ORDERS, HttpMethod.POST, new HttpEntity<>(request, headers), responseType); + + // assert + // Note: Collectors.toMap()์—์„œ ์ค‘๋ณต ํ‚ค๋กœ ์ธํ•ด IllegalStateException ๋ฐœ์ƒ + // ์ด๋Š” 500 Internal Server Error๋กœ ๋ณ€ํ™˜๋˜๊ฑฐ๋‚˜, 400 Bad Request๋กœ ์ฒ˜๋ฆฌ๋  ์ˆ˜ ์žˆ์Œ + assertThat(response.getStatusCode().value() == 400 || response.getStatusCode().value() == 500).isTrue(); + } + + @DisplayName("Point ์—”ํ‹ฐํ‹ฐ๊ฐ€ ์—†๋Š” ์‚ฌ์šฉ์ž๋กœ ์ฃผ๋ฌธ ์‹œ๋„ ์‹œ, `404 Not Found` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•˜๊ณ  ์žฌ๊ณ ๊ฐ€ ๋กค๋ฐฑ๋œ๋‹ค.") + @Test + void returnNotFoundAndRollbackStock_whenPointDoesNotExist() { + // arrange + // Point๊ฐ€ ์—†๋Š” ์‚ฌ์šฉ์ž ์ƒ์„ฑ + String userWithoutPointId = "userWithoutPoint"; + UserV1Dto.UserRegisterRequest userRequest = new UserV1Dto.UserRegisterRequest( + userWithoutPointId, + "test2@test.com", + "1993-03-13", + "male" + ); + ParameterizedTypeReference> userResponseType = new ParameterizedTypeReference<>() { + }; + testRestTemplate.exchange(ENDPOINT_USER, HttpMethod.POST, new HttpEntity<>(userRequest), userResponseType); + // Point๋Š” ์ƒ์„ฑํ•˜์ง€ ์•Š์Œ + + // ์ดˆ๊ธฐ ์žฌ๊ณ  ํ™•์ธ + Supply initialSupply = supplyJpaRepository.findByProductId(productId1).orElseThrow(); + int initialStock = initialSupply.getStock().quantity(); + + OrderV1Dto.OrderRequest request = new OrderV1Dto.OrderRequest( + List.of( + new OrderV1Dto.OrderRequest.OrderItemRequest(productId1, 1) + ) + ); + HttpHeaders headers = new HttpHeaders(); + headers.add("X-USER-ID", userWithoutPointId); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + ResponseEntity> response = testRestTemplate.exchange( + ENDPOINT_ORDERS, HttpMethod.POST, new HttpEntity<>(request, headers), responseType); + + // assert + assertThat(response.getStatusCode().value()).isEqualTo(404); + // ์žฌ๊ณ ๊ฐ€ ๋กค๋ฐฑ๋˜์–ด ์›๋ž˜๋Œ€๋กœ ๋ณต๊ตฌ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ + Supply afterRollbackSupply = supplyJpaRepository.findByProductId(productId1).orElseThrow(); + int afterStock = afterRollbackSupply.getStock().quantity(); + // ์žฌ๊ณ ๊ฐ€ ์›๋ž˜๋Œ€๋กœ ๋ณต๊ตฌ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ (์ดˆ๊ธฐ ์žฌ๊ณ ์™€ ๋™์ผํ•ด์•ผ ํ•จ) + assertThat(afterStock).isEqualTo(initialStock); + } + + @DisplayName("์žฌ๊ณ  ์ฐจ๊ฐ ํ›„ ํฌ์ธํŠธ ๋ถ€์กฑ ์‹œ, ๋กค๋ฐฑ๋˜์–ด ์žฌ๊ณ ๊ฐ€ ๋ณต๊ตฌ๋œ๋‹ค.") + @Test + void should_rollbackStock_whenPointInsufficientAfterStockDecrease() { + // arrange + // ์ดˆ๊ธฐ ์žฌ๊ณ  ํ™•์ธ + Supply initialSupply = supplyJpaRepository.findByProductId(productId1).orElseThrow(); + int initialStock = initialSupply.getStock().quantity(); + + // ํฌ์ธํŠธ๋ฅผ ๊ฑฐ์˜ ๋ชจ๋‘ ์‚ฌ์šฉ + HttpHeaders headers = createHeaders(); + OrderV1Dto.OrderRequest firstOrder = new OrderV1Dto.OrderRequest( + List.of( + new OrderV1Dto.OrderRequest.OrderItemRequest(productId1, 9) // 90000์› ์‚ฌ์šฉ + ) + ); + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + testRestTemplate.exchange(ENDPOINT_ORDERS, HttpMethod.POST, new HttpEntity<>(firstOrder, headers), responseType); + // ๋‚จ์€ ํฌ์ธํŠธ: 10000์› + + // ํฌ์ธํŠธ ๋ถ€์กฑํ•œ ์ฃผ๋ฌธ ์‹œ๋„ (์žฌ๊ณ ๋Š” ์ถฉ๋ถ„) + OrderV1Dto.OrderRequest request = new OrderV1Dto.OrderRequest( + List.of( + new OrderV1Dto.OrderRequest.OrderItemRequest(productId1, 2) // 20000์› ํ•„์š” (๋ถ€์กฑ) + ) + ); + + // act + ResponseEntity> response = testRestTemplate.exchange( + ENDPOINT_ORDERS, HttpMethod.POST, new HttpEntity<>(request, headers), responseType); + + // assert + assertThat(response.getStatusCode().value()).isEqualTo(400); + // ์žฌ๊ณ ๊ฐ€ ๋กค๋ฐฑ๋˜์–ด ์›๋ž˜๋Œ€๋กœ ๋ณต๊ตฌ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ + Supply afterRollbackSupply = supplyJpaRepository.findByProductId(productId1).orElseThrow(); + int afterStock = afterRollbackSupply.getStock().quantity(); + // ์ฒซ ์ฃผ๋ฌธ์—์„œ 9๊ฐœ ์ฐจ๊ฐ๋˜์—ˆ์œผ๋ฏ€๋กœ, ์ดˆ๊ธฐ ์žฌ๊ณ  - 9 = ํ˜„์žฌ ์žฌ๊ณ ์—ฌ์•ผ ํ•จ + assertThat(afterStock).isEqualTo(initialStock - 9); + } + + @DisplayName("๋ถ€๋ถ„ ์žฌ๊ณ  ๋ถ€์กฑ ์‹œ, ๋กค๋ฐฑ๋˜์–ด ์žฌ๊ณ ๊ฐ€ ๋ณต๊ตฌ๋œ๋‹ค.") + @Test + void should_rollbackStock_whenPartialStockInsufficient() { + // arrange + // ์ดˆ๊ธฐ ์žฌ๊ณ  ํ™•์ธ + Supply initialSupply1 = supplyJpaRepository.findByProductId(productId1).orElseThrow(); + int initialStock1 = initialSupply1.getStock().quantity(); + Supply initialSupply2 = supplyJpaRepository.findByProductId(productId2).orElseThrow(); + int initialStock2 = initialSupply2.getStock().quantity(); + + // productId1์€ ์ถฉ๋ถ„ํ•˜์ง€๋งŒ productId2๋Š” ๋ถ€์กฑํ•œ ์ฃผ๋ฌธ + OrderV1Dto.OrderRequest request = new OrderV1Dto.OrderRequest( + List.of( + new OrderV1Dto.OrderRequest.OrderItemRequest(productId1, 10), // ์žฌ๊ณ  ์ถฉ๋ถ„ + new OrderV1Dto.OrderRequest.OrderItemRequest(productId2, 99999) // ์žฌ๊ณ  ๋ถ€์กฑ + ) + ); + HttpHeaders headers = createHeaders(); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + ResponseEntity> response = testRestTemplate.exchange( + ENDPOINT_ORDERS, HttpMethod.POST, new HttpEntity<>(request, headers), responseType); + + // assert + assertThat(response.getStatusCode().value()).isEqualTo(400); + // ๋ชจ๋“  ์žฌ๊ณ ๊ฐ€ ๋กค๋ฐฑ๋˜์–ด ์›๋ž˜๋Œ€๋กœ ๋ณต๊ตฌ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ + Supply afterRollbackSupply1 = supplyJpaRepository.findByProductId(productId1).orElseThrow(); + int afterStock1 = afterRollbackSupply1.getStock().quantity(); + Supply afterRollbackSupply2 = supplyJpaRepository.findByProductId(productId2).orElseThrow(); + int afterStock2 = afterRollbackSupply2.getStock().quantity(); + + assertThat(afterStock1).isEqualTo(initialStock1); + assertThat(afterStock2).isEqualTo(initialStock2); + } + } + + @DisplayName("GET /api/v1/orders") + @Nested + class GetOrderList { + @DisplayName("๋กœ๊ทธ์ธํ•œ ์œ ์ €๊ฐ€ ์ฃผ๋ฌธ ๋ชฉ๋ก ์กฐํšŒ์— ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ์ฃผ๋ฌธ ๋ชฉ๋ก์„ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnOrderList_whenGetOrderListSuccess() { + // arrange + HttpHeaders headers = createHeaders(); + // ์ฃผ๋ฌธ ์ƒ์„ฑ + OrderV1Dto.OrderRequest orderRequest = new OrderV1Dto.OrderRequest( + List.of( + new OrderV1Dto.OrderRequest.OrderItemRequest(productId1, 1) + ) + ); + ParameterizedTypeReference> orderResponseType = new ParameterizedTypeReference<>() { + }; + testRestTemplate.exchange(ENDPOINT_ORDERS, HttpMethod.POST, new HttpEntity<>(orderRequest, headers), orderResponseType); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + ResponseEntity> response = testRestTemplate.exchange( + ENDPOINT_ORDERS, HttpMethod.GET, new HttpEntity<>(null, headers), responseType); + + // assert + // Note: ํ˜„์žฌ todo ์ƒํƒœ์ด์ง€๋งŒ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๋Š” ์ž‘์„ฑํ•จ + assertThat(response.getStatusCode().is2xxSuccessful()).isTrue(); + } + + @DisplayName("๋น„๋กœ๊ทธ์ธ ์œ ์ €๊ฐ€ ์ฃผ๋ฌธ ๋ชฉ๋ก ์กฐํšŒ๋ฅผ ์‹œ๋„ํ•  ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnBadRequest_whenXUserIdHeaderIsMissing() { + // arrange + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + ResponseEntity> response = testRestTemplate.exchange( + ENDPOINT_ORDERS, HttpMethod.GET, new HttpEntity<>(null, null), responseType); + + // assert + assertThat(response.getStatusCode().value()).isEqualTo(400); + } + + @DisplayName("X-USER-ID ํ—ค๋”๊ฐ€ ๋นˆ ๋ฌธ์ž์—ด์ผ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnBadRequest_whenXUserIdHeaderIsEmpty() { + // arrange + HttpHeaders headers = new HttpHeaders(); + headers.add("X-USER-ID", ""); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + ResponseEntity> response = testRestTemplate.exchange( + ENDPOINT_ORDERS, HttpMethod.GET, new HttpEntity<>(null, headers), responseType); + + // assert + assertThat(response.getStatusCode().value()).isEqualTo(400); + } + + @DisplayName("X-USER-ID ํ—ค๋”๊ฐ€ ๊ณต๋ฐฑ๋งŒ ์žˆ์„ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnBadRequest_whenXUserIdHeaderIsBlank() { + // arrange + HttpHeaders headers = new HttpHeaders(); + headers.add("X-USER-ID", " "); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + ResponseEntity> response = testRestTemplate.exchange( + ENDPOINT_ORDERS, HttpMethod.GET, new HttpEntity<>(null, headers), responseType); + + // assert + assertThat(response.getStatusCode().value()).isEqualTo(400); + } + + @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ์ € ID๋กœ ์ฃผ๋ฌธ ๋ชฉ๋ก ์กฐํšŒ๋ฅผ ์‹œ๋„ํ•  ๊ฒฝ์šฐ, `404 Not Found` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnNotFound_whenUserIdDoesNotExist() { + // arrange + HttpHeaders headers = new HttpHeaders(); + headers.add("X-USER-ID", "nonexist"); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + ResponseEntity> response = testRestTemplate.exchange( + ENDPOINT_ORDERS, HttpMethod.GET, new HttpEntity<>(null, headers), responseType); + + // assert + assertThat(response.getStatusCode().value() == 404).isTrue(); + } + } + + @DisplayName("GET /api/v1/orders/{orderId}") + @Nested + class GetOrderDetail { + private Long orderId; + + @BeforeEach + void setupOrder() { + // ์ฃผ๋ฌธ ์ƒ์„ฑ + HttpHeaders headers = createHeaders(); + OrderV1Dto.OrderRequest orderRequest = new OrderV1Dto.OrderRequest( + List.of( + new OrderV1Dto.OrderRequest.OrderItemRequest(productId1, 1) + ) + ); + ParameterizedTypeReference> orderResponseType = new ParameterizedTypeReference<>() { + }; + ResponseEntity> orderResponse = testRestTemplate.exchange( + ENDPOINT_ORDERS, HttpMethod.POST, new HttpEntity<>(orderRequest, headers), orderResponseType); + if (orderResponse.getStatusCode().is2xxSuccessful() && orderResponse.getBody() != null) { + orderId = orderResponse.getBody().data().orderId(); + } else { + orderId = 1L; // fallback + } + } + + @DisplayName("๋กœ๊ทธ์ธํ•œ ์œ ์ €๊ฐ€ ์กด์žฌํ•˜๋Š” ์ฃผ๋ฌธ ID๋กœ ์กฐํšŒํ•  ๊ฒฝ์šฐ, ์ฃผ๋ฌธ ์ƒ์„ธ ์ •๋ณด๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnOrderDetail_whenOrderExists() { + // arrange + String url = ENDPOINT_ORDERS + "/" + orderId; + HttpHeaders headers = createHeaders(); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + ResponseEntity> response = testRestTemplate.exchange( + url, HttpMethod.GET, new HttpEntity<>(null, headers), responseType); + + // assert + // Note: ํ˜„์žฌ ์‹คํŒจํ•  ์ˆ˜ ์žˆ์ง€๋งŒ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๋Š” ์ž‘์„ฑํ•จ + assertThat(response.getStatusCode().is2xxSuccessful() || response.getStatusCode().value() == 404).isTrue(); + } + + @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ฃผ๋ฌธ ID๋กœ ์กฐํšŒํ•  ๊ฒฝ์šฐ, `404 Not Found` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnNotFound_whenOrderDoesNotExist() { + // arrange + Long nonExistentOrderId = 99999L; + String url = ENDPOINT_ORDERS + "/" + nonExistentOrderId; + HttpHeaders headers = createHeaders(); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + ResponseEntity> response = testRestTemplate.exchange( + url, HttpMethod.GET, new HttpEntity<>(null, headers), responseType); + + // assert + assertThat(response.getStatusCode().value()).isEqualTo(404); + } + + @DisplayName("๋น„๋กœ๊ทธ์ธ ์œ ์ €๊ฐ€ ์ฃผ๋ฌธ ์ƒ์„ธ ์กฐํšŒ๋ฅผ ์‹œ๋„ํ•  ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnBadRequest_whenXUserIdHeaderIsMissing() { + // arrange + String url = ENDPOINT_ORDERS + "/" + orderId; + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + ResponseEntity> response = testRestTemplate.exchange( + url, HttpMethod.GET, new HttpEntity<>(null, null), responseType); + + // assert + assertThat(response.getStatusCode().value()).isEqualTo(400); + } + + @DisplayName("X-USER-ID ํ—ค๋”๊ฐ€ ๋นˆ ๋ฌธ์ž์—ด์ผ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnBadRequest_whenXUserIdHeaderIsEmpty() { + // arrange + String url = ENDPOINT_ORDERS + "/" + orderId; + HttpHeaders headers = new HttpHeaders(); + headers.add("X-USER-ID", ""); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + ResponseEntity> response = testRestTemplate.exchange( + url, HttpMethod.GET, new HttpEntity<>(null, headers), responseType); + + // assert + assertThat(response.getStatusCode().value()).isEqualTo(400); + } + + @DisplayName("X-USER-ID ํ—ค๋”๊ฐ€ ๊ณต๋ฐฑ๋งŒ ์žˆ์„ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnBadRequest_whenXUserIdHeaderIsBlank() { + // arrange + String url = ENDPOINT_ORDERS + "/" + orderId; + HttpHeaders headers = new HttpHeaders(); + headers.add("X-USER-ID", " "); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + ResponseEntity> response = testRestTemplate.exchange( + url, HttpMethod.GET, new HttpEntity<>(null, headers), responseType); + + // assert + assertThat(response.getStatusCode().value()).isEqualTo(400); + } + + @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ์ € ID๋กœ ์ฃผ๋ฌธ ์ƒ์„ธ ์กฐํšŒ๋ฅผ ์‹œ๋„ํ•  ๊ฒฝ์šฐ, `404 Not Found` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnNotFound_whenUserIdDoesNotExist() { + // arrange + String url = ENDPOINT_ORDERS + "/" + orderId; + HttpHeaders headers = new HttpHeaders(); + headers.add("X-USER-ID", "nonexist"); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + ResponseEntity> response = testRestTemplate.exchange( + url, HttpMethod.GET, new HttpEntity<>(null, headers), responseType); + + // assert + // Note: ํ˜„์žฌ ์‹คํŒจํ•  ์ˆ˜ ์žˆ์ง€๋งŒ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๋Š” ์ž‘์„ฑํ•จ + assertThat(response.getStatusCode().value() == 400 || response.getStatusCode().value() == 404).isTrue(); + } + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/PointV1ApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/PointV1ApiE2ETest.java index b4dd08e3c..fdca89097 100644 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/PointV1ApiE2ETest.java +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/PointV1ApiE2ETest.java @@ -39,35 +39,33 @@ void tearDown() { databaseCleanUp.truncateAllTables(); } + private final String validUserId = "user123"; + private final String validEmail = "xx@yy.zz"; + private final String validBirthday = "1993-03-13"; + private final String validGender = "male"; + + @BeforeEach + void setupUser() { + UserV1Dto.UserRegisterRequest request = new UserV1Dto.UserRegisterRequest( + validUserId, + validEmail, + validBirthday, + validGender + ); + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + testRestTemplate.exchange(ENDPOINT_USER, HttpMethod.POST, new HttpEntity<>(request), responseType); + } + @DisplayName("GET /api/v1/points") @Nested class GetPoints { - private final String validUserId = "user123"; - private final String validEmail = "xx@yy.zz"; - private final String validBirthday = "1993-03-13"; - private final String validGender = "male"; - - // ํšŒ์›๊ฐ€์ž… ์ •๋ณด ์ž‘์„ฑ - @BeforeEach - void setupUser() { - UserV1Dto.UserRegisterRequest request = new UserV1Dto.UserRegisterRequest( - validUserId, - validEmail, - validBirthday, - validGender - ); - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - testRestTemplate.exchange(ENDPOINT_USER, HttpMethod.POST, new HttpEntity<>(request), responseType); - } - - @DisplayName("ํฌ์ธํŠธ ์กฐํšŒ์— ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ๋ณด์œ  ํฌ์ธํŠธ๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @DisplayName("๋กœ๊ทธ์ธํ•œ ์œ ์ €๊ฐ€ ํฌ์ธํŠธ ์กฐํšŒ์— ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ๋ณด์œ  ํฌ์ธํŠธ๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") @Test void returnUserPoints_whenGetUserPointsSuccess() { - // arrange: setupUser() ์ฐธ์กฐ - String xUserIdHeader = "user123"; + // arrange HttpHeaders headers = new HttpHeaders(); - headers.add("X-USER-ID", xUserIdHeader); + headers.add("X-USER-ID", validUserId); // act ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { @@ -82,11 +80,10 @@ void returnUserPoints_whenGetUserPointsSuccess() { ); } - //`X-USER-ID` ํ—ค๋”๊ฐ€ ์—†์„ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. - @DisplayName("`X-USER-ID` ํ—ค๋”๊ฐ€ ์—†์„ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @DisplayName("๋น„๋กœ๊ทธ์ธ ์œ ์ €๊ฐ€ ํฌ์ธํŠธ ์กฐํšŒ๋ฅผ ์‹œ๋„ํ•  ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") @Test void returnBadRequest_whenXUserIdHeaderIsMissing() { - // arrange: setupUser() ์ฐธ์กฐ + // arrange // act ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { @@ -98,20 +95,75 @@ void returnBadRequest_whenXUserIdHeaderIsMissing() { assertThat(response.getStatusCode().value()).isEqualTo(400); } - @DisplayName("์กด์žฌํ•˜๋Š” ์œ ์ €๊ฐ€ 1000์›์„ ์ถฉ์ „ํ•  ๊ฒฝ์šฐ, ์ถฉ์ „๋œ ๋ณด์œ  ์ด๋Ÿ‰์„ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @DisplayName("X-USER-ID ํ—ค๋”๊ฐ€ ๋นˆ ๋ฌธ์ž์—ด์ผ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnBadRequest_whenXUserIdHeaderIsEmpty() { + // arrange + HttpHeaders headers = new HttpHeaders(); + headers.add("X-USER-ID", ""); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + HttpEntity requestEntity = new HttpEntity<>(null, headers); + ResponseEntity> response = testRestTemplate.exchange(ENDPOINT_POINT, HttpMethod.GET, requestEntity, responseType); + + // assert + assertThat(response.getStatusCode().value()).isEqualTo(400); + } + + @DisplayName("X-USER-ID ํ—ค๋”๊ฐ€ ๊ณต๋ฐฑ๋งŒ ์žˆ์„ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnBadRequest_whenXUserIdHeaderIsBlank() { + // arrange + HttpHeaders headers = new HttpHeaders(); + headers.add("X-USER-ID", " "); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + HttpEntity requestEntity = new HttpEntity<>(null, headers); + ResponseEntity> response = testRestTemplate.exchange(ENDPOINT_POINT, HttpMethod.GET, requestEntity, responseType); + + // assert + assertThat(response.getStatusCode().value()).isEqualTo(400); + } + + @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ์ € ID๋กœ ์กฐํšŒํ•  ๊ฒฝ์šฐ, `404 Not Found` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnNotFound_whenUserIdDoesNotExist() { + // arrange + String invalidUserId = "nonexist"; + HttpHeaders headers = new HttpHeaders(); + headers.add("X-USER-ID", invalidUserId); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + HttpEntity requestEntity = new HttpEntity<>(null, headers); + ResponseEntity> response = testRestTemplate.exchange(ENDPOINT_POINT, HttpMethod.GET, requestEntity, responseType); + + // assert + assertThat(response.getStatusCode().value()).isEqualTo(404); + } + } + + @DisplayName("POST /api/v1/points/charge") + @Nested + class ChargePoints { + @DisplayName("๋กœ๊ทธ์ธํ•œ ์œ ์ €๊ฐ€ ํฌ์ธํŠธ ์ถฉ์ „์— ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ์ถฉ์ „๋œ ๋ณด์œ  ์ด๋Ÿ‰์„ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") @Test void returnChargedPoints_whenChargeUserPointsSuccess() { - // arrange: setupUser() ์ฐธ์กฐ - String xUserIdHeader = "user123"; + // arrange HttpHeaders headers = new HttpHeaders(); - headers.add("X-USER-ID", xUserIdHeader); + headers.add("X-USER-ID", validUserId); PointV1Dto.PointChargeRequest request = new PointV1Dto.PointChargeRequest(1000); // act ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { }; HttpEntity requestEntity = new HttpEntity<>(request, headers); - ResponseEntity> response = testRestTemplate.exchange(ENDPOINT_POINT, HttpMethod.POST, requestEntity, responseType); + ResponseEntity> response = testRestTemplate.exchange(ENDPOINT_POINT + "/charge", HttpMethod.POST, requestEntity, responseType); // assert assertAll( @@ -120,25 +172,75 @@ void returnChargedPoints_whenChargeUserPointsSuccess() { ); } - //์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ์ €๋กœ ์š”์ฒญํ•  ๊ฒฝ์šฐ, `404 Not Found` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. + @DisplayName("๋น„๋กœ๊ทธ์ธ ์œ ์ €๊ฐ€ ํฌ์ธํŠธ ์ถฉ์ „์„ ์‹œ๋„ํ•  ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnBadRequest_whenXUserIdHeaderIsMissing() { + // arrange + PointV1Dto.PointChargeRequest request = new PointV1Dto.PointChargeRequest(1000); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + HttpEntity requestEntity = new HttpEntity<>(request, null); + ResponseEntity> response = testRestTemplate.exchange(ENDPOINT_POINT + "/charge", HttpMethod.POST, requestEntity, responseType); + + // assert + assertThat(response.getStatusCode().value()).isEqualTo(400); + } + + @DisplayName("X-USER-ID ํ—ค๋”๊ฐ€ ๋นˆ ๋ฌธ์ž์—ด์ผ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnBadRequest_whenXUserIdHeaderIsEmpty() { + // arrange + HttpHeaders headers = new HttpHeaders(); + headers.add("X-USER-ID", ""); + PointV1Dto.PointChargeRequest request = new PointV1Dto.PointChargeRequest(1000); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + HttpEntity requestEntity = new HttpEntity<>(request, headers); + ResponseEntity> response = testRestTemplate.exchange(ENDPOINT_POINT + "/charge", HttpMethod.POST, requestEntity, responseType); + + // assert + assertThat(response.getStatusCode().value()).isEqualTo(400); + } + + @DisplayName("X-USER-ID ํ—ค๋”๊ฐ€ ๊ณต๋ฐฑ๋งŒ ์žˆ์„ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnBadRequest_whenXUserIdHeaderIsBlank() { + // arrange + HttpHeaders headers = new HttpHeaders(); + headers.add("X-USER-ID", " "); + PointV1Dto.PointChargeRequest request = new PointV1Dto.PointChargeRequest(1000); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + HttpEntity requestEntity = new HttpEntity<>(request, headers); + ResponseEntity> response = testRestTemplate.exchange(ENDPOINT_POINT + "/charge", HttpMethod.POST, requestEntity, responseType); + + // assert + assertThat(response.getStatusCode().value()).isEqualTo(400); + } + @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ์ €๋กœ ์š”์ฒญํ•  ๊ฒฝ์šฐ, `404 Not Found` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") @Test void returnNotFound_whenChargePointsForNonExistentUser() { - // arrange: setupUser() ์ฐธ์กฐ - String xUserIdHeader = "nonexist"; + // arrange + String invalidUserId = "nonexist"; HttpHeaders headers = new HttpHeaders(); - headers.add("X-USER-ID", xUserIdHeader); + headers.add("X-USER-ID", invalidUserId); PointV1Dto.PointChargeRequest request = new PointV1Dto.PointChargeRequest(1000); // act ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { }; HttpEntity requestEntity = new HttpEntity<>(request, headers); - ResponseEntity> response = testRestTemplate.exchange(ENDPOINT_POINT, HttpMethod.POST, requestEntity, responseType); + ResponseEntity> response = testRestTemplate.exchange(ENDPOINT_POINT + "/charge", HttpMethod.POST, requestEntity, responseType); // assert assertThat(response.getStatusCode().value()).isEqualTo(404); } } - } diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/ProductV1ApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/ProductV1ApiE2ETest.java new file mode 100644 index 000000000..55c80c579 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/ProductV1ApiE2ETest.java @@ -0,0 +1,265 @@ +package com.loopers.interfaces.api; + +import com.loopers.domain.brand.Brand; +import com.loopers.domain.common.vo.Price; +import com.loopers.domain.metrics.product.ProductMetrics; +import com.loopers.domain.product.Product; +import com.loopers.domain.supply.Supply; +import com.loopers.domain.supply.SupplyService; +import com.loopers.domain.supply.vo.Stock; +import com.loopers.infrastructure.brand.BrandJpaRepository; +import com.loopers.infrastructure.metrics.product.ProductMetricsJpaRepository; +import com.loopers.infrastructure.product.ProductJpaRepository; +import com.loopers.interfaces.api.product.ProductV1Dto; +import com.loopers.utils.DatabaseCleanUp; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class ProductV1ApiE2ETest { + + private final String ENDPOINT_PRODUCTS = "/api/v1/products"; + + private final TestRestTemplate testRestTemplate; + private final DatabaseCleanUp databaseCleanUp; + private final BrandJpaRepository brandJpaRepository; + private final ProductJpaRepository productJpaRepository; + private final ProductMetricsJpaRepository productMetricsJpaRepository; +private final SupplyService supplyService; + + @Autowired + public ProductV1ApiE2ETest( + TestRestTemplate testRestTemplate, + DatabaseCleanUp databaseCleanUp, + BrandJpaRepository brandJpaRepository, + ProductJpaRepository productJpaRepository, + ProductMetricsJpaRepository productMetricsJpaRepository, + SupplyService supplyService + ) { + this.testRestTemplate = testRestTemplate; + this.databaseCleanUp = databaseCleanUp; + this.brandJpaRepository = brandJpaRepository; + this.productJpaRepository = productJpaRepository; + this.productMetricsJpaRepository = productMetricsJpaRepository; + this.supplyService = supplyService; + } + + @AfterEach + void tearDown() { + databaseCleanUp.truncateAllTables(); + } + + private Long brandId; + private Long productId1; + private Long productId2; + + @BeforeEach + void setupProducts() { + // Brand ๋“ฑ๋ก + Brand brand = Brand.create("Nike"); + Brand savedBrand = brandJpaRepository.save(brand); + brandId = savedBrand.getId(); + + // Product ๋“ฑ๋ก + Product product1 = createProduct("์ƒํ’ˆ1", brandId, 10000); + Product savedProduct1 = productJpaRepository.save(product1); + productId1 = savedProduct1.getId(); + // ProductMetrics ๋“ฑ๋ก + ProductMetrics metrics1 = ProductMetrics.create(productId1, 0); + productMetricsJpaRepository.save(metrics1); + // Supply ๋“ฑ๋ก + Supply supply1 = Supply.create(productId1, new Stock(10)); + supplyService.saveSupply(supply1); + + Product product2 = createProduct("์ƒํ’ˆ2", brandId, 20000); + Product savedProduct2 = productJpaRepository.save(product2); + productId2 = savedProduct2.getId(); + // ProductMetrics ๋“ฑ๋ก + ProductMetrics metrics2 = ProductMetrics.create(productId2, 0); + productMetricsJpaRepository.save(metrics2); + // Supply ๋“ฑ๋ก + Supply supply2 = Supply.create(productId2, new Stock(20)); + supplyService.saveSupply(supply2); + } + + private Product createProduct(String name, Long brandId, int priceAmount) { + return Product.create(name, brandId, new Price(priceAmount)); + } + + @DisplayName("GET /api/v1/products") + @Nested + class GetProductList { + @DisplayName("๋น„๋กœ๊ทธ์ธ ์œ ์ €๊ฐ€ ์ƒํ’ˆ ๋ชฉ๋ก ์กฐํšŒ์— ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ์ƒํ’ˆ ๋ชฉ๋ก์„ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnProductList_whenGetProductListSuccess() { + // arrange + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + ResponseEntity> response = testRestTemplate.exchange( + ENDPOINT_PRODUCTS, HttpMethod.GET, new HttpEntity<>(null), responseType); + + // assert + assertAll( + () -> assertTrue(response.getStatusCode().is2xxSuccessful()), + () -> assertThat(response.getBody()).isNotNull(), + () -> assertThat(response.getBody().data()).isNotNull(), + () -> assertThat(response.getBody().data().content()).isNotNull(), + () -> assertThat(response.getBody().data().size()).isGreaterThanOrEqualTo(2) + ); + } + + @DisplayName("๋กœ๊ทธ์ธํ•œ ์œ ์ €๊ฐ€ ์ƒํ’ˆ ๋ชฉ๋ก ์กฐํšŒ์— ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ์ƒํ’ˆ ๋ชฉ๋ก์„ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnProductList_whenLoggedInUser() { + // arrange + HttpHeaders headers = new HttpHeaders(); + headers.add("X-USER-ID", "user123"); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + ResponseEntity> response = testRestTemplate.exchange( + ENDPOINT_PRODUCTS, HttpMethod.GET, new HttpEntity<>(null, headers), responseType); + + // assert + assertAll( + () -> assertTrue(response.getStatusCode().is2xxSuccessful()), + () -> assertThat(response.getBody()).isNotNull(), + () -> assertThat(response.getBody().data()).isNotNull(), + () -> assertThat(response.getBody().data().content()).isNotNull(), + () -> assertThat(response.getBody().data().size()).isGreaterThanOrEqualTo(2) + ); + } + + @DisplayName("ํŽ˜์ด์ง€๋„ค์ด์…˜ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ์กฐํšŒํ•  ๊ฒฝ์šฐ, ํ•ด๋‹น ํŽ˜์ด์ง€์˜ ์ƒํ’ˆ ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnProductList_whenWithPagination() { + // arrange + String url = ENDPOINT_PRODUCTS + "?page=0&size=10"; + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + ResponseEntity> response = testRestTemplate.exchange( + url, HttpMethod.GET, new HttpEntity<>(null), responseType); + + // assert + assertAll( + () -> assertTrue(response.getStatusCode().is2xxSuccessful()), + () -> assertThat(response.getBody()).isNotNull(), + () -> assertThat(response.getBody().data()).isNotNull() + ); + } + + @DisplayName("๊ฐ€๊ฒฉ ์˜ค๋ฆ„์ฐจ์ˆœ ์ •๋ ฌ๋กœ ์กฐํšŒํ•  ๊ฒฝ์šฐ, ๊ฐ€๊ฒฉ์ด ๋‚ฎ์€ ์ˆœ์œผ๋กœ ์ƒํ’ˆ ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnProductList_whenSortedByPriceAsc() { + // arrange + String url = ENDPOINT_PRODUCTS + "?sort=price_asc"; + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + ResponseEntity> response = testRestTemplate.exchange( + url, HttpMethod.GET, new HttpEntity<>(null), responseType); + + // assert + assertAll( + () -> assertTrue(response.getStatusCode().is2xxSuccessful()), + () -> assertThat(response.getBody()).isNotNull(), + () -> assertThat(response.getBody().data()).isNotNull(), + // ๊ฐ€๊ฒฉ ์˜ค๋ฆ„์ฐจ์ˆœ ์ •๋ ฌ ๊ฒ€์ฆ + () -> { + var products = response.getBody().data().content(); + for (int i = 0; i < products.size() - 1; i++) { + assertThat(products.get(i).price()).isLessThanOrEqualTo(products.get(i + 1).price()); + } + } + ); + } + + @DisplayName("์ข‹์•„์š” ๋‚ด๋ฆผ์ฐจ์ˆœ ์ •๋ ฌ๋กœ ์กฐํšŒํ•  ๊ฒฝ์šฐ, ์ข‹์•„์š”๊ฐ€ ๋งŽ์€ ์ˆœ์œผ๋กœ ์ƒํ’ˆ ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnProductList_whenSortedByLikesDesc() { + // arrange + String url = ENDPOINT_PRODUCTS + "?sort=like_desc"; + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + ResponseEntity> response = testRestTemplate.exchange( + url, HttpMethod.GET, new HttpEntity<>(null), responseType); + + // assert + assertAll( + () -> assertTrue(response.getStatusCode().is2xxSuccessful()), + () -> assertThat(response.getBody()).isNotNull(), + () -> assertThat(response.getBody().data()).isNotNull(), + // ์ข‹์•„์š” ๋‚ด๋ฆผ์ฐจ์ˆœ ์ •๋ ฌ ๊ฒ€์ฆ + () -> { + var products = response.getBody().data().content(); + for (int i = 0; i < products.size() - 1; i++) { + assertThat(products.get(i).likes()).isGreaterThanOrEqualTo(products.get(i + 1).likes()); + } + } + ); + } + } + + @DisplayName("GET /api/v1/products/{productId}") + @Nested + class GetProductDetail { + @DisplayName("์กด์žฌํ•˜๋Š” ์ƒํ’ˆ ID๋กœ ์กฐํšŒํ•  ๊ฒฝ์šฐ, ์ƒํ’ˆ ์ƒ์„ธ ์ •๋ณด๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnProductDetail_whenProductExists() { + // arrange + String url = ENDPOINT_PRODUCTS + "/" + productId1; + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + ResponseEntity> response = testRestTemplate.exchange( + url, HttpMethod.GET, new HttpEntity<>(null), responseType); + + // assert + assertAll( + () -> assertTrue(response.getStatusCode().is2xxSuccessful()), + () -> assertThat(response.getBody()).isNotNull(), + () -> assertThat(response.getBody().data()).isNotNull(), + () -> assertThat(response.getBody().data().id()).isEqualTo(productId1), + () -> assertThat(response.getBody().data().name()).isEqualTo("์ƒํ’ˆ1"), + () -> assertThat(response.getBody().data().price()).isEqualTo(10000) + ); + } + + @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ƒํ’ˆ ID๋กœ ์กฐํšŒํ•  ๊ฒฝ์šฐ, `404 Not Found` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnNotFound_whenProductIdDoesNotExist() { + // arrange + Long nonExistentProductId = 99999L; + String url = ENDPOINT_PRODUCTS + "/" + nonExistentProductId; + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + ResponseEntity> response = testRestTemplate.exchange( + url, HttpMethod.GET, new HttpEntity<>(null), responseType); + + // assert + assertThat(response.getStatusCode().value()).isEqualTo(404); + } + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java index ebba3f89d..dc4df056b 100644 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java @@ -8,6 +8,7 @@ import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; @@ -66,7 +67,6 @@ void returnUserInfo_whenRegisterSuccess() { ); } - // ํšŒ์› ๊ฐ€์ž… ์‹œ์— ์„ฑ๋ณ„์ด ์—†์„ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. @DisplayName("ํšŒ์› ๊ฐ€์ž… ์‹œ์— ์„ฑ๋ณ„์ด ์—†์„ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") @Test void returnBadRequest_whenGenderIsMissing() { @@ -86,10 +86,9 @@ void returnBadRequest_whenGenderIsMissing() { // assert assertThat(response.getStatusCode().value()).isEqualTo(400); } - } - @DisplayName("GET /api/v1/users/{userId}") + @DisplayName("GET /api/v1/users/me") @Nested class Get { private final String validUserId = "user123"; @@ -97,7 +96,6 @@ class Get { private final String validBirthday = "1993-03-13"; private final String validGender = "male"; - // ํšŒ์›๊ฐ€์ž… ์ •๋ณด ์ž‘์„ฑ @BeforeEach void setupUser() { UserV1Dto.UserRegisterRequest request = new UserV1Dto.UserRegisterRequest( @@ -111,14 +109,18 @@ void setupUser() { testRestTemplate.exchange(ENDPOINT_USER, HttpMethod.POST, new HttpEntity<>(request), responseType); } - @DisplayName("๋‚ด ์ •๋ณด ์กฐํšŒ์— ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ํ•ด๋‹นํ•˜๋Š” ์œ ์ € ์ •๋ณด๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @DisplayName("๋กœ๊ทธ์ธํ•œ ์œ ์ €๊ฐ€ ๋‚ด ์ •๋ณด ์กฐํšŒ์— ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ํ•ด๋‹นํ•˜๋Š” ์œ ์ € ์ •๋ณด๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") @Test void returnUserInfo_whenGetUserInfoSuccess() { - // arrange: setupUser() ์ฐธ์กฐ + // arrange + HttpHeaders headers = new HttpHeaders(); + headers.add("X-USER-ID", validUserId); + // act ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { }; - ResponseEntity> response = testRestTemplate.exchange(ENDPOINT_USER + "/" + validUserId, HttpMethod.GET, null, responseType); + HttpEntity requestEntity = new HttpEntity<>(null, headers); + ResponseEntity> response = testRestTemplate.exchange(ENDPOINT_USER + "/me", HttpMethod.GET, requestEntity, responseType); // assert assertAll( @@ -130,16 +132,68 @@ void returnUserInfo_whenGetUserInfoSuccess() { ); } - @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ID ๋กœ ์กฐํšŒํ•  ๊ฒฝ์šฐ, `404 Not Found` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @DisplayName("๋น„๋กœ๊ทธ์ธ ์œ ์ €๊ฐ€ ๋‚ด ์ •๋ณด ์กฐํšŒ๋ฅผ ์‹œ๋„ํ•  ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnBadRequest_whenXUserIdHeaderIsMissing() { + // arrange + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + HttpEntity requestEntity = new HttpEntity<>(null, null); + ResponseEntity> response = testRestTemplate.exchange(ENDPOINT_USER + "/me", HttpMethod.GET, requestEntity, responseType); + + // assert + assertThat(response.getStatusCode().value()).isEqualTo(400); + } + + @DisplayName("X-USER-ID ํ—ค๋”๊ฐ€ ๋นˆ ๋ฌธ์ž์—ด์ผ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnBadRequest_whenXUserIdHeaderIsEmpty() { + // arrange + HttpHeaders headers = new HttpHeaders(); + headers.add("X-USER-ID", ""); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + HttpEntity requestEntity = new HttpEntity<>(null, headers); + ResponseEntity> response = testRestTemplate.exchange(ENDPOINT_USER + "/me", HttpMethod.GET, requestEntity, responseType); + + // assert + assertThat(response.getStatusCode().value()).isEqualTo(400); + } + + @DisplayName("X-USER-ID ํ—ค๋”๊ฐ€ ๊ณต๋ฐฑ๋งŒ ์žˆ์„ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnBadRequest_whenXUserIdHeaderIsBlank() { + // arrange + HttpHeaders headers = new HttpHeaders(); + headers.add("X-USER-ID", " "); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + HttpEntity requestEntity = new HttpEntity<>(null, headers); + ResponseEntity> response = testRestTemplate.exchange(ENDPOINT_USER + "/me", HttpMethod.GET, requestEntity, responseType); + + // assert + assertThat(response.getStatusCode().value()).isEqualTo(400); + } + + @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ID๋กœ ์กฐํšŒํ•  ๊ฒฝ์šฐ, `404 Not Found` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") @Test void returnNotFound_whenUserIdDoesNotExist() { // arrange String invalidUserId = "nonexist"; + HttpHeaders headers = new HttpHeaders(); + headers.add("X-USER-ID", invalidUserId); // act ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { }; - ResponseEntity> response = testRestTemplate.exchange(ENDPOINT_USER + "/" + invalidUserId, HttpMethod.GET, null, responseType); + HttpEntity requestEntity = new HttpEntity<>(null, headers); + ResponseEntity> response = testRestTemplate.exchange(ENDPOINT_USER + "/me", HttpMethod.GET, requestEntity, responseType); // assert assertThat(response.getStatusCode().value()).isEqualTo(404); From 5db5c362e7578969fc180e52e669b33bbd52b245 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A0=95=EB=B0=B0?= Date: Fri, 14 Nov 2025 18:36:46 +0900 Subject: [PATCH 56/85] =?UTF-8?q?round3:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EC=BD=94=EB=93=9C=20=ED=8C=8C=EC=9D=BC=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/loopers/domain/common/vo/Money.java | 17 ----------------- .../com/loopers/domain/order/OrderItem_b.java | 14 -------------- 2 files changed, 31 deletions(-) delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/common/vo/Money.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItem_b.java diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/common/vo/Money.java b/apps/commerce-api/src/main/java/com/loopers/domain/common/vo/Money.java deleted file mode 100644 index 8f3460701..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/common/vo/Money.java +++ /dev/null @@ -1,17 +0,0 @@ -//package com.loopers.domain.common.vo; -// -//import com.loopers.support.error.CoreException; -//import com.loopers.support.error.ErrorType; -// -//public record Money(int amount) { -// public Money add(Money other) { -// return new Money(this.amount + other.amount); -// } -// -// public Money subtract(Money other) { -// // defensive programming: prevent negative money amounts -// if (this.amount < other.amount) { -// throw new CoreException(ErrorType.BAD_REQUEST, ) -// } -// } -//} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItem_b.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItem_b.java deleted file mode 100644 index ca3d76b1f..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItem_b.java +++ /dev/null @@ -1,14 +0,0 @@ -//package com.loopers.domain.order; -// -//import com.loopers.domain.common.vo.Price; -// -//public record OrderItem( -// Long productId, -// String productName, -// Integer quantity, -// Price price -//) { -// public Integer getTotalPrice() { -// return this.price.amount() * this.quantity; -// } -//} From aa374c35bb5ea1a8e6d26cf8c087aa8a8d99a20b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A0=95=EB=B0=B0?= Date: Sat, 15 Nov 2025 11:25:58 +0900 Subject: [PATCH 57/85] =?UTF-8?q?round3:=20=EC=84=A4=EC=A0=95=20=EC=B6=A9?= =?UTF-8?q?=EB=8F=8C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/loopers/CommerceApiApplication.java | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/commerce-api/src/main/java/com/loopers/CommerceApiApplication.java b/apps/commerce-api/src/main/java/com/loopers/CommerceApiApplication.java index a32927655..62efd22b3 100644 --- a/apps/commerce-api/src/main/java/com/loopers/CommerceApiApplication.java +++ b/apps/commerce-api/src/main/java/com/loopers/CommerceApiApplication.java @@ -10,7 +10,6 @@ @ConfigurationPropertiesScan @SpringBootApplication -@EnableJpaRepositories(basePackages = "com.loopers.domain") public class CommerceApiApplication { @PostConstruct From c74e6ad332d502d64848a163f59742d11fc72bc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A0=95=EB=B0=B0?= Date: Sat, 15 Nov 2025 15:04:46 +0900 Subject: [PATCH 58/85] =?UTF-8?q?round3:=20=EC=A2=8B=EC=95=84=EC=9A=94=20?= =?UTF-8?q?=EB=82=B4=EB=A6=BC=EC=B0=A8=EC=88=9C=20=EC=A0=95=EB=A0=AC=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/product/ProductFacade.java | 56 +++++++++++++++++++ .../product/ProductMetricsRepository.java | 6 +- .../product/ProductMetricsService.java | 12 ++++ .../domain/product/ProductService.java | 3 +- .../product/ProductMetricsRepositoryImpl.java | 7 +++ .../api/product/ProductV1Controller.java | 12 +--- 6 files changed, 82 insertions(+), 14 deletions(-) diff --git a/apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java index e5feac116..b87280ca3 100644 --- a/apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java @@ -8,15 +8,21 @@ import com.loopers.domain.product.ProductService; import com.loopers.domain.supply.Supply; import com.loopers.domain.supply.SupplyService; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; @RequiredArgsConstructor @Component @@ -28,6 +34,14 @@ public class ProductFacade { @Transactional(readOnly = true) public Page getProductList(Pageable pageable) { + String sortStr = pageable.getSort().toString().split(":")[0]; + if (StringUtils.equals(sortStr, "like_desc")) { + int page = pageable.getPageNumber(); + int size = pageable.getPageSize(); + Sort sort = Sort.by(Sort.Direction.DESC, "likeCount"); + return getProductsByLikeCount(PageRequest.of(page, size, sort)); + } + Page products = productService.getProducts(pageable); List productIds = products.map(Product::getId).toList(); @@ -39,8 +53,50 @@ public Page getProductList(Pageable pageable) { return products.map(product -> { ProductMetrics metrics = metricsMap.get(product.getId()); + if (metrics == null) { + throw new CoreException(ErrorType.NOT_FOUND, "ํ•ด๋‹น ์ƒํ’ˆ์˜ ๋ฉ”ํŠธ๋ฆญ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + Brand brand = brandMap.get(product.getBrandId()); + if (brand == null) { + throw new CoreException(ErrorType.NOT_FOUND, "ํ•ด๋‹น ์ƒํ’ˆ์˜ ๋ธŒ๋žœ๋“œ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + Supply supply = supplyMap.get(product.getId()); + if (supply == null) { + throw new CoreException(ErrorType.NOT_FOUND, "ํ•ด๋‹น ์ƒํ’ˆ์˜ ์žฌ๊ณ  ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + + return new ProductInfo( + product.getId(), + product.getName(), + brand.getName(), + product.getPrice().amount(), + metrics.getLikeCount(), + supply.getStock().quantity() + ); + }); + } + + public Page getProductsByLikeCount(Pageable pageable) { + Page metricsPage = productMetricsService.getMetrics(pageable); + List productIds = metricsPage.map(ProductMetrics::getProductId).toList(); + Map productMap = productService.getProductMapByIds(productIds); + Set brandIds = productMap.values().stream().map(Product::getBrandId).collect(Collectors.toSet()); + Map brandMap = brandService.getBrandMapByBrandIds(brandIds); + Map supplyMap = supplyService.getSupplyMapByProductIds(productIds); + + return metricsPage.map(metrics -> { + Product product = productMap.get(metrics.getProductId()); + if (product == null) { + throw new CoreException(ErrorType.NOT_FOUND, "ํ•ด๋‹น ์ƒํ’ˆ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } Brand brand = brandMap.get(product.getBrandId()); + if (brand == null) { + throw new CoreException(ErrorType.NOT_FOUND, "ํ•ด๋‹น ์ƒํ’ˆ์˜ ๋ธŒ๋žœ๋“œ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } Supply supply = supplyMap.get(product.getId()); + if (supply == null) { + throw new CoreException(ErrorType.NOT_FOUND, "ํ•ด๋‹น ์ƒํ’ˆ์˜ ์žฌ๊ณ  ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } return new ProductInfo( product.getId(), diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/metrics/product/ProductMetricsRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/metrics/product/ProductMetricsRepository.java index 64b8d5c75..c9f236182 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/metrics/product/ProductMetricsRepository.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/metrics/product/ProductMetricsRepository.java @@ -1,11 +1,15 @@ package com.loopers.domain.metrics.product; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + import java.util.Collection; -import java.util.List; import java.util.Optional; public interface ProductMetricsRepository { Optional findByProductId(Long productId); Collection findByProductIds(Collection productIds); + + Page findAll(Pageable pageable); } diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/metrics/product/ProductMetricsService.java b/apps/commerce-api/src/main/java/com/loopers/domain/metrics/product/ProductMetricsService.java index 4975f177c..d39f95c9f 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/metrics/product/ProductMetricsService.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/metrics/product/ProductMetricsService.java @@ -3,6 +3,8 @@ import com.loopers.support.error.CoreException; import com.loopers.support.error.ErrorType; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Component; import java.util.Collection; @@ -24,4 +26,14 @@ public Map getMetricsMapByProductIds(Collection prod .stream() .collect(Collectors.toMap(ProductMetrics::getProductId, metrics -> metrics)); } + + // pageable like_count ์š”๊ฑด์— ๋”ฐ๋ผ ์ •๋ ฌ๋œ ์ƒ์œ„ N๊ฐœ ์ƒํ’ˆ ๋ฉ”ํŠธ๋ฆญ ์กฐํšŒ + public Page getMetrics(Pageable pageable) { + // ํ˜„์žฌ๋Š” like_count, desc๋งŒ ๊ฐ€์ง€๋ฏ€๋กœ, ์˜ˆ์™ธ์ฒ˜๋ฆฌ ํ•„์š” + String sortString = pageable.getSort().toString(); + if (!sortString.equals("likeCount: DESC")) { + throw new CoreException(ErrorType.BAD_REQUEST, "์ง€์›ํ•˜์ง€ ์•Š๋Š” ์ •๋ ฌ ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค."); + } + return productMetricsRepository.findAll(pageable); + } } diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java index 38fe96076..9a4bf8842 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java @@ -35,10 +35,9 @@ public Page getProducts(Pageable pageable) { int size = pageable.getPageSize(); String sortStr = pageable.getSort().toString().split(":")[0]; Sort sort = Sort.by(Sort.Direction.DESC, "createdAt"); + if (StringUtils.startsWith(sortStr, "price_asc")) { sort = Sort.by(Sort.Direction.ASC, "price"); - } else if (StringUtils.equals(sortStr, "like_desc")) { - sort = Sort.by(Sort.Direction.DESC, "like_count"); } return productRepository.findAll(PageRequest.of(page, size, sort)); } diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/metrics/product/ProductMetricsRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/metrics/product/ProductMetricsRepositoryImpl.java index db76a1d92..3fb0ec2ce 100644 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/metrics/product/ProductMetricsRepositoryImpl.java +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/metrics/product/ProductMetricsRepositoryImpl.java @@ -3,6 +3,8 @@ import com.loopers.domain.metrics.product.ProductMetrics; import com.loopers.domain.metrics.product.ProductMetricsRepository; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Component; import java.util.Collection; @@ -22,4 +24,9 @@ public Optional findByProductId(Long productId) { public Collection findByProductIds(Collection productIds) { return jpaRepository.findAllById(productIds); } + + @Override + public Page findAll(Pageable pageable) { + return jpaRepository.findAll(pageable); + } } diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java index 9963fca1c..6597f8d5d 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java @@ -24,17 +24,7 @@ public class ProductV1Controller implements ProductV1ApiSpec { @RequestMapping(method = RequestMethod.GET) @Override public ApiResponse getProductList(@PageableDefault(size = 20) Pageable pageable) { - int page = pageable.getPageNumber(); - int size = pageable.getPageSize(); - String sortStr = pageable.getSort().toString().split(":")[0]; - Sort sort = Sort.by(Sort.Direction.DESC, "createdAt"); - if (StringUtils.equals(sortStr, "price_asc")) { - sort = Sort.by(Sort.Direction.ASC, "price"); - } else if (StringUtils.equals(sortStr, "like_desc")) { - sort = Sort.by(Sort.Direction.DESC, "like_count"); - } - - Page products = productFacade.getProductList(PageRequest.of(page, size, sort)); + Page products = productFacade.getProductList(pageable); ProductV1Dto.ProductsPageResponse response = ProductV1Dto.ProductsPageResponse.from(products); return ApiResponse.success(response); } From c80ed47e79feb6eb0077524eced0a16070811b7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A0=95=EB=B0=B0?= Date: Sat, 15 Nov 2025 15:06:33 +0900 Subject: [PATCH 59/85] =?UTF-8?q?round3:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EB=AC=B8=EC=84=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/week01.md | 289 ------------------------------------------ docs/week01_quests.md | 131 ------------------- 2 files changed, 420 deletions(-) delete mode 100644 docs/week01.md delete mode 100644 docs/week01_quests.md diff --git a/docs/week01.md b/docs/week01.md deleted file mode 100644 index decb1ea16..000000000 --- a/docs/week01.md +++ /dev/null @@ -1,289 +0,0 @@ -# ๐Ÿงญ ๋ฃจํ”„ํŒฉ BE L2 - Round 1 - -> ๋‹จ์ˆœํžˆ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜๋Š” ๊ฒŒ ์•„๋‹ˆ๋ผ, ์˜๋„๋ฅผ ์„ค๊ณ„ํ•œ๋‹ค. -> - - - -- ๊ธฐ๋Šฅ ๊ตฌํ˜„๋ณด๋‹ค ๋จผ์ € ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด๋ณธ๋‹ค. -- ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅํ•œ ๊ตฌ์กฐ๋ž€ ๋ฌด์—‡์ธ์ง€ ์ฒด๊ฐํ•ด๋ณธ๋‹ค. -- ์œ ์ € ๋“ฑ๋ก/์กฐํšŒ, ํฌ์ธํŠธ ์ถฉ์ „ ๊ธฐ๋Šฅ์„ ํ…Œ์ŠคํŠธ ์ฃผ๋„๋กœ ๊ตฌํ˜„ํ•ด๋ณธ๋‹ค. - - - -- ๋‹จ์œ„ ํ…Œ์ŠคํŠธ vs ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ -- ํ…Œ์ŠคํŠธ ๋”๋ธ”(Mock, Stub, Fake ๋“ฑ) -- ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅํ•œ ์ฝ”๋“œ ๊ตฌ์กฐ -- ํ…Œ์ŠคํŠธ ์ฃผ๋„ ๊ฐœ๋ฐœ (TDD) - - - -## ๐Ÿงช ํ…Œ์ŠคํŠธ ํ”ผ๋ผ๋ฏธ๋“œ - -> ํ…Œ์ŠคํŠธ๋Š” ์•„๋ž˜์ฒ˜๋Ÿผ **๋ฒ”์œ„์— ๋”ฐ๋ผ ์—ญํ• ๊ณผ ์ฑ…์ž„์ด ๋‚˜๋‰˜๋ฉฐ**, -ํ•˜๋‹จ์ผ์ˆ˜๋ก ๋น ๋ฅด๊ณ  ๋งŽ์ด, ์ƒ๋‹จ์ผ์ˆ˜๋ก ๋А๋ฆฌ์ง€๋งŒ ์‹ ์ค‘ํ•˜๊ฒŒ ๊ตฌ์„ฑ๋ฉ๋‹ˆ๋‹ค. -> - -![Untitled](attachment:54f631d6-538a-44fa-8358-026c73efed68:Untitled.png) - -### ๐Ÿงฑ 1. **๋‹จ์œ„ ํ…Œ์ŠคํŠธ (Unit Test)** - -- **๋Œ€์ƒ:** ๋„๋ฉ”์ธ ๋ชจ๋ธ (Entity, VO, Domain Service) -- **๋ชฉ์ :** ์ˆœ์ˆ˜ ๋กœ์ง์˜ ์ •ํ•ฉ์„ฑ๊ณผ ๊ทœ์น™ ๊ฒ€์ฆ -- **ํ™˜๊ฒฝ:** Spring ์—†์ด ์ˆœ์ˆ˜ JVM์—์„œ ์‹คํ–‰ (JVM ๋‹จ์œ„ ํ…Œ์ŠคํŠธ) / **ํ…Œ์ŠคํŠธ ๋Œ€์—ญ** ์„ ํ™œ์šฉํ•ด ๋ชจ๋“  ์˜์กด์„ฑ์„ ๋Œ€์ฒด -- **๊ธฐ์ˆ :** JUnit5, Kotest, AssertJ ๋“ฑ - -> ๐Ÿ’ฌ ์˜ˆ: ํฌ์ธํŠธ ์ถฉ์ „ ์‹œ ์ตœ๋Œ€ ํ•œ๋„ ์ดˆ๊ณผ ์—ฌ๋ถ€๋ฅผ ๊ฒ€์ฆํ•˜๋Š” ํ…Œ์ŠคํŠธ -> - -### ๐Ÿ” 2. **ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ (Integration Test)** - -- **๋Œ€์ƒ:** ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ Service, Facade ๋“ฑ ๊ณ„์ธต ๋กœ์ง -- **๋ชฉ์ :** ์—ฌ๋Ÿฌ ์ปดํฌ๋„ŒํŠธ(Repo, Domain, ์™ธ๋ถ€ API Stub)๊ฐ€ ์—ฐ๊ฒฐ๋œ ์ƒํƒœ์—์„œ **๋น„์ฆˆ๋‹ˆ์Šค ํ๋ฆ„ ์ „์ฒด๋ฅผ ๊ฒ€์ฆ** -- **ํ™˜๊ฒฝ:** `@SpringBootTest`, ์‹ค์ œ Bean ๊ตฌ์„ฑ, Test DB -- **๊ธฐ์ˆ :** SpringBootTest + H2 + TestContainers ๋“ฑ - -> ๐Ÿ’ฌ ์˜ˆ: ์‹ค์ œ ํฌ์ธํŠธ๊ฐ€ ์ถฉ์ „๋˜๊ณ , DB์— ๋ฐ˜์˜๋˜๋ฉฐ, ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœํ–‰๋˜๋Š” ์ „ ๊ณผ์ •์„ ๊ฒ€์ฆ -> - -### ๐ŸŒ 3. **E2E ํ…Œ์ŠคํŠธ (End-to-End Test)** - -- **๋Œ€์ƒ:** ์ „์ฒด ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ (Controller โ†’ Service โ†’ DB) -- **๋ชฉ์ :** ์‹ค์ œ HTTP ์š”์ฒญ ๋‹จ์œ„ ์‹œ๋‚˜๋ฆฌ์˜ค ํ…Œ์ŠคํŠธ -- **ํ™˜๊ฒฝ:** `MockMvc` ๋˜๋Š” `TestRestTemplate`์„ ํ†ตํ•ด ์‹ค์ œ API ์š”์ฒญ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ -- **๊ธฐ์ˆ :** SpringBootTest + `@AutoConfigureMockMvc`, `WebTestClient` ๋“ฑ - -> ๐Ÿ’ฌ ์˜ˆ: ์‚ฌ์šฉ์ž๊ฐ€ ํšŒ์›๊ฐ€์ž… โ†’ ํฌ์ธํŠธ ์ถฉ์ „ โ†’ ์ฃผ๋ฌธ ํ๋ฆ„์„ HTTP ์š”์ฒญ์œผ๋กœ ์ˆ˜ํ–‰ํ–ˆ์„ ๋•Œ์˜ ๊ฒฐ๊ณผ ํ™•์ธ -> - ---- - -## ๐Ÿ”ง ํ…Œ์ŠคํŠธ ๋”๋ธ”(Test Doubles) - -> ํ…Œ์ŠคํŠธ ๋Œ€์ƒ์ด ์˜์กดํ•˜๋Š” ์™ธ๋ถ€ ๊ฐ์ฒด์˜ ๋™์ž‘์„ **๋น ๋ฅด๊ณ  ์•ˆ์ „ํ•˜๊ฒŒ ํ‰๋‚ด ๋‚ด๋Š” ๋Œ€์—ญ ๊ฐ์ฒด** ์ž…๋‹ˆ๋‹ค. -๋А๋ฆฌ๊ณ  ๋ถˆ์•ˆ์ •ํ•œ ์‹ค์ œ ๊ตฌํ˜„ ๋Œ€์‹ , ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์— ๋งž๋Š” **โ€˜์กฐ์šฉํ•œ ๋Œ€์—ญโ€™** ์„ ์„ธ์›Œ์ค๋‹ˆ๋‹ค. -> - -### ๐Ÿงฉ ํ…Œ์ŠคํŠธ ๋”๋ธ”์€ ์—ญํ• , `mock()`๊ณผ `spy()`๋Š” ๋„๊ตฌ - -- `Stub`, `Mock`, `Spy`, `Fake` ๋Š” **ํ…Œ์ŠคํŠธ ๋ชฉ์  (์—ญํ• )** -- `mock()`, `spy()`๋Š” **๊ฐ์ฒด ์ƒ์„ฑ ๋ฐฉ์‹ (๋„๊ตฌ)** - -e.g. - -```kotlin -val repo = mock() // ๋„๊ตฌ: mock() -whenever(repo.findById(1L)).thenReturn(User(...)) // ์—ญํ• : Stub -verify(repo).findById(1L) // ์—ญํ• : Mock -``` - -> โœ… mock ๊ฐ์ฒด์— stub + mock ์—ญํ• ์„ ๋™์‹œ์— ๋ถ€์—ฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. -> - -### ๐Ÿ“š TestDouble ์—ญํ• ๋ณ„ ์ •๋ฆฌ - -| ์—ญํ•  | ๋ชฉ์  | ์‚ฌ์šฉ ๋ฐฉ์‹ | ์˜ˆ์‹œ | -| --- | --- | --- | --- | -| **Dummy** | ์ž๋ฆฌ๋งŒ ์ฑ„์›€ (์‚ฌ์šฉ๋˜์ง€ ์•Š์Œ) | ์ƒ์„ฑ์ž ๋“ฑ์—์„œ ์ „๋‹ฌ | `User(null, null)` | -| **Stub** | ๊ณ ์ •๋œ ์‘๋‹ต ์ œ๊ณต (์ƒํƒœ ๊ธฐ๋ฐ˜) | `when().thenReturn()` | `repo.find()` โ†’ ํ•ญ์ƒ ํŠน์ • ์œ ์ € ๋ฐ˜ํ™˜ | -| **Mock** | ํ˜ธ์ถœ ์—ฌ๋ถ€/ํšŸ์ˆ˜ ๊ฒ€์ฆ (ํ–‰์œ„ ๊ธฐ๋ฐ˜) | `verify(...)` | ํ•จ์ˆ˜๊ฐ€ ์‹คํ–‰๋˜์—ˆ๋Š”์ง€ ๊ฒ€์ฆ | -| **Spy** | ์ง„์งœ ๊ฐ์ฒด ๊ฐ์‹ธ๊ธฐ + ์ผ๋ถ€ ์กฐ์ž‘ | `spy()` + `doReturn()` | ์ง„์งœ ์„œ๋น„์Šค ๊ฐ์‹ธ๊ณ  ์ผ๋ถ€๋งŒ stub | -| **Fake** | ์‹ค์ œ์ฒ˜๋Ÿผ ๋™์ž‘ํ•˜๋Š” ๊ฐ€์งœ ๊ตฌํ˜„์ฒด | ์ง์ ‘ ํด๋ž˜์Šค ๊ตฌํ˜„ | **InMemoryUserRepository** | - -### ๐Ÿ” TestDouble ์‹ค์ „ ์˜ˆ์ œ - -### ๐Ÿ“ฆ Stub ์˜ˆ์ œ - -```kotlin -val userRepo = mock() -whenever(userRepo.findById(1L)).thenReturn(User("alen")) -``` - -- ํ๋ฆ„๋งŒ ํ†ต์ œํ•˜๊ณ  ์‹ถ์€ ๊ฒฝ์šฐ -- โ€œ์ด๋ ‡๊ฒŒ ํ˜ธ์ถœํ•˜๋ฉด, ์ด๋ ‡๊ฒŒ ์‘๋‹ตํ•ด์ค˜โ€ - -### ๐Ÿ“ฌ Mock ์˜ˆ์ œ - -```kotlin -val speaker = mock() -speaker.say("hello") -verify(speaker, times(1)).say("hello") -``` - -- ํ˜ธ์ถœ ์—ฌ๋ถ€๊ฐ€ ๊ฒ€์ฆ ๋Œ€์ƒ -- โ€œ๋„ˆ ์ด๋ ‡๊ฒŒ ๋™์ž‘ํ–ˆ๋‹ˆ?โ€ - -### ๐Ÿ•ต๏ธ Spy ์˜ˆ์ œ - -```kotlin -val friend = Friend() -val spyFriend = spy(friend) -spyFriend.hangout() -verify(spyFriend).hangout() -``` - -- ์ง„์งœ ๊ฐ์ฒด์ฒ˜๋Ÿผ ๋™์ž‘ํ•˜๋ฉด์„œ ์ผ๋ถ€๋งŒ ์กฐ์ž‘ -- "๋กœ์ง์€ ๊ทธ๋Œ€๋กœ ์“ฐ๊ณ , ํŠน์ • ๋™์ž‘๋งŒ ๋ฎ์–ด์”Œ์šฐ๊ณ  / ๊ฒ€์ฆํ•˜๊ณ  ์‹ถ๋‹ค" - -### ๐Ÿงช Fake ์˜ˆ์ œ - -```kotlin -class InMemoryUserRepository : UserRepository { - private val data = mutableMapOf() - - override fun save(user: User) { data[user.id] = user } - override fun findById(id: Long): User? = data[user.id] -} -``` - -- ์‹ค์ œ DB ์—†์ด ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅํ•œ ์ €์žฅ์†Œ ๊ตฌํ˜„ -- "์™„์ „ํžˆ ๋…๋ฆฝ์ ์ธ ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์ด ํ•„์š”ํ•  ๋•Œโ€ - ---- - -## ๐Ÿงฑ ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅํ•œ ๊ตฌ์กฐ - -> **๊ฒ€์ฆํ•˜๊ณ  ์‹ถ์€ ๋กœ์ง์„, ์™ธ๋ถ€ ์˜์กด์„ฑ๊ณผ ๊ฒฉ๋ฆฌ๋œ ์ƒํƒœ์—์„œ ๋‹จ๋…์œผ๋กœ ๊ฒ€์ฆํ•  ์ˆ˜ ์žˆ๋Š” ๊ตฌ์กฐ**์ž…๋‹ˆ๋‹ค. -> -> -> ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅํ•œ ๊ตฌ์กฐ๋ž€, ๊ฒ€์ฆํ•˜๊ณ  ์‹ถ์€ ์ฝ”๋“œ๋งŒ ์ •ํ™•ํžˆ ๊บผ๋‚ด์„œ **์กฐ์šฉํ•˜๊ณ  ๋‹จ๋‹จํ•˜๊ฒŒ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋Š” ๊ตฌ์กฐ**๋‹ค. -> - -### โŒ ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์–ด๋ ค์šด ๊ตฌ์กฐ์˜ ํŠน์ง• - -| ๋ฌธ์ œ | ์„ค๋ช… | -| --- | --- | -| **๋‚ด๋ถ€์—์„œ ์˜์กด ๊ฐ์ฒด ์ง์ ‘ ์ƒ์„ฑ (`new`)** | ํ…Œ์ŠคํŠธ ๋Œ€์—ญ์œผ๋กœ ๋Œ€์ฒด ๋ถˆ๊ฐ€ โ†’ ํ…Œ์ŠคํŠธ ๊ฒฉ๋ฆฌ ๋ถˆ๊ฐ€๋Šฅ | -| **ํ•˜๋‚˜์˜ ํ•จ์ˆ˜๊ฐ€ ๋„ˆ๋ฌด ๋งŽ์€ ์ฑ…์ž„** | ํ…Œ์ŠคํŠธ ๋Œ€์ƒ์ด ๋ชจํ˜ธํ•ด์ง โ†’ ์‹คํŒจ ์›์ธ ์ถ”์  ์–ด๋ ค์›€ | -| **์™ธ๋ถ€ API ํ˜ธ์ถœ, DB ์ ‘๊ทผ ๋“ฑ์ด ํ•˜๋“œ์ฝ”๋”ฉ** | ์‹ค์ œ ํ™˜๊ฒฝ ์—†์ด ํ…Œ์ŠคํŠธ ๋ถˆ๊ฐ€๋Šฅ โ†’ ๋А๋ฆฌ๊ณ  ๋ถˆ์•ˆ์ • | -| **private ๋กœ์ง, static ๋ฉ”์„œ๋“œ ๋‚จ์šฉ** | ์™ธ๋ถ€์—์„œ ๋กœ์ง ๋ถ„๋ฆฌ ๋ถˆ๊ฐ€ โ†’ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ๋ถˆ๊ฐ€ | - -### โœ… ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅํ•œ ๊ตฌ์กฐ๋กœ ๋ณ€๊ฒฝ - -| ํฌ์ธํŠธ | ์„ค๋ช… | -| --- | --- | -| **์™ธ๋ถ€ ์˜์กด์„ฑ ๋ถ„๋ฆฌ** | ์ธํ„ฐํŽ˜์ด์Šคํ™” + ์ƒ์„ฑ์ž ์ฃผ์ž…(DI) | -| **๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ๋ถ„๋ฆฌ** | ๋„๋ฉ”์ธ ์—”ํ‹ฐํ‹ฐ or ์ „์šฉ Service์—์„œ ์ฑ…์ž„ ๋ถ„์‚ฐ | -| **์ฑ…์ž„ ๋‹จ์ผํ™”** | ํ•œ ํ•จ์ˆ˜๋Š” ํ•œ ์—ญํ• ๋งŒ (e.g. ๊ฒฐ์ œ๋งŒ, ์žฌ๊ณ ๋งŒ ๋“ฑ) | -| **์ƒํƒœ ์ค‘์‹ฌ ์„ค๊ณ„** | โ€œ์ž…๋ ฅ โ†’ ์ƒํƒœ ๋ณ€ํ™” โ†’ ๊ฒฐ๊ณผโ€ ๊ตฌ์กฐ๋กœ ์ •๋ฆฌ | - -### ๐Ÿ” ์‚ฌ๋ก€๋กœ ์‚ดํŽด๋ณด๊ธฐ - -```kotlin -class OrderService { - fun completeOrder(userId: Long, productId: Long) { - val user = UserJpaRepository().findById(userId) - val product = ProductJpaRepository().findById(productId) - - if (product.stock <= 0) throw IllegalStateException() - product.stock-- - - if (user.point < product.price) throw IllegalStateException() - user.point -= product.price - - OrderRepository().save(Order(user, product)) - } -} -``` - -- ์™ธ๋ถ€ ์˜์กด์„ฑ ์ง์ ‘ ์ƒ์„ฑ โ†’ Mock/Fake ๋ถˆ๊ฐ€ -- ๋„๋ฉ”์ธ ๋กœ์ง, ์ƒํƒœ๋ณ€๊ฒฝ, ์™ธ๋ถ€ ํ˜ธ์ถœ์ด ํ•œ ๊ณณ์— ๋ชฐ๋ ค ์žˆ์Œ -- `OrderServiceTest` ํ•˜๋‚˜๋กœ ๋ชจ๋“  ์ผ€์ด์Šค ์ปค๋ฒ„ํ•ด์•ผ ํ•จ โ†’ ์‹คํŒจ ์‹œ ์–ด๋””์„œ ์ž˜๋ชป๋๋Š”์ง€ ์ถ”์  ๋ถˆ๊ฐ€ - ---- - -```kotlin -class OrderService( - private val userReader: UserReader, - private val productReader: ProductReader, - private val orderRepository: OrderRepository, -) { - fun completeOrder(command: OrderCommand) { - val user = userReader.get(command.userId) - val product = productReader.get(command.productId) - - product.decreaseStock() - user.pay(product.price) - - orderRepository.save(Order(user, product)) - } -} -``` - -- ์™ธ๋ถ€๋Š” ์ธํ„ฐํŽ˜์ด์Šค๋กœ ์ฃผ์ž… โ†’ Fake/Mock ๊ฐ€๋Šฅ -- ๋กœ์ง์€ `user.pay()`, `product.decreaseStock()` ์ฒ˜๋Ÿผ ๋„๋ฉ”์ธ์œผ๋กœ ์œ„์ž„ -- ํ…Œ์ŠคํŠธ ๋‹จ์œ„๋ณ„๋กœ ๋‚˜๋ˆŒ ์ˆ˜ ์žˆ์Œ โ†’ `UserTest`, `ProductTest`, `OrderServiceTest` - ---- - -## ๐Ÿ” TDD (Test-Driven Development) - -> TDD๋Š” ํ…Œ์ŠคํŠธ์˜ ์ˆœ์„œ๋ณด๋‹ค -**โ€์„ค๊ณ„ ๋‹จ์œ„๋ฅผ ์ž˜๊ฒŒ ์ชผ๊ฐœ๊ณ , ๊ทธ๊ฒƒ์ด ๊ฒ€์ฆ ๊ฐ€๋Šฅํ•˜๊ฒŒ ๊ตฌํ˜„๋˜์—ˆ๋Š”๊ฐ€โ€**๊ฐ€ ํ•ต์‹ฌ์ด๋‹ค. -> - -### ๐Ÿ”„ 3๋‹จ๊ณ„ ๋ฃจํ”„: Red โ†’ Green โ†’ Refactor - -``` -< ๋ฐ˜๋ณต > -1. ์‹คํŒจํ•˜๋Š” ํ…Œ์ŠคํŠธ ์ž‘์„ฑ (Red) -2. ํ†ต๊ณผํ•  ์ตœ์†Œํ•œ์˜ ์ฝ”๋“œ ์ž‘์„ฑ (Green) -3. ๊ตฌ์กฐ ๊ฐœ์„  ๋ฐ ๋ฆฌํŒฉํ† ๋ง (Refactor) -``` - -### ๐Ÿง  ๊ทธ๋Ÿฐ๋ฐ ๊ผญ ํ…Œ์ŠคํŠธ๋ฅผ ๋จผ์ € ์จ์•ผ ํ• ๊นŒ? - -| **์ „๋žต** | **์ด๋ฆ„** | **์„ค๋ช…** | -| --- | --- | --- | -| ๐Ÿงช TFD (Test First Development) | ํ…Œ์ŠคํŠธ ๋จผ์ € ์ž‘์„ฑ โ†’ ์ฝ”๋“œ๋ฅผ ๋งž์ถฐ ๊ตฌํ˜„ | ๋„๋ฉ”์ธ/๋กœ์ง ์ค‘์‹ฌ์— ์ ํ•ฉ | -| ๐Ÿ— TLD (Test Last Development) | ์ฝ”๋“œ๋ฅผ ๋จผ์ € ์ž‘์„ฑ โ†’ ํ…Œ์ŠคํŠธ๋Š” ๋‚˜์ค‘์— ์ž‘์„ฑ | API/๊ณ„์ธต ์„ค๊ณ„๊ฐ€ ๋จผ์ € ํ•„์š”ํ•œ ์ƒํ™ฉ์— ์ ํ•ฉ | - -### ๐ŸŸข TDD๊ฐ€ ํ•„์š”ํ•œ ์ด์œ  - -- **์š”๊ตฌ์‚ฌํ•ญ์„ ๋จผ์ € ์ •๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค** -- **์ž‘๊ฒŒ ์ชผ๊ฐœ๊ณ  ์ ์ง„์ ์œผ๋กœ ์„ค๊ณ„ํ•˜๊ฒŒ ๋œ๋‹ค** -- **์ธํ„ฐํŽ˜์ด์Šค ์„ค๊ณ„๊ฐ€ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ๋‚˜์˜จ๋‹ค** -- **๋ฆฌํŒฉํ† ๋ง์ด ๊ฐ€๋Šฅํ•ด์ง„๋‹ค** - - - -| ๊ตฌ๋ถ„ | ๋งํฌ | -| --- | --- | -| ๐Ÿ”ข ํ…Œ์ŠคํŠธ ํ”ผ๋ผ๋ฏธ๋“œ | [Testing Pyramid - Martin Fowler](https://martinfowler.com/bliki/TestPyramid.html) | -| ๐Ÿงช JUnit5 | [JUnit5 ๊ณต์‹ ๋ฌธ์„œ](https://junit.org/junit5/docs/current/user-guide/) | -| โš™๏ธ Mockito | [Mockito ๊ณต์‹ ๋ฌธ์„œ](https://site.mockito.org/) | -| ๐Ÿงฐ Mockito-Kotlin | [GitHub: mockito-kotlin](https://github.com/mockito/mockito-kotlin) | -| ๐Ÿงต Spring ํ…Œ์ŠคํŠธ | [Spring Boot Testing Guide](https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.testing) | - -> ๋ณธ ๊ณผ์ •์—์„œ๋Š” ์›ํ™œํ•œ ๋ฉ˜ํ† ๋ง์„ ์œ„ํ•ด `JUnit5 + Mockito` ๊ธฐ๋ฐ˜์œผ๋กœ ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค. -> - - - -> ๋‹ค์Œ ์ฃผ์—๋Š” ๋ณธ๊ฒฉ์ ์œผ๋กœ ์šฐ๋ฆฌ๋งŒ์˜ e-commerce ์‹œ์Šคํ…œ์„ **์„ค๊ณ„** ํ•ด๋ด…๋‹ˆ๋‹ค. -> \ No newline at end of file diff --git a/docs/week01_quests.md b/docs/week01_quests.md deleted file mode 100644 index 9d6ae8170..000000000 --- a/docs/week01_quests.md +++ /dev/null @@ -1,131 +0,0 @@ - - -# ๐Ÿ“ Round 1 Quests - ---- - -## ๐Ÿงช Implementation Quest - -> ์ง€์ •๋œ **๋‹จ์œ„ ํ…Œ์ŠคํŠธ / ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ / E2E ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค**๋ฅผ ํ•„์ˆ˜๋กœ ๊ตฌํ˜„ํ•˜๊ณ , ๋ชจ๋“  ํ…Œ์ŠคํŠธ๋ฅผ ํ†ต๊ณผ์‹œํ‚ค๋Š” ๊ฒƒ์„ ๋ชฉํ‘œ๋กœ ํ•ฉ๋‹ˆ๋‹ค. -> - -### ํšŒ์› ๊ฐ€์ž… - -**๐Ÿงฑ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ** - -- [ ] ID ๊ฐ€ `์˜๋ฌธ ๋ฐ ์ˆซ์ž 10์ž ์ด๋‚ด` ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. -- [ ] ์ด๋ฉ”์ผ์ด `xx@yy.zz` ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. -- [ ] ์ƒ๋…„์›”์ผ์ด `yyyy-MM-dd` ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. - -**๐Ÿ”— ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ** - -- [ ] ํšŒ์› ๊ฐ€์ž…์‹œ User ์ €์žฅ์ด ์ˆ˜ํ–‰๋œ๋‹ค. ( spy ๊ฒ€์ฆ ) -- [ ] ์ด๋ฏธ ๊ฐ€์ž…๋œ ID ๋กœ ํšŒ์›๊ฐ€์ž… ์‹œ๋„ ์‹œ, ์‹คํŒจํ•œ๋‹ค. - -**๐ŸŒ E2E ํ…Œ์ŠคํŠธ** - -- [ ] ํšŒ์› ๊ฐ€์ž…์ด ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ์ƒ์„ฑ๋œ ์œ ์ € ์ •๋ณด๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค. -- [ ] ํšŒ์› ๊ฐ€์ž… ์‹œ์— ์„ฑ๋ณ„์ด ์—†์„ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. - -### ๋‚ด ์ •๋ณด ์กฐํšŒ - -**๐Ÿ”— ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ** - -- [ ] ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•  ๊ฒฝ์šฐ, ํšŒ์› ์ •๋ณด๊ฐ€ ๋ฐ˜ํ™˜๋œ๋‹ค. -- [ ] ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ, null ์ด ๋ฐ˜ํ™˜๋œ๋‹ค. - -**๐ŸŒ E2E ํ…Œ์ŠคํŠธ** - -- [ ] ๋‚ด ์ •๋ณด ์กฐํšŒ์— ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ํ•ด๋‹นํ•˜๋Š” ์œ ์ € ์ •๋ณด๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค. -- [ ] ์กด์žฌํ•˜์ง€ ์•Š๋Š” ID ๋กœ ์กฐํšŒํ•  ๊ฒฝ์šฐ, `404 Not Found` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. - -### ํฌ์ธํŠธ ์กฐํšŒ - -**๐Ÿ”— ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ** - -- [ ] ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•  ๊ฒฝ์šฐ, ๋ณด์œ  ํฌ์ธํŠธ๊ฐ€ ๋ฐ˜ํ™˜๋œ๋‹ค. -- [ ] ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ, null ์ด ๋ฐ˜ํ™˜๋œ๋‹ค. - -**๐ŸŒ E2E ํ…Œ์ŠคํŠธ** - -- [ ] ํฌ์ธํŠธ ์กฐํšŒ์— ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ๋ณด์œ  ํฌ์ธํŠธ๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค. -- [ ] `X-USER-ID` ํ—ค๋”๊ฐ€ ์—†์„ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. - -### ํฌ์ธํŠธ ์ถฉ์ „ - -**๐Ÿงฑ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ** - -- [ ] 0 ์ดํ•˜์˜ ์ •์ˆ˜๋กœ ํฌ์ธํŠธ๋ฅผ ์ถฉ์ „ ์‹œ ์‹คํŒจํ•œ๋‹ค. - -**๐Ÿ”— ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ** - -- [ ] ์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ์ € ID ๋กœ ์ถฉ์ „์„ ์‹œ๋„ํ•œ ๊ฒฝ์šฐ, ์‹คํŒจํ•œ๋‹ค. - -**๐ŸŒ E2E ํ…Œ์ŠคํŠธ** - -- [ ] ์กด์žฌํ•˜๋Š” ์œ ์ €๊ฐ€ 1000์›์„ ์ถฉ์ „ํ•  ๊ฒฝ์šฐ, ์ถฉ์ „๋œ ๋ณด์œ  ์ด๋Ÿ‰์„ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค. -- [ ] ์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ์ €๋กœ ์š”์ฒญํ•  ๊ฒฝ์šฐ, `404 Not Found` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. - -## โœ… Checklist - -- [ ] ํ…Œ์ŠคํŠธ๋Š” ๋ชจ๋‘ ํ†ต๊ณผํ•ด์•ผ ํ•˜๋ฉฐ, `@Test` ๊ธฐ๋ฐ˜์œผ๋กœ ๋ช…์‹œ์ ์œผ๋กœ ์ž‘์„ฑ -- [ ] ๊ฐ ํ…Œ์ŠคํŠธ๋Š” ํ…Œ์ŠคํŠธ ๋ช…/์„ค๋ช…/์ž…๋ ฅ/์˜ˆ์ƒ ๊ฒฐ๊ณผ๊ฐ€ ๋ถ„๋ช…ํ•ด์•ผ ํ•จ -- [ ] E2E ํ…Œ์ŠคํŠธ๋Š” ์‹ค์ œ HTTP ์š”์ฒญ์„ ํ†ตํ•ด ํ๋ฆ„์„ ๊ฒ€์ฆํ•  ๊ฒƒ - ---- - -## โœ๏ธ Technical Writing Quest - -> ์ด๋ฒˆ ์ฃผ์— ํ•™์Šตํ•œ ๋‚ด์šฉ, ๊ณผ์ œ ์ง„ํ–‰์„ ๋˜๋Œ์•„๋ณด๋ฉฐ -**"๋‚ด๊ฐ€ ์–ด๋–ค ํŒ๋‹จ์„ ํ•˜๊ณ  ์™œ ๊ทธ๋ ‡๊ฒŒ ๊ตฌํ˜„ํ–ˆ๋Š”์ง€"** ๋ฅผ ๊ธ€๋กœ ์ •๋ฆฌํ•ด๋ด…๋‹ˆ๋‹ค. -> -> -> **์ข‹์€ ๋ธ”๋กœ๊ทธ ๊ธ€์€ ๋‚ด๊ฐ€ ๊ฒช์€ ๋ฌธ์ œ๋ฅผ, ํƒ€์ธ๋„ ๊ณต๊ฐํ•  ์ˆ˜ ์žˆ๊ฒŒ ์ •๋ฆฌํ•œ ๊ธ€์ž…๋‹ˆ๋‹ค.** -> -> ์ด ๊ธ€์€ ๋‹จ์ˆœ ๊ณผ์ œ๊ฐ€ ์•„๋‹ˆ๋ผ, **ํ–ฅํ›„ ์ด์ง์— ๋„์›€์ด ๋  ์ˆ˜ ์žˆ๋Š” ํฌํŠธํด๋ฆฌ์˜ค** ๊ฐ€ ๋  ์ˆ˜ ์žˆ์–ด์š”. -> - -### ๐Ÿ“š Technical Writing Guide - -### โœ… ์ž‘์„ฑ ๊ธฐ์ค€ - -| ํ•ญ๋ชฉ | ์„ค๋ช… | -| --- | --- | -| **ํ˜•์‹** | ๋ธ”๋กœ๊ทธ | -| **๊ธธ์ด** | ์ œํ•œ ์—†์Œ, ๋‹จ ๊ผญ **1์ค„ ์š”์•ฝ (TL;DR)** ์„ ํฌํ•จํ•ด ์ฃผ์„ธ์š” | -| **ํฌ์ธํŠธ** | โ€œ๋ฌด์—‡์„ ํ–ˆ๋‹คโ€ ๋ณด๋‹ค **โ€œ์™œ ๊ทธ๋ ‡๊ฒŒ ํŒ๋‹จํ–ˆ๋Š”๊ฐ€โ€** ์ค‘์‹ฌ | -| **์˜ˆ์‹œ ํฌํ•จ** | ์ฝ”๋“œ ๋น„๊ต, ํ๋ฆ„๋„, ๋ฆฌํŒฉํ† ๋ง ์ „ํ›„ ์˜ˆ์‹œ ๋“ฑ ์ž์œ ๋กญ๊ฒŒ | -| **ํ†ค** | ์‹ค๋ ฅ์€ ๋ณด์ด์ง€๋งŒ, ์ž๋งŒํ•˜์ง€ ์•Š๊ณ , **๊ณ ๋ฏผ์ด ์ฝํžˆ๋Š” ๊ธ€**์˜ˆ: โ€œ์ฒ˜์Œ์—” mock์œผ๋กœ ์ถฉ๋ถ„ํ•˜๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ์ง€๋งŒ, ๋‚˜์ค‘์— fake๋กœ ๊ต์ฒดํ•˜๊ฒŒ ๋œ ์ด์œ ๋Š”โ€ฆโ€ | - ---- - -### โœจ ์ข‹์€ ํ†ค์€ ์ด๋Ÿฐ ๋А๋‚Œ์ด์—์š” - -> ๋‚ด๊ฐ€ ๊ฒช์€ ์‹ค์ „์  ๊ณ ๋ฏผ์„ ๋‹ค๋ฅธ ๊ฐœ๋ฐœ์ž๋„ ๊ณต๊ฐํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ’€์–ด๋‚ด์ž -> - -| ํŠน์ง• | ์˜ˆ์‹œ | -| --- | --- | -| ๐Ÿค” ๋‚ด ์–ธ์–ด๋กœ ์„ค๋ช…ํ•œ ๊ฐœ๋… | Stub๊ณผ Mock์˜ ์ฐจ์ด๋ฅผ ์ด๋ฒˆ ์ฃผ๋ฌธ ํ…Œ์ŠคํŠธ์—์„œ ์ฒ˜์Œ ์‹ค๊ฐํ–ˆ๋‹ค | -| ๐Ÿ’ญ ํŒ๋‹จ ํ๋ฆ„์ด ๋“œ๋Ÿฌ๋‚˜๋Š” ๊ธ€ | ์ฒ˜์Œ์—” ๋„๋ฉ”์ธ์„ ๋‚˜๋ˆ„์ง€ ์•Š์•˜๋Š”๋ฐ, ํ…Œ์ŠคํŠธ๊ฐ€ ์–ด๋ ค์›Œ์ง€๋ฉฐ ๋ถ„๋ฆฌํ–ˆ๋‹ค | -| ๐Ÿ“ ์ •๋ณด ๋‚˜์—ด๋ณด๋‹ค ์ธ์‚ฌ์ดํŠธ ์ค‘์‹ฌ | ํ…Œ์ŠคํŠธ๋Š” ์ž‘์„ฑํ–ˆ์ง€๋งŒ, ๊ตฌ์กฐ๋Š” ๋งŒ์กฑ์Šค๋Ÿฝ์ง€ ์•Š๋‹ค. ๋‹ค์Œ์—”โ€ฆ | - -### โŒ ํ”ผํ•ด์•ผ ํ•  ์Šคํƒ€์ผ - -| ์˜ˆ์‹œ | ์ด์œ  | -| --- | --- | -| ๋งŽ์ด ๋ถ€์กฑํ–ˆ๊ณ , ๋ฐ˜์„ฑํ•ฉ๋‹ˆ๋‹คโ€ฆ | ํšŒ๊ณ ๊ฐ€ ์•„๋‹ˆ๋ผ ์ผ๊ธฐ์ฒ˜๋Ÿผ ๋ณด์ž…๋‹ˆ๋‹ค | -| Stub์€ ์‘๋‹ต์„ ์ง€์ •ํ•˜๊ณ โ€ฆ | ๋‚ด ์ƒ๊ฐ์ด ์•„๋‹Œ ์š”์•ฝ๋ฌธ์ฒ˜๋Ÿผ ๋ณด์ž…๋‹ˆ๋‹ค | -| ํ…Œ์ŠคํŠธ๊ฐ€ ์ง„๋ฆฌ๋‹ค | ๋„ˆ๋ฌด ๋‹จ์ •์ ์ด๊ฑฐ๋‚˜ ์˜ค๋งŒํ•ด ๋ณด์ž…๋‹ˆ๋‹ค | - -### ๐ŸŽฏ Feature Suggestions - -- ๋‚ด๊ฐ€ ์ž‘์„ฑํ•œ ํ…Œ์ŠคํŠธ ์ค‘ ๊ฐ€์žฅ ์˜๋ฏธ ์žˆ์—ˆ๋˜ ๊ฒƒ 1๊ฐ€์ง€ -- ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅํ•œ ๊ตฌ์กฐ๋ฅผ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด ํ•œ ๋ฆฌํŒฉํ† ๋ง -- Mock, Stub, Fake ์ค‘ ์‹ค์ œ ํ™œ์šฉ ๊ฒฝํ—˜๊ณผ ๋‚˜๋งŒ์˜ ๊ตฌ๋ถ„ ๊ธฐ์ค€ -- TDD ๋ฐฉ์‹์œผ๋กœ ์ ‘๊ทผํ•˜๊ฑฐ๋‚˜ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•ด๋ณด๋ฉฐ ์–ด๋ ค์› ๋˜ ์  \ No newline at end of file From c5754ffe7e7c4d2aeff7ad026181f823d5f55896 Mon Sep 17 00:00:00 2001 From: adminhelper <201401339hs@gmail.com> Date: Tue, 18 Nov 2025 01:02:46 +0900 Subject: [PATCH 60/85] =?UTF-8?q?fix=20:=20PR=EC=BD=94=EB=93=9C=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 --- .../loopers/application/like/LikeFacade.java | 2 -- .../application/order/OrderFacade.java | 6 ++-- .../application/product/ProductFacade.java | 6 ++-- .../java/com/loopers/domain/brand/Brand.java | 2 +- .../loopers/domain/brand/BrandService.java | 1 - .../java/com/loopers/domain/order/Order.java | 1 + .../java/com/loopers/domain/point/Point.java | 1 - .../loopers/domain/point/PointService.java | 4 +-- .../com/loopers/domain/product/Product.java | 5 ++- .../domain/product/ProductDomainService.java | 2 ++ .../domain/product/ProductService.java | 18 +++++++++-- .../com/loopers/domain/user/UserService.java | 2 +- .../brand/BrandJpaRepository.java | 3 -- .../product/ProductRepositoryImpl.java | 8 +++-- .../user/UserRepositoryImpl.java | 3 +- .../interfaces/api/point/PointV1ApiSpec.java | 2 +- .../com/loopers/domain/user/UserTest.java | 6 ++-- docs/2round/03-class-diagram.md | 6 ---- docs/3round/3round.md | 32 +++++++++---------- 19 files changed, 59 insertions(+), 51 deletions(-) diff --git a/apps/commerce-api/src/main/java/com/loopers/application/like/LikeFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/like/LikeFacade.java index 5d885672e..d9dd33205 100644 --- a/apps/commerce-api/src/main/java/com/loopers/application/like/LikeFacade.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/like/LikeFacade.java @@ -3,7 +3,6 @@ import com.loopers.domain.like.LikeService; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; /** * packageName : com.loopers.application.like @@ -18,7 +17,6 @@ */ @Component @RequiredArgsConstructor -@Transactional public class LikeFacade { private final LikeService likeService; diff --git a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java index 9e06282b4..2fba4b4aa 100644 --- a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java @@ -4,7 +4,6 @@ import com.loopers.domain.order.OrderItem; import com.loopers.domain.order.OrderService; import com.loopers.domain.order.OrderStatus; -import com.loopers.domain.point.Point; import com.loopers.domain.point.PointService; import com.loopers.domain.product.Product; import com.loopers.domain.product.ProductService; @@ -40,7 +39,7 @@ public class OrderFacade { public OrderInfo createOrder(CreateOrderCommand command) { if (command == null || command.items() == null || command.items().isEmpty()) { - throw new CoreException(ErrorType.NOT_FOUND, "์ƒํ’ˆ ์ •๋ณด๊ฐ€ ๋น„์–ด์žˆ์Šต๋‹ˆ๋‹ค"); + throw new CoreException(ErrorType.BAD_REQUEST, "์ƒํ’ˆ ์ •๋ณด๊ฐ€ ๋น„์–ด์žˆ์Šต๋‹ˆ๋‹ค"); } Order order = Order.create(command.userId()); @@ -71,8 +70,7 @@ public OrderInfo createOrder(CreateOrderCommand command) { order.updateTotalAmount(totalAmount); - Point point = pointService.findPointByUserId(command.userId()); - point.use(totalAmount); + pointService.usePoint(command.userId(), totalAmount); //์ €์žฅ Order saved = orderService.createOrder(order); diff --git a/apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java index 7b83360e7..e6a25de23 100644 --- a/apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java @@ -31,10 +31,10 @@ public class ProductFacade { private final LikeService likeService; private final ProductDomainService productDomainService; - public Page getProducts(Pageable pageable) { - return productService.getProducts(pageable) + public Page getProducts(String sort, Pageable pageable) { + return productService.getProducts(sort ,pageable) .map(product -> { - Brand brand = brandService.getBrand(product.getId()); + Brand brand = brandService.getBrand(product.getBrandId()); long likeCount = likeService.countByProductId(product.getId()); return ProductInfo.of(product, brand, likeCount); }); diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/brand/Brand.java b/apps/commerce-api/src/main/java/com/loopers/domain/brand/Brand.java index bacd46b25..d334ccebf 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/brand/Brand.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/brand/Brand.java @@ -40,7 +40,7 @@ public static Brand create(String name) { private String requireValidName(String name) { if (name == null || name.isEmpty()) { - throw new CoreException(ErrorType.BAD_REQUEST, "์ƒํ’ˆ๋ช… ๋น„์–ด ์žˆ์„์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + throw new CoreException(ErrorType.BAD_REQUEST, "๋ธŒ๋žœ๋“œ๋ช…์€ ๋น„์–ด ์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); } return name.trim(); } diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandService.java b/apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandService.java index 6aa724710..e0f58c77b 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandService.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandService.java @@ -23,7 +23,6 @@ public class BrandService { private final BrandRepository brandRepository; - @Transactional public void save(Brand brand) { brandRepository.save(brand); } diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/Order.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/Order.java index 9d7b9d3f2..84f299c6b 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/order/Order.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/order/Order.java @@ -58,6 +58,7 @@ public static Order create(String userId) { } public void addOrderItem(OrderItem orderItem) { + orderItem.setOrder(this); this.orderItems.add(orderItem); } diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java b/apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java index ea0c18c9d..bc28a902a 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java @@ -42,7 +42,6 @@ public void charge(Long chargeAmount) { throw new CoreException(ErrorType.BAD_REQUEST, "0์› ์ดํ•˜๋กœ ํฌ์ธํŠธ๋ฅผ ์ถฉ์ „ ํ• ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); } this.balance += chargeAmount; - new Point(this.userId, this.balance); } public void use(Long useAmount) { diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java b/apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java index 1a6293f91..9c9570615 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java @@ -30,11 +30,11 @@ public Point usePoint(String userId, Long useAmount) { .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "ํฌ์ธํŠธ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); if (useAmount == null || useAmount <= 0) { - throw new CoreException(ErrorType.NOT_FOUND, "์ฐจ๊ฐํ•  ํฌ์ธํŠธ๋Š” 1 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + throw new CoreException(ErrorType.BAD_REQUEST, "์ฐจ๊ฐํ•  ํฌ์ธํŠธ๋Š” 1 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); } if (point.getBalance() < useAmount) { - throw new CoreException(ErrorType.NOT_FOUND, "ํฌ์ธํŠธ๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค."); + throw new CoreException(ErrorType.BAD_REQUEST, "ํฌ์ธํŠธ๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค."); } point.use(useAmount); diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java index cade20580..29968402f 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java @@ -83,7 +83,7 @@ private Long requireValidPrice(Long price) { return price; } - public Long requireValidLikeCount(Long likeCount) { + private Long requireValidLikeCount(Long likeCount) { if (likeCount == null || likeCount < 0) { throw new CoreException(ErrorType.BAD_REQUEST, "์ข‹์•„์š” ๊ฐœ์ˆ˜๋Š” 0๊ฐœ ๋ฏธ๋งŒ์œผ๋กœ ์„ค์ •ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); } @@ -91,6 +91,9 @@ public Long requireValidLikeCount(Long likeCount) { } private Long requireValidStock(Long stock) { + if (stock == null) { + throw new CoreException(ErrorType.BAD_REQUEST, "์ƒํ’ˆ ์žฌ๊ณ ๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."); + } if (stock < 0) { throw new CoreException(ErrorType.BAD_REQUEST, "์ƒํ’ˆ ์žฌ๊ณ ๋Š” 0 ๋ฏธ๋งŒ์œผ๋กœ ์„ค์ •ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); } diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductDomainService.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductDomainService.java index f86edfddd..166aff66b 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductDomainService.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductDomainService.java @@ -7,6 +7,7 @@ import com.loopers.support.error.ErrorType; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; /** * packageName : com.loopers.domain.product @@ -27,6 +28,7 @@ public class ProductDomainService { private final BrandRepository brandRepository; private final LikeRepository likeRepository; + @Transactional(readOnly = true) public ProductDetail getProductDetail(Long id) { Product product = productRepository.findById(id) .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "์ƒํ’ˆ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java index a9c03fc80..067f194ae 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java @@ -4,7 +4,9 @@ import com.loopers.support.error.ErrorType; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; @@ -27,8 +29,20 @@ public class ProductService { private final ProductRepository productRepository; @Transactional(readOnly = true) - public Page getProducts(Pageable pageable) { - return productRepository.findAll(pageable); + public Page getProducts(String sort, Pageable pageable) { + Sort sortOption = switch (sort) { + case "price_asc" -> Sort.by("price").ascending(); + case "likes_desc" -> Sort.by("likeCount").descending(); + default -> Sort.by("createdAt").descending(); // latest + }; + + Pageable sortedPageable = PageRequest.of( + pageable.getPageNumber(), + pageable.getPageSize(), + sortOption + ); + + return productRepository.findAll(sortedPageable); } public Product getProduct(Long productId) { diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java index 57353968a..3cc033076 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java @@ -15,7 +15,7 @@ public class UserService { @Transactional public User register(String userId, String email, String birth, String gender) { userRepository.findByUserId(userId).ifPresent(user -> { - throw new CoreException(ErrorType.CONFLICT, "์ด๋ฏธ ๊ฐ€์ž…๋œ ์‚ฌ์šฉ์žID ์ž…๋‹ˆ๋‹ค."); + throw new CoreException(ErrorType.CONFLICT, "์ด๋ฏธ ๊ฐ€์ž…๋œ ์‚ฌ์šฉ์ž ID ์ž…๋‹ˆ๋‹ค."); }); User user = new User(userId, email, birth, gender); diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandJpaRepository.java index 111990a22..759f3caf1 100644 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandJpaRepository.java +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandJpaRepository.java @@ -3,8 +3,6 @@ import com.loopers.domain.brand.Brand; import org.springframework.data.jpa.repository.JpaRepository; -import java.util.Optional; - /** * packageName : com.loopers.infrastructure.brand * fileName : BrandJpaRepository @@ -17,5 +15,4 @@ * 2025. 11. 12. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ */ public interface BrandJpaRepository extends JpaRepository { - Optional findById(Long id); } diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java index ba0feb19c..dbad0d9d5 100644 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java @@ -2,6 +2,8 @@ import com.loopers.domain.product.Product; import com.loopers.domain.product.ProductRepository; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -38,13 +40,15 @@ public Optional findById(Long id) { @Override public void incrementLikeCount(Long productId) { - Product product = productJpaRepository.findById(productId).get(); + Product product = productJpaRepository.findById(productId) + .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "์ƒํ’ˆ์„ ์ฐพ์„์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); product.increaseLikeCount(); } @Override public void decrementLikeCount(Long productId) { - Product product = productJpaRepository.findById(productId).get(); + Product product = productJpaRepository.findById(productId) + .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "์ƒํ’ˆ์„ ์ฐพ์„์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); product.decreaseLikeCount(); } diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java index 25f05bc6e..8fb6f7bdf 100644 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java @@ -20,8 +20,7 @@ public Optional findByUserId(String userId) { @Override public User save(User user) { - userJpaRepository.save(user); - return user; + return userJpaRepository.save(user); } } diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1ApiSpec.java index faa21f303..6f0458399 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1ApiSpec.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1ApiSpec.java @@ -22,7 +22,7 @@ ApiResponse getPoint( description = "ํšŒ์›์˜ ํฌ์ธํŠธ๋ฅผ ์ถฉ์ „ํ•œ๋‹ค." ) ApiResponse chargePoint( - @Schema(name = "ํฌ์ธํŠธ ์ถฉ์ „ ์š”์ฒญ", description = "์กฐํšŒํ•  ํšŒ์› ID") + @Schema(name = "ํฌ์ธํŠธ ์ถฉ์ „ ์š”์ฒญ", description = "์ถฉ์ „ํ•  ํฌ์ธํŠธ ์ •๋ณด๋ฅผ ํฌํ•จํ•œ ์š”์ฒญ") PointV1Dto.ChargePointRequest request ); } diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserTest.java index a8f8948ca..7d74fdfe2 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserTest.java @@ -42,12 +42,12 @@ void throwsException_whenInvalidEmailFormat() { void throwsException_whenInvalidBirthFormat() { // given String userId = "yh45g"; - String invalidEmail = "invalid-email-format"; // '@' ์—†์Œ - String birth = "1994-12-05"; + String email = "valid@loopers.com"; + String invalidBirth = "19941205"; // ํ˜•์‹ ์˜ค๋ฅ˜: ํ•˜์ดํ”ˆ ์—†์Œ String gender = "MALE"; // when & then - assertThrows(CoreException.class, () -> new User(userId, invalidEmail, birth, gender)); + assertThrows(CoreException.class, () -> new User(userId, email, invalidBirth, gender)); } } } diff --git a/docs/2round/03-class-diagram.md b/docs/2round/03-class-diagram.md index 45421dc8b..8d39cfd0a 100644 --- a/docs/2round/03-class-diagram.md +++ b/docs/2round/03-class-diagram.md @@ -32,11 +32,6 @@ class Product { Long stock } -class Stock { - Long productId - int quantity -} - class Like { Long id String userId @@ -73,7 +68,6 @@ class Payment { %% ๊ด€๊ณ„ ์„ค์ • User --> Point Brand --> Product -Product --> Stock Product --> Like User --> Like User --> Order diff --git a/docs/3round/3round.md b/docs/3round/3round.md index 61df49f68..b9f333cca 100644 --- a/docs/3round/3round.md +++ b/docs/3round/3round.md @@ -24,7 +24,7 @@ ## โœ… Checklist - [x] ์ƒํ’ˆ ์ •๋ณด ๊ฐ์ฒด๋Š” ๋ธŒ๋žœ๋“œ ์ •๋ณด, ์ข‹์•„์š” ์ˆ˜๋ฅผ ํฌํ•จํ•œ๋‹ค. -- [ ] ์ƒํ’ˆ์˜ ์ •๋ ฌ ์กฐ๊ฑด(`latest`, `price_asc`, `likes_desc`) ์„ ๊ณ ๋ คํ•œ ์กฐํšŒ ๊ธฐ๋Šฅ์„ ์„ค๊ณ„ํ–ˆ๋‹ค +- [x] ์ƒํ’ˆ์˜ ์ •๋ ฌ ์กฐ๊ฑด(`latest`, `price_asc`, `likes_desc`) ์„ ๊ณ ๋ คํ•œ ์กฐํšŒ ๊ธฐ๋Šฅ์„ ์„ค๊ณ„ํ–ˆ๋‹ค - [x] ์ƒํ’ˆ์€ ์žฌ๊ณ ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๊ณ , ์ฃผ๋ฌธ ์‹œ ์ฐจ๊ฐํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•œ๋‹ค - [x] ์žฌ๊ณ ๋Š” ๊ฐ์†Œ๋งŒ ๊ฐ€๋Šฅํ•˜๋ฉฐ ์Œ์ˆ˜ ๋ฐฉ์ง€๋Š” ๋„๋ฉ”์ธ ๋ ˆ๋ฒจ์—์„œ ์ฒ˜๋ฆฌ๋œ๋‹ค @@ -33,28 +33,28 @@ - [x] ์ข‹์•„์š”๋Š” ์œ ์ €์™€ ์ƒํ’ˆ ๊ฐ„์˜ ๊ด€๊ณ„๋กœ ๋ณ„๋„ ๋„๋ฉ”์ธ์œผ๋กœ ๋ถ„๋ฆฌํ–ˆ๋‹ค - [x] ์ค‘๋ณต ์ข‹์•„์š” ๋ฐฉ์ง€๋ฅผ ์œ„ํ•œ ๋ฉฑ๋“ฑ์„ฑ ์ฒ˜๋ฆฌ๊ฐ€ ๊ตฌํ˜„๋˜์—ˆ๋‹ค - [x] ์ƒํ’ˆ์˜ ์ข‹์•„์š” ์ˆ˜๋Š” ์ƒํ’ˆ ์ƒ์„ธ/๋ชฉ๋ก ์กฐํšŒ์—์„œ ํ•จ๊ป˜ ์ œ๊ณต๋œ๋‹ค -- [ ] ๋‹จ์œ„ ํ…Œ์ŠคํŠธ์—์„œ ์ข‹์•„์š” ๋“ฑ๋ก/์ทจ์†Œ/์ค‘๋ณต ๋ฐฉ์ง€ ํ๋ฆ„์„ ๊ฒ€์ฆํ–ˆ๋‹ค +- [x] ๋‹จ์œ„ ํ…Œ์ŠคํŠธ์—์„œ ์ข‹์•„์š” ๋“ฑ๋ก/์ทจ์†Œ/์ค‘๋ณต ๋ฐฉ์ง€ ํ๋ฆ„์„ ๊ฒ€์ฆํ–ˆ๋‹ค ### ๐Ÿ›’ Order ๋„๋ฉ”์ธ -- [ ] ์ฃผ๋ฌธ์€ ์—ฌ๋Ÿฌ ์ƒํ’ˆ์„ ํฌํ•จํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ๊ฐ ์ƒํ’ˆ์˜ ์ˆ˜๋Ÿ‰์„ ๋ช…์‹œํ•œ๋‹ค -- [ ] ์ฃผ๋ฌธ ์‹œ ์ƒํ’ˆ์˜ ์žฌ๊ณ  ์ฐจ๊ฐ, ์œ ์ € ํฌ์ธํŠธ ์ฐจ๊ฐ ๋“ฑ์„ ์ˆ˜ํ–‰ํ•œ๋‹ค -- [ ] ์žฌ๊ณ  ๋ถ€์กฑ, ํฌ์ธํŠธ ๋ถ€์กฑ ๋“ฑ ์˜ˆ์™ธ ํ๋ฆ„์„ ๊ณ ๋ คํ•ด ์„ค๊ณ„๋˜์—ˆ๋‹ค -- [ ] ๋‹จ์œ„ ํ…Œ์ŠคํŠธ์—์„œ ์ •์ƒ ์ฃผ๋ฌธ / ์˜ˆ์™ธ ์ฃผ๋ฌธ ํ๋ฆ„์„ ๋ชจ๋‘ ๊ฒ€์ฆํ–ˆ๋‹ค +- [x] ์ฃผ๋ฌธ์€ ์—ฌ๋Ÿฌ ์ƒํ’ˆ์„ ํฌํ•จํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ๊ฐ ์ƒํ’ˆ์˜ ์ˆ˜๋Ÿ‰์„ ๋ช…์‹œํ•œ๋‹ค +- [x] ์ฃผ๋ฌธ ์‹œ ์ƒํ’ˆ์˜ ์žฌ๊ณ  ์ฐจ๊ฐ, ์œ ์ € ํฌ์ธํŠธ ์ฐจ๊ฐ ๋“ฑ์„ ์ˆ˜ํ–‰ํ•œ๋‹ค +- [x] ์žฌ๊ณ  ๋ถ€์กฑ, ํฌ์ธํŠธ ๋ถ€์กฑ ๋“ฑ ์˜ˆ์™ธ ํ๋ฆ„์„ ๊ณ ๋ คํ•ด ์„ค๊ณ„๋˜์—ˆ๋‹ค +- [x] ๋‹จ์œ„ ํ…Œ์ŠคํŠธ์—์„œ ์ •์ƒ ์ฃผ๋ฌธ / ์˜ˆ์™ธ ์ฃผ๋ฌธ ํ๋ฆ„์„ ๋ชจ๋‘ ๊ฒ€์ฆํ–ˆ๋‹ค ### ๐Ÿงฉ ๋„๋ฉ”์ธ ์„œ๋น„์Šค -- [ ] ๋„๋ฉ”์ธ ๊ฐ„ ํ˜‘๋ ฅ ๋กœ์ง์€ Domain Service์— ์œ„์น˜์‹œ์ผฐ๋‹ค -- [ ] ์ƒํ’ˆ ์ƒ์„ธ ์กฐํšŒ ์‹œ Product + Brand ์ •๋ณด ์กฐํ•ฉ์€ ๋„๋ฉ”์ธ ์„œ๋น„์Šค์—์„œ ์ฒ˜๋ฆฌํ–ˆ๋‹ค -- [ ] ๋ณตํ•ฉ ์œ ์Šค์ผ€์ด์Šค๋Š” Application Layer์— ์กด์žฌํ•˜๊ณ , ๋„๋ฉ”์ธ ๋กœ์ง์€ ์œ„์ž„๋˜์—ˆ๋‹ค -- [ ] ๋„๋ฉ”์ธ ์„œ๋น„์Šค๋Š” ์ƒํƒœ ์—†์ด, ๋„๋ฉ”์ธ ๊ฐ์ฒด์˜ ํ˜‘๋ ฅ ์ค‘์‹ฌ์œผ๋กœ ์„ค๊ณ„๋˜์—ˆ๋‹ค +- [x] ๋„๋ฉ”์ธ ๊ฐ„ ํ˜‘๋ ฅ ๋กœ์ง์€ Domain Service์— ์œ„์น˜์‹œ์ผฐ๋‹ค +- [x] ์ƒํ’ˆ ์ƒ์„ธ ์กฐํšŒ ์‹œ Product + Brand ์ •๋ณด ์กฐํ•ฉ์€ ๋„๋ฉ”์ธ ์„œ๋น„์Šค์—์„œ ์ฒ˜๋ฆฌํ–ˆ๋‹ค +- [x] ๋ณตํ•ฉ ์œ ์Šค์ผ€์ด์Šค๋Š” Application Layer์— ์กด์žฌํ•˜๊ณ , ๋„๋ฉ”์ธ ๋กœ์ง์€ ์œ„์ž„๋˜์—ˆ๋‹ค +- [x] ๋„๋ฉ”์ธ ์„œ๋น„์Šค๋Š” ์ƒํƒœ ์—†์ด, ๋„๋ฉ”์ธ ๊ฐ์ฒด์˜ ํ˜‘๋ ฅ ์ค‘์‹ฌ์œผ๋กœ ์„ค๊ณ„๋˜์—ˆ๋‹ค ### **๐Ÿงฑ ์†Œํ”„ํŠธ์›จ์–ด ์•„ํ‚คํ…์ฒ˜ & ์„ค๊ณ„** -- [ ] ์ „์ฒด ํ”„๋กœ์ ํŠธ์˜ ๊ตฌ์„ฑ์€ ์•„๋ž˜ ์•„ํ‚คํ…์ฒ˜๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๊ตฌ์„ฑ๋˜์—ˆ๋‹ค +- [x] ์ „์ฒด ํ”„๋กœ์ ํŠธ์˜ ๊ตฌ์„ฑ์€ ์•„๋ž˜ ์•„ํ‚คํ…์ฒ˜๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๊ตฌ์„ฑ๋˜์—ˆ๋‹ค - Application โ†’ **Domain** โ† Infrastructure -- [ ] Application Layer๋Š” ๋„๋ฉ”์ธ ๊ฐ์ฒด๋ฅผ ์กฐํ•ฉํ•ด ํ๋ฆ„์„ orchestration ํ–ˆ๋‹ค -- [ ] ํ•ต์‹ฌ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์€ Entity, VO, Domain Service ์— ์œ„์น˜ํ•œ๋‹ค -- [ ] Repository Interface๋Š” Domain Layer ์— ์ •์˜๋˜๊ณ , ๊ตฌํ˜„์ฒด๋Š” Infra์— ์œ„์น˜ํ•œ๋‹ค -- [ ] ํŒจํ‚ค์ง€๋Š” ๊ณ„์ธต + ๋„๋ฉ”์ธ ๊ธฐ์ค€์œผ๋กœ ๊ตฌ์„ฑ๋˜์—ˆ๋‹ค (`/domain/order`, `/application/like` ๋“ฑ) -- [ ] ํ…Œ์ŠคํŠธ๋Š” ์™ธ๋ถ€ ์˜์กด์„ฑ์„ ๋ถ„๋ฆฌํ•˜๊ณ , Fake/Stub ๋“ฑ์„ ์‚ฌ์šฉํ•ด ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๊ฐ€ ๊ฐ€๋Šฅํ•˜๊ฒŒ ๊ตฌ์„ฑ๋˜์—ˆ๋‹ค \ No newline at end of file +- [x] Application Layer๋Š” ๋„๋ฉ”์ธ ๊ฐ์ฒด๋ฅผ ์กฐํ•ฉํ•ด ํ๋ฆ„์„ orchestration ํ–ˆ๋‹ค +- [x] ํ•ต์‹ฌ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์€ Entity, VO, Domain Service ์— ์œ„์น˜ํ•œ๋‹ค +- [x] Repository Interface๋Š” Domain Layer ์— ์ •์˜๋˜๊ณ , ๊ตฌํ˜„์ฒด๋Š” Infra์— ์œ„์น˜ํ•œ๋‹ค +- [x] ํŒจํ‚ค์ง€๋Š” ๊ณ„์ธต + ๋„๋ฉ”์ธ ๊ธฐ์ค€์œผ๋กœ ๊ตฌ์„ฑ๋˜์—ˆ๋‹ค (`/domain/order`, `/application/like` ๋“ฑ) +- [x] ํ…Œ์ŠคํŠธ๋Š” ์™ธ๋ถ€ ์˜์กด์„ฑ์„ ๋ถ„๋ฆฌํ•˜๊ณ , Fake/Stub ๋“ฑ์„ ์‚ฌ์šฉํ•ด ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๊ฐ€ ๊ฐ€๋Šฅํ•˜๊ฒŒ ๊ตฌ์„ฑ๋˜์—ˆ๋‹ค \ No newline at end of file From be18c88a093c7086d9683b346c48376d5b6f687f Mon Sep 17 00:00:00 2001 From: BOB <56067193+adminhelper@users.noreply.github.com> Date: Fri, 21 Nov 2025 16:55:58 +0900 Subject: [PATCH 61/85] =?UTF-8?q?Revert=20"[volume-3]=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EB=AA=A8=EB=8D=B8=EB=A7=81=20=EB=B0=8F=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/example/ExampleFacade.java | 17 ++ .../application/example/ExampleInfo.java | 13 ++ .../loopers/application/like/LikeFacade.java | 32 ---- .../application/order/CreateOrderCommand.java | 19 -- .../application/order/OrderFacade.java | 81 --------- .../loopers/application/order/OrderInfo.java | 42 ----- .../application/order/OrderItemCommand.java | 17 -- .../application/order/OrderItemInfo.java | 32 ---- .../application/point/PointFacade.java | 28 --- .../loopers/application/point/PointInfo.java | 13 -- .../product/ProductDetailInfo.java | 32 ---- .../application/product/ProductFacade.java | 47 ----- .../application/product/ProductInfo.java | 33 ---- .../loopers/application/user/UserFacade.java | 27 --- .../loopers/application/user/UserInfo.java | 14 -- .../java/com/loopers/domain/brand/Brand.java | 47 ----- .../loopers/domain/brand/BrandRepository.java | 20 --- .../loopers/domain/brand/BrandService.java | 35 ---- .../loopers/domain/example/ExampleModel.java | 44 +++++ .../domain/example/ExampleRepository.java | 7 + .../domain/example/ExampleService.java | 20 +++ .../java/com/loopers/domain/like/Like.java | 63 ------- .../loopers/domain/like/LikeRepository.java | 25 --- .../com/loopers/domain/like/LikeService.java | 49 ----- .../java/com/loopers/domain/order/Order.java | 86 --------- .../com/loopers/domain/order/OrderItem.java | 91 ---------- .../loopers/domain/order/OrderRepository.java | 21 --- .../loopers/domain/order/OrderService.java | 28 --- .../com/loopers/domain/order/OrderStatus.java | 42 ----- .../java/com/loopers/domain/point/Point.java | 56 ------ .../loopers/domain/point/PointRepository.java | 10 -- .../loopers/domain/point/PointService.java | 43 ----- .../com/loopers/domain/product/Product.java | 120 ------------- .../loopers/domain/product/ProductDetail.java | 45 ----- .../domain/product/ProductDomainService.java | 41 ----- .../domain/product/ProductRepository.java | 29 --- .../domain/product/ProductService.java | 53 ------ .../java/com/loopers/domain/user/User.java | 82 --------- .../loopers/domain/user/UserRepository.java | 10 -- .../com/loopers/domain/user/UserService.java | 30 ---- .../brand/BrandJpaRepository.java | 18 -- .../brand/BrandRepositoryImpl.java | 36 ---- .../example/ExampleJpaRepository.java | 6 + .../example/ExampleRepositoryImpl.java | 19 ++ .../like/LikeJpaRepository.java | 23 --- .../like/LikeRepositoryImpl.java | 46 ----- .../order/OrderJpaRepository.java | 18 -- .../order/OrderRepositoryImpl.java | 36 ---- .../point/PointJpaRepository.java | 11 -- .../point/PointRepositoryImpl.java | 25 --- .../product/ProductJpaRepository.java | 19 -- .../product/ProductRepositoryImpl.java | 59 ------ .../user/UserJpaRepository.java | 11 -- .../user/UserRepositoryImpl.java | 26 --- .../api/example/ExampleV1ApiSpec.java | 19 ++ .../api/example/ExampleV1Controller.java | 28 +++ .../interfaces/api/example/ExampleV1Dto.java | 15 ++ .../interfaces/api/point/PointV1ApiSpec.java | 28 --- .../api/point/PointV1Controller.java | 31 ---- .../interfaces/api/point/PointV1Dto.java | 18 -- .../interfaces/api/user/UserV1ApiSpec.java | 28 --- .../interfaces/api/user/UserV1Controller.java | 31 ---- .../interfaces/api/user/UserV1Dto.java | 24 --- .../com/loopers/domain/brand/BrandTest.java | 42 ----- .../domain/example/ExampleModelTest.java | 65 +++++++ .../ExampleServiceIntegrationTest.java | 72 ++++++++ .../like/LikeServiceIntegrationTest.java | 155 ---------------- .../com/loopers/domain/like/LikeTest.java | 91 ---------- .../order/OrderServiceIntegrationTest.java | 170 ------------------ .../com/loopers/domain/order/OrderTest.java | 122 ------------- .../point/PointServiceIntegrationTest.java | 108 ----------- .../com/loopers/domain/point/PointTest.java | 117 ------------ .../ProductServiceIntegrationTest.java | 43 ----- .../loopers/domain/product/ProductTest.java | 95 ---------- .../user/UserServiceIntegrationTest.java | 112 ------------ .../com/loopers/domain/user/UserTest.java | 53 ------ .../interfaces/api/ExampleV1ApiE2ETest.java | 114 ++++++++++++ .../api/point/PointV1ControllerTest.java | 156 ---------------- .../api/user/UserV1ControllerTest.java | 148 --------------- docs/1round/1round.md | 67 ------- docs/2round/01-requirements.md | 104 ----------- docs/2round/02-sequence-diagrams.md | 164 ----------------- docs/2round/03-class-diagram.md | 78 -------- docs/2round/04-erd.md | 74 -------- docs/2round/2round.md | 37 ---- docs/3round/3round.md | 60 ------- 86 files changed, 439 insertions(+), 3927 deletions(-) create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/example/ExampleFacade.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/example/ExampleInfo.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/application/like/LikeFacade.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/application/order/CreateOrderCommand.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/application/order/OrderInfo.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/application/order/OrderItemCommand.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/application/order/OrderItemInfo.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/application/point/PointInfo.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/application/product/ProductDetailInfo.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/application/product/ProductInfo.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/application/user/UserInfo.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/brand/Brand.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandRepository.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandService.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleModel.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleRepository.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleService.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/like/Like.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/like/LikeRepository.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/like/LikeService.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/order/Order.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItem.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/order/OrderRepository.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/order/OrderStatus.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/point/PointRepository.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/product/ProductDetail.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/product/ProductDomainService.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/product/ProductRepository.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/user/User.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandJpaRepository.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandRepositoryImpl.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/example/ExampleJpaRepository.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/example/ExampleRepositoryImpl.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeJpaRepository.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeRepositoryImpl.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderJpaRepository.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderRepositoryImpl.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointJpaRepository.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointRepositoryImpl.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserJpaRepository.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1ApiSpec.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1Controller.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1Dto.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1ApiSpec.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Controller.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Dto.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1ApiSpec.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java delete mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandTest.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/example/ExampleModelTest.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/example/ExampleServiceIntegrationTest.java delete mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/like/LikeServiceIntegrationTest.java delete mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/like/LikeTest.java delete mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/order/OrderServiceIntegrationTest.java delete mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/order/OrderTest.java delete mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java delete mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/point/PointTest.java delete mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/product/ProductServiceIntegrationTest.java delete mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/product/ProductTest.java delete mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java delete mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/user/UserTest.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/interfaces/api/ExampleV1ApiE2ETest.java delete mode 100644 apps/commerce-api/src/test/java/com/loopers/interfaces/api/point/PointV1ControllerTest.java delete mode 100644 apps/commerce-api/src/test/java/com/loopers/interfaces/api/user/UserV1ControllerTest.java delete mode 100644 docs/1round/1round.md delete mode 100644 docs/2round/01-requirements.md delete mode 100644 docs/2round/02-sequence-diagrams.md delete mode 100644 docs/2round/03-class-diagram.md delete mode 100644 docs/2round/04-erd.md delete mode 100644 docs/2round/2round.md delete mode 100644 docs/3round/3round.md diff --git a/apps/commerce-api/src/main/java/com/loopers/application/example/ExampleFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/example/ExampleFacade.java new file mode 100644 index 000000000..552a9ad62 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/example/ExampleFacade.java @@ -0,0 +1,17 @@ +package com.loopers.application.example; + +import com.loopers.domain.example.ExampleModel; +import com.loopers.domain.example.ExampleService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class ExampleFacade { + private final ExampleService exampleService; + + public ExampleInfo getExample(Long id) { + ExampleModel example = exampleService.getExample(id); + return ExampleInfo.from(example); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/example/ExampleInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/example/ExampleInfo.java new file mode 100644 index 000000000..877aba96c --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/example/ExampleInfo.java @@ -0,0 +1,13 @@ +package com.loopers.application.example; + +import com.loopers.domain.example.ExampleModel; + +public record ExampleInfo(Long id, String name, String description) { + public static ExampleInfo from(ExampleModel model) { + return new ExampleInfo( + model.getId(), + model.getName(), + model.getDescription() + ); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/like/LikeFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/like/LikeFacade.java deleted file mode 100644 index d9dd33205..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/application/like/LikeFacade.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.loopers.application.like; - -import com.loopers.domain.like.LikeService; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; - -/** - * packageName : com.loopers.application.like - * fileName : LikeFacade - * author : byeonsungmun - * date : 2025. 11. 14. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 14. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -@Component -@RequiredArgsConstructor -public class LikeFacade { - - private final LikeService likeService; - - public void createLike(String userId, Long productId) { - likeService.like(userId, productId); - } - - public void deleteLike(String userId, Long productId) { - likeService.unlike(userId, productId); - } -} - diff --git a/apps/commerce-api/src/main/java/com/loopers/application/order/CreateOrderCommand.java b/apps/commerce-api/src/main/java/com/loopers/application/order/CreateOrderCommand.java deleted file mode 100644 index 683e39cdd..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/application/order/CreateOrderCommand.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.loopers.application.order; - -import java.util.List; - -/** - * packageName : com.loopers.application.order - * fileName : CreateOrderCommand - * author : byeonsungmun - * date : 2025. 11. 14. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 14. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -public record CreateOrderCommand( - String userId, - List items -) {} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java deleted file mode 100644 index 2fba4b4aa..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java +++ /dev/null @@ -1,81 +0,0 @@ -package com.loopers.application.order; - -import com.loopers.domain.order.Order; -import com.loopers.domain.order.OrderItem; -import com.loopers.domain.order.OrderService; -import com.loopers.domain.order.OrderStatus; -import com.loopers.domain.point.PointService; -import com.loopers.domain.product.Product; -import com.loopers.domain.product.ProductService; -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; - -/** - * packageName : com.loopers.application.order - * fileName : OrderFacade - * author : byeonsungmun - * date : 2025. 11. 13. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 13. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ - -@Slf4j -@Component -@RequiredArgsConstructor -public class OrderFacade { - - private final OrderService orderService; - private final ProductService productService; - private final PointService pointService; - - @Transactional - public OrderInfo createOrder(CreateOrderCommand command) { - - if (command == null || command.items() == null || command.items().isEmpty()) { - throw new CoreException(ErrorType.BAD_REQUEST, "์ƒํ’ˆ ์ •๋ณด๊ฐ€ ๋น„์–ด์žˆ์Šต๋‹ˆ๋‹ค"); - } - - Order order = Order.create(command.userId()); - - for (OrderItemCommand itemCommand : command.items()) { - - //์ƒํ’ˆ๊ฐ€์ ธ์˜ค๊ณ  - Product product = productService.getProduct(itemCommand.productId()); - - // ์žฌ๊ณ ๊ฐ์†Œ - product.decreaseStock(itemCommand.quantity()); - - // OrderItem์ƒ์„ฑ - OrderItem orderItem = OrderItem.create( - product.getId(), - product.getName(), - itemCommand.quantity(), - product.getPrice()); - - order.addOrderItem(orderItem); - orderItem.setOrder(order); - } - - //์ด ๊ฐ€๊ฒฉ๊ตฌํ•˜๊ณ  - long totalAmount = order.getOrderItems().stream() - .mapToLong(OrderItem::getAmount) - .sum(); - - order.updateTotalAmount(totalAmount); - - pointService.usePoint(command.userId(), totalAmount); - - //์ €์žฅ - Order saved = orderService.createOrder(order); - saved.updateStatus(OrderStatus.COMPLETE); - - return OrderInfo.from(saved); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderInfo.java deleted file mode 100644 index 70028c27c..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderInfo.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.loopers.application.order; - -import com.loopers.domain.order.Order; -import com.loopers.domain.order.OrderStatus; - -import java.time.LocalDateTime; -import java.util.List; - -/** - * packageName : com.loopers.application.order - * fileName : OrderInfo - * author : byeonsungmun - * date : 2025. 11. 14. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 14. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -public record OrderInfo( - Long orderId, - String userId, - Long totalAmount, - OrderStatus status, - LocalDateTime createdAt, - List items -) { - public static OrderInfo from(Order order) { - List itemInfos = order.getOrderItems().stream() - .map(OrderItemInfo::from) - .toList(); - - return new OrderInfo( - order.getId(), - order.getUserId(), - order.getTotalAmount(), - order.getStatus(), - order.getCreatedAt(), - itemInfos - ); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderItemCommand.java b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderItemCommand.java deleted file mode 100644 index 1ac46862f..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderItemCommand.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.loopers.application.order; - -/** - * packageName : com.loopers.application.order - * fileName : OrderItemCommand - * author : byeonsungmun - * date : 2025. 11. 14. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 14. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -public record OrderItemCommand( - Long productId, - Long quantity -) {} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderItemInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderItemInfo.java deleted file mode 100644 index b3f2359c6..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderItemInfo.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.loopers.application.order; - -import com.loopers.domain.order.OrderItem; - -/** - * packageName : com.loopers.application.order - * fileName : OrderInfo - * author : byeonsungmun - * date : 2025. 11. 13. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 13. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -public record OrderItemInfo( - Long productId, - String productName, - Long quantity, - Long price, - Long amount -) { - public static OrderItemInfo from(OrderItem item) { - return new OrderItemInfo( - item.getProductId(), - item.getProductName(), - item.getQuantity(), - item.getPrice(), - item.getAmount() - ); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java deleted file mode 100644 index 009be1cec..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.loopers.application.point; - -import com.loopers.domain.point.Point; -import com.loopers.domain.point.PointService; -import com.loopers.interfaces.api.point.PointV1Dto; -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; - -@RequiredArgsConstructor -@Component -public class PointFacade { - private final PointService pointService; - - public PointInfo getPoint(String userId) { - Point point = pointService.findPointByUserId(userId); - - if (point == null) { - throw new CoreException(ErrorType.NOT_FOUND, "์‚ฌ์šฉ์ž ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); - } - return PointInfo.from(point); - } - - public PointInfo chargePoint(PointV1Dto.ChargePointRequest request) { - return PointInfo.from(pointService.chargePoint(request.userId(), request.chargeAmount())); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/point/PointInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/point/PointInfo.java deleted file mode 100644 index 65497297b..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/application/point/PointInfo.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.loopers.application.point; - -import com.loopers.domain.point.Point; - -public record PointInfo(String userId, Long amount) { - public static PointInfo from(Point info) { - return new PointInfo( - info.getUserId(), - info.getBalance() - ); - } - -} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/product/ProductDetailInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductDetailInfo.java deleted file mode 100644 index 2a9ecee27..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/application/product/ProductDetailInfo.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.loopers.application.product; - -import com.loopers.domain.product.ProductDetail; - -/** - * packageName : com.loopers.application.product - * fileName : ProductDetail - * author : byeonsungmun - * date : 2025. 11. 13. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 13. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -public record ProductDetailInfo( - Long id, - String name, - String brandName, - Long price, - Long likeCount -) { - public static ProductDetailInfo from(ProductDetail productDetail) { - return new ProductDetailInfo( - productDetail.getId(), - productDetail.getName(), - productDetail.getBrandName(), - productDetail.getPrice(), - productDetail.getLikeCount() - ); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java deleted file mode 100644 index e6a25de23..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.loopers.application.product; - -import com.loopers.domain.brand.Brand; -import com.loopers.domain.brand.BrandService; -import com.loopers.domain.like.LikeService; -import com.loopers.domain.product.ProductDetail; -import com.loopers.domain.product.ProductDomainService; -import com.loopers.domain.product.ProductService; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Component; - -/** - * packageName : com.loopers.application.product - * fileName : ProdcutFacade - * author : byeonsungmun - * date : 2025. 11. 10. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 10. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -@Component -@RequiredArgsConstructor -public class ProductFacade { - - private final ProductService productService; - private final BrandService brandService; - private final LikeService likeService; - private final ProductDomainService productDomainService; - - public Page getProducts(String sort, Pageable pageable) { - return productService.getProducts(sort ,pageable) - .map(product -> { - Brand brand = brandService.getBrand(product.getBrandId()); - long likeCount = likeService.countByProductId(product.getId()); - return ProductInfo.of(product, brand, likeCount); - }); - } - - public ProductDetailInfo getProduct(Long id) { - ProductDetail productDetail = productDomainService.getProductDetail(id); - return ProductDetailInfo.from(productDetail); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/product/ProductInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductInfo.java deleted file mode 100644 index 8bcd93dd8..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/application/product/ProductInfo.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.loopers.application.product; - -import com.loopers.domain.brand.Brand; -import com.loopers.domain.product.Product; - -/** - * packageName : com.loopers.application.product - * fileName : ProductInfo - * author : byeonsungmun - * date : 2025. 11. 10. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 10. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -public record ProductInfo( - Long id, - String name, - String brandName, - Long price, - Long likeCount -) { - public static ProductInfo of(Product product, Brand brand, Long likeCount) { - return new ProductInfo( - product.getId(), - product.getName(), - brand.getName(), - product.getPrice(), - likeCount - ); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java deleted file mode 100644 index f42bd5206..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.loopers.application.user; - -import com.loopers.domain.user.User; -import com.loopers.domain.user.UserService; -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; - -@RequiredArgsConstructor -@Component -public class UserFacade { - private final UserService userService; - - public UserInfo register(String userId, String email, String birth, String gender) { - User user = userService.register(userId, email, birth, gender); - return UserInfo.from(user); - } - - public UserInfo getUser(String userId) { - User user = userService.findUserByUserId(userId); - if (user == null) { - throw new CoreException(ErrorType.NOT_FOUND, "์‚ฌ์šฉ์ž ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); - } - return UserInfo.from(user); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/user/UserInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/user/UserInfo.java deleted file mode 100644 index 08f5cea43..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/application/user/UserInfo.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.loopers.application.user; - -import com.loopers.domain.user.User; - -public record UserInfo(String userId, String email, String birth, String gender) { - public static UserInfo from(User user) { - return new UserInfo( - user.getUserId(), - user.getEmail(), - user.getBirth(), - user.getGender() - ); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/brand/Brand.java b/apps/commerce-api/src/main/java/com/loopers/domain/brand/Brand.java deleted file mode 100644 index d334ccebf..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/brand/Brand.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.loopers.domain.brand; - -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import jakarta.persistence.*; -import lombok.Getter; - -/** - * packageName : com.loopers.domain.brand - * fileName : Brand - * author : byeonsungmun - * date : 2025. 11. 11. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 11. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -@Entity -@Table(name = "brand") -@Getter -public class Brand { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(nullable = false) - private String name; - - protected Brand() {} - - private Brand(String name) { - this.name = requireValidName(name); - } - - public static Brand create(String name) { - return new Brand(name); - } - - - private String requireValidName(String name) { - if (name == null || name.isEmpty()) { - throw new CoreException(ErrorType.BAD_REQUEST, "๋ธŒ๋žœ๋“œ๋ช…์€ ๋น„์–ด ์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); - } - return name.trim(); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandRepository.java deleted file mode 100644 index c558b23fc..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandRepository.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.loopers.domain.brand; - -import java.util.Optional; - -/** - * packageName : com.loopers.domain.brand - * fileName : BrandRepository - * author : byeonsungmun - * date : 2025. 11. 12. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 12. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -public interface BrandRepository { - Optional findById(Long id); - - void save(Brand brand); -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandService.java b/apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandService.java deleted file mode 100644 index e0f58c77b..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandService.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.loopers.domain.brand; - -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; - -/** - * packageName : com.loopers.domain.brand - * fileName : BrandService - * author : byeonsungmun - * date : 2025. 11. 12. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 12. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -@Component -@RequiredArgsConstructor -public class BrandService { - - private final BrandRepository brandRepository; - - public void save(Brand brand) { - brandRepository.save(brand); - } - - @Transactional(readOnly = true) - public Brand getBrand(Long id) { - return brandRepository.findById(id) - .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "๋ธŒ๋žœ๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleModel.java b/apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleModel.java new file mode 100644 index 000000000..c588c4a8a --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleModel.java @@ -0,0 +1,44 @@ +package com.loopers.domain.example; + +import com.loopers.domain.BaseEntity; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; + +@Entity +@Table(name = "example") +public class ExampleModel extends BaseEntity { + + private String name; + private String description; + + protected ExampleModel() {} + + public ExampleModel(String name, String description) { + if (name == null || name.isBlank()) { + throw new CoreException(ErrorType.BAD_REQUEST, "์ด๋ฆ„์€ ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + if (description == null || description.isBlank()) { + throw new CoreException(ErrorType.BAD_REQUEST, "์„ค๋ช…์€ ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + + this.name = name; + this.description = description; + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public void update(String newDescription) { + if (newDescription == null || newDescription.isBlank()) { + throw new CoreException(ErrorType.BAD_REQUEST, "์„ค๋ช…์€ ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + this.description = newDescription; + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleRepository.java new file mode 100644 index 000000000..3625e5662 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleRepository.java @@ -0,0 +1,7 @@ +package com.loopers.domain.example; + +import java.util.Optional; + +public interface ExampleRepository { + Optional find(Long id); +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleService.java b/apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleService.java new file mode 100644 index 000000000..c0e8431e8 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleService.java @@ -0,0 +1,20 @@ +package com.loopers.domain.example; + +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Component +public class ExampleService { + + private final ExampleRepository exampleRepository; + + @Transactional(readOnly = true) + public ExampleModel getExample(Long id) { + return exampleRepository.find(id) + .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "[id = " + id + "] ์˜ˆ์‹œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/like/Like.java b/apps/commerce-api/src/main/java/com/loopers/domain/like/Like.java deleted file mode 100644 index 4430b496a..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/like/Like.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.loopers.domain.like; - -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import jakarta.persistence.*; -import lombok.Getter; - -import java.time.LocalDateTime; - -/** - * packageName : com.loopers.domain.like - * fileName : Like - * author : byeonsungmun - * date : 2025. 11. 11. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 11. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -@Entity -@Table(name = "product_like") -@Getter -public class Like { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(name = "ref_user_id", nullable = false) - private String userId; - - @Column(name = "ref_product_id", nullable = false) - private Long productId; - - @Column(nullable = false) - private LocalDateTime createdAt; - - protected Like() {} - - private Like(String userId, Long productId) { - this.userId = requireValidUserId(userId); - this.productId = requireValidProductId(productId); - this.createdAt = LocalDateTime.now(); - } - - public static Like create(String userId, Long productId) { - return new Like(userId, productId); - } - - private String requireValidUserId(String userId) { - if (userId == null || userId.isEmpty()) { - throw new CoreException(ErrorType.BAD_REQUEST, "์‚ฌ์šฉ์ž ID๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."); - } - return userId; - } - - private Long requireValidProductId(Long productId) { - if (productId == null || productId <= 0) { - throw new CoreException(ErrorType.BAD_REQUEST, "์ƒํ’ˆ ID๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."); - } - return productId; - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/like/LikeRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/like/LikeRepository.java deleted file mode 100644 index 945b10235..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/like/LikeRepository.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.loopers.domain.like; - -import java.util.Optional; - -/** - * packageName : com.loopers.domain.like - * fileName : LikeRepository - * author : byeonsungmun - * date : 2025. 11. 12. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 12. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -public interface LikeRepository { - - Optional findByUserIdAndProductId(String userId, Long productId); - - void save(Like like); - - void delete(Like like); - - long countByProductId(Long productId); -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/like/LikeService.java b/apps/commerce-api/src/main/java/com/loopers/domain/like/LikeService.java deleted file mode 100644 index 41ae90b6a..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/like/LikeService.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.loopers.domain.like; - -import com.loopers.domain.product.ProductRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; - -/** - * packageName : com.loopers.application.like - * fileName : LikeService - * author : byeonsungmun - * date : 2025. 11. 12. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 12. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -@Component -@RequiredArgsConstructor -public class LikeService { - - private final LikeRepository likeRepository; - private final ProductRepository productRepository; - - @Transactional - public void like(String userId, Long productId) { - if (likeRepository.findByUserIdAndProductId(userId, productId).isPresent()) return; - - Like like = Like.create(userId, productId); - likeRepository.save(like); - productRepository.incrementLikeCount(productId); - } - - @Transactional - public void unlike(String userId, Long productId) { - likeRepository.findByUserIdAndProductId(userId, productId) - .ifPresent(like -> { - likeRepository.delete(like); - productRepository.decrementLikeCount(productId); - }); - } - - @Transactional(readOnly = true) - public long countByProductId(Long productId) { - return likeRepository.countByProductId(productId); - } - -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/Order.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/Order.java deleted file mode 100644 index 84f299c6b..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/order/Order.java +++ /dev/null @@ -1,86 +0,0 @@ -package com.loopers.domain.order; - -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import jakarta.persistence.*; -import lombok.Getter; - -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; - -/** - * packageName : com.loopers.domain.order - * fileName : Order - * author : byeonsungmun - * date : 2025. 11. 11. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 11. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -@Entity -@Table(name = "orders") -@Getter -public class Order { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(name = "ref_user_id", nullable = false) - private String userId; - - @Column(nullable = false) - private Long totalAmount; - - @Enumerated(EnumType.STRING) - private OrderStatus status; - - @Column(nullable = false) - private LocalDateTime createdAt; - - @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true) - private List orderItems = new ArrayList<>(); - - protected Order() {} - - private Order(String userId, OrderStatus status) { - this.userId = requiredValidUserId(userId); - this.totalAmount = 0L; - this.status = requiredValidStatus(status); - this.createdAt = LocalDateTime.now(); - } - - public static Order create(String userId) { - return new Order(userId, OrderStatus.PENDING); - } - - public void addOrderItem(OrderItem orderItem) { - orderItem.setOrder(this); - this.orderItems.add(orderItem); - } - - private OrderStatus requiredValidStatus(OrderStatus status) { - if (status == null) { - throw new CoreException(ErrorType.BAD_REQUEST, "์ฃผ๋ฌธ ์ƒํƒœ๋Š” ํ•„์ˆ˜ ์ž…๋‹ˆ๋‹ค."); - } - return status; - } - - private String requiredValidUserId(String userId) { - if (userId == null || userId.isEmpty()) { - throw new CoreException(ErrorType.BAD_REQUEST, "์‚ฌ์šฉ์ž ID๋Š” ํ•„์ˆ˜ ์ž…๋‹ˆ๋‹ค."); - } - return userId; - } - - public void updateTotalAmount(long totalAmount) { - this.totalAmount = totalAmount; - } - - public void updateStatus(OrderStatus status) { - this.status = status; - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItem.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItem.java deleted file mode 100644 index dce97a44a..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItem.java +++ /dev/null @@ -1,91 +0,0 @@ -package com.loopers.domain.order; - -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import jakarta.persistence.*; -import lombok.Getter; -import lombok.Setter; - -/** - * packageName : com.loopers.domain.order - * fileName : OrderItem - * author : byeonsungmun - * date : 2025. 11. 11. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 11. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ - -@Entity -@Table(name = "order_item") -@Getter -public class OrderItem { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Setter - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "order_id", nullable = false) - private Order order; - - @Column(name = "ref_product_id", nullable = false) - private Long productId; - - @Column(name = "ref_product_name", nullable = false) - private String productName; - - @Column(nullable = false) - private Long quantity; - - @Column(nullable = false) - private Long price; - - protected OrderItem() {} - - private OrderItem(Long productId, String productName, Long quantity, Long price) { - this.productId = requiredValidProductId(productId); - this.productName = requiredValidProductName(productName); - this.quantity = requiredQuantity(quantity); - this.price = requiredPrice(price); - } - - public static OrderItem create(Long productId, String productName, Long quantity, Long price) { - return new OrderItem(productId, productName, quantity, price); - } - - public Long getAmount() { - return quantity * price; - } - - private Long requiredValidProductId(Long productId) { - if (productId == null || productId <= 0) { - throw new CoreException(ErrorType.BAD_REQUEST, "์ƒํ’ˆ ID๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."); - } - return productId; - } - - private String requiredValidProductName(String productName) { - if (productName == null || productName.isEmpty()) { - throw new CoreException(ErrorType.BAD_REQUEST, "์ƒํ’ˆ๋ช…์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."); - } - return productName; - } - - private Long requiredQuantity(Long quantity) { - if (quantity == null || quantity <= 0) { - throw new CoreException(ErrorType.BAD_REQUEST, "์ˆ˜๋Ÿ‰์€ 1๊ฐœ ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); - } - return quantity; - } - - private Long requiredPrice(Long price) { - if (price == null || price < 0) { - throw new CoreException(ErrorType.BAD_REQUEST, "๊ฐ€๊ฒฉ์€ 0์› ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); - } - return price; - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderRepository.java deleted file mode 100644 index c80262041..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderRepository.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.loopers.domain.order; - -import java.util.Optional; - -/** - * packageName : com.loopers.domain.order - * fileName : OrderRepository - * author : byeonsungmun - * date : 2025. 11. 13. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 13. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -public interface OrderRepository { - - Order save(Order order); - - Optional findById(Long orderId); -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java deleted file mode 100644 index a66be03d3..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.loopers.domain.order; - -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; - -/** - * packageName : com.loopers.domain.order - * fileName : OrderService - * author : byeonsungmun - * date : 2025. 11. 13. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 13. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -@Component -@RequiredArgsConstructor -public class OrderService { - - private final OrderRepository orderRepository; - - @Transactional - public Order createOrder(Order order) { - return orderRepository.save(order); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderStatus.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderStatus.java deleted file mode 100644 index 14ea592ef..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderStatus.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.loopers.domain.order; - -/** - * packageName : com.loopers.domain.order - * fileName : OrderStatus - * author : byeonsungmun - * date : 2025. 11. 11. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 11. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -public enum OrderStatus { - - COMPLETE("๊ฒฐ์ œ์„ฑ๊ณต"), - CANCEL("๊ฒฐ์ œ์ทจ์†Œ"), - FAIL("๊ฒฐ์ œ์‹คํŒจ"), - PENDING("๊ฒฐ์ œ์ค‘"); - - private final String description; - - OrderStatus(String description) { - this.description = description; - } - - public boolean isCompleted() { - return this == COMPLETE; - } - - public boolean isPending() { - return this == PENDING; - } - - public boolean isCanceled() { - return this == CANCEL; - } - - public String description() { - return description; - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java b/apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java deleted file mode 100644 index bc28a902a..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.loopers.domain.point; - -import com.loopers.domain.BaseEntity; -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import jakarta.persistence.*; -import lombok.Getter; - -@Entity -@Table(name = "point") -@Getter -public class Point extends BaseEntity { - - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Id - private Long id; - - private String userId; - - private Long balance; - - protected Point() {} - - private Point(String userId, Long balance) { - this.userId = requireValidUserId(userId); - this.balance = balance; - } - - public static Point create(String userId, Long balance) { - return new Point(userId, balance); - } - - String requireValidUserId(String userId) { - if(userId == null || userId.isEmpty()) { - throw new CoreException(ErrorType.BAD_REQUEST, "์‚ฌ์šฉ์ž ID๊ฐ€ ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); - } - return userId; - } - - public void charge(Long chargeAmount) { - if (chargeAmount == null || chargeAmount <= 0) { - throw new CoreException(ErrorType.BAD_REQUEST, "0์› ์ดํ•˜๋กœ ํฌ์ธํŠธ๋ฅผ ์ถฉ์ „ ํ• ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); - } - this.balance += chargeAmount; - } - - public void use(Long useAmount) { - if (useAmount == null || useAmount <= 0) { - throw new CoreException(ErrorType.BAD_REQUEST, "0์› ์ดํ•˜๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); - } - if (this.balance < useAmount) { - throw new CoreException(ErrorType.BAD_REQUEST, "ํฌ์ธํŠธ๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค."); - } - this.balance -= useAmount; - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/point/PointRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/point/PointRepository.java deleted file mode 100644 index 314022491..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/point/PointRepository.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.loopers.domain.point; - -import java.util.Optional; - -public interface PointRepository { - - Optional findByUserId(String userId); - - Point save(Point point); -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java b/apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java deleted file mode 100644 index 9c9570615..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.loopers.domain.point; - -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; - -@RequiredArgsConstructor -@Component -public class PointService { - - private final PointRepository pointRepository; - - @Transactional(readOnly = true) - public Point findPointByUserId(String userId) { - return pointRepository.findByUserId(userId).orElse(null); - } - - @Transactional - public Point chargePoint(String userId, Long chargeAmount) { - Point point = pointRepository.findByUserId(userId).orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "ํฌ์ธํŠธ๋ฅผ ์ถฉ์ „ํ• ์ˆ˜ ์—†๋Š” ์‚ฌ์šฉ์ž์ž…๋‹ˆ๋‹ค.")); - point.charge(chargeAmount); - return pointRepository.save(point); - } - - @Transactional - public Point usePoint(String userId, Long useAmount) { - Point point = pointRepository.findByUserId(userId) - .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "ํฌ์ธํŠธ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); - - if (useAmount == null || useAmount <= 0) { - throw new CoreException(ErrorType.BAD_REQUEST, "์ฐจ๊ฐํ•  ํฌ์ธํŠธ๋Š” 1 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); - } - - if (point.getBalance() < useAmount) { - throw new CoreException(ErrorType.BAD_REQUEST, "ํฌ์ธํŠธ๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค."); - } - - point.use(useAmount); - return pointRepository.save(point); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java deleted file mode 100644 index 29968402f..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java +++ /dev/null @@ -1,120 +0,0 @@ -package com.loopers.domain.product; - -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import jakarta.persistence.*; -import lombok.Getter; - -/** - * packageName : com.loopers.domain.product - * fileName : Product - * author : byeonsungmun - * date : 2025. 11. 10. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 10. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ - -@Entity -@Table(name = "product") -@Getter -public class Product { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(name = "ref_brand_id", nullable = false) - private Long brandId; - - @Column(nullable = false) - private String name; - - @Column(nullable = false) - private Long price; - - @Column - private Long likeCount; - - @Column(nullable = false) - private Long stock; - - protected Product() {} - - private Product(Long brandId, String name, Long price, Long likeCount, Long stock) { - this.brandId = requireValidBrandId(brandId); - this.name = requireValidName(name); - this.price = requireValidPrice(price); - this.likeCount = requireValidLikeCount(likeCount); - this.stock = requireValidStock(stock); - } - - public static Product create(Long brandId, String name, Long price, Long stock) { - return new Product( - brandId, - name, - price, - 0L, - stock - ); - } - - private Long requireValidBrandId(Long brandId) { - if (brandId == null || brandId <= 0) { - throw new CoreException(ErrorType.BAD_REQUEST, "๋ธŒ๋žœ๋“œ ID๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."); - } - - return brandId; - } - - private String requireValidName(String name) { - if (name == null || name.isEmpty()) { - throw new CoreException(ErrorType.BAD_REQUEST, "์ƒํ’ˆ๋ช…์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."); - } - return name; - } - - private Long requireValidPrice(Long price) { - if (price == null || price < 0) { - throw new CoreException(ErrorType.BAD_REQUEST, "์ƒํ’ˆ ๊ฐ€๊ฒฉ์€ 0์› ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); - } - return price; - } - - private Long requireValidLikeCount(Long likeCount) { - if (likeCount == null || likeCount < 0) { - throw new CoreException(ErrorType.BAD_REQUEST, "์ข‹์•„์š” ๊ฐœ์ˆ˜๋Š” 0๊ฐœ ๋ฏธ๋งŒ์œผ๋กœ ์„ค์ •ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); - } - return likeCount; - } - - private Long requireValidStock(Long stock) { - if (stock == null) { - throw new CoreException(ErrorType.BAD_REQUEST, "์ƒํ’ˆ ์žฌ๊ณ ๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."); - } - if (stock < 0) { - throw new CoreException(ErrorType.BAD_REQUEST, "์ƒํ’ˆ ์žฌ๊ณ ๋Š” 0 ๋ฏธ๋งŒ์œผ๋กœ ์„ค์ •ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); - } - return stock; - } - - public void increaseLikeCount() { - this.likeCount++; - } - - public void decreaseLikeCount() { - if (this.likeCount > 0) this.likeCount--; - } - - public void decreaseStock(Long quantity) { - if (quantity <= 0) { - throw new CoreException(ErrorType.BAD_REQUEST, "์ฐจ๊ฐ ์ˆ˜๋Ÿ‰์€ 0๋ณด๋‹ค ์ปค์•ผ ํ•ฉ๋‹ˆ๋‹ค."); - } - if (this.stock - quantity < 0) { - throw new CoreException(ErrorType.BAD_REQUEST, "์žฌ๊ณ ๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค."); - } - this.stock -= quantity; - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductDetail.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductDetail.java deleted file mode 100644 index 808bff196..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductDetail.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.loopers.domain.product; - -import com.loopers.domain.brand.Brand; -import lombok.Getter; - -/** - * packageName : com.loopers.domain.product - * fileName : ProductDetail - * author : byeonsungmun - * date : 2025. 11. 13. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 13. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -@Getter -public class ProductDetail { - - private Long id; - private String name; - private String brandName; - private Long price; - private Long likeCount; - - protected ProductDetail() {} - - private ProductDetail(Long id, String name, String brandName, Long price, Long likeCount) { - this.id = id; - this.name = name; - this.brandName = brandName; - this.price = price; - this.likeCount = likeCount; - } - - public static ProductDetail of(Product product, Brand brand, Long likeCount) { - return new ProductDetail( - product.getId(), - product.getName(), - brand.getName(), - product.getPrice(), - likeCount - ); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductDomainService.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductDomainService.java deleted file mode 100644 index 166aff66b..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductDomainService.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.loopers.domain.product; - -import com.loopers.domain.brand.Brand; -import com.loopers.domain.brand.BrandRepository; -import com.loopers.domain.like.LikeRepository; -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; - -/** - * packageName : com.loopers.domain.product - * fileName : ProductDetailService - * author : byeonsungmun - * date : 2025. 11. 13. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 13. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -@Component -@RequiredArgsConstructor -public class ProductDomainService { - - private final ProductRepository productRepository; - private final BrandRepository brandRepository; - private final LikeRepository likeRepository; - - @Transactional(readOnly = true) - public ProductDetail getProductDetail(Long id) { - Product product = productRepository.findById(id) - .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "์ƒํ’ˆ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); - Brand brand = brandRepository.findById(product.getBrandId()) - .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "๋ธŒ๋žœ๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค")); - long likeCount = likeRepository.countByProductId(id); - - return ProductDetail.of(product, brand, likeCount); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductRepository.java deleted file mode 100644 index dadda62a0..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductRepository.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.loopers.domain.product; - -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; - -import java.util.Optional; - -/** - * packageName : com.loopers.domain.product - * fileName : ProductRepositroy - * author : byeonsungmun - * date : 2025. 11. 12. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 12. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -public interface ProductRepository { - Page findAll(Pageable pageable); - - Optional findById(Long id); - - void incrementLikeCount(Long productId); - - void decrementLikeCount(Long productId); - - Product save(Product product); -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java deleted file mode 100644 index 067f194ae..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.loopers.domain.product; - -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; - -/** - * packageName : com.loopers.domain.product - * fileName : ProductService - * author : byeonsungmun - * date : 2025. 11. 12. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 12. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ - -@Component -@RequiredArgsConstructor -public class ProductService { - - private final ProductRepository productRepository; - - @Transactional(readOnly = true) - public Page getProducts(String sort, Pageable pageable) { - Sort sortOption = switch (sort) { - case "price_asc" -> Sort.by("price").ascending(); - case "likes_desc" -> Sort.by("likeCount").descending(); - default -> Sort.by("createdAt").descending(); // latest - }; - - Pageable sortedPageable = PageRequest.of( - pageable.getPageNumber(), - pageable.getPageSize(), - sortOption - ); - - return productRepository.findAll(sortedPageable); - } - - public Product getProduct(Long productId) { - return productRepository.findById(productId) - .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "ํ•ด๋‹น ์ƒํ’ˆ์ด ์—†์Šต๋‹ˆ๋‹ค")); - } -} - diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java deleted file mode 100644 index 287b84cf8..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java +++ /dev/null @@ -1,82 +0,0 @@ -package com.loopers.domain.user; - -import com.loopers.domain.BaseEntity; -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.Table; -import lombok.Getter; - -import java.util.regex.Pattern; - -@Entity -@Table(name = "user") -@Getter -public class User extends BaseEntity { - - private static final Pattern USERID_PATTERN = Pattern.compile("^[a-zA-Z0-9]{1,10}$"); - private static final Pattern BIRTH_PATTERN = Pattern.compile("^\\d{4}-\\d{2}-\\d{2}$"); - private static final Pattern EMAIL_PATTERN = Pattern.compile("^[\\w.+-]+@[\\w.-]+\\.[a-zA-Z]{2,}$"); - - @Column(unique = true, nullable = false) - private String userId; - - @Column(nullable = false) - private String email; - - @Column(nullable = false) - private String birth; - - @Column(nullable = false) - private String gender; - - protected User() {} - - public User(String userId, String email, String birth, String gender) { - this.userId = requireValidUserId(userId); - this.email = requireValidEmail(email); - this.birth = requireValidBirthDate(birth); - this.gender = requireValidGender(gender); - } - - String requireValidUserId(String userId) { - if(userId == null || userId.isEmpty()) { - throw new CoreException(ErrorType.BAD_REQUEST, "์‚ฌ์šฉ์ž ID๊ฐ€ ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); - } - - if (!USERID_PATTERN.matcher(userId).matches()) { - throw new CoreException(ErrorType.BAD_REQUEST, "์‚ฌ์šฉ์ž ID๊ฐ€ ์˜๋ฌธ ๋ฐ ์ˆซ์ž 10์ž ์ด๋‚ด์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค."); - } - return userId; - } - - String requireValidEmail(String email) { - if(email == null || email.isBlank()) { - throw new CoreException(ErrorType.BAD_REQUEST, "์ด๋ฉ”์ผ์ด ๋น„์–ด ์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); - } - - if(!EMAIL_PATTERN.matcher(email).matches()) { - throw new CoreException(ErrorType.BAD_REQUEST, "์ด๋ฉ”์ผ ํ˜•์‹์— ๋งž์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ex)xx@yy.zz"); - } - return email; - } - - String requireValidBirthDate(String birth) { - if (birth == null || birth.isBlank()) { - throw new CoreException(ErrorType.BAD_REQUEST, "์ƒ๋…„์›”์ผ์ด ๋น„์–ด ์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); - } - - if(!BIRTH_PATTERN.matcher(birth).matches()) { - throw new CoreException(ErrorType.BAD_REQUEST, "์ƒ๋…„์›”์ผ ํ˜•์‹์ด ๋งž์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); - } - return birth; - } - - String requireValidGender(String gender) { - if(gender == null || gender.isBlank()) { - throw new CoreException(ErrorType.BAD_REQUEST, "์„ฑ๋ณ„์ด ๋น„์–ด์žˆ์Šต๋‹ˆ๋‹ค."); - } - return gender; - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java deleted file mode 100644 index f4b26266e..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.loopers.domain.user; - -import java.util.Optional; - -public interface UserRepository { - - Optional findByUserId(String userId); - - User save(User user); -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java deleted file mode 100644 index 3cc033076..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.loopers.domain.user; - -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; - -@Component -@RequiredArgsConstructor -public class UserService { - - private final UserRepository userRepository; - - @Transactional - public User register(String userId, String email, String birth, String gender) { - userRepository.findByUserId(userId).ifPresent(user -> { - throw new CoreException(ErrorType.CONFLICT, "์ด๋ฏธ ๊ฐ€์ž…๋œ ์‚ฌ์šฉ์ž ID ์ž…๋‹ˆ๋‹ค."); - }); - - User user = new User(userId, email, birth, gender); - return userRepository.save(user); - } - - @Transactional(readOnly = true) - public User findUserByUserId(String userId) { - return userRepository.findByUserId(userId).orElse(null); - } - -} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandJpaRepository.java deleted file mode 100644 index 759f3caf1..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandJpaRepository.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.loopers.infrastructure.brand; - -import com.loopers.domain.brand.Brand; -import org.springframework.data.jpa.repository.JpaRepository; - -/** - * packageName : com.loopers.infrastructure.brand - * fileName : BrandJpaRepository - * author : byeonsungmun - * date : 2025. 11. 12. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 12. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -public interface BrandJpaRepository extends JpaRepository { -} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandRepositoryImpl.java deleted file mode 100644 index f23e6e5d9..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandRepositoryImpl.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.loopers.infrastructure.brand; - -import com.loopers.domain.brand.Brand; -import com.loopers.domain.brand.BrandRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; - -import java.util.Optional; - -/** - * packageName : com.loopers.infrastructure.brand - * fileName : BrandRepositroyImpl - * author : byeonsungmun - * date : 2025. 11. 12. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 12. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -@RequiredArgsConstructor -@Component -public class BrandRepositoryImpl implements BrandRepository { - - private final BrandJpaRepository jpaRepository; - - @Override - public Optional findById(Long id) { - return jpaRepository.findById(id); - } - - @Override - public void save(Brand brand) { - jpaRepository.save(brand); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/example/ExampleJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/example/ExampleJpaRepository.java new file mode 100644 index 000000000..ce6d3ead0 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/example/ExampleJpaRepository.java @@ -0,0 +1,6 @@ +package com.loopers.infrastructure.example; + +import com.loopers.domain.example.ExampleModel; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ExampleJpaRepository extends JpaRepository {} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/example/ExampleRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/example/ExampleRepositoryImpl.java new file mode 100644 index 000000000..37f2272f0 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/example/ExampleRepositoryImpl.java @@ -0,0 +1,19 @@ +package com.loopers.infrastructure.example; + +import com.loopers.domain.example.ExampleModel; +import com.loopers.domain.example.ExampleRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +@RequiredArgsConstructor +@Component +public class ExampleRepositoryImpl implements ExampleRepository { + private final ExampleJpaRepository exampleJpaRepository; + + @Override + public Optional find(Long id) { + return exampleJpaRepository.findById(id); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeJpaRepository.java deleted file mode 100644 index 865a30db7..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeJpaRepository.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.loopers.infrastructure.like; - -import com.loopers.domain.like.Like; -import org.springframework.data.jpa.repository.JpaRepository; - -import java.util.Optional; - -/** - * packageName : com.loopers.infrastructure.like - * fileName : LikeJpaRepository - * author : byeonsungmun - * date : 2025. 11. 12. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 12. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -public interface LikeJpaRepository extends JpaRepository { - Optional findByUserIdAndProductId(String userId, Long productId); - - long countByProductId(Long productId); -} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeRepositoryImpl.java deleted file mode 100644 index e037b6efb..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeRepositoryImpl.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.loopers.infrastructure.like; - -import com.loopers.domain.like.Like; -import com.loopers.domain.like.LikeRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; - -import java.util.Optional; - -/** - * packageName : com.loopers.infrastructure.like - * fileName : LikeRepositoryImpl - * author : byeonsungmun - * date : 2025. 11. 12. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 12. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -@Component -@RequiredArgsConstructor -public class LikeRepositoryImpl implements LikeRepository { - - private final LikeJpaRepository likeJpaRepository; - - @Override - public Optional findByUserIdAndProductId(String userId, Long productId) { - return likeJpaRepository.findByUserIdAndProductId(userId, productId); - } - - @Override - public void save(Like like) { - likeJpaRepository.save(like); - } - - @Override - public void delete(Like like) { - likeJpaRepository.delete(like); - } - - @Override - public long countByProductId(Long productId) { - return likeJpaRepository.countByProductId(productId); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderJpaRepository.java deleted file mode 100644 index 39cfb136d..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderJpaRepository.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.loopers.infrastructure.order; - -import com.loopers.domain.order.Order; -import org.springframework.data.jpa.repository.JpaRepository; - -/** - * packageName : com.loopers.infrastructure.order - * fileName : OrderJpaRepository - * author : byeonsungmun - * date : 2025. 11. 14. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 14. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -public interface OrderJpaRepository extends JpaRepository { -} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderRepositoryImpl.java deleted file mode 100644 index f8c7b5b68..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderRepositoryImpl.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.loopers.infrastructure.order; - -import com.loopers.domain.order.Order; -import com.loopers.domain.order.OrderRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; - -import java.util.Optional; - -/** - * packageName : com.loopers.infrastructure.order - * fileName : OrderRepositroyImpl - * author : byeonsungmun - * date : 2025. 11. 14. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 14. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -@Component -@RequiredArgsConstructor -public class OrderRepositoryImpl implements OrderRepository { - - private final OrderJpaRepository orderJpaRepository; - - @Override - public Order save(Order order) { - return orderJpaRepository.save(order); - } - - @Override - public Optional findById(Long orderId) { - return orderJpaRepository.findById(orderId); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointJpaRepository.java deleted file mode 100644 index a35a56151..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointJpaRepository.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.loopers.infrastructure.point; - -import com.loopers.domain.point.Point; -import org.springframework.data.jpa.repository.JpaRepository; - -import java.util.Optional; - -public interface PointJpaRepository extends JpaRepository { - - Optional findByUserId(String userId); -} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointRepositoryImpl.java deleted file mode 100644 index 530191b66..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointRepositoryImpl.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.loopers.infrastructure.point; - -import com.loopers.domain.point.Point; -import com.loopers.domain.point.PointRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; - -import java.util.Optional; - -@RequiredArgsConstructor -@Component -public class PointRepositoryImpl implements PointRepository { - - private final PointJpaRepository pointJpaRepository; - - @Override - public Optional findByUserId(String userId) { - return pointJpaRepository.findByUserId(userId); - } - - @Override - public Point save(Point point) { - return pointJpaRepository.save(point); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.java deleted file mode 100644 index 5ceaae067..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.loopers.infrastructure.product; - -import com.loopers.domain.product.Product; -import org.springframework.data.jpa.repository.JpaRepository; - -/** - * packageName : com.loopers.infrastructure.product - * fileName : ProductJpaRepository - * author : byeonsungmun - * date : 2025. 11. 13. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 13. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -public interface ProductJpaRepository extends JpaRepository { - -} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java deleted file mode 100644 index dbad0d9d5..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.loopers.infrastructure.product; - -import com.loopers.domain.product.Product; -import com.loopers.domain.product.ProductRepository; -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Component; - -import java.util.Optional; - -/** - * packageName : com.loopers.infrastructure.product - * fileName : ProductRepositoryImpl - * author : byeonsungmun - * date : 2025. 11. 13. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 13. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -@Component -@RequiredArgsConstructor -public class ProductRepositoryImpl implements ProductRepository { - - private final ProductJpaRepository productJpaRepository; - - @Override - public Page findAll(Pageable pageable) { - return productJpaRepository.findAll(pageable); - } - - @Override - public Optional findById(Long id) { - return productJpaRepository.findById(id); - } - - @Override - public void incrementLikeCount(Long productId) { - Product product = productJpaRepository.findById(productId) - .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "์ƒํ’ˆ์„ ์ฐพ์„์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); - product.increaseLikeCount(); - } - - @Override - public void decrementLikeCount(Long productId) { - Product product = productJpaRepository.findById(productId) - .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "์ƒํ’ˆ์„ ์ฐพ์„์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); - product.decreaseLikeCount(); - } - - @Override - public Product save(Product product) { - return productJpaRepository.save(product); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserJpaRepository.java deleted file mode 100644 index f80a5bc52..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserJpaRepository.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.loopers.infrastructure.user; - -import com.loopers.domain.user.User; -import org.springframework.data.jpa.repository.JpaRepository; - -import java.util.Optional; - -public interface UserJpaRepository extends JpaRepository { - - Optional findByUserId(String userId); -} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java deleted file mode 100644 index 8fb6f7bdf..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.loopers.infrastructure.user; - -import com.loopers.domain.user.User; -import com.loopers.domain.user.UserRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; - -import java.util.Optional; - -@RequiredArgsConstructor -@Component -public class UserRepositoryImpl implements UserRepository { - - private final UserJpaRepository userJpaRepository; - - @Override - public Optional findByUserId(String userId) { - return userJpaRepository.findByUserId(userId); - } - - @Override - public User save(User user) { - return userJpaRepository.save(user); - } -} - diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1ApiSpec.java new file mode 100644 index 000000000..219e3101e --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1ApiSpec.java @@ -0,0 +1,19 @@ +package com.loopers.interfaces.api.example; + +import com.loopers.interfaces.api.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.tags.Tag; + +@Tag(name = "Example V1 API", description = "Loopers ์˜ˆ์‹œ API ์ž…๋‹ˆ๋‹ค.") +public interface ExampleV1ApiSpec { + + @Operation( + summary = "์˜ˆ์‹œ ์กฐํšŒ", + description = "ID๋กœ ์˜ˆ์‹œ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค." + ) + ApiResponse getExample( + @Schema(name = "์˜ˆ์‹œ ID", description = "์กฐํšŒํ•  ์˜ˆ์‹œ์˜ ID") + Long exampleId + ); +} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1Controller.java new file mode 100644 index 000000000..917376016 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1Controller.java @@ -0,0 +1,28 @@ +package com.loopers.interfaces.api.example; + +import com.loopers.application.example.ExampleFacade; +import com.loopers.application.example.ExampleInfo; +import com.loopers.interfaces.api.ApiResponse; +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; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/v1/examples") +public class ExampleV1Controller implements ExampleV1ApiSpec { + + private final ExampleFacade exampleFacade; + + @GetMapping("/{exampleId}") + @Override + public ApiResponse getExample( + @PathVariable(value = "exampleId") Long exampleId + ) { + ExampleInfo info = exampleFacade.getExample(exampleId); + ExampleV1Dto.ExampleResponse response = ExampleV1Dto.ExampleResponse.from(info); + return ApiResponse.success(response); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1Dto.java new file mode 100644 index 000000000..4ecf0eea5 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1Dto.java @@ -0,0 +1,15 @@ +package com.loopers.interfaces.api.example; + +import com.loopers.application.example.ExampleInfo; + +public class ExampleV1Dto { + public record ExampleResponse(Long id, String name, String description) { + public static ExampleResponse from(ExampleInfo info) { + return new ExampleResponse( + info.id(), + info.name(), + info.description() + ); + } + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1ApiSpec.java deleted file mode 100644 index 6f0458399..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1ApiSpec.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.loopers.interfaces.api.point; - -import com.loopers.interfaces.api.ApiResponse; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.tags.Tag; - -@Tag(name = "Point V1 API", description = "Point API ์ž…๋‹ˆ๋‹ค.") -public interface PointV1ApiSpec { - - @Operation( - summary = "ํฌ์ธํŠธ ํšŒ์› ์กฐํšŒ", - description = "ํšŒ์›์˜ ํฌ์ธํŠธ๋ฅผ ์กฐํšŒํ•œ๋‹ค." - ) - ApiResponse getPoint( - @Schema(name = "ํšŒ์› Id", description = "์กฐํšŒํ•  ํšŒ์› ID") - String userId - ); - - @Operation( - summary = "ํฌ์ธํŠธ ์ถฉ์ „", - description = "ํšŒ์›์˜ ํฌ์ธํŠธ๋ฅผ ์ถฉ์ „ํ•œ๋‹ค." - ) - ApiResponse chargePoint( - @Schema(name = "ํฌ์ธํŠธ ์ถฉ์ „ ์š”์ฒญ", description = "์ถฉ์ „ํ•  ํฌ์ธํŠธ ์ •๋ณด๋ฅผ ํฌํ•จํ•œ ์š”์ฒญ") - PointV1Dto.ChargePointRequest request - ); -} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Controller.java deleted file mode 100644 index 866fce9b3..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Controller.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.loopers.interfaces.api.point; - -import com.loopers.application.point.PointFacade; -import com.loopers.application.point.PointInfo; -import com.loopers.interfaces.api.ApiResponse; -import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.*; - -@RequiredArgsConstructor -@RestController -@RequestMapping("/api/v1/points") -public class PointV1Controller implements PointV1ApiSpec { - - private final PointFacade pointFacade; - - @Override - @GetMapping - public ApiResponse getPoint(@RequestHeader("X-USER-ID") String userId) { - PointInfo pointInfo = pointFacade.getPoint(userId); - PointV1Dto.PointResponse response = PointV1Dto.PointResponse.from(pointInfo); - return ApiResponse.success(response); - } - - @Override - @PatchMapping("/charge") - public ApiResponse chargePoint(@RequestBody PointV1Dto.ChargePointRequest request) { - PointInfo pointInfo = pointFacade.chargePoint(request); - PointV1Dto.PointResponse response = PointV1Dto.PointResponse.from(pointInfo); - return ApiResponse.success(response); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Dto.java deleted file mode 100644 index b0b3d050e..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Dto.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.loopers.interfaces.api.point; - -import com.loopers.application.point.PointInfo; - -public class PointV1Dto { - - public record ChargePointRequest(String userId, Long chargeAmount) { - } - - public record PointResponse(String userId, Long amount) { - public static PointResponse from(PointInfo info) { - return new PointResponse( - info.userId(), - info.amount() - ); - } - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1ApiSpec.java deleted file mode 100644 index 1bed68e62..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1ApiSpec.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.loopers.interfaces.api.user; - -import com.loopers.interfaces.api.ApiResponse; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.tags.Tag; - -@Tag(name = "Users V1 API", description = "Users API ์ž…๋‹ˆ๋‹ค.") -public interface UserV1ApiSpec { - - @Operation( - summary = "ํšŒ์› ๊ฐ€์ž…", - description = "ํšŒ์› ๊ฐ€์ž…์„ ํ•ฉ๋‹ˆ๋‹ค." - ) - ApiResponse register( - @Schema(name = "ํšŒ์› ๊ฐ€์ž… ์š”์ฒญ", description = "ํšŒ์›๊ฐ€์ž…") - UserV1Dto.RegisterRequest request - ); - - @Operation( - summary = "ํšŒ์› ์กฐํšŒ", - description = "ํ•ด๋‹น ํšŒ์›์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค." - ) - ApiResponse getUser( - @Schema(name = "ํšŒ์› ID", description = "์กฐํšŒํ•  ํšŒ์› ID") - String userId - ); -} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java deleted file mode 100644 index aed39ae1f..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.loopers.interfaces.api.user; - -import com.loopers.application.user.UserFacade; -import com.loopers.application.user.UserInfo; -import com.loopers.interfaces.api.ApiResponse; -import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.*; - -@RequiredArgsConstructor -@RestController -@RequestMapping("/api/v1/users") -public class UserV1Controller implements UserV1ApiSpec { - - private final UserFacade userFacade; - - @Override - @PostMapping("/register") - public ApiResponse register(@RequestBody UserV1Dto.RegisterRequest request) { - UserInfo userInfo = userFacade.register(request.userId(), request.mail(), request.birth(), request.gender()); - UserV1Dto.UserResponse response = UserV1Dto.UserResponse.from(userInfo); - return ApiResponse.success(response); - } - - @Override - @GetMapping("/{userId}") - public ApiResponse getUser(@PathVariable String userId) { - UserInfo userInfo = userFacade.getUser(userId); - UserV1Dto.UserResponse response = UserV1Dto.UserResponse.from(userInfo); - return ApiResponse.success(response); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java deleted file mode 100644 index 263214848..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.loopers.interfaces.api.user; - -import com.loopers.application.user.UserInfo; - -public class UserV1Dto { - public record RegisterRequest( - String userId, - String mail, - String birth, - String gender - ) { - } - - public record UserResponse(String userId, String email, String birth, String gender) { - public static UserResponse from(UserInfo info) { - return new UserResponse( - info.userId(), - info.email(), - info.birth(), - info.gender() - ); - } - } -} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandTest.java deleted file mode 100644 index 9541c11f4..000000000 --- a/apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandTest.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.loopers.domain.brand; - -import com.loopers.support.error.CoreException; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -/** - * packageName : com.loopers.domain.brand - * fileName : BrandTest - * author : byeonsungmun - * date : 2025. 11. 14. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 14. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -class BrandTest { - - @DisplayName("Brand ๋‹จ์œ„ ํ…Œ์ŠคํŠธ") - @Nested - class CreateBrandTest { - - @Test - @DisplayName("๋ธŒ๋žœ๋“œ ์ƒ์„ฑ ์„ฑ๊ณต") - void createBrandSuccess() { - Brand brand = Brand.create("Nike"); - assertThat(brand.getName()).isEqualTo("Nike"); - } - - @Test - @DisplayName("๋ธŒ๋žœ๋“œ ์ด๋ฆ„์ด ์—†์œผ๋ฉด ์˜ˆ์™ธ") - void createBrandFail() { - assertThatThrownBy(() -> Brand.create("")) - .isInstanceOf(CoreException.class); - } - } -} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/example/ExampleModelTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/example/ExampleModelTest.java new file mode 100644 index 000000000..44ca7576e --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/example/ExampleModelTest.java @@ -0,0 +1,65 @@ +package com.loopers.domain.example; + +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class ExampleModelTest { + @DisplayName("์˜ˆ์‹œ ๋ชจ๋ธ์„ ์ƒ์„ฑํ•  ๋•Œ, ") + @Nested + class Create { + @DisplayName("์ œ๋ชฉ๊ณผ ์„ค๋ช…์ด ๋ชจ๋‘ ์ฃผ์–ด์ง€๋ฉด, ์ •์ƒ์ ์œผ๋กœ ์ƒ์„ฑ๋œ๋‹ค.") + @Test + void createsExampleModel_whenNameAndDescriptionAreProvided() { + // arrange + String name = "์ œ๋ชฉ"; + String description = "์„ค๋ช…"; + + // act + ExampleModel exampleModel = new ExampleModel(name, description); + + // assert + assertAll( + () -> assertThat(exampleModel.getId()).isNotNull(), + () -> assertThat(exampleModel.getName()).isEqualTo(name), + () -> assertThat(exampleModel.getDescription()).isEqualTo(description) + ); + } + + @DisplayName("์ œ๋ชฉ์ด ๋นˆ์นธ์œผ๋กœ๋งŒ ์ด๋ฃจ์–ด์ ธ ์žˆ์œผ๋ฉด, BAD_REQUEST ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") + @Test + void throwsBadRequestException_whenTitleIsBlank() { + // arrange + String name = " "; + + // act + CoreException result = assertThrows(CoreException.class, () -> { + new ExampleModel(name, "์„ค๋ช…"); + }); + + // assert + assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); + } + + @DisplayName("์„ค๋ช…์ด ๋น„์–ด์žˆ์œผ๋ฉด, BAD_REQUEST ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") + @Test + void throwsBadRequestException_whenDescriptionIsEmpty() { + // arrange + String description = ""; + + // act + CoreException result = assertThrows(CoreException.class, () -> { + new ExampleModel("์ œ๋ชฉ", description); + }); + + // assert + assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); + } + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/example/ExampleServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/example/ExampleServiceIntegrationTest.java new file mode 100644 index 000000000..bbd5fdbe1 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/example/ExampleServiceIntegrationTest.java @@ -0,0 +1,72 @@ +package com.loopers.domain.example; + +import com.loopers.infrastructure.example.ExampleJpaRepository; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import com.loopers.utils.DatabaseCleanUp; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@SpringBootTest +class ExampleServiceIntegrationTest { + @Autowired + private ExampleService exampleService; + + @Autowired + private ExampleJpaRepository exampleJpaRepository; + + @Autowired + private DatabaseCleanUp databaseCleanUp; + + @AfterEach + void tearDown() { + databaseCleanUp.truncateAllTables(); + } + + @DisplayName("์˜ˆ์‹œ๋ฅผ ์กฐํšŒํ•  ๋•Œ,") + @Nested + class Get { + @DisplayName("์กด์žฌํ•˜๋Š” ์˜ˆ์‹œ ID๋ฅผ ์ฃผ๋ฉด, ํ•ด๋‹น ์˜ˆ์‹œ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnsExampleInfo_whenValidIdIsProvided() { + // arrange + ExampleModel exampleModel = exampleJpaRepository.save( + new ExampleModel("์˜ˆ์‹œ ์ œ๋ชฉ", "์˜ˆ์‹œ ์„ค๋ช…") + ); + + // act + ExampleModel result = exampleService.getExample(exampleModel.getId()); + + // assert + assertAll( + () -> assertThat(result).isNotNull(), + () -> assertThat(result.getId()).isEqualTo(exampleModel.getId()), + () -> assertThat(result.getName()).isEqualTo(exampleModel.getName()), + () -> assertThat(result.getDescription()).isEqualTo(exampleModel.getDescription()) + ); + } + + @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์˜ˆ์‹œ ID๋ฅผ ์ฃผ๋ฉด, NOT_FOUND ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") + @Test + void throwsException_whenInvalidIdIsProvided() { + // arrange + Long invalidId = 999L; // Assuming this ID does not exist + + // act + CoreException exception = assertThrows(CoreException.class, () -> { + exampleService.getExample(invalidId); + }); + + // assert + assertThat(exception.getErrorType()).isEqualTo(ErrorType.NOT_FOUND); + } + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeServiceIntegrationTest.java deleted file mode 100644 index 0be07a6fb..000000000 --- a/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeServiceIntegrationTest.java +++ /dev/null @@ -1,155 +0,0 @@ -package com.loopers.domain.like; - -import com.loopers.domain.product.Product; -import com.loopers.domain.product.ProductRepository; -import com.loopers.domain.user.User; -import com.loopers.domain.user.UserRepository; -import com.loopers.utils.DatabaseCleanUp; -import org.junit.jupiter.api.*; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.transaction.annotation.Transactional; - -import static org.assertj.core.api.Assertions.*; - -/** - * packageName : com.loopers.domain.like - * fileName : LikeServiceIntegrationTest - * author : byeonsungmun - * date : 2025. 11. 14. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 14. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -@SpringBootTest -class LikeServiceIntegrationTest { - - @Autowired - private LikeService likeService; - - @Autowired - private LikeRepository likeRepository; - - @Autowired - private ProductRepository productRepository; - - @Autowired - private UserRepository userRepository; - - @Autowired - private DatabaseCleanUp cleanUp; - - @AfterEach - void tearDown() { - cleanUp.truncateAllTables(); - } - - @Nested - @DisplayName("์ข‹์•„์š” ๊ธฐ๋Šฅ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ") - class LikeTests { - - @Test - @DisplayName("์ข‹์•„์š” ์ƒ์„ฑ ์„ฑ๊ณต โ†’ ์ข‹์•„์š” ์ €์žฅ + ์ƒํ’ˆ์˜ likeCount ์ฆ๊ฐ€") - @Transactional - void likeSuccess() { - // given - User user = userRepository.save(new User("user1", "u1@mail.com", "1990-01-01", "MALE")); - Product product = productRepository.save(Product.create(1L, "์ƒํ’ˆA", 1000L, 10L)); - - // when - likeService.like(user.getUserId(), product.getId()); - - // then - Like saved = likeRepository.findByUserIdAndProductId("user1", 1L).orElse(null); - assertThat(saved).isNotNull(); - - Product updated = productRepository.findById(1L).get(); - assertThat(updated.getLikeCount()).isEqualTo(1L); - } - - @Test - @DisplayName("์ค‘๋ณต ์ข‹์•„์š” ์‹œ likeCount ์ฆ๊ฐ€ ์•ˆ ํ•˜๊ณ  ์ €์žฅ๋„ ์•ˆ ๋จ") - @Transactional - void duplicateLike() { - // given - userRepository.save(new User("user1", "u1@mail.com", "1990-01-01", "MALE")); - productRepository.save(Product.create(1L, "์ƒํ’ˆA", 1000L, 10L)); - - likeService.like("user1", 1L); - - // when - likeService.like("user1", 1L); // ์ค‘๋ณต ํ˜ธ์ถœ - - // then - long likeCount = likeRepository.countByProductId(1L); - assertThat(likeCount).isEqualTo(1L); - - Product updated = productRepository.findById(1L).get(); - assertThat(updated.getLikeCount()).isEqualTo(1L); // ์ฆ๊ฐ€ X - } - - @Test - @DisplayName("์ข‹์•„์š” ์ทจ์†Œ ์„ฑ๊ณต โ†’ like ์‚ญ์ œ + ์ƒํ’ˆ์˜ likeCount ๊ฐ์†Œ") - @Transactional - void unlikeSuccess() { - // given - userRepository.save(new User("user1", "u1@mail.com", "1990-01-01", "MALE")); - productRepository.save(Product.create(1L, "์ƒํ’ˆA", 1000L, 10L)); - - likeService.like("user1", 1L); - - // when - likeService.unlike("user1", 1L); - - // then - Like like = likeRepository.findByUserIdAndProductId("user1", 1L).orElse(null); - assertThat(like).isNull(); - - Product updated = productRepository.findById(1L).get(); - assertThat(updated.getLikeCount()).isEqualTo(0L); - } - - @Test - @DisplayName("์—†๋Š” ์ข‹์•„์š” ์ทจ์†Œ ์‹œ likeCount ๊ฐ์†Œ ์•ˆ ํ•จ") - @Transactional - void unlikeNonExisting() { - // given - userRepository.save(new User("user1", "u1@mail.com", "1990-01-01", "MALE")); - Product product = Product.create(1L, "์ƒํ’ˆA", 1000L, 10L); - product.increaseLikeCount(); - product.increaseLikeCount(); - product.increaseLikeCount(); - product.increaseLikeCount(); - product.increaseLikeCount(); - - productRepository.save(product); - // when โ€” ํ˜ธ์ถœ์€ ํ•ด๋„ - likeService.unlike("user1", 1L); - - // then โ€” ๋ณ€ํ™” ์—†์Œ - Product updated = productRepository.findById(1L).get(); - assertThat(updated.getLikeCount()).isEqualTo(5L); - } - - @Test - @DisplayName("countByProductId ์ •์ƒ ์กฐํšŒ") - @Transactional - void countTest() { - // given - userRepository.save(new User("user1", "u1@mail.com", "1990-01-01", "MALE")); - userRepository.save(new User("user2", "u2@mail.com", "1991-01-01", "MALE")); - productRepository.save(Product.create(1L, "์ƒํ’ˆA", 1000L, 10L)); - - likeService.like("user1", 1L); - likeService.like("user2", 1L); - - // when - long count = likeService.countByProductId(1L); - - // then - assertThat(count).isEqualTo(2L); - } - } -} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeTest.java deleted file mode 100644 index d5b8bd851..000000000 --- a/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeTest.java +++ /dev/null @@ -1,91 +0,0 @@ -package com.loopers.domain.like; - -import com.loopers.support.error.CoreException; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -import java.time.LocalDateTime; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; - -/** - * packageName : com.loopers.domain.like - * fileName : LikeTest - * author : byeonsungmun - * date : 2025. 11. 13. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 13. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -class LikeTest { - - - @DisplayName("์ •์ƒ์ ์œผ๋กœ Like ์—”ํ‹ฐํ‹ฐ๋ฅผ ์ƒ์„ฑ์ˆ˜ ํ•  ์žˆ๋‹ค") - @Nested - class LikeCreate { - - @DisplayName("Like์ƒ์„ฑ์ž๋ฅผ ์ƒ์„ฑํ•œ๋‹ค") - @Test - void createLike_success() { - // given - String userId = "user-001"; - Long productId = 100L; - - // when - Like like = Like.create(userId, productId); - - // then - assertThat(like.getUserId()).isEqualTo(userId); - assertThat(like.getProductId()).isEqualTo(productId); - assertThat(like.getCreatedAt()).isBeforeOrEqualTo(LocalDateTime.now()); - } - - @Test - @DisplayName("userId๊ฐ€ null์ด๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค") - void createLike_invalidUserId_null() { - // given - String userId = null; - Long productId = 100L; - - // when & then - assertThrows(CoreException.class, () -> Like.create(userId, productId)); - } - - @Test - @DisplayName("userId๊ฐ€ ๋นˆ ๋ฌธ์ž์—ด์ด๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค") - void createLike_invalidUserId_empty() { - // given - String userId = ""; - Long productId = 100L; - - // when & then - assertThrows(CoreException.class, () -> Like.create(userId, productId)); - } - - @Test - @DisplayName("productId๊ฐ€ null์ด๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค") - void createLike_invalidProductId_null() { - // given - String userId = "user-001"; - Long productId = null; - - // when & then - assertThrows(CoreException.class, () -> Like.create(userId, productId)); - } - - @Test - @DisplayName("productId๊ฐ€ 0 ์ดํ•˜์ด๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค") - void createLike_invalidProductId_zeroOrNegative() { - // given - String userId = "user-001"; - Long productId = -1L; - - // when & then - assertThrows(CoreException.class, () -> Like.create(userId, productId)); - } - } -} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderServiceIntegrationTest.java deleted file mode 100644 index 149e71540..000000000 --- a/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderServiceIntegrationTest.java +++ /dev/null @@ -1,170 +0,0 @@ -package com.loopers.domain.order; - -import com.loopers.application.order.CreateOrderCommand; -import com.loopers.application.order.OrderFacade; -import com.loopers.application.order.OrderInfo; -import com.loopers.application.order.OrderItemCommand; -import com.loopers.domain.point.Point; -import com.loopers.domain.point.PointRepository; -import com.loopers.domain.product.Product; -import com.loopers.domain.product.ProductRepository; -import com.loopers.utils.DatabaseCleanUp; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.transaction.annotation.Transactional; - -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -/** - * packageName : com.loopers.domain.order - * fileName : OrderServiceIntegrationTest - * author : byeonsungmun - * date : 2025. 11. 14. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 14. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -@SpringBootTest -public class OrderServiceIntegrationTest { - - @Autowired - private OrderFacade orderFacade; - - @Autowired - private ProductRepository productRepository; - - @Autowired - private PointRepository pointRepository; - - @Autowired - private OrderRepository orderRepository; - - @Autowired - private DatabaseCleanUp databaseCleanUp; - - @AfterEach - void tearDown() { - databaseCleanUp.truncateAllTables(); - } - - @Nested - @DisplayName("์ฃผ๋ฌธ ์ƒ์„ฑ ์„ฑ๊ณต") - class OrderCreateSuccess { - - @Test - @Transactional - void createOrder_success() { - - // given - Product p1 = productRepository.save(Product.create(1L, "์•„๋ฉ”๋ฆฌ์นด๋…ธ", 3000L, 100L)); - Product p2 = productRepository.save(Product.create(1L, "๋ผ๋–ผ", 4000L, 200L)); - - pointRepository.save(Point.create("user1", 20000L)); - - CreateOrderCommand command = new CreateOrderCommand( - "user1", - List.of( - new OrderItemCommand(p1.getId(), 2L), // 6000์› - new OrderItemCommand(p2.getId(), 1L) // 4000์› - ) - ); - - // when - OrderInfo info = orderFacade.createOrder(command); - - // then - Order saved = orderRepository.findById(info.orderId()).orElseThrow(); - - assertThat(saved.getStatus()).isEqualTo(OrderStatus.COMPLETE); - assertThat(saved.getTotalAmount()).isEqualTo(10000L); - assertThat(saved.getOrderItems()).hasSize(2); - - // ์žฌ๊ณ  ๊ฐ์†Œ ํ™•์ธ - Product updated1 = productRepository.findById(p1.getId()).get(); - Product updated2 = productRepository.findById(p2.getId()).get(); - assertThat(updated1.getStock()).isEqualTo(98); - assertThat(updated2.getStock()).isEqualTo(199); - - // ํฌ์ธํŠธ ๊ฐ์†Œ ํ™•์ธ - Point point = pointRepository.findByUserId("user1").get(); - assertThat(point.getBalance()).isEqualTo(10000L); // 20000 - 10000 - - } - } - - @Nested - @DisplayName("์ฃผ๋ฌธ ์‹คํŒจ ์ผ€์ด์Šค") - class OrderCreateFail { - - @Test - @Transactional - @DisplayName("์žฌ๊ณ  ๋ถ€์กฑ์œผ๋กœ ์‹คํŒจ") - void insufficientStock_fail() { - Product item = productRepository.save(Product.create(1L, "์ƒํ’ˆ", 1000L, 1L)); - pointRepository.save(Point.create("user1", 5000L)); - - CreateOrderCommand command = new CreateOrderCommand( - "user1", - List.of(new OrderItemCommand(item.getId(), 5L)) - ); - - assertThatThrownBy(() -> orderFacade.createOrder(command)) - .isInstanceOf(RuntimeException.class); // ๋„ˆ์˜ ๋„๋ฉ”์ธ ์˜ˆ์™ธ ํƒ€์ž… ๋งž์ถฐ๋„ ๋จ - } - - @Test - @Transactional - @DisplayName("ํฌ์ธํŠธ ๋ถ€์กฑ์œผ๋กœ ์‹คํŒจ") - void insufficientPoint_fail() { - Product item = productRepository.save(Product.create(1L, "์ƒํ’ˆ", 1000L, 10L)); - pointRepository.save(Point.create("user1", 2000L)); // ๋ถ€์กฑ - - CreateOrderCommand command = new CreateOrderCommand( - "user1", - List.of(new OrderItemCommand(item.getId(), 5L)) // ์ด 5000์› - ); - - assertThatThrownBy(() -> orderFacade.createOrder(command)) - .hasMessageContaining("ํฌ์ธํŠธ"); // ๋ฉ”์‹œ์ง€ ๋งž์ถ”๋ฉด ๋” ์ •ํ™•ํ•˜๊ฒŒ ๊ฐ€๋Šฅ - } - - @Test - @Transactional - @DisplayName("์—†๋Š” ์ƒํ’ˆ ์ฃผ๋ฌธ ์‹œ ์‹คํŒจ") - void noProduct_fail() { - pointRepository.save(Point.create("user1", 10000L)); - - CreateOrderCommand command = new CreateOrderCommand( - "user1", - List.of(new OrderItemCommand(999L, 1L)) - ); - - assertThatThrownBy(() -> orderFacade.createOrder(command)) - .isInstanceOf(RuntimeException.class); - } - - @Test - @Transactional - @DisplayName("์œ ์ € ํฌ์ธํŠธ ์ •๋ณด ์—†์œผ๋ฉด ์‹คํŒจ") - void noUserPoint_fail() { - Product item = productRepository.save(Product.create(1L, "์ƒํ’ˆ", 1000L, 10L)); - - CreateOrderCommand command = new CreateOrderCommand( - "user1", - List.of(new OrderItemCommand(item.getId(), 1L)) - ); - - assertThatThrownBy(() -> orderFacade.createOrder(command)) - .isInstanceOf(RuntimeException.class); - } - } -} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderTest.java deleted file mode 100644 index 60ed16ecc..000000000 --- a/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderTest.java +++ /dev/null @@ -1,122 +0,0 @@ -package com.loopers.domain.order; - -import com.loopers.support.error.CoreException; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -/** - * packageName : com.loopers.domain.order - * fileName : OrderTest - * author : byeonsungmun - * date : 2025. 11. 14. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 14. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -class OrderTest { - - @Nested - @DisplayName("Order ์ƒ์„ฑ ํ…Œ์ŠคํŠธ") - class CreateOrderTest { - - @Test - @DisplayName("์ฃผ๋ฌธ ์ƒ์„ฑ ์„ฑ๊ณต") - void createOrderSuccess() { - // when - Order order = Order.create("user123"); - - // then - assertThat(order.getUserId()).isEqualTo("user123"); - assertThat(order.getStatus()).isEqualTo(OrderStatus.PENDING); - assertThat(order.getTotalAmount()).isEqualTo(0L); - assertThat(order.getCreatedAt()).isNotNull(); - assertThat(order.getOrderItems()).isEmpty(); - } - - @Test - @DisplayName("userId๊ฐ€ null์ด๋ฉด ์ƒ์„ฑ ์‹คํŒจ") - void createOrderFailUserIdNull() { - assertThatThrownBy(() -> Order.create(null)) - .isInstanceOf(CoreException.class) - .hasMessageContaining("์‚ฌ์šฉ์ž ID๋Š” ํ•„์ˆ˜"); - } - - @Test - @DisplayName("userId๊ฐ€ ๊ณต๋ฐฑ์ด๋ฉด ์ƒ์„ฑ ์‹คํŒจ") - void createOrderFailUserIdBlank() { - assertThatThrownBy(() -> Order.create("")) - .isInstanceOf(CoreException.class) - .hasMessageContaining("์‚ฌ์šฉ์ž ID๋Š” ํ•„์ˆ˜"); - } - } - - @Nested - @DisplayName("Order ์ƒํƒœ ์—…๋ฐ์ดํŠธ ํ…Œ์ŠคํŠธ") - class UpdateStatusTest { - - @Test - @DisplayName("์ฃผ๋ฌธ ์ƒํƒœ ์—…๋ฐ์ดํŠธ ์„ฑ๊ณต") - void updateStatusSuccess() { - // given - Order order = Order.create("user123"); - - // when - order.updateStatus(OrderStatus.COMPLETE); - - // then - assertThat(order.getStatus()).isEqualTo(OrderStatus.COMPLETE); - } - } - - @Nested - @DisplayName("์ด์•ก ์—…๋ฐ์ดํŠธ ํ…Œ์ŠคํŠธ") - class UpdateAmountTest { - - @Test - @DisplayName("์ด์•ก ์—…๋ฐ์ดํŠธ ์„ฑ๊ณต") - void updateTotalAmountSuccess() { - // given - Order order = Order.create("user123"); - - // when - order.updateTotalAmount(5000L); - - // then - assertThat(order.getTotalAmount()).isEqualTo(5000L); - } - } - - @Nested - @DisplayName("OrderItem ์ถ”๊ฐ€ ํ…Œ์ŠคํŠธ") - class AddOrderItemTest { - - @Test - @DisplayName("OrderItem ์ถ”๊ฐ€ ์„ฑ๊ณต") - void addOrderItemSuccess() { - // given - Order order = Order.create("user123"); - - OrderItem item = OrderItem.create( - 1L, - "์ƒํ’ˆ๋ช…", - 2L, - 1000L - ); - - // when - order.addOrderItem(item); - item.setOrder(order); - - // then - assertThat(order.getOrderItems()).hasSize(1); - assertThat(order.getOrderItems().getFirst().getProductName()).isEqualTo("์ƒํ’ˆ๋ช…"); - assertThat(item.getOrder()).isEqualTo(order); - } - } -} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java deleted file mode 100644 index b623bc9c7..000000000 --- a/apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java +++ /dev/null @@ -1,108 +0,0 @@ -package com.loopers.domain.point; - -import com.loopers.domain.user.User; -import com.loopers.domain.user.UserRepository; -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import com.loopers.utils.DatabaseCleanUp; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; - -@SpringBootTest -class PointServiceIntegrationTest { - - @Autowired - private PointRepository pointRepository; - - @Autowired - private UserRepository userRepository; - - @Autowired - private PointService pointService; - - @Autowired - private DatabaseCleanUp databaseCleanUp; - - @AfterEach - void tearDown() { - databaseCleanUp.truncateAllTables(); - } - - @DisplayName("ํฌ์ธํŠธ ์กฐํšŒ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ") - @Nested - class PointUser { - - @DisplayName("ํ•ด๋‹น ID์˜ ํšŒ์›์ด ์กด์žฌํ•  ๊ฒฝ์šฐ, ๋ณด์œ  ํฌ์ธํŠธ๊ฐ€ ๋ฐ˜ํ™˜๋œ๋‹ค.") - @Test - void returnPointInfo_whenValidIdIsProvided() { - //given - String id = "yh45g"; - String email = "yh45g@loopers.com"; - String birth = "1994-12-05"; - String gender = "MALE"; - - userRepository.save(new User(id, email, birth, gender)); - pointRepository.save(Point.create(id, 0L)); - - //when - Point result = pointService.findPointByUserId(id); - - //then - assertThat(result.getUserId()).isEqualTo(id); - assertThat(result.getBalance()).isEqualTo(0L); - } - - @DisplayName("ํšŒ์›์ด ์กด์žฌ ํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ, null ์ด ๋ฐ˜ํ™˜๋œ๋‹ค.") - @Test - void returnNull_whenInvalidUserIdIsProvided() { - //given - String id = "yh45g"; - - //when - Point point = pointService.findPointByUserId(id); - - //then - assertThat(point).isNull(); - } - } - - @DisplayName("ํฌ์ธํŠธ ์ถฉ์ „ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ") - @Nested - class Charge { - - @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ์ € ID ๋กœ ์ถฉ์ „์„ ์‹œ๋„ํ•œ ๊ฒฝ์šฐ, ์‹คํŒจํ•œ๋‹ค.") - @Test - void throwsChargeAmountFailException_whenUserIDIsNotProvided() { - //given - String id = "yh45g"; - - //when - CoreException exception = assertThrows(CoreException.class, () -> pointService.chargePoint(id, 1000L)); - - //then - assertThat(exception.getErrorType()).isEqualTo(ErrorType.NOT_FOUND); - } - - @Test - @DisplayName("ํšŒ์›์ด ์กด์žฌํ•˜๋ฉด ํฌ์ธํŠธ ์ถฉ์ „ ์„ฑ๊ณต") - void chargeSuccess() { - // given - String userId = "user2"; - userRepository.save(new User(userId, "yh45g@loopers.com", "1994-12-05", "MALE")); - pointRepository.save(Point.create(userId, 1000L)); - - // when - Point updated = pointService.chargePoint(userId, 500L); - - // then - assertThat(updated.getBalance()).isEqualTo(1500L); - } - } -} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/point/PointTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/point/PointTest.java deleted file mode 100644 index f33fb2821..000000000 --- a/apps/commerce-api/src/test/java/com/loopers/domain/point/PointTest.java +++ /dev/null @@ -1,117 +0,0 @@ -package com.loopers.domain.point; - -import com.loopers.support.error.CoreException; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; -import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; - -class PointTest { - - @Nested - @DisplayName("ํฌ์ธํŠธ ์ƒ์„ฑ ํ…Œ์ŠคํŠธ") - class CreatePointTest { - - @Test - @DisplayName("ํฌ์ธํŠธ ์ƒ์„ฑ ์„ฑ๊ณต") - void createPointSuccess() { - // when - Point point = Point.create("user123", 100L); - - // then - assertThat(point.getUserId()).isEqualTo("user123"); - assertThat(point.getBalance()).isEqualTo(100L); - } - - @Test - @DisplayName("userId๊ฐ€ null์ด๋ฉด ์ƒ์„ฑ ์‹คํŒจ") - void createPointFailUserIdNull() { - assertThatThrownBy(() -> Point.create(null, 100L)) - .isInstanceOf(CoreException.class) - .hasMessageContaining("์‚ฌ์šฉ์ž ID๊ฐ€ ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); - } - - @Test - @DisplayName("userId๊ฐ€ ๊ณต๋ฐฑ์ด๋ฉด ์ƒ์„ฑ ์‹คํŒจ") - void createPointFailUserIdBlank() { - assertThatThrownBy(() -> Point.create("", 100L)) - .isInstanceOf(CoreException.class) - .hasMessageContaining("์‚ฌ์šฉ์ž ID๊ฐ€ ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); - } - } - - @Nested - @DisplayName("ํฌ์ธํŠธ ์ถฉ์ „ ํ…Œ์ŠคํŠธ") - class ChargePointTest { - - @Test - @DisplayName("ํฌ์ธํŠธ ์ถฉ์ „ ์„ฑ๊ณต") - void chargeSuccess() { - // given - Point point = Point.create("user123", 100L); - - // when - point.charge(50L); - - // then - assertThat(point.getBalance()).isEqualTo(150L); - } - - @Test - @DisplayName("ํฌ์ธํŠธ ์ถฉ์ „ ์‹คํŒจ - 0 ์ดํ•˜ ์ž…๋ ฅ") - void chargeFailZeroOrNegative() { - Point point = Point.create("user123", 100L); - - assertThatThrownBy(() -> point.charge(0L)) - .isInstanceOf(CoreException.class) - .hasMessageContaining("์ถฉ์ „"); - - assertThatThrownBy(() -> point.charge(-10L)) - .isInstanceOf(CoreException.class); - } - } - - @Nested - @DisplayName("ํฌ์ธํŠธ ์‚ฌ์šฉ ํ…Œ์ŠคํŠธ") - class UsePointTest { - - @Test - @DisplayName("ํฌ์ธํŠธ ์‚ฌ์šฉ ์„ฑ๊ณต") - void useSuccess() { - // given - Point point = Point.create("user123", 100L); - - // when - point.use(40L); - - // then - assertThat(point.getBalance()).isEqualTo(60L); - } - - @Test - @DisplayName("ํฌ์ธํŠธ ์‚ฌ์šฉ ์‹คํŒจ - 0 ์ดํ•˜ ์ž…๋ ฅ") - void useFailZeroOrNegative() { - Point point = Point.create("user123", 100L); - - assertThatThrownBy(() -> point.use(0L)) - .isInstanceOf(CoreException.class) - .hasMessageContaining("์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); - - assertThatThrownBy(() -> point.use(-10L)) - .isInstanceOf(CoreException.class); - } - - @Test - @DisplayName("ํฌ์ธํŠธ ์‚ฌ์šฉ ์‹คํŒจ - ์ž”์•ก ๋ถ€์กฑ") - void useFailNotEnough() { - Point point = Point.create("user123", 50L); - - assertThatThrownBy(() -> point.use(100L)) - .isInstanceOf(CoreException.class) - .hasMessageContaining("ํฌ์ธํŠธ๊ฐ€ ๋ถ€์กฑ"); - } - } - -} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductServiceIntegrationTest.java deleted file mode 100644 index 8ad61a194..000000000 --- a/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductServiceIntegrationTest.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.loopers.domain.product; - -import com.loopers.utils.DatabaseCleanUp; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; - -/** - * packageName : com.loopers.domain.product - * fileName : ProductServiceIntegrationTest - * author : byeonsungmun - * date : 2025. 11. 13. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 13. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ - -@SpringBootTest -public class ProductServiceIntegrationTest { - - @Autowired - private ProductService productService; - - @Autowired - private DatabaseCleanUp databaseCleanUp; - - @AfterEach - void tearDown() { - databaseCleanUp.truncateAllTables(); - } - - @Nested - @DisplayName("์ƒํ’ˆ ๋ชฉ๋ก ์กฐํšŒ ํ…Œ์ŠคํŠธ") - class ProductListTests { - - Product product; - - } -} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductTest.java deleted file mode 100644 index c2c6fdd9b..000000000 --- a/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductTest.java +++ /dev/null @@ -1,95 +0,0 @@ -package com.loopers.domain.product; - -import com.loopers.support.error.CoreException; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -/** - * packageName : com.loopers.domain.product - * fileName : ProductTest - * author : byeonsungmun - * date : 2025. 11. 10. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 10. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -class ProductTest { - @DisplayName("Product ์ข‹์•„์š” ์ˆ˜ ์ฆ๊ฐ ํ…Œ์ŠคํŠธ") - @Nested - class LikeCountChange { - - @DisplayName("์ข‹์•„์š” ์ˆ˜๋ฅผ ์ฆ๊ฐ€์‹œํ‚จ๋‹ค.") - @Test - void increaseLikeCount_incrementsLikeCount() { - // given - Product product = Product.create(11L, "๋‚˜์ดํ‚ค ์—์–ด2", 30000L, 10L); - - // when - product.increaseLikeCount(); - - // then - assertEquals(1L, product.getLikeCount()); - } - - @DisplayName("์ข‹์•„์š” ์ˆ˜๋ฅผ ๊ฐ์†Œ์‹œํ‚จ๋‹ค. 0 ๋ฏธ๋งŒ์œผ๋กœ๋Š” ๊ฐ์†Œํ•˜์ง€ ์•Š๋Š”๋‹ค.") - @Test - void decreaseLikeCount_decrementsLikeCountButNotBelowZero() { - // given - Product product = Product.create(11L, "๋‚˜์ดํ‚ค ์—์–ด2", 30000L, 1L); - - // when - product.decreaseLikeCount(); - - // then - assertEquals(0L, product.getLikeCount()); - - // when decrease again - product.decreaseLikeCount(); - - // then likeCount should not go below 0 - assertEquals(0L, product.getLikeCount()); - } - } - - @DisplayName("Product ์žฌ๊ณ  ์ฐจ๊ฐ ํ…Œ์ŠคํŠธ") - @Nested - class Stock { - - @DisplayName("์žฌ๊ณ ๋ฅผ ์ •์ƒ ์ฐจ๊ฐํ•œ๋‹ค.") - @Test - void decreaseStock_successfullyDecreasesStock() { - // given - Product product = Product.create(1L, "๋‚˜์ดํ‚ค ์—์–ด2", 30000L, 10L); - - // when - product.decreaseStock(3L); - - // then - assertEquals(7, product.getStock()); - } - - @DisplayName("์ฐจ๊ฐ ์ˆ˜๋Ÿ‰์ด 0 ์ดํ•˜์ด๋ฉด ์˜ˆ์™ธ ๋ฐœ์ƒ") - @Test - void decreaseStock_withInvalidQuantity_throwsException() { - Product product = Product.create(1L, "๋‚˜์ดํ‚ค ์—์–ด2", 30000L, 10L); - - assertThrows(CoreException.class, () -> product.decreaseStock(0L)); - assertThrows(CoreException.class, () -> product.decreaseStock(-1L)); - } - - @DisplayName("์žฌ๊ณ ๋ณด๋‹ค ํฐ ์ˆ˜๋Ÿ‰ ์ฐจ๊ฐ ์‹œ ์˜ˆ์™ธ ๋ฐœ์ƒ") - @Test - void decreaseStock_withInsufficientStock_throwsException() { - Product product = Product.create(1L, "๋‚˜์ดํ‚ค ์—์–ด2", 30000L, 10L); - - assertThrows(CoreException.class, () -> product.decreaseStock(11L)); - } - } -} - diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java deleted file mode 100644 index 71091883f..000000000 --- a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java +++ /dev/null @@ -1,112 +0,0 @@ -package com.loopers.domain.user; - -import com.loopers.support.error.CoreException; -import com.loopers.utils.DatabaseCleanUp; -import org.junit.jupiter.api.*; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertAll; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; - -@SpringBootTest -class UserServiceIntegrationTest { - - @Autowired - private UserRepository userRepository; - - @Autowired - private UserService userService; - - @Autowired - private DatabaseCleanUp databaseCleanUp; - - @AfterEach - void tearDown() { - databaseCleanUp.truncateAllTables(); - } - - @DisplayName("ํšŒ์› ๊ฐ€์ž… ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ") - @Nested - class UserRegister { - - @DisplayName("ํšŒ์› ๊ฐ€์ž…์‹œ User ์ €์žฅ์ด ์ˆ˜ํ–‰๋œ๋‹ค.") - @Test - void save_whenUserRegister() { - //given - String userId = "yh45g"; - String email = "yh45g@loopers.com"; - String brith = "1994-12-05"; - String gender = "Male"; - - UserRepository userRepositorySpy = spy(userRepository); - UserService userServiceSpy = new UserService(userRepositorySpy); - - //when - userServiceSpy.register(userId, email, brith, gender); - - //then - verify(userRepositorySpy).save(any(User.class)); - } - - @DisplayName("์ด๋ฏธ ๊ฐ€์ž…๋œ ID ๋กœ ํšŒ์›๊ฐ€์ž… ์‹œ๋„ ์‹œ, ์‹คํŒจํ•œ๋‹ค.") - @Test - void throwsException_whenDuplicateUserId() { - //given - String userId = "yh45g"; - String email = "yh45g@loopers.com"; - String brith = "1994-12-05"; - String gender = "Male"; - - //when - userService.register(userId, email, brith, gender); - - //then - Assertions.assertThrows(CoreException.class, () - -> userService.register(userId, email, brith, gender)); - } - } - - @DisplayName("๋‚ด ์ •๋ณด ์กฐํšŒ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ") - @Nested - class Get { - - @DisplayName("ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•  ๊ฒฝ์šฐ, ํšŒ์› ์ •๋ณด๊ฐ€ ๋ฐ˜ํ™˜๋œ๋‹ค.") - @Test - void returnsUser_whenValidIdIsProvided() { - //given - String userId = "yh45g"; - String email = "yh45g@loopers.com"; - String brith = "1994-12-05"; - String gender = "Male"; - - //when - userService.register(userId, email, brith, gender); - User user = userService.findUserByUserId(userId); - - //then - assertAll( - () -> assertThat(user.getUserId()).isEqualTo(userId), - () -> assertThat(user.getEmail()).isEqualTo(email), - () -> assertThat(user.getBirth()).isEqualTo(brith), - () -> assertThat(user.getGender()).isEqualTo(gender) - ); - } - - @DisplayName("ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ, null ์ด ๋ฐ˜ํ™˜๋œ๋‹ค.") - @Test - void returnNull_whenInvalidUserIdIsProvided() { - //given - String userId = "yh45g"; - - //when - User user = userService.findUserByUserId(userId); - - //then - assertThat(user).isNull(); - } - } -} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserTest.java deleted file mode 100644 index 7d74fdfe2..000000000 --- a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserTest.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.loopers.domain.user; - -import com.loopers.support.error.CoreException; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertThrows; - -class UserTest { - @DisplayName("User ๋‹จ์œ„ ํ…Œ์ŠคํŠธ") - @Nested - class Create { - @DisplayName("ID ๊ฐ€ ์˜๋ฌธ ๋ฐ ์ˆซ์ž 10์ž ์ด๋‚ด ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค.") - @Test - void throwsException_whenInvalidUserIdFormat() { - // given - String invalidUserId = "invalid_id_123"; // 10์ž ์ดˆ๊ณผ + ํŠน์ˆ˜๋ฌธ์ž ํฌํ•จ - String email = "valid@loopers.com"; - String birth = "1994-12-05"; - String gender = "MALE"; - - // when & then - assertThrows(CoreException.class, () -> new User(invalidUserId, email, birth, gender)); - } - - @DisplayName("์ด๋ฉ”์ผ์ด `xx@yy.zz` ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค.") - @Test - void throwsException_whenInvalidEmailFormat() { - // given - String userId = "yh45g"; - String invalidEmail = "invalid-email-format"; // '@' ์—†์Œ - String birth = "1994-12-05"; - String gender = "MALE"; - - // when & then - assertThrows(CoreException.class, () -> new User(userId, invalidEmail, birth, gender)); - } - - @DisplayName("์ƒ๋…„์›”์ผ์ด `yyyy-MM-dd` ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค.") - @Test - void throwsException_whenInvalidBirthFormat() { - // given - String userId = "yh45g"; - String email = "valid@loopers.com"; - String invalidBirth = "19941205"; // ํ˜•์‹ ์˜ค๋ฅ˜: ํ•˜์ดํ”ˆ ์—†์Œ - String gender = "MALE"; - - // when & then - assertThrows(CoreException.class, () -> new User(userId, email, invalidBirth, gender)); - } - } -} diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/ExampleV1ApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/ExampleV1ApiE2ETest.java new file mode 100644 index 000000000..1bb3dba65 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/ExampleV1ApiE2ETest.java @@ -0,0 +1,114 @@ +package com.loopers.interfaces.api; + +import com.loopers.domain.example.ExampleModel; +import com.loopers.infrastructure.example.ExampleJpaRepository; +import com.loopers.interfaces.api.example.ExampleV1Dto; +import com.loopers.utils.DatabaseCleanUp; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import java.util.function.Function; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class ExampleV1ApiE2ETest { + + private static final Function ENDPOINT_GET = id -> "/api/v1/examples/" + id; + + private final TestRestTemplate testRestTemplate; + private final ExampleJpaRepository exampleJpaRepository; + private final DatabaseCleanUp databaseCleanUp; + + @Autowired + public ExampleV1ApiE2ETest( + TestRestTemplate testRestTemplate, + ExampleJpaRepository exampleJpaRepository, + DatabaseCleanUp databaseCleanUp + ) { + this.testRestTemplate = testRestTemplate; + this.exampleJpaRepository = exampleJpaRepository; + this.databaseCleanUp = databaseCleanUp; + } + + @AfterEach + void tearDown() { + databaseCleanUp.truncateAllTables(); + } + + @DisplayName("GET /api/v1/examples/{id}") + @Nested + class Get { + @DisplayName("์กด์žฌํ•˜๋Š” ์˜ˆ์‹œ ID๋ฅผ ์ฃผ๋ฉด, ํ•ด๋‹น ์˜ˆ์‹œ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnsExampleInfo_whenValidIdIsProvided() { + // arrange + ExampleModel exampleModel = exampleJpaRepository.save( + new ExampleModel("์˜ˆ์‹œ ์ œ๋ชฉ", "์˜ˆ์‹œ ์„ค๋ช…") + ); + String requestUrl = ENDPOINT_GET.apply(exampleModel.getId()); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; + ResponseEntity> response = + testRestTemplate.exchange(requestUrl, HttpMethod.GET, new HttpEntity<>(null), responseType); + + // assert + assertAll( + () -> assertTrue(response.getStatusCode().is2xxSuccessful()), + () -> assertThat(response.getBody().data().id()).isEqualTo(exampleModel.getId()), + () -> assertThat(response.getBody().data().name()).isEqualTo(exampleModel.getName()), + () -> assertThat(response.getBody().data().description()).isEqualTo(exampleModel.getDescription()) + ); + } + + @DisplayName("์ˆซ์ž๊ฐ€ ์•„๋‹Œ ID ๋กœ ์š”์ฒญํ•˜๋ฉด, 400 BAD_REQUEST ์‘๋‹ต์„ ๋ฐ›๋Š”๋‹ค.") + @Test + void throwsBadRequest_whenIdIsNotProvided() { + // arrange + String requestUrl = "/api/v1/examples/๋‚˜๋‚˜"; + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; + ResponseEntity> response = + testRestTemplate.exchange(requestUrl, HttpMethod.GET, new HttpEntity<>(null), responseType); + + // assert + assertAll( + () -> assertTrue(response.getStatusCode().is4xxClientError()), + () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST) + ); + } + + @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์˜ˆ์‹œ ID๋ฅผ ์ฃผ๋ฉด, 404 NOT_FOUND ์‘๋‹ต์„ ๋ฐ›๋Š”๋‹ค.") + @Test + void throwsException_whenInvalidIdIsProvided() { + // arrange + Long invalidId = -1L; + String requestUrl = ENDPOINT_GET.apply(invalidId); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; + ResponseEntity> response = + testRestTemplate.exchange(requestUrl, HttpMethod.GET, new HttpEntity<>(null), responseType); + + // assert + assertAll( + () -> assertTrue(response.getStatusCode().is4xxClientError()), + () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND) + ); + } + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/point/PointV1ControllerTest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/point/PointV1ControllerTest.java deleted file mode 100644 index 7d7a2c18c..000000000 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/point/PointV1ControllerTest.java +++ /dev/null @@ -1,156 +0,0 @@ -package com.loopers.interfaces.api.point; - -import com.loopers.domain.point.Point; -import com.loopers.domain.point.PointRepository; -import com.loopers.domain.user.User; -import com.loopers.domain.user.UserRepository; -import com.loopers.interfaces.api.ApiResponse; -import com.loopers.utils.DatabaseCleanUp; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.web.client.TestRestTemplate; -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.http.*; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertAll; -import static org.junit.jupiter.api.Assertions.assertTrue; - -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -class PointV1ControllerTest { - - private static final String GET_USER_POINT_ENDPOINT = "/api/v1/points"; - private static final String POST_USER_POINT_ENDPOINT = "/api/v1/points/charge"; - - @Autowired - private PointRepository pointRepository; - - @Autowired - private UserRepository userRepository; - - @Autowired - private DatabaseCleanUp databaseCleanUp; - - @Autowired - private TestRestTemplate testRestTemplate; - - @AfterEach - void tearDown() { - databaseCleanUp.truncateAllTables(); - } - - @DisplayName("GET /api/v1/points") - @Nested - class UserPoint { - - @DisplayName("ํฌ์ธํŠธ ์กฐํšŒ์— ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ๋ณด์œ  ํฌ์ธํŠธ๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnPoint_whenValidUserIdIsProvided() { - //given - String id = "yh45g"; - String email = "yh45g@loopers.com"; - String birth = "1994-12-05"; - String gender = "MALE"; - Long amount = 1000L; - - userRepository.save(new User(id, email, birth, gender)); - pointRepository.save(Point.create(id, amount)); - - HttpHeaders headers = new HttpHeaders(); - headers.add("X-USER-ID", id); - - //when - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; - ResponseEntity> response = - testRestTemplate.exchange(GET_USER_POINT_ENDPOINT, HttpMethod.GET, new HttpEntity<>(null, headers), responseType); - - //then - assertAll( - () -> assertTrue(response.getStatusCode().is2xxSuccessful()), - () -> assertThat(response.getBody().data().userId()).isEqualTo(id), - () -> assertThat(response.getBody().data().amount()).isEqualTo(1000L) - ); - } - - @DisplayName("ํ•ด๋‹น ID์˜ ํšŒ์›์ด ์กด์žฌํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ, null์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnNull_whenUserIdExists() { - //given - String id = "yh45g"; - - HttpHeaders headers = new HttpHeaders(); - headers.add("X-USER-ID", id); - - //when - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; - ResponseEntity> response = - testRestTemplate.exchange(GET_USER_POINT_ENDPOINT, HttpMethod.GET, new HttpEntity<>(null, headers), responseType); - - //then - assertAll( - () -> assertTrue(response.getStatusCode().is4xxClientError()), - () -> assertThat(response.getBody().data()).isNull() - ); - } - } - - @DisplayName("POST /api/v1/points/charge") - @Nested - class Charge { - - @DisplayName("์กด์žฌํ•˜๋Š” ์œ ์ €๊ฐ€ 1000์›์„ ์ถฉ์ „ํ•  ๊ฒฝ์šฐ, ์ถฉ์ „๋œ ๋ณด์œ  ์ด๋Ÿ‰์„ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnsTotalPoint_whenChargeUserPoint() { - //given - String id = "yh45g"; - String email = "yh45g@loopers.com"; - String birth = "1994-12-05"; - String gender = "MALE"; - - userRepository.save(new User(id, email, birth, gender)); - pointRepository.save(Point.create(id, 0L)); - - PointV1Dto.ChargePointRequest request = new PointV1Dto.ChargePointRequest(id, 1000L); - - HttpHeaders headers = new HttpHeaders(); - headers.add("X-USER-ID", id); - - //when - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; - ResponseEntity> response = - testRestTemplate.exchange(POST_USER_POINT_ENDPOINT, HttpMethod.PATCH, new HttpEntity<>(request, headers), responseType); - - //then - assertAll( - () -> assertTrue(response.getStatusCode().is2xxSuccessful()), - () -> assertThat(response.getBody().data().userId()).isEqualTo(id), - () -> assertThat(response.getBody().data().amount()).isEqualTo(1000L) - ); - } - - @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ์ €๋กœ ์š”์ฒญํ•  ๊ฒฝ์šฐ, 404 Not Found ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void throwsException_whenInvalidUserIdIsProvided() { - //given - String id = "yh45g"; - - HttpHeaders headers = new HttpHeaders(); - headers.add("X-USER-ID", id); - - //when - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; - ResponseEntity> response = - testRestTemplate.exchange(POST_USER_POINT_ENDPOINT, HttpMethod.PATCH, new HttpEntity<>(null, headers), responseType); - - //then - assertAll( - () -> assertTrue(response.getStatusCode().is4xxClientError()), - () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND) - ); - } - } -} diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/user/UserV1ControllerTest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/user/UserV1ControllerTest.java deleted file mode 100644 index defe2fcd5..000000000 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/user/UserV1ControllerTest.java +++ /dev/null @@ -1,148 +0,0 @@ -package com.loopers.interfaces.api.user; - -import com.loopers.domain.user.User; -import com.loopers.infrastructure.user.UserJpaRepository; -import com.loopers.interfaces.api.ApiResponse; -import com.loopers.utils.DatabaseCleanUp; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.web.client.TestRestTemplate; -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpMethod; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; - -import java.util.function.Function; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertAll; -import static org.junit.jupiter.api.Assertions.assertTrue; - -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -class UserV1ControllerTest { - - private static final String USER_REGISTER_ENDPOINT = "/api/v1/users/register"; - private static final Function GET_USER_ENDPOINT = id -> "/api/v1/users/" + id; - - @Autowired - private TestRestTemplate testRestTemplate; - - @Autowired - private UserJpaRepository userJpaRepository; - - @Autowired - private DatabaseCleanUp databaseCleanUp; - - @AfterEach - void tearDown() { - databaseCleanUp.truncateAllTables(); - } - - @DisplayName("POST /api/v1/users") - @Nested - class RegisterUser { - @DisplayName("ํšŒ์› ๊ฐ€์ž…์ด ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ์ƒ์„ฑ๋œ ์œ ์ € ์ •๋ณด๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void registerUser_whenSuccessResponseUser() { - //given - String userId = "yh45g"; - String email = "yh45g@loopers.com"; - String birth = "1994-12-05"; - String gender = "MALE"; - - UserV1Dto.RegisterRequest request = new UserV1Dto.RegisterRequest(userId, email, birth, gender); - - //when - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; - ResponseEntity> response = - testRestTemplate.exchange(USER_REGISTER_ENDPOINT, HttpMethod.POST, new HttpEntity<>(request), responseType); - - //then - assertAll( - () -> assertTrue(response.getStatusCode().is2xxSuccessful()), - () -> assertThat(response.getBody().data().userId()).isEqualTo(userId), - () -> assertThat(response.getBody().data().email()).isEqualTo(email), - () -> assertThat(response.getBody().data().birth()).isEqualTo(birth), - () -> assertThat(response.getBody().data().gender()).isEqualTo(gender) - ); - } - @DisplayName("ํšŒ์› ๊ฐ€์ž… ์‹œ์— ์„ฑ๋ณ„์ด ์—†์„ ๊ฒฝ์šฐ, 400 BAD_REQUEST ์‘๋‹ต์„ ๋ฐ›๋Š”๋‹ค.") - @Test - void throwsBadRequest_whenGenderIsNotProvided() { - //given - String userId = "yh45g"; - String email = "yh45g@loopers.com"; - String birth = "1994-12-05"; - String gender = null; - - UserV1Dto.RegisterRequest request = new UserV1Dto.RegisterRequest(userId, email, birth, gender); - - //when - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; - ResponseEntity> response = - testRestTemplate.exchange(USER_REGISTER_ENDPOINT, HttpMethod.POST, new HttpEntity<>(request), responseType); - - //then - assertAll( - () -> assertTrue(response.getStatusCode().is4xxClientError()), - () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST) - ); - } - } - - @DisplayName("GET /api/v1/users/{userId}") - @Nested - class GetUserById { - @DisplayName("๋‚ด ์ •๋ณด ์กฐํšŒ์— ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ํ•ด๋‹นํ•˜๋Š” ์œ ์ € ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void getUserById_whenSuccessResponseUser() { - //given - String userId = "yh45g"; - String email = "yh45g@loopers.com"; - String birth = "1994-12-05"; - String gender = "MALE"; - - userJpaRepository.save(new User(userId, email, birth, gender)); - - String requestUrl = GET_USER_ENDPOINT.apply(userId); - - //when - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; - ResponseEntity> response = - testRestTemplate.exchange(requestUrl, HttpMethod.GET, new HttpEntity<>(null), responseType); - - //then - assertAll( - () -> assertTrue(response.getStatusCode().is2xxSuccessful()), - () -> assertThat(response.getBody().data().userId()).isEqualTo(userId), - () -> assertThat(response.getBody().data().email()).isEqualTo(email), - () -> assertThat(response.getBody().data().birth()).isEqualTo(birth), - () -> assertThat(response.getBody().data().gender()).isEqualTo(gender) - ); - } - - @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ID ๋กœ ์กฐํšŒํ•  ๊ฒฝ์šฐ, 404 Not Found ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void throwsException_whenInvalidUserIdIsProvided() { - //given - String userId = "notUserId"; - String requestUrl = GET_USER_ENDPOINT.apply(userId); - - //when - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; - ResponseEntity> response = - testRestTemplate.exchange(requestUrl, HttpMethod.GET, new HttpEntity<>(null), responseType); - - //then - assertAll( - () -> assertTrue(response.getStatusCode().is4xxClientError()), - () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND) - ); - } - } -} diff --git a/docs/1round/1round.md b/docs/1round/1round.md deleted file mode 100644 index 106d6c809..000000000 --- a/docs/1round/1round.md +++ /dev/null @@ -1,67 +0,0 @@ -## ๐Ÿงช Implementation Quest - -> ์ง€์ •๋œ **๋‹จ์œ„ ํ…Œ์ŠคํŠธ / ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ / E2E ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค**๋ฅผ ํ•„์ˆ˜๋กœ ๊ตฌํ˜„ํ•˜๊ณ , ๋ชจ๋“  ํ…Œ์ŠคํŠธ๋ฅผ ํ†ต๊ณผ์‹œํ‚ค๋Š” ๊ฒƒ์„ ๋ชฉํ‘œ๋กœ ํ•ฉ๋‹ˆ๋‹ค. -> - -### ํšŒ์› ๊ฐ€์ž… - -**๐Ÿงฑ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ** - -- [x] ID ๊ฐ€ `์˜๋ฌธ ๋ฐ ์ˆซ์ž 10์ž ์ด๋‚ด` ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. -- [x] ์ด๋ฉ”์ผ์ด `xx@yy.zz` ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. -- [x] ์ƒ๋…„์›”์ผ์ด `yyyy-MM-dd` ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. - -**๐Ÿ”— ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ** - -- [x] ํšŒ์› ๊ฐ€์ž…์‹œ User ์ €์žฅ์ด ์ˆ˜ํ–‰๋œ๋‹ค. ( spy ๊ฒ€์ฆ ) -- [x] ์ด๋ฏธ ๊ฐ€์ž…๋œ ID ๋กœ ํšŒ์›๊ฐ€์ž… ์‹œ๋„ ์‹œ, ์‹คํŒจํ•œ๋‹ค. - -**๐ŸŒ E2E ํ…Œ์ŠคํŠธ** - -- [x] ํšŒ์› ๊ฐ€์ž…์ด ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ์ƒ์„ฑ๋œ ์œ ์ € ์ •๋ณด๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค. -- [x] ํšŒ์› ๊ฐ€์ž… ์‹œ์— ์„ฑ๋ณ„์ด ์—†์„ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. - -### ๋‚ด ์ •๋ณด ์กฐํšŒ - -**๐Ÿ”— ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ** - -- [x] ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•  ๊ฒฝ์šฐ, ํšŒ์› ์ •๋ณด๊ฐ€ ๋ฐ˜ํ™˜๋œ๋‹ค. -- [x] ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ, null ์ด ๋ฐ˜ํ™˜๋œ๋‹ค. - -**๐ŸŒ E2E ํ…Œ์ŠคํŠธ** - -- [x] ๋‚ด ์ •๋ณด ์กฐํšŒ์— ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ํ•ด๋‹นํ•˜๋Š” ์œ ์ € ์ •๋ณด๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค. -- [x] ์กด์žฌํ•˜์ง€ ์•Š๋Š” ID ๋กœ ์กฐํšŒํ•  ๊ฒฝ์šฐ, `404 Not Found` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. - -### ํฌ์ธํŠธ ์กฐํšŒ - -**๐Ÿ”— ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ** - -- [x] ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•  ๊ฒฝ์šฐ, ๋ณด์œ  ํฌ์ธํŠธ๊ฐ€ ๋ฐ˜ํ™˜๋œ๋‹ค. -- [x] ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ, null ์ด ๋ฐ˜ํ™˜๋œ๋‹ค. - -**๐ŸŒ E2E ํ…Œ์ŠคํŠธ** - -- [x] ํฌ์ธํŠธ ์กฐํšŒ์— ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ๋ณด์œ  ํฌ์ธํŠธ๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค. -- [x] `X-USER-ID` ํ—ค๋”๊ฐ€ ์—†์„ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. - -### ํฌ์ธํŠธ ์ถฉ์ „ - -**๐Ÿงฑ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ** - -- [X] 0 ์ดํ•˜์˜ ์ •์ˆ˜๋กœ ํฌ์ธํŠธ๋ฅผ ์ถฉ์ „ ์‹œ ์‹คํŒจํ•œ๋‹ค. - -**๐Ÿ”— ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ** - -- [X] ์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ์ € ID ๋กœ ์ถฉ์ „์„ ์‹œ๋„ํ•œ ๊ฒฝ์šฐ, ์‹คํŒจํ•œ๋‹ค. - -**๐ŸŒ E2E ํ…Œ์ŠคํŠธ** - -- [X] ์กด์žฌํ•˜๋Š” ์œ ์ €๊ฐ€ 1000์›์„ ์ถฉ์ „ํ•  ๊ฒฝ์šฐ, ์ถฉ์ „๋œ ๋ณด์œ  ์ด๋Ÿ‰์„ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค. -- [X] ์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ์ €๋กœ ์š”์ฒญํ•  ๊ฒฝ์šฐ, `404 Not Found` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. - -## โœ… Checklist - -- [X] ํ…Œ์ŠคํŠธ๋Š” ๋ชจ๋‘ ํ†ต๊ณผํ•ด์•ผ ํ•˜๋ฉฐ, `@Test` ๊ธฐ๋ฐ˜์œผ๋กœ ๋ช…์‹œ์ ์œผ๋กœ ์ž‘์„ฑ -- [X] ๊ฐ ํ…Œ์ŠคํŠธ๋Š” ํ…Œ์ŠคํŠธ ๋ช…/์„ค๋ช…/์ž…๋ ฅ/์˜ˆ์ƒ ๊ฒฐ๊ณผ๊ฐ€ ๋ถ„๋ช…ํ•ด์•ผ ํ•จ -- [X] E2E ํ…Œ์ŠคํŠธ๋Š” ์‹ค์ œ HTTP ์š”์ฒญ์„ ํ†ตํ•ด ํ๋ฆ„์„ ๊ฒ€์ฆํ•  ๊ฒƒ \ No newline at end of file diff --git a/docs/2round/01-requirements.md b/docs/2round/01-requirements.md deleted file mode 100644 index 3296c21c6..000000000 --- a/docs/2round/01-requirements.md +++ /dev/null @@ -1,104 +0,0 @@ -# ์œ ์ €-์‹œ๋‚˜๋ฆฌ์˜ค - -## ์ƒํ’ˆ ๋ชฉ๋ก -1. ์‚ฌ์šฉ์ž๊ฐ€ ๋ชจ๋“  ์ƒํ’ˆ์— ๋Œ€ํ•ด์„œ ๋ณผ์ˆ˜ ์žˆ๋‹ค. -2. ํŒ๋งค์ค‘์ธ ์ƒํ’ˆ์— ๋Œ€ํ•œ ํŒ๋งค๋ช…, ํŒ๋งค๊ธˆ์•ก, ํŒ๋งค๋ธŒ๋žœ๋“œ, ์ด๋ฏธ์ง€, ์ข‹์•„์š”ํ•œ ์ƒํ’ˆ์„ ๋ณผ์ˆ˜์žˆ๋‹ค. -3. ์‚ฌ์šฉ์ž๋Š” ์›ํ•˜๋Š” ๋ธŒ๋žœ๋“œ๋ณ„๋กœ ์ƒํ’ˆ์„ ๋ณผ์ˆ˜ ์žˆ๋‹ค. -4. ์‚ฌ์šฉ์ž๋Š” ์›ํ•˜๋Š” ์ƒํ’ˆ์—๋Œ€ํ•ด์„œ ์ข‹์•„์š”๋ฅผ ๋ˆ„๋ฅผ์ˆ˜์žˆ๋‹ค. -5. ์‚ฌ์šฉ์ž๋Š” ์›ํ•˜๋Š” ์กฐ๊ฑด์— ๋”ฐ๋ผ ์ƒํ’ˆ์„ ๋ณผ์ˆ˜์žˆ๋‹ค. -6. ์‚ฌ์šฉ์ž๋Š” ์›ํ•˜๋Š” ํŽ˜์ด์ง€๋กœ ์ด๋™ํ• ์ˆ˜์žˆ๋‹ค. - --[๊ธฐ๋Šฅ] -1. ์ „์ฒด ์ƒํ’ˆ ๋ชฉ๋ก ์กฐํšŒ -2. ๋ธŒ๋žœ๋“œ ์ƒํ’ˆ ๋ชฉ๋ก ์กฐํšŒ -3. ์ƒํ’ˆ ์ข‹์•„์š” ๋“ฑ๋ก -4. ํ•„ํ„ฐ๋ง (์ตœ์‹ ์ˆœ, ํŒ๋งค์ˆœ, ์ข‹์•„์š” ์ˆœ, ๋‚จ์ž/์—ฌ์ž) -5. ํŽ˜์ด์ง• - --[์ œ์•ฝ] -1. ๋กœ๊ทธ์ธ์€ X-USER-ID๋กœ ํŒ๋‹จ, ๋น„ํšŒ์›์ผ๊ฒฝ์šฐ "๋กœ๊ทธ์ธ ํ›„ ์ข‹์•„์š” ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค" ๋ฉ”์‹œ์ง€ ์ „๋‹ฌํ•œ๋‹ค -2. ์ƒํ’ˆ์ด ์—†์„๊ฒฝ์šฐ 404_NOTFOUND ์ œ๊ณตํ•œ๋‹ค. - ---- -## ์ƒํ’ˆ ์ƒ์„ธ -1. ์‚ฌ์šฉ์ž๋Š” ์›ํ•˜๋Š” ํŒ๋งค์ค‘ ์ƒํ’ˆ(ํŒ๋งค๋ช…,ํŒ๋งค๊ธˆ์•ก,ํŒ๋งค๋ธŒ๋žœ๋“œ,์ด๋ฏธ์ง€,์ข‹์•„์š”ํ•œ ์ƒํ’ˆ์„ ๋ณผ์ˆ˜์žˆ๋‹ค. - -[๊ธฐ๋Šฅ] -1. ์‚ฌ์šฉ์ž๊ฐ€ ์›ํ•˜๋Š” ์ƒํ’ˆ๋ฒˆํ˜ธ๋กœ ์กฐํšŒ -2. ์ƒํ’ˆ ์ข‹์•„์š” ๋“ฑ๋ก / ์ทจ์†Œ - -[์ œ์•ฝ] -1. ๋กœ๊ทธ์ธ์€ X-USER-ID๋กœ ํŒ๋‹จ, ๋น„ํšŒ์›์ผ๊ฒฝ์šฐ "๋กœ๊ทธ์ธ ํ›„ ์ข‹์•„์š” ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค" ๋ฉ”์‹œ์ง€ ์ „๋‹ฌํ•œ๋‹ค -2. ์ƒํ’ˆ์ด ์กด์žฌํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ 404_NOTFOUND ์ œ๊ณตํ•œ๋‹ค. - ---- -## ์ข‹์•„์š” -1. ์‚ฌ์šฉ์ž๊ฐ€ ์›ํ•˜๋Š” ์ƒํ’ˆ์— ๋Œ€ํ•ด์„œ ์ข‹์•„์š”๋ฅผ ๋ˆ„๋ฅผ ์ˆ˜ ์žˆ๋‹ค. -2. ์‚ฌ์šฉ์ž๊ฐ€ ์›ํ•˜๋Š” ์ƒํ’ˆ์— ๋Œ€ํ•ด์„œ ์ข‹์•„์š”๋ฅผ ์ทจ์†Œ ํ•  ์ˆ˜ ์žˆ๋‹ค. -2. ์‚ฌ์šฉ์ž๊ฐ€ ์ข‹์•„ํ•˜๋Š” ์ƒํ’ˆ์— ๋Œ€ํ•ด ๋ชฉ๋ก์„ ๋ณผ์ˆ˜์žˆ๋‹ค. - -[๊ธฐ๋Šฅ] -1. ์ข‹์•„์š” ๋ˆ„๋ฅธ ์ƒํ’ˆ์—๋Œ€ํ•ด ๋ชฉ๋ก ์กฐํšŒ -2. ์‚ฌ์šฉ๊ฐ€ ์›ํ•˜๋Š” ์ƒํ’ˆ์— ๋Œ€ํ•œ ๋“ฑ๋ก/์ทจ์†Œ, ๋‹จ ๋“ฑ๋ก/ํ•ด์ œ (๋ฉฑ๋“ฑ์„ฑ) - -[์ œ์•ฝ] -1. ๋กœ๊ทธ์ธ์€ X-USER-ID๋กœ ํŒ๋‹จ, ๋น„ํšŒ์›์ผ๊ฒฝ์šฐ "๋กœ๊ทธ์ธ ํ›„ ์ข‹์•„์š” ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค" ๋ฉ”์‹œ์ง€ ์ „๋‹ฌํ•œ๋‹ค -2. ํ•ด๋‹น ์ƒํ’ˆ์— ์ข‹์•„์š”๋ฅผ ์ฒ˜์Œ ๋“ฑ๋ก ํ• ๋•Œ๋Š” 201_Created ์ œ๊ณตํ•œ๋‹ค -3. ํ•ด๋‹น ์ƒํ’ˆ์— ์ข‹์•„์š”๋ฅผ ํ•œ๋ฒˆ๋” ๋“ฑ๋ก ํ• ๋•Œ๋Š” 200_OK ์ œ๊ณตํ•œ๋‹ค -4. ํ•ด๋‹น ์ƒํ’ˆ์— ์ข‹์•„์š”๊ฐ€ ๋“ฑ๋ก ๋œ ์ƒํƒœ์—์„œ ์ทจ์†Œ ํ• ๋•Œ๋Š” 204 No Content ์ œ๊ณตํ•œ๋‹ค -5. ํ•ด๋‹น ์ƒํ’ˆ์— ์ข‹์•„์š”๊ฐ€ ์ทจ์†Œ๊ฐ€ ๋œ ์ƒํƒœ์—์„œ ํ•œ๋ฒˆ๋” ์ทจ์†Œ ํ• ๋•Œ๋Š” 204 No Content ์ œ๊ณตํ•œ๋‹ค ---- -## ๋ธŒ๋žœ๋“œ -1. ์‚ฌ์šฉ์ž๋Š” ๋ชจ๋“  ๋ธŒ๋žœ๋“œ์˜ ์ƒํ’ˆ์„ ๋ณผ์ˆ˜์žˆ๋‹ค. -2. ์‚ฌ์šฉ์ž๋Š” ํŠน์ • ๋ธŒ๋žœ๋“œ์— ๋Œ€ํ•œ ์ƒํ’ˆ๋งŒ ๋ณผ์ˆ˜์žˆ๋‹ค. -3. ์‚ฌ์šฉ์ž๋Š” ํŠน์ • ๊ธฐ์ค€์œผ๋กœ ์ƒํ’ˆ์„ ๋ณผ์ˆ˜์žˆ๋‹ค. (์ตœ์‹ ์ˆœ, ํŒ๋งค์ˆœ, ์ข‹์•„์š” ์ˆœ) -4. ์‚ฌ์šฉ์ž๋Š” ์›ํ•˜๋Š” ํŽ˜์ด์ง€๋กœ ์ด๋™ํ• ์ˆ˜์žˆ๋‹ค. - [๊ธฐ๋Šฅ] -1. ๋ชจ๋“  ๋ธŒ๋žœ๋“œ ์ƒํ’ˆ ์กฐํšŒ -2. ํŠน์ • ๋ธŒ๋žœ๋“œ ์ƒํ’ˆ ์กฐํšŒ -3. ํ•„ํ„ฐ๋ง (์ตœ์‹ ์ˆœ, ํŒ๋งค์ˆœ, ์ข‹์•„์š” ์ˆœ, ๋‚จ์ž/์—ฌ์ž) -4. ํŽ˜์ด์ง• - [์ œ์•ฝ] -1. ๋ธŒ๋žœ๋“œ๊ฐ€ ์—†์„์‹œ 404_NOTFOUND๋ฅผ ์ œ๊ณตํ•œ๋‹ค ---- -## ์ฃผ๋ฌธ -1. ์‚ฌ์šฉ์ž๊ฐ€ ์ƒํ’ˆ ๋ชฉ๋ก์—์„œ ์›ํ•˜๋Š” ์ƒํ’ˆ์„ ์„ ํƒํ•˜์—ฌ ์ฃผ๋ฌธํ•  ์ˆ˜ ์žˆ๋‹ค. -2. ํ•œ๊ฐœ์˜ ์ฃผ๋ฌธ์€ ์—ฌ๋Ÿฌ ์ƒํ’ˆ์„ ํฌํ•จํ•  ์ˆ˜ ์žˆ๋‹ค. -3. ์‚ฌ์šฉ์ž๊ฐ€ ์ฃผ๋ฌธ ๋‚ด์—ญ์„ ์กฐํšŒํ•ด ์–ด๋–ค ์ƒํ’ˆ์„ ์ฃผ๋ฌธํ–ˆ๋Š”์ง€ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค. -4. ์‚ฌ์šฉ์ž๋Š” ๊ฒฐ์ œ ์ „์ด๋ผ๋ฉด ์ฃผ๋ฌธ์„ ์ทจ์†Œํ•  ์ˆ˜ ์žˆ๋‹ค. -5. ์‚ฌ์šฉ์ž๋Š” ์ฃผ๋ฌธ ์ƒ์„ธ ํ™”๋ฉด์—์„œ ์ƒํ’ˆ ์ •๋ณด, ์ˆ˜๋Ÿ‰, ๊ฒฐ์ œ ๊ธˆ์•ก, ์ƒํƒœ ๋“ฑ์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค. - [๊ธฐ๋Šฅ] -1. ์ฃผ๋ฌธ ์ƒ์„ฑ -2. ์ฃผ๋ฌธ ๋ชฉ๋ก ์กฐํšŒ -3. ์ฃผ๋ฌธ ์ƒ์„ธ ์กฐํšŒ -4. ์ฃผ๋ฌธ ์ทจ์†Œ -5. ์ฃผ๋ฌธ์— ๋Œ€ํ•œ ์ƒํƒœ๊ด€๋ฆฌ - [์ œ์•ฝ] -1. ์ฃผ๋ฌธ ์‹œ ์ƒํ’ˆ ์žฌ๊ณ  ํ™•์ธ ๋ฐ ๋ถ€์กฑ ์‹œ ์˜ˆ์™ธ ๋ฐœ์ƒ -2. ํฌ์ธํŠธ ์ž”์•ก ๋ถ€์กฑ ์‹œ ์ฃผ๋ฌธ ๋ถˆ๊ฐ€ -3. ๋™์ผํ•œ ์ฃผ๋ฌธ ์š”์ฒญ์ด ์ค‘๋ณต์œผ๋กœ ๋“ค์–ด์™€๋„ ํ•œ ๋ฒˆ๋งŒ ์ฒ˜๋ฆฌ ---- -## ๊ฒฐ์ œ -1. ์‚ฌ์šฉ์ž๋Š” ์ฃผ๋ฌธํ•œ ์ƒํ’ˆ์— ๋Œ€ํ•ด ํฌ์ธํŠธ๋กœ ๊ฒฐ์ œํ•  ์ˆ˜ ์žˆ๋‹ค. -2. ๊ฒฐ์ œ๊ฐ€ ์™„๋ฃŒ๋˜๋ฉด ์ฃผ๋ฌธ ์ƒํƒœ๊ฐ€ ๊ฒฐ์ œ ์™„๋ฃŒ๋กœ ๋ณ€๊ฒฝ๋œ๋‹ค. -3. ๊ฒฐ์ œ ์ค‘ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ๊ฒฐ์ œ ์‹คํŒจ๋กœ ์ฒ˜๋ฆฌ๋˜๋ฉฐ ํฌ์ธํŠธ์™€ ์žฌ๊ณ ๋Š” ๋ณต๊ตฌ๋œ๋‹ค. -4. ๊ฒฐ์ œ ์™„๋ฃŒ ํ›„์—๋Š” ์ฃผ๋ฌธ ์ทจ์†Œ๊ฐ€ ๋ถˆ๊ฐ€๋Šฅํ•˜๋‹ค. - [๊ธฐ๋Šฅ] -1. ๊ฒฐ์ œ์š”์ฒญ -2. ๊ฒฐ์ œ ๊ฒฐ๊ณผ ๋ฐ˜์˜ -3. ๊ฒฐ์ œ ์‹คํŒจ ์ฒ˜๋ฆฌ -4. ๊ฒฐ์ œ ๋‚ด์—ญ ์กฐํšŒ - [์ œ์•ฝ] -1. ๋™์ผ ์ฃผ๋ฌธ์— ๋Œ€ํ•ด ์ค‘๋ณต ๊ฒฐ์ œ ์š”์ฒญ ์‹œ ํ•œ ๋ฒˆ๋งŒ ์ฒ˜๋ฆฌ -2. ํฌ์ธํŠธ ์ฐจ๊ฐ์‹คํŒจ ์‹œ ๋ณต๊ตฌ -3. ์™ธ๋ถ€๊ฒฐ์ œ ์‹œ์Šคํ…œ ๊ฒฐ์ œ ์‹œ์Šคํ…œ ์ฒ˜๋ฆฌ ์‹คํŒจ์‹œ ์˜ˆ์™ธ์ฒ˜๋ฆฌ - ----- -## Ubiquitous -| ํ•œ๊ตญ์–ด | ์˜์–ด | -|--------|------| -| ์‚ฌ์šฉ์ž | User | -| ํฌ์ธํŠธ | Point | -| ์ƒํ’ˆ | Product | -| ๋ธŒ๋žœ๋“œ | Brand | -| ์ข‹์•„์š” | Like | -| ์ฃผ๋ฌธ | Order | -| ์žฌ๊ณ  | Stock | -| ๊ฐ€๊ฒฉ | Price | -| ๊ฒฐ์ œ | Payment | \ No newline at end of file diff --git a/docs/2round/02-sequence-diagrams.md b/docs/2round/02-sequence-diagrams.md deleted file mode 100644 index 5264a4dc0..000000000 --- a/docs/2round/02-sequence-diagrams.md +++ /dev/null @@ -1,164 +0,0 @@ -# ์‹œํ€€์Šค ๋‹ค์ด์–ด๊ทธ๋žจ - -### 1. ์ƒํ’ˆ ๋ชฉ๋ก ์กฐํšŒ -```mermaid -sequenceDiagram - participant User - participant ProductController - participant ProductService - participant ProductRepository - participant BrandRepository - participant LikeRepository - - User->>ProductController: GET /api/v1/products - ProductController->>ProductService: getProductList - ProductService->>ProductRepository: findAllWithPaging - ProductService->>BrandRepository: findBrandInfoForProducts() - ProductService->>LikeRepository: countLikesForProducts() - ProductRepository-->>ProductService: productList - ProductService-->>ProductController: productListResponse - ProductController-->>User: 200 OK (์ƒํ’ˆ ๋ชฉ๋ก + ๋ธŒ๋žœ๋“œ + ์ข‹์•„์š” ์ˆ˜) -``` ---- -### 2. ์ƒํ’ˆ ์ƒ์„ธ ์กฐํšŒ -```mermaid -sequenceDiagram - participant User - participant ProductController - participant ProductService - participant ProductRepository - participant BrandRepository - participant LikeRepository - - User->>ProductController: GET /api/v1/products/{productId} - ProductController->>ProductService: getProductDetail(productId, userId) - ProductService->>ProductRepository: findById(productId) - ProductService->>BrandRepository: findBrandInfo(brandId) - ProductService->>LikeRepository: existsByUserIdAndProductId(userId, productId) - ProductRepository-->>ProductService: productDetail - ProductService-->>ProductController: productDetailResponse - ProductController-->>User: 200 OK (์ƒํ’ˆ ์ƒ์„ธ ์ •๋ณด) -``` ---- -### 3. ์ƒํ’ˆ ์ข‹์•„์š” ๋“ฑ๋ก/์ทจ์†Œ -```mermaid -sequenceDiagram - participant User - participant LikeController - participant LikeService - participant LikeRepository - - User->>LikeController: POST /api/v1/like/products/{productId} - LikeController->>LikeService: toggleLike(userId, productId) - LikeService->>LikeRepository: existsByUserIdAndProductId(userId, productId) - alt ์ข‹์•„์š”๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Œ - LikeService->>LikeRepository: save(userId, productId) - LikeService-->>LikeController: 201 Created - else ์ด๋ฏธ ์ข‹์•„์š” ๋˜์–ด์žˆ์Œ - LikeService->>LikeRepository: delete(userId, productId) - LikeService-->>LikeController: 204 No Content - end - LikeController-->>User: ์‘๋‹ต (์ƒํƒœ์ฝ”๋“œ์— ๋”ฐ๋ผ ๋‹ค๋ฆ„) -``` ---- - -### 4. ๋ธŒ๋žœ๋“œ๋ณ„ ์ƒํ’ˆ ์กฐํšŒ -```mermaid -sequenceDiagram - participant User - participant BrandController - participant BrandService - participant ProductRepository - participant BrandRepository - - User->>BrandController: GET /api/v1/brands/{brandId}/products - BrandController->>BrandService: getProductsByBrand(brandId, sort, page) - BrandService->>BrandRepository: findById(brandId) - BrandService->>ProductRepository: findByBrandId(brandId, sort, page) - BrandRepository-->>BrandService: brandInfo - ProductRepository-->>BrandService: productList - BrandService-->>BrandController: productListResponse - BrandController-->>User: 200 OK (๋ธŒ๋žœ๋“œ ์ƒํ’ˆ ๋ชฉ๋ก) -``` ---- -### 5. ์ฃผ๋ฌธ ์ƒ์„ฑ -```mermaid -sequenceDiagram - participant User - participant OrderController - participant OrderService - participant ProductReader - participant StockService - participant PointService - participant OrderRepository - - User->>OrderController: POST /api/v1/orders (items[]) - OrderController->>OrderService: createOrder(userId, items) - OrderService->>ProductReader: getProductsByIds(productIds) - loop ๊ฐ ์ƒํ’ˆ์— ๋Œ€ํ•ด - OrderService->>StockService: checkAndDecreaseStock(productId, quantity) - end - OrderService->>PointService: deductPoint(userId, totalPrice) - alt ์žฌ๊ณ  ๋˜๋Š” ํฌ์ธํŠธ ๋ถ€์กฑ - OrderService-->>OrderController: throw Exception - OrderController-->>User: 400 Bad Request - else ์ •์ƒ - OrderService->>OrderRepository: save(order, orderItems) - OrderService-->>OrderController: OrderResponse - OrderController-->>User: 201 Created (์ฃผ๋ฌธ ์™„๋ฃŒ) - end -``` ---- -### 6. ์ฃผ๋ฌธ ๋ชฉ๋ก ๋ฐ ์ƒ์„ธ ์กฐํšŒ -```mermaid -sequenceDiagram - participant User - participant OrderController - participant OrderService - participant OrderRepository - participant ProductRepository - - User->>OrderController: GET /api/v1/orders - OrderController->>OrderService: getOrderList(userId) - OrderService->>OrderRepository: findByUserId(userId) - OrderRepository-->>OrderService: orderList - OrderService-->>OrderController: orderListResponse - OrderController-->>User: 200 OK (์ฃผ๋ฌธ ๋ชฉ๋ก) - - User->>OrderController: GET /api/v1/orders/{orderId} - OrderController->>OrderService: getOrderDetail(orderId, userId) - OrderService->>OrderRepository: findById(orderId) - OrderService->>ProductRepository: findProductsInOrder(orderId) - OrderRepository-->>OrderService: orderDetail - OrderService-->>OrderController: orderDetailResponse - OrderController-->>User: 200 OK (์ฃผ๋ฌธ ์ƒ์„ธ) -``` ---- -### 7. ๊ฒฐ์ œ ์ฒ˜๋ฆฌ -```mermaid -sequenceDiagram - participant User - participant PaymentController - participant PaymentService - participant PaymentGateway - participant OrderRepository - participant PointService - participant StockService - - User->>PaymentController: POST /api/v1/payments (orderId) - PaymentController->>PaymentService: processPayment(orderId, userId) - PaymentService->>OrderRepository: findById(orderId) - PaymentService->>PaymentGateway: requestPayment(orderId, amount) - alt ๊ฒฐ์ œ ์„ฑ๊ณต - PaymentGateway-->>PaymentService: SUCCESS - PaymentService->>OrderRepository: updateStatus(orderId, PAID) - PaymentService-->>PaymentController: successResponse - PaymentController-->>User: 200 OK (๊ฒฐ์ œ ์™„๋ฃŒ) - else ๊ฒฐ์ œ ์‹คํŒจ - PaymentGateway-->>PaymentService: FAILED - PaymentService->>PointService: rollbackPoint(userId, amount) - PaymentService->>StockService: restoreStock(orderId) - PaymentService->>OrderRepository: updateStatus(orderId, FAILED) - PaymentController-->>User: 500 Internal Server Error (๊ฒฐ์ œ ์‹คํŒจ) - end -``` diff --git a/docs/2round/03-class-diagram.md b/docs/2round/03-class-diagram.md deleted file mode 100644 index 8d39cfd0a..000000000 --- a/docs/2round/03-class-diagram.md +++ /dev/null @@ -1,78 +0,0 @@ -# ํด๋ž˜์Šค ๋‹ค์ด์–ด๊ทธ๋žจ - -```mermaid -classDiagram -direction TB - -class User { - Long id - String userId - String name - String email - String gender -} - -class Point { - Long id - String userId - Long balance -} - -class Brand { - Long id - String name -} - -class Product { - Long id - Long brandId - String name - Long price - Long likeCount; - Long stock -} - -class Like { - Long id - String userId - Long productId - LocalDateTime createdAt -} - -class Order { - Long id - String userId - Long totalPrice - OrderStatus status - LocalDateTime createdAt - List orderItems -} - -class OrderItem { - Long id - Order order - Long productId - String productName - Long quantity - Long price -} - -class Payment { - Long id - Long orderId - String status - String paymentRequestId - LocalDateTime createdAt -} - -%% ๊ด€๊ณ„ ์„ค์ • -User --> Point -Brand --> Product -Product --> Like -User --> Like -User --> Order -Order --> OrderItem -Order --> Payment -OrderItem --> Product - -``` \ No newline at end of file diff --git a/docs/2round/04-erd.md b/docs/2round/04-erd.md deleted file mode 100644 index 6389b2202..000000000 --- a/docs/2round/04-erd.md +++ /dev/null @@ -1,74 +0,0 @@ -# erd - -```mermaid -erDiagram - USER { - bigint id PK - varchar user_id - varchar name - varchar email - varchar gender - } - - POINT { - bigint id PK - varchar user_id FK - bigint balance - } - - BRAND { - bigint id PK - varchar name - } - - PRODUCT { - bigint id PK - bigint brand_id FK - varchar name - bigint price - bigint like_count - bigint stock - } - - LIKE { - bigint id PK - varchar user_id FK - bigint product_id FK - datetime created_at - } - - ORDERS { - bigint id PK - varchar user_id FK - bigint total_amount - varchar status - datetime created_at - } - - ORDER_ITEM { - bigint id PK - bigint order_id FK - bigint product_id FK - varchar product_name - bigint quantity - bigint price - } - - PAYMENT { - bigint id PK - bigint order_id FK - varchar status - varchar payment_request_id - datetime created_at - } - - %% ๊ด€๊ณ„ (cardinality) - USER ||--|| POINT : "1:1" - BRAND ||--o{ PRODUCT : "1:N" - PRODUCT ||--o{ LIKE : "1:N" - USER ||--o{ LIKE : "1:N" - USER ||--o{ ORDERS : "1:N" - ORDERS ||--o{ ORDER_ITEM : "1:N" - ORDER_ITEM }o--|| PRODUCT : "N:1" - ORDERS ||--|| PAYMENT : "1:1" -``` \ No newline at end of file diff --git a/docs/2round/2round.md b/docs/2round/2round.md deleted file mode 100644 index 84fdc982c..000000000 --- a/docs/2round/2round.md +++ /dev/null @@ -1,37 +0,0 @@ -## โœ๏ธ Design Quest - -> **์ด์ปค๋จธ์Šค ๋„๋ฉ”์ธ(์ƒํ’ˆ, ๋ธŒ๋žœ๋“œ, ์ข‹์•„์š”, ์ฃผ๋ฌธ ๋“ฑ)์— ๋Œ€ํ•œ ์„ค๊ณ„**๋ฅผ ์™„๋ฃŒํ•˜๊ณ , ๋‹ค์Œ ์ฃผ๋ถ€ํ„ฐ ๊ฐœ๋ฐœ ๊ฐ€๋Šฅํ•œ ์ˆ˜์ค€์˜ ์„ค๊ณ„ ๋ฌธ์„œ๋ฅผ ์ •๋ฆฌํ•˜์—ฌ PR๋กœ ์ œ์ถœํ•ฉ๋‹ˆ๋‹ค. -> - -### ๐Ÿ“‹ ๊ณผ์ œ ์ •๋ณด - -- **์„ค๊ณ„ ๋ฒ”์œ„** - - ์ƒํ’ˆ ๋ชฉ๋ก / ์ƒํ’ˆ ์ƒ์„ธ / ๋ธŒ๋žœ๋“œ ์กฐํšŒ - - ์ƒํ’ˆ ์ข‹์•„์š” ๋“ฑ๋ก/์ทจ์†Œ (๋ฉฑ๋“ฑ ๋™์ž‘) - - ์ฃผ๋ฌธ ์ƒ์„ฑ ๋ฐ ๊ฒฐ์ œ ํ๋ฆ„ (์žฌ๊ณ  ์ฐจ๊ฐ, ํฌ์ธํŠธ ์ฐจ๊ฐ, ์™ธ๋ถ€ ์‹œ์Šคํ…œ ์—ฐ๋™) -- **์ œ์™ธ ๋„๋ฉ”์ธ** - - ํšŒ์›๊ฐ€์ž…, ํฌ์ธํŠธ ์ถฉ์ „ (1์ฃผ์ฐจ ๊ตฌํ˜„ ์™„๋ฃŒ ๊ธฐ์ค€) -- **์š”๊ตฌ์‚ฌํ•ญ ๊ธฐ๋ฐ˜** - - ๋ฃจํ”„ํŒฉ ์ด์ปค๋จธ์Šค ์‹œ๋‚˜๋ฆฌ์˜ค ๋ฌธ์„œ๋ฅผ ๊ธฐ์ค€์œผ๋กœ ๊ธฐ๋Šฅ/์ œ์•ฝ์‚ฌํ•ญ์„ ์„ค๊ณ„์— ๋ฐ˜์˜ํ•ฉ๋‹ˆ๋‹ค. -- **์ œ์ถœ ๋ฐฉ์‹** - 1. ์•„๋ž˜ ํŒŒ์ผ๋“ค์„ ํ”„๋กœ์ ํŠธ ๋‚ด `docs/week2/` ํด๋”์— `.md`๋กœ ์ €์žฅ - 2. Github PR๋กœ ์ œ์ถœ - - PR ์ œ๋ชฉ: `[2์ฃผ์ฐจ] ์„ค๊ณ„ ๋ฌธ์„œ ์ œ์ถœ - ํ™๊ธธ๋™` - - PR ๋ณธ๋ฌธ์— ๋ฆฌ๋ทฐ ํฌ์ธํŠธ ํฌํ•จ (์˜ˆ: ๊ณ ๋ฏผํ•œ ์ง€์  ๋“ฑ) - -### โœ… ์ œ์ถœ ํŒŒ์ผ ๋ชฉ๋ก (.docs/design ๋””๋ ‰ํ† ๋ฆฌ ๋‚ด) - -| ํŒŒ์ผ๋ช… | ๋‚ด์šฉ | -| --- | --- | -| `01-requirements.md` | ์œ ์ € ์‹œ๋‚˜๋ฆฌ์˜ค ๊ธฐ๋ฐ˜ ๊ธฐ๋Šฅ ์ •์˜, ์š”๊ตฌ์‚ฌํ•ญ ๋ช…์„ธ | -| `02-sequence-diagrams.md` | ์‹œํ€€์Šค ๋‹ค์ด์–ด๊ทธ๋žจ ์ตœ์†Œ 2๊ฐœ ์ด์ƒ (Mermaid ๊ธฐ๋ฐ˜ ์ž‘์„ฑ ๊ถŒ์žฅ) | -| `03-class-diagram.md` | ๋„๋ฉ”์ธ ๊ฐ์ฒด ์„ค๊ณ„ (ํด๋ž˜์Šค ๋‹ค์ด์–ด๊ทธ๋žจ or ์„ค๋ช… ์ค‘์‹ฌ) | -| `04-erd.md` | ์ „์ฒด ํ…Œ์ด๋ธ” ๊ตฌ์กฐ ๋ฐ ๊ด€๊ณ„ ์ •๋ฆฌ (ERD Mermaid ์ž‘์„ฑ ๊ฐ€๋Šฅ) | - -## โœ… Checklist - -- [ ] ์ƒํ’ˆ/๋ธŒ๋žœ๋“œ/์ข‹์•„์š”/์ฃผ๋ฌธ ๋„๋ฉ”์ธ์ด ๋ชจ๋‘ ํฌํ•จ๋˜์–ด ์žˆ๋Š”๊ฐ€? -- [ ] ๊ธฐ๋Šฅ ์š”๊ตฌ์‚ฌํ•ญ์ด ์œ ์ € ์ค‘์‹ฌ์œผ๋กœ ์ •๋ฆฌ๋˜์–ด ์žˆ๋Š”๊ฐ€? -- [ ] ์‹œํ€€์Šค ๋‹ค์ด์–ด๊ทธ๋žจ์—์„œ ์ฑ…์ž„ ๊ฐ์ฒด๊ฐ€ ๋“œ๋Ÿฌ๋‚˜๋Š”๊ฐ€? -- [ ] ํด๋ž˜์Šค ๊ตฌ์กฐ๊ฐ€ ๋„๋ฉ”์ธ ์„ค๊ณ„๋ฅผ ์ž˜ ํ‘œํ˜„ํ•˜๊ณ  ์žˆ๋Š”๊ฐ€? -- [ ] ERD ์„ค๊ณ„ ์‹œ ๋ฐ์ดํ„ฐ ์ •ํ•ฉ์„ฑ์„ ๊ณ ๋ คํ•˜์—ฌ ๊ตฌ์„ฑํ•˜์˜€๋Š”๊ฐ€? \ No newline at end of file diff --git a/docs/3round/3round.md b/docs/3round/3round.md deleted file mode 100644 index b9f333cca..000000000 --- a/docs/3round/3round.md +++ /dev/null @@ -1,60 +0,0 @@ -# ๐Ÿ“ Round 3 Quests - ---- - -## ๐Ÿ’ป Implementation Quest - -> *** ๋„๋ฉ”์ธ ๋ชจ๋ธ๋ง**์„ ํ†ตํ•ด Product, Brand, Like, Order ๊ธฐ๋Šฅ์˜ ํ•ต์‹ฌ ๊ฐœ๋…์„ **Entity, Value Object, Domain Service ๋“ฑ ์ ํ•ฉํ•œ** **์ฝ”๋“œ**๋กœ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค. -* ๋ ˆ์ด์–ด๋“œ ์•„ํ‚คํ…์ฒ˜ + DIP ๋ฅผ ์ ์šฉํ•ด ์œ ์—ฐํ•˜๊ณ  ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅํ•œ ๊ตฌ์กฐ๋กœ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค. -* **Application Layer๋ฅผ ๊ฒฝ๋Ÿ‰ ์ˆ˜์ค€**์œผ๋กœ ๊ตฌ์„ฑํ•˜์—ฌ, ๋„๋ฉ”์ธ ๊ฐ์ฒด๋ฅผ ์กฐํ•ฉํ•˜๋Š” ํ๋ฆ„์„ ์‹ค์ œ ๊ตฌํ˜„ํ•ด๋ด…๋‹ˆ๋‹ค. -* **๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑ**ํ•˜์—ฌ ๋„๋ฉ”์ธ ๋กœ์ง์˜ ์ •ํ•ฉ์„ฑ๊ณผ ๊ทœ์น™์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. -> - -### ๐Ÿ“‹ ๊ณผ์ œ ์ •๋ณด - -- ์ƒํ’ˆ, ๋ธŒ๋žœ๋“œ, ์ข‹์•„์š”, ์ฃผ๋ฌธ ๊ธฐ๋Šฅ์˜ **๋„๋ฉ”์ธ ๋ชจ๋ธ ๋ฐ ๋„๋ฉ”์ธ ์„œ๋น„์Šค**๋ฅผ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค. -- ๋„๋ฉ”์ธ ๊ฐ„ ํ˜‘๋ ฅ ํ๋ฆ„์„ ์„ค๊ณ„ํ•˜๊ณ , ํ•„์š”ํ•œ ๋กœ์ง์„ **๋„๋ฉ”์ธ ์„œ๋น„์Šค**๋กœ ๋ถ„๋ฆฌํ•ฉ๋‹ˆ๋‹ค. -- Application Layer์—์„œ ๋„๋ฉ”์ธ ๊ฐ์ฒด๋ฅผ ์กฐํ•ฉํ•˜๋Š” ํ๋ฆ„์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค. - (์˜ˆ: `ProductFacade.getProductDetail(productId)` โ†’ `Product + Brand + Like ์กฐํ•ฉ`) -- Repository Interface ์™€ ๊ตฌํ˜„์ฒด๋Š” ๋ถ„๋ฆฌํ•˜๊ณ , ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅ์„ฑ์„ ๊ณ ๋ คํ•œ ๊ตฌ์กฐ๋ฅผ ์„ค๊ณ„ํ•ฉ๋‹ˆ๋‹ค. -- ๋ชจ๋“  ํ•ต์‹ฌ ๋„๋ฉ”์ธ ๋กœ์ง์— ๋Œ€ํ•ด ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•˜๊ณ , ์˜ˆ์™ธ/๊ฒฝ๊ณ„ ์ผ€์ด์Šค๋„ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. - -### ๐Ÿท Product / Brand ๋„๋ฉ”์ธ - -## โœ… Checklist - -- [x] ์ƒํ’ˆ ์ •๋ณด ๊ฐ์ฒด๋Š” ๋ธŒ๋žœ๋“œ ์ •๋ณด, ์ข‹์•„์š” ์ˆ˜๋ฅผ ํฌํ•จํ•œ๋‹ค. -- [x] ์ƒํ’ˆ์˜ ์ •๋ ฌ ์กฐ๊ฑด(`latest`, `price_asc`, `likes_desc`) ์„ ๊ณ ๋ คํ•œ ์กฐํšŒ ๊ธฐ๋Šฅ์„ ์„ค๊ณ„ํ–ˆ๋‹ค -- [x] ์ƒํ’ˆ์€ ์žฌ๊ณ ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๊ณ , ์ฃผ๋ฌธ ์‹œ ์ฐจ๊ฐํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•œ๋‹ค -- [x] ์žฌ๊ณ ๋Š” ๊ฐ์†Œ๋งŒ ๊ฐ€๋Šฅํ•˜๋ฉฐ ์Œ์ˆ˜ ๋ฐฉ์ง€๋Š” ๋„๋ฉ”์ธ ๋ ˆ๋ฒจ์—์„œ ์ฒ˜๋ฆฌ๋œ๋‹ค - -### ๐Ÿ‘ Like ๋„๋ฉ”์ธ - -- [x] ์ข‹์•„์š”๋Š” ์œ ์ €์™€ ์ƒํ’ˆ ๊ฐ„์˜ ๊ด€๊ณ„๋กœ ๋ณ„๋„ ๋„๋ฉ”์ธ์œผ๋กœ ๋ถ„๋ฆฌํ–ˆ๋‹ค -- [x] ์ค‘๋ณต ์ข‹์•„์š” ๋ฐฉ์ง€๋ฅผ ์œ„ํ•œ ๋ฉฑ๋“ฑ์„ฑ ์ฒ˜๋ฆฌ๊ฐ€ ๊ตฌํ˜„๋˜์—ˆ๋‹ค -- [x] ์ƒํ’ˆ์˜ ์ข‹์•„์š” ์ˆ˜๋Š” ์ƒํ’ˆ ์ƒ์„ธ/๋ชฉ๋ก ์กฐํšŒ์—์„œ ํ•จ๊ป˜ ์ œ๊ณต๋œ๋‹ค -- [x] ๋‹จ์œ„ ํ…Œ์ŠคํŠธ์—์„œ ์ข‹์•„์š” ๋“ฑ๋ก/์ทจ์†Œ/์ค‘๋ณต ๋ฐฉ์ง€ ํ๋ฆ„์„ ๊ฒ€์ฆํ–ˆ๋‹ค - -### ๐Ÿ›’ Order ๋„๋ฉ”์ธ - -- [x] ์ฃผ๋ฌธ์€ ์—ฌ๋Ÿฌ ์ƒํ’ˆ์„ ํฌํ•จํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ๊ฐ ์ƒํ’ˆ์˜ ์ˆ˜๋Ÿ‰์„ ๋ช…์‹œํ•œ๋‹ค -- [x] ์ฃผ๋ฌธ ์‹œ ์ƒํ’ˆ์˜ ์žฌ๊ณ  ์ฐจ๊ฐ, ์œ ์ € ํฌ์ธํŠธ ์ฐจ๊ฐ ๋“ฑ์„ ์ˆ˜ํ–‰ํ•œ๋‹ค -- [x] ์žฌ๊ณ  ๋ถ€์กฑ, ํฌ์ธํŠธ ๋ถ€์กฑ ๋“ฑ ์˜ˆ์™ธ ํ๋ฆ„์„ ๊ณ ๋ คํ•ด ์„ค๊ณ„๋˜์—ˆ๋‹ค -- [x] ๋‹จ์œ„ ํ…Œ์ŠคํŠธ์—์„œ ์ •์ƒ ์ฃผ๋ฌธ / ์˜ˆ์™ธ ์ฃผ๋ฌธ ํ๋ฆ„์„ ๋ชจ๋‘ ๊ฒ€์ฆํ–ˆ๋‹ค - -### ๐Ÿงฉ ๋„๋ฉ”์ธ ์„œ๋น„์Šค - -- [x] ๋„๋ฉ”์ธ ๊ฐ„ ํ˜‘๋ ฅ ๋กœ์ง์€ Domain Service์— ์œ„์น˜์‹œ์ผฐ๋‹ค -- [x] ์ƒํ’ˆ ์ƒ์„ธ ์กฐํšŒ ์‹œ Product + Brand ์ •๋ณด ์กฐํ•ฉ์€ ๋„๋ฉ”์ธ ์„œ๋น„์Šค์—์„œ ์ฒ˜๋ฆฌํ–ˆ๋‹ค -- [x] ๋ณตํ•ฉ ์œ ์Šค์ผ€์ด์Šค๋Š” Application Layer์— ์กด์žฌํ•˜๊ณ , ๋„๋ฉ”์ธ ๋กœ์ง์€ ์œ„์ž„๋˜์—ˆ๋‹ค -- [x] ๋„๋ฉ”์ธ ์„œ๋น„์Šค๋Š” ์ƒํƒœ ์—†์ด, ๋„๋ฉ”์ธ ๊ฐ์ฒด์˜ ํ˜‘๋ ฅ ์ค‘์‹ฌ์œผ๋กœ ์„ค๊ณ„๋˜์—ˆ๋‹ค - -### **๐Ÿงฑ ์†Œํ”„ํŠธ์›จ์–ด ์•„ํ‚คํ…์ฒ˜ & ์„ค๊ณ„** - -- [x] ์ „์ฒด ํ”„๋กœ์ ํŠธ์˜ ๊ตฌ์„ฑ์€ ์•„๋ž˜ ์•„ํ‚คํ…์ฒ˜๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๊ตฌ์„ฑ๋˜์—ˆ๋‹ค - - Application โ†’ **Domain** โ† Infrastructure -- [x] Application Layer๋Š” ๋„๋ฉ”์ธ ๊ฐ์ฒด๋ฅผ ์กฐํ•ฉํ•ด ํ๋ฆ„์„ orchestration ํ–ˆ๋‹ค -- [x] ํ•ต์‹ฌ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์€ Entity, VO, Domain Service ์— ์œ„์น˜ํ•œ๋‹ค -- [x] Repository Interface๋Š” Domain Layer ์— ์ •์˜๋˜๊ณ , ๊ตฌํ˜„์ฒด๋Š” Infra์— ์œ„์น˜ํ•œ๋‹ค -- [x] ํŒจํ‚ค์ง€๋Š” ๊ณ„์ธต + ๋„๋ฉ”์ธ ๊ธฐ์ค€์œผ๋กœ ๊ตฌ์„ฑ๋˜์—ˆ๋‹ค (`/domain/order`, `/application/like` ๋“ฑ) -- [x] ํ…Œ์ŠคํŠธ๋Š” ์™ธ๋ถ€ ์˜์กด์„ฑ์„ ๋ถ„๋ฆฌํ•˜๊ณ , Fake/Stub ๋“ฑ์„ ์‚ฌ์šฉํ•ด ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๊ฐ€ ๊ฐ€๋Šฅํ•˜๊ฒŒ ๊ตฌ์„ฑ๋˜์—ˆ๋‹ค \ No newline at end of file From 0074ea918de0aee3995308e4ab57f0fba05428a5 Mon Sep 17 00:00:00 2001 From: BOB <56067193+adminhelper@users.noreply.github.com> Date: Fri, 21 Nov 2025 16:55:58 +0900 Subject: [PATCH 62/85] =?UTF-8?q?Revert=20"[volume-3]=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EB=AA=A8=EB=8D=B8=EB=A7=81=20=EB=B0=8F=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/example/ExampleFacade.java | 17 ++ .../application/example/ExampleInfo.java | 13 ++ .../loopers/application/like/LikeFacade.java | 32 ---- .../application/order/CreateOrderCommand.java | 19 -- .../application/order/OrderFacade.java | 81 --------- .../loopers/application/order/OrderInfo.java | 42 ----- .../application/order/OrderItemCommand.java | 17 -- .../application/order/OrderItemInfo.java | 32 ---- .../application/point/PointFacade.java | 28 --- .../loopers/application/point/PointInfo.java | 13 -- .../product/ProductDetailInfo.java | 32 ---- .../application/product/ProductFacade.java | 47 ----- .../application/product/ProductInfo.java | 33 ---- .../loopers/application/user/UserFacade.java | 27 --- .../loopers/application/user/UserInfo.java | 14 -- .../java/com/loopers/domain/brand/Brand.java | 47 ----- .../loopers/domain/brand/BrandRepository.java | 20 --- .../loopers/domain/brand/BrandService.java | 35 ---- .../loopers/domain/example/ExampleModel.java | 44 +++++ .../domain/example/ExampleRepository.java | 7 + .../domain/example/ExampleService.java | 20 +++ .../java/com/loopers/domain/like/Like.java | 63 ------- .../loopers/domain/like/LikeRepository.java | 25 --- .../com/loopers/domain/like/LikeService.java | 49 ----- .../java/com/loopers/domain/order/Order.java | 86 --------- .../com/loopers/domain/order/OrderItem.java | 91 ---------- .../loopers/domain/order/OrderRepository.java | 21 --- .../loopers/domain/order/OrderService.java | 28 --- .../com/loopers/domain/order/OrderStatus.java | 42 ----- .../java/com/loopers/domain/point/Point.java | 56 ------ .../loopers/domain/point/PointRepository.java | 10 -- .../loopers/domain/point/PointService.java | 43 ----- .../com/loopers/domain/product/Product.java | 120 ------------- .../loopers/domain/product/ProductDetail.java | 45 ----- .../domain/product/ProductDomainService.java | 41 ----- .../domain/product/ProductRepository.java | 29 --- .../domain/product/ProductService.java | 53 ------ .../java/com/loopers/domain/user/User.java | 82 --------- .../loopers/domain/user/UserRepository.java | 10 -- .../com/loopers/domain/user/UserService.java | 30 ---- .../brand/BrandJpaRepository.java | 18 -- .../brand/BrandRepositoryImpl.java | 36 ---- .../example/ExampleJpaRepository.java | 6 + .../example/ExampleRepositoryImpl.java | 19 ++ .../like/LikeJpaRepository.java | 23 --- .../like/LikeRepositoryImpl.java | 46 ----- .../order/OrderJpaRepository.java | 18 -- .../order/OrderRepositoryImpl.java | 36 ---- .../point/PointJpaRepository.java | 11 -- .../point/PointRepositoryImpl.java | 25 --- .../product/ProductJpaRepository.java | 19 -- .../product/ProductRepositoryImpl.java | 59 ------ .../user/UserJpaRepository.java | 11 -- .../user/UserRepositoryImpl.java | 26 --- .../api/example/ExampleV1ApiSpec.java | 19 ++ .../api/example/ExampleV1Controller.java | 28 +++ .../interfaces/api/example/ExampleV1Dto.java | 15 ++ .../interfaces/api/point/PointV1ApiSpec.java | 28 --- .../api/point/PointV1Controller.java | 31 ---- .../interfaces/api/point/PointV1Dto.java | 18 -- .../interfaces/api/user/UserV1ApiSpec.java | 28 --- .../interfaces/api/user/UserV1Controller.java | 31 ---- .../interfaces/api/user/UserV1Dto.java | 24 --- .../com/loopers/domain/brand/BrandTest.java | 42 ----- .../domain/example/ExampleModelTest.java | 65 +++++++ .../ExampleServiceIntegrationTest.java | 72 ++++++++ .../like/LikeServiceIntegrationTest.java | 155 ---------------- .../com/loopers/domain/like/LikeTest.java | 91 ---------- .../order/OrderServiceIntegrationTest.java | 170 ------------------ .../com/loopers/domain/order/OrderTest.java | 122 ------------- .../point/PointServiceIntegrationTest.java | 108 ----------- .../com/loopers/domain/point/PointTest.java | 117 ------------ .../ProductServiceIntegrationTest.java | 43 ----- .../loopers/domain/product/ProductTest.java | 95 ---------- .../user/UserServiceIntegrationTest.java | 112 ------------ .../com/loopers/domain/user/UserTest.java | 53 ------ .../interfaces/api/ExampleV1ApiE2ETest.java | 114 ++++++++++++ .../api/point/PointV1ControllerTest.java | 156 ---------------- .../api/user/UserV1ControllerTest.java | 148 --------------- docs/1round/1round.md | 67 ------- docs/2round/01-requirements.md | 104 ----------- docs/2round/02-sequence-diagrams.md | 164 ----------------- docs/2round/03-class-diagram.md | 78 -------- docs/2round/04-erd.md | 74 -------- docs/2round/2round.md | 37 ---- docs/3round/3round.md | 60 ------- 86 files changed, 439 insertions(+), 3927 deletions(-) create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/example/ExampleFacade.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/example/ExampleInfo.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/application/like/LikeFacade.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/application/order/CreateOrderCommand.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/application/order/OrderInfo.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/application/order/OrderItemCommand.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/application/order/OrderItemInfo.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/application/point/PointInfo.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/application/product/ProductDetailInfo.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/application/product/ProductInfo.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/application/user/UserInfo.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/brand/Brand.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandRepository.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandService.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleModel.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleRepository.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleService.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/like/Like.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/like/LikeRepository.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/like/LikeService.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/order/Order.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItem.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/order/OrderRepository.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/order/OrderStatus.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/point/PointRepository.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/product/ProductDetail.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/product/ProductDomainService.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/product/ProductRepository.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/user/User.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandJpaRepository.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandRepositoryImpl.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/example/ExampleJpaRepository.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/example/ExampleRepositoryImpl.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeJpaRepository.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeRepositoryImpl.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderJpaRepository.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderRepositoryImpl.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointJpaRepository.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointRepositoryImpl.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserJpaRepository.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1ApiSpec.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1Controller.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1Dto.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1ApiSpec.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Controller.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Dto.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1ApiSpec.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java delete mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandTest.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/example/ExampleModelTest.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/example/ExampleServiceIntegrationTest.java delete mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/like/LikeServiceIntegrationTest.java delete mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/like/LikeTest.java delete mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/order/OrderServiceIntegrationTest.java delete mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/order/OrderTest.java delete mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java delete mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/point/PointTest.java delete mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/product/ProductServiceIntegrationTest.java delete mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/product/ProductTest.java delete mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java delete mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/user/UserTest.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/interfaces/api/ExampleV1ApiE2ETest.java delete mode 100644 apps/commerce-api/src/test/java/com/loopers/interfaces/api/point/PointV1ControllerTest.java delete mode 100644 apps/commerce-api/src/test/java/com/loopers/interfaces/api/user/UserV1ControllerTest.java delete mode 100644 docs/1round/1round.md delete mode 100644 docs/2round/01-requirements.md delete mode 100644 docs/2round/02-sequence-diagrams.md delete mode 100644 docs/2round/03-class-diagram.md delete mode 100644 docs/2round/04-erd.md delete mode 100644 docs/2round/2round.md delete mode 100644 docs/3round/3round.md diff --git a/apps/commerce-api/src/main/java/com/loopers/application/example/ExampleFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/example/ExampleFacade.java new file mode 100644 index 000000000..552a9ad62 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/example/ExampleFacade.java @@ -0,0 +1,17 @@ +package com.loopers.application.example; + +import com.loopers.domain.example.ExampleModel; +import com.loopers.domain.example.ExampleService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class ExampleFacade { + private final ExampleService exampleService; + + public ExampleInfo getExample(Long id) { + ExampleModel example = exampleService.getExample(id); + return ExampleInfo.from(example); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/example/ExampleInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/example/ExampleInfo.java new file mode 100644 index 000000000..877aba96c --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/example/ExampleInfo.java @@ -0,0 +1,13 @@ +package com.loopers.application.example; + +import com.loopers.domain.example.ExampleModel; + +public record ExampleInfo(Long id, String name, String description) { + public static ExampleInfo from(ExampleModel model) { + return new ExampleInfo( + model.getId(), + model.getName(), + model.getDescription() + ); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/like/LikeFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/like/LikeFacade.java deleted file mode 100644 index d9dd33205..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/application/like/LikeFacade.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.loopers.application.like; - -import com.loopers.domain.like.LikeService; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; - -/** - * packageName : com.loopers.application.like - * fileName : LikeFacade - * author : byeonsungmun - * date : 2025. 11. 14. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 14. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -@Component -@RequiredArgsConstructor -public class LikeFacade { - - private final LikeService likeService; - - public void createLike(String userId, Long productId) { - likeService.like(userId, productId); - } - - public void deleteLike(String userId, Long productId) { - likeService.unlike(userId, productId); - } -} - diff --git a/apps/commerce-api/src/main/java/com/loopers/application/order/CreateOrderCommand.java b/apps/commerce-api/src/main/java/com/loopers/application/order/CreateOrderCommand.java deleted file mode 100644 index 683e39cdd..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/application/order/CreateOrderCommand.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.loopers.application.order; - -import java.util.List; - -/** - * packageName : com.loopers.application.order - * fileName : CreateOrderCommand - * author : byeonsungmun - * date : 2025. 11. 14. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 14. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -public record CreateOrderCommand( - String userId, - List items -) {} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java deleted file mode 100644 index 2fba4b4aa..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java +++ /dev/null @@ -1,81 +0,0 @@ -package com.loopers.application.order; - -import com.loopers.domain.order.Order; -import com.loopers.domain.order.OrderItem; -import com.loopers.domain.order.OrderService; -import com.loopers.domain.order.OrderStatus; -import com.loopers.domain.point.PointService; -import com.loopers.domain.product.Product; -import com.loopers.domain.product.ProductService; -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; - -/** - * packageName : com.loopers.application.order - * fileName : OrderFacade - * author : byeonsungmun - * date : 2025. 11. 13. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 13. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ - -@Slf4j -@Component -@RequiredArgsConstructor -public class OrderFacade { - - private final OrderService orderService; - private final ProductService productService; - private final PointService pointService; - - @Transactional - public OrderInfo createOrder(CreateOrderCommand command) { - - if (command == null || command.items() == null || command.items().isEmpty()) { - throw new CoreException(ErrorType.BAD_REQUEST, "์ƒํ’ˆ ์ •๋ณด๊ฐ€ ๋น„์–ด์žˆ์Šต๋‹ˆ๋‹ค"); - } - - Order order = Order.create(command.userId()); - - for (OrderItemCommand itemCommand : command.items()) { - - //์ƒํ’ˆ๊ฐ€์ ธ์˜ค๊ณ  - Product product = productService.getProduct(itemCommand.productId()); - - // ์žฌ๊ณ ๊ฐ์†Œ - product.decreaseStock(itemCommand.quantity()); - - // OrderItem์ƒ์„ฑ - OrderItem orderItem = OrderItem.create( - product.getId(), - product.getName(), - itemCommand.quantity(), - product.getPrice()); - - order.addOrderItem(orderItem); - orderItem.setOrder(order); - } - - //์ด ๊ฐ€๊ฒฉ๊ตฌํ•˜๊ณ  - long totalAmount = order.getOrderItems().stream() - .mapToLong(OrderItem::getAmount) - .sum(); - - order.updateTotalAmount(totalAmount); - - pointService.usePoint(command.userId(), totalAmount); - - //์ €์žฅ - Order saved = orderService.createOrder(order); - saved.updateStatus(OrderStatus.COMPLETE); - - return OrderInfo.from(saved); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderInfo.java deleted file mode 100644 index 70028c27c..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderInfo.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.loopers.application.order; - -import com.loopers.domain.order.Order; -import com.loopers.domain.order.OrderStatus; - -import java.time.LocalDateTime; -import java.util.List; - -/** - * packageName : com.loopers.application.order - * fileName : OrderInfo - * author : byeonsungmun - * date : 2025. 11. 14. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 14. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -public record OrderInfo( - Long orderId, - String userId, - Long totalAmount, - OrderStatus status, - LocalDateTime createdAt, - List items -) { - public static OrderInfo from(Order order) { - List itemInfos = order.getOrderItems().stream() - .map(OrderItemInfo::from) - .toList(); - - return new OrderInfo( - order.getId(), - order.getUserId(), - order.getTotalAmount(), - order.getStatus(), - order.getCreatedAt(), - itemInfos - ); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderItemCommand.java b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderItemCommand.java deleted file mode 100644 index 1ac46862f..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderItemCommand.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.loopers.application.order; - -/** - * packageName : com.loopers.application.order - * fileName : OrderItemCommand - * author : byeonsungmun - * date : 2025. 11. 14. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 14. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -public record OrderItemCommand( - Long productId, - Long quantity -) {} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderItemInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderItemInfo.java deleted file mode 100644 index b3f2359c6..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderItemInfo.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.loopers.application.order; - -import com.loopers.domain.order.OrderItem; - -/** - * packageName : com.loopers.application.order - * fileName : OrderInfo - * author : byeonsungmun - * date : 2025. 11. 13. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 13. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -public record OrderItemInfo( - Long productId, - String productName, - Long quantity, - Long price, - Long amount -) { - public static OrderItemInfo from(OrderItem item) { - return new OrderItemInfo( - item.getProductId(), - item.getProductName(), - item.getQuantity(), - item.getPrice(), - item.getAmount() - ); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java deleted file mode 100644 index 009be1cec..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.loopers.application.point; - -import com.loopers.domain.point.Point; -import com.loopers.domain.point.PointService; -import com.loopers.interfaces.api.point.PointV1Dto; -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; - -@RequiredArgsConstructor -@Component -public class PointFacade { - private final PointService pointService; - - public PointInfo getPoint(String userId) { - Point point = pointService.findPointByUserId(userId); - - if (point == null) { - throw new CoreException(ErrorType.NOT_FOUND, "์‚ฌ์šฉ์ž ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); - } - return PointInfo.from(point); - } - - public PointInfo chargePoint(PointV1Dto.ChargePointRequest request) { - return PointInfo.from(pointService.chargePoint(request.userId(), request.chargeAmount())); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/point/PointInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/point/PointInfo.java deleted file mode 100644 index 65497297b..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/application/point/PointInfo.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.loopers.application.point; - -import com.loopers.domain.point.Point; - -public record PointInfo(String userId, Long amount) { - public static PointInfo from(Point info) { - return new PointInfo( - info.getUserId(), - info.getBalance() - ); - } - -} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/product/ProductDetailInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductDetailInfo.java deleted file mode 100644 index 2a9ecee27..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/application/product/ProductDetailInfo.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.loopers.application.product; - -import com.loopers.domain.product.ProductDetail; - -/** - * packageName : com.loopers.application.product - * fileName : ProductDetail - * author : byeonsungmun - * date : 2025. 11. 13. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 13. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -public record ProductDetailInfo( - Long id, - String name, - String brandName, - Long price, - Long likeCount -) { - public static ProductDetailInfo from(ProductDetail productDetail) { - return new ProductDetailInfo( - productDetail.getId(), - productDetail.getName(), - productDetail.getBrandName(), - productDetail.getPrice(), - productDetail.getLikeCount() - ); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java deleted file mode 100644 index e6a25de23..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.loopers.application.product; - -import com.loopers.domain.brand.Brand; -import com.loopers.domain.brand.BrandService; -import com.loopers.domain.like.LikeService; -import com.loopers.domain.product.ProductDetail; -import com.loopers.domain.product.ProductDomainService; -import com.loopers.domain.product.ProductService; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Component; - -/** - * packageName : com.loopers.application.product - * fileName : ProdcutFacade - * author : byeonsungmun - * date : 2025. 11. 10. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 10. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -@Component -@RequiredArgsConstructor -public class ProductFacade { - - private final ProductService productService; - private final BrandService brandService; - private final LikeService likeService; - private final ProductDomainService productDomainService; - - public Page getProducts(String sort, Pageable pageable) { - return productService.getProducts(sort ,pageable) - .map(product -> { - Brand brand = brandService.getBrand(product.getBrandId()); - long likeCount = likeService.countByProductId(product.getId()); - return ProductInfo.of(product, brand, likeCount); - }); - } - - public ProductDetailInfo getProduct(Long id) { - ProductDetail productDetail = productDomainService.getProductDetail(id); - return ProductDetailInfo.from(productDetail); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/product/ProductInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductInfo.java deleted file mode 100644 index 8bcd93dd8..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/application/product/ProductInfo.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.loopers.application.product; - -import com.loopers.domain.brand.Brand; -import com.loopers.domain.product.Product; - -/** - * packageName : com.loopers.application.product - * fileName : ProductInfo - * author : byeonsungmun - * date : 2025. 11. 10. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 10. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -public record ProductInfo( - Long id, - String name, - String brandName, - Long price, - Long likeCount -) { - public static ProductInfo of(Product product, Brand brand, Long likeCount) { - return new ProductInfo( - product.getId(), - product.getName(), - brand.getName(), - product.getPrice(), - likeCount - ); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java deleted file mode 100644 index f42bd5206..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.loopers.application.user; - -import com.loopers.domain.user.User; -import com.loopers.domain.user.UserService; -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; - -@RequiredArgsConstructor -@Component -public class UserFacade { - private final UserService userService; - - public UserInfo register(String userId, String email, String birth, String gender) { - User user = userService.register(userId, email, birth, gender); - return UserInfo.from(user); - } - - public UserInfo getUser(String userId) { - User user = userService.findUserByUserId(userId); - if (user == null) { - throw new CoreException(ErrorType.NOT_FOUND, "์‚ฌ์šฉ์ž ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); - } - return UserInfo.from(user); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/user/UserInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/user/UserInfo.java deleted file mode 100644 index 08f5cea43..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/application/user/UserInfo.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.loopers.application.user; - -import com.loopers.domain.user.User; - -public record UserInfo(String userId, String email, String birth, String gender) { - public static UserInfo from(User user) { - return new UserInfo( - user.getUserId(), - user.getEmail(), - user.getBirth(), - user.getGender() - ); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/brand/Brand.java b/apps/commerce-api/src/main/java/com/loopers/domain/brand/Brand.java deleted file mode 100644 index d334ccebf..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/brand/Brand.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.loopers.domain.brand; - -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import jakarta.persistence.*; -import lombok.Getter; - -/** - * packageName : com.loopers.domain.brand - * fileName : Brand - * author : byeonsungmun - * date : 2025. 11. 11. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 11. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -@Entity -@Table(name = "brand") -@Getter -public class Brand { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(nullable = false) - private String name; - - protected Brand() {} - - private Brand(String name) { - this.name = requireValidName(name); - } - - public static Brand create(String name) { - return new Brand(name); - } - - - private String requireValidName(String name) { - if (name == null || name.isEmpty()) { - throw new CoreException(ErrorType.BAD_REQUEST, "๋ธŒ๋žœ๋“œ๋ช…์€ ๋น„์–ด ์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); - } - return name.trim(); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandRepository.java deleted file mode 100644 index c558b23fc..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandRepository.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.loopers.domain.brand; - -import java.util.Optional; - -/** - * packageName : com.loopers.domain.brand - * fileName : BrandRepository - * author : byeonsungmun - * date : 2025. 11. 12. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 12. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -public interface BrandRepository { - Optional findById(Long id); - - void save(Brand brand); -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandService.java b/apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandService.java deleted file mode 100644 index e0f58c77b..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandService.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.loopers.domain.brand; - -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; - -/** - * packageName : com.loopers.domain.brand - * fileName : BrandService - * author : byeonsungmun - * date : 2025. 11. 12. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 12. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -@Component -@RequiredArgsConstructor -public class BrandService { - - private final BrandRepository brandRepository; - - public void save(Brand brand) { - brandRepository.save(brand); - } - - @Transactional(readOnly = true) - public Brand getBrand(Long id) { - return brandRepository.findById(id) - .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "๋ธŒ๋žœ๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleModel.java b/apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleModel.java new file mode 100644 index 000000000..c588c4a8a --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleModel.java @@ -0,0 +1,44 @@ +package com.loopers.domain.example; + +import com.loopers.domain.BaseEntity; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; + +@Entity +@Table(name = "example") +public class ExampleModel extends BaseEntity { + + private String name; + private String description; + + protected ExampleModel() {} + + public ExampleModel(String name, String description) { + if (name == null || name.isBlank()) { + throw new CoreException(ErrorType.BAD_REQUEST, "์ด๋ฆ„์€ ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + if (description == null || description.isBlank()) { + throw new CoreException(ErrorType.BAD_REQUEST, "์„ค๋ช…์€ ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + + this.name = name; + this.description = description; + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public void update(String newDescription) { + if (newDescription == null || newDescription.isBlank()) { + throw new CoreException(ErrorType.BAD_REQUEST, "์„ค๋ช…์€ ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + this.description = newDescription; + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleRepository.java new file mode 100644 index 000000000..3625e5662 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleRepository.java @@ -0,0 +1,7 @@ +package com.loopers.domain.example; + +import java.util.Optional; + +public interface ExampleRepository { + Optional find(Long id); +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleService.java b/apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleService.java new file mode 100644 index 000000000..c0e8431e8 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleService.java @@ -0,0 +1,20 @@ +package com.loopers.domain.example; + +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Component +public class ExampleService { + + private final ExampleRepository exampleRepository; + + @Transactional(readOnly = true) + public ExampleModel getExample(Long id) { + return exampleRepository.find(id) + .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "[id = " + id + "] ์˜ˆ์‹œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/like/Like.java b/apps/commerce-api/src/main/java/com/loopers/domain/like/Like.java deleted file mode 100644 index 4430b496a..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/like/Like.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.loopers.domain.like; - -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import jakarta.persistence.*; -import lombok.Getter; - -import java.time.LocalDateTime; - -/** - * packageName : com.loopers.domain.like - * fileName : Like - * author : byeonsungmun - * date : 2025. 11. 11. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 11. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -@Entity -@Table(name = "product_like") -@Getter -public class Like { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(name = "ref_user_id", nullable = false) - private String userId; - - @Column(name = "ref_product_id", nullable = false) - private Long productId; - - @Column(nullable = false) - private LocalDateTime createdAt; - - protected Like() {} - - private Like(String userId, Long productId) { - this.userId = requireValidUserId(userId); - this.productId = requireValidProductId(productId); - this.createdAt = LocalDateTime.now(); - } - - public static Like create(String userId, Long productId) { - return new Like(userId, productId); - } - - private String requireValidUserId(String userId) { - if (userId == null || userId.isEmpty()) { - throw new CoreException(ErrorType.BAD_REQUEST, "์‚ฌ์šฉ์ž ID๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."); - } - return userId; - } - - private Long requireValidProductId(Long productId) { - if (productId == null || productId <= 0) { - throw new CoreException(ErrorType.BAD_REQUEST, "์ƒํ’ˆ ID๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."); - } - return productId; - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/like/LikeRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/like/LikeRepository.java deleted file mode 100644 index 945b10235..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/like/LikeRepository.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.loopers.domain.like; - -import java.util.Optional; - -/** - * packageName : com.loopers.domain.like - * fileName : LikeRepository - * author : byeonsungmun - * date : 2025. 11. 12. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 12. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -public interface LikeRepository { - - Optional findByUserIdAndProductId(String userId, Long productId); - - void save(Like like); - - void delete(Like like); - - long countByProductId(Long productId); -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/like/LikeService.java b/apps/commerce-api/src/main/java/com/loopers/domain/like/LikeService.java deleted file mode 100644 index 41ae90b6a..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/like/LikeService.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.loopers.domain.like; - -import com.loopers.domain.product.ProductRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; - -/** - * packageName : com.loopers.application.like - * fileName : LikeService - * author : byeonsungmun - * date : 2025. 11. 12. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 12. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -@Component -@RequiredArgsConstructor -public class LikeService { - - private final LikeRepository likeRepository; - private final ProductRepository productRepository; - - @Transactional - public void like(String userId, Long productId) { - if (likeRepository.findByUserIdAndProductId(userId, productId).isPresent()) return; - - Like like = Like.create(userId, productId); - likeRepository.save(like); - productRepository.incrementLikeCount(productId); - } - - @Transactional - public void unlike(String userId, Long productId) { - likeRepository.findByUserIdAndProductId(userId, productId) - .ifPresent(like -> { - likeRepository.delete(like); - productRepository.decrementLikeCount(productId); - }); - } - - @Transactional(readOnly = true) - public long countByProductId(Long productId) { - return likeRepository.countByProductId(productId); - } - -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/Order.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/Order.java deleted file mode 100644 index 84f299c6b..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/order/Order.java +++ /dev/null @@ -1,86 +0,0 @@ -package com.loopers.domain.order; - -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import jakarta.persistence.*; -import lombok.Getter; - -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; - -/** - * packageName : com.loopers.domain.order - * fileName : Order - * author : byeonsungmun - * date : 2025. 11. 11. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 11. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -@Entity -@Table(name = "orders") -@Getter -public class Order { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(name = "ref_user_id", nullable = false) - private String userId; - - @Column(nullable = false) - private Long totalAmount; - - @Enumerated(EnumType.STRING) - private OrderStatus status; - - @Column(nullable = false) - private LocalDateTime createdAt; - - @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true) - private List orderItems = new ArrayList<>(); - - protected Order() {} - - private Order(String userId, OrderStatus status) { - this.userId = requiredValidUserId(userId); - this.totalAmount = 0L; - this.status = requiredValidStatus(status); - this.createdAt = LocalDateTime.now(); - } - - public static Order create(String userId) { - return new Order(userId, OrderStatus.PENDING); - } - - public void addOrderItem(OrderItem orderItem) { - orderItem.setOrder(this); - this.orderItems.add(orderItem); - } - - private OrderStatus requiredValidStatus(OrderStatus status) { - if (status == null) { - throw new CoreException(ErrorType.BAD_REQUEST, "์ฃผ๋ฌธ ์ƒํƒœ๋Š” ํ•„์ˆ˜ ์ž…๋‹ˆ๋‹ค."); - } - return status; - } - - private String requiredValidUserId(String userId) { - if (userId == null || userId.isEmpty()) { - throw new CoreException(ErrorType.BAD_REQUEST, "์‚ฌ์šฉ์ž ID๋Š” ํ•„์ˆ˜ ์ž…๋‹ˆ๋‹ค."); - } - return userId; - } - - public void updateTotalAmount(long totalAmount) { - this.totalAmount = totalAmount; - } - - public void updateStatus(OrderStatus status) { - this.status = status; - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItem.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItem.java deleted file mode 100644 index dce97a44a..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItem.java +++ /dev/null @@ -1,91 +0,0 @@ -package com.loopers.domain.order; - -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import jakarta.persistence.*; -import lombok.Getter; -import lombok.Setter; - -/** - * packageName : com.loopers.domain.order - * fileName : OrderItem - * author : byeonsungmun - * date : 2025. 11. 11. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 11. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ - -@Entity -@Table(name = "order_item") -@Getter -public class OrderItem { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Setter - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "order_id", nullable = false) - private Order order; - - @Column(name = "ref_product_id", nullable = false) - private Long productId; - - @Column(name = "ref_product_name", nullable = false) - private String productName; - - @Column(nullable = false) - private Long quantity; - - @Column(nullable = false) - private Long price; - - protected OrderItem() {} - - private OrderItem(Long productId, String productName, Long quantity, Long price) { - this.productId = requiredValidProductId(productId); - this.productName = requiredValidProductName(productName); - this.quantity = requiredQuantity(quantity); - this.price = requiredPrice(price); - } - - public static OrderItem create(Long productId, String productName, Long quantity, Long price) { - return new OrderItem(productId, productName, quantity, price); - } - - public Long getAmount() { - return quantity * price; - } - - private Long requiredValidProductId(Long productId) { - if (productId == null || productId <= 0) { - throw new CoreException(ErrorType.BAD_REQUEST, "์ƒํ’ˆ ID๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."); - } - return productId; - } - - private String requiredValidProductName(String productName) { - if (productName == null || productName.isEmpty()) { - throw new CoreException(ErrorType.BAD_REQUEST, "์ƒํ’ˆ๋ช…์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."); - } - return productName; - } - - private Long requiredQuantity(Long quantity) { - if (quantity == null || quantity <= 0) { - throw new CoreException(ErrorType.BAD_REQUEST, "์ˆ˜๋Ÿ‰์€ 1๊ฐœ ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); - } - return quantity; - } - - private Long requiredPrice(Long price) { - if (price == null || price < 0) { - throw new CoreException(ErrorType.BAD_REQUEST, "๊ฐ€๊ฒฉ์€ 0์› ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); - } - return price; - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderRepository.java deleted file mode 100644 index c80262041..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderRepository.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.loopers.domain.order; - -import java.util.Optional; - -/** - * packageName : com.loopers.domain.order - * fileName : OrderRepository - * author : byeonsungmun - * date : 2025. 11. 13. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 13. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -public interface OrderRepository { - - Order save(Order order); - - Optional findById(Long orderId); -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java deleted file mode 100644 index a66be03d3..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.loopers.domain.order; - -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; - -/** - * packageName : com.loopers.domain.order - * fileName : OrderService - * author : byeonsungmun - * date : 2025. 11. 13. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 13. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -@Component -@RequiredArgsConstructor -public class OrderService { - - private final OrderRepository orderRepository; - - @Transactional - public Order createOrder(Order order) { - return orderRepository.save(order); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderStatus.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderStatus.java deleted file mode 100644 index 14ea592ef..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderStatus.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.loopers.domain.order; - -/** - * packageName : com.loopers.domain.order - * fileName : OrderStatus - * author : byeonsungmun - * date : 2025. 11. 11. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 11. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -public enum OrderStatus { - - COMPLETE("๊ฒฐ์ œ์„ฑ๊ณต"), - CANCEL("๊ฒฐ์ œ์ทจ์†Œ"), - FAIL("๊ฒฐ์ œ์‹คํŒจ"), - PENDING("๊ฒฐ์ œ์ค‘"); - - private final String description; - - OrderStatus(String description) { - this.description = description; - } - - public boolean isCompleted() { - return this == COMPLETE; - } - - public boolean isPending() { - return this == PENDING; - } - - public boolean isCanceled() { - return this == CANCEL; - } - - public String description() { - return description; - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java b/apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java deleted file mode 100644 index bc28a902a..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.loopers.domain.point; - -import com.loopers.domain.BaseEntity; -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import jakarta.persistence.*; -import lombok.Getter; - -@Entity -@Table(name = "point") -@Getter -public class Point extends BaseEntity { - - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Id - private Long id; - - private String userId; - - private Long balance; - - protected Point() {} - - private Point(String userId, Long balance) { - this.userId = requireValidUserId(userId); - this.balance = balance; - } - - public static Point create(String userId, Long balance) { - return new Point(userId, balance); - } - - String requireValidUserId(String userId) { - if(userId == null || userId.isEmpty()) { - throw new CoreException(ErrorType.BAD_REQUEST, "์‚ฌ์šฉ์ž ID๊ฐ€ ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); - } - return userId; - } - - public void charge(Long chargeAmount) { - if (chargeAmount == null || chargeAmount <= 0) { - throw new CoreException(ErrorType.BAD_REQUEST, "0์› ์ดํ•˜๋กœ ํฌ์ธํŠธ๋ฅผ ์ถฉ์ „ ํ• ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); - } - this.balance += chargeAmount; - } - - public void use(Long useAmount) { - if (useAmount == null || useAmount <= 0) { - throw new CoreException(ErrorType.BAD_REQUEST, "0์› ์ดํ•˜๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); - } - if (this.balance < useAmount) { - throw new CoreException(ErrorType.BAD_REQUEST, "ํฌ์ธํŠธ๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค."); - } - this.balance -= useAmount; - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/point/PointRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/point/PointRepository.java deleted file mode 100644 index 314022491..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/point/PointRepository.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.loopers.domain.point; - -import java.util.Optional; - -public interface PointRepository { - - Optional findByUserId(String userId); - - Point save(Point point); -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java b/apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java deleted file mode 100644 index 9c9570615..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.loopers.domain.point; - -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; - -@RequiredArgsConstructor -@Component -public class PointService { - - private final PointRepository pointRepository; - - @Transactional(readOnly = true) - public Point findPointByUserId(String userId) { - return pointRepository.findByUserId(userId).orElse(null); - } - - @Transactional - public Point chargePoint(String userId, Long chargeAmount) { - Point point = pointRepository.findByUserId(userId).orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "ํฌ์ธํŠธ๋ฅผ ์ถฉ์ „ํ• ์ˆ˜ ์—†๋Š” ์‚ฌ์šฉ์ž์ž…๋‹ˆ๋‹ค.")); - point.charge(chargeAmount); - return pointRepository.save(point); - } - - @Transactional - public Point usePoint(String userId, Long useAmount) { - Point point = pointRepository.findByUserId(userId) - .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "ํฌ์ธํŠธ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); - - if (useAmount == null || useAmount <= 0) { - throw new CoreException(ErrorType.BAD_REQUEST, "์ฐจ๊ฐํ•  ํฌ์ธํŠธ๋Š” 1 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); - } - - if (point.getBalance() < useAmount) { - throw new CoreException(ErrorType.BAD_REQUEST, "ํฌ์ธํŠธ๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค."); - } - - point.use(useAmount); - return pointRepository.save(point); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java deleted file mode 100644 index 29968402f..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java +++ /dev/null @@ -1,120 +0,0 @@ -package com.loopers.domain.product; - -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import jakarta.persistence.*; -import lombok.Getter; - -/** - * packageName : com.loopers.domain.product - * fileName : Product - * author : byeonsungmun - * date : 2025. 11. 10. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 10. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ - -@Entity -@Table(name = "product") -@Getter -public class Product { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(name = "ref_brand_id", nullable = false) - private Long brandId; - - @Column(nullable = false) - private String name; - - @Column(nullable = false) - private Long price; - - @Column - private Long likeCount; - - @Column(nullable = false) - private Long stock; - - protected Product() {} - - private Product(Long brandId, String name, Long price, Long likeCount, Long stock) { - this.brandId = requireValidBrandId(brandId); - this.name = requireValidName(name); - this.price = requireValidPrice(price); - this.likeCount = requireValidLikeCount(likeCount); - this.stock = requireValidStock(stock); - } - - public static Product create(Long brandId, String name, Long price, Long stock) { - return new Product( - brandId, - name, - price, - 0L, - stock - ); - } - - private Long requireValidBrandId(Long brandId) { - if (brandId == null || brandId <= 0) { - throw new CoreException(ErrorType.BAD_REQUEST, "๋ธŒ๋žœ๋“œ ID๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."); - } - - return brandId; - } - - private String requireValidName(String name) { - if (name == null || name.isEmpty()) { - throw new CoreException(ErrorType.BAD_REQUEST, "์ƒํ’ˆ๋ช…์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."); - } - return name; - } - - private Long requireValidPrice(Long price) { - if (price == null || price < 0) { - throw new CoreException(ErrorType.BAD_REQUEST, "์ƒํ’ˆ ๊ฐ€๊ฒฉ์€ 0์› ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); - } - return price; - } - - private Long requireValidLikeCount(Long likeCount) { - if (likeCount == null || likeCount < 0) { - throw new CoreException(ErrorType.BAD_REQUEST, "์ข‹์•„์š” ๊ฐœ์ˆ˜๋Š” 0๊ฐœ ๋ฏธ๋งŒ์œผ๋กœ ์„ค์ •ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); - } - return likeCount; - } - - private Long requireValidStock(Long stock) { - if (stock == null) { - throw new CoreException(ErrorType.BAD_REQUEST, "์ƒํ’ˆ ์žฌ๊ณ ๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."); - } - if (stock < 0) { - throw new CoreException(ErrorType.BAD_REQUEST, "์ƒํ’ˆ ์žฌ๊ณ ๋Š” 0 ๋ฏธ๋งŒ์œผ๋กœ ์„ค์ •ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); - } - return stock; - } - - public void increaseLikeCount() { - this.likeCount++; - } - - public void decreaseLikeCount() { - if (this.likeCount > 0) this.likeCount--; - } - - public void decreaseStock(Long quantity) { - if (quantity <= 0) { - throw new CoreException(ErrorType.BAD_REQUEST, "์ฐจ๊ฐ ์ˆ˜๋Ÿ‰์€ 0๋ณด๋‹ค ์ปค์•ผ ํ•ฉ๋‹ˆ๋‹ค."); - } - if (this.stock - quantity < 0) { - throw new CoreException(ErrorType.BAD_REQUEST, "์žฌ๊ณ ๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค."); - } - this.stock -= quantity; - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductDetail.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductDetail.java deleted file mode 100644 index 808bff196..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductDetail.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.loopers.domain.product; - -import com.loopers.domain.brand.Brand; -import lombok.Getter; - -/** - * packageName : com.loopers.domain.product - * fileName : ProductDetail - * author : byeonsungmun - * date : 2025. 11. 13. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 13. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -@Getter -public class ProductDetail { - - private Long id; - private String name; - private String brandName; - private Long price; - private Long likeCount; - - protected ProductDetail() {} - - private ProductDetail(Long id, String name, String brandName, Long price, Long likeCount) { - this.id = id; - this.name = name; - this.brandName = brandName; - this.price = price; - this.likeCount = likeCount; - } - - public static ProductDetail of(Product product, Brand brand, Long likeCount) { - return new ProductDetail( - product.getId(), - product.getName(), - brand.getName(), - product.getPrice(), - likeCount - ); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductDomainService.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductDomainService.java deleted file mode 100644 index 166aff66b..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductDomainService.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.loopers.domain.product; - -import com.loopers.domain.brand.Brand; -import com.loopers.domain.brand.BrandRepository; -import com.loopers.domain.like.LikeRepository; -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; - -/** - * packageName : com.loopers.domain.product - * fileName : ProductDetailService - * author : byeonsungmun - * date : 2025. 11. 13. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 13. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -@Component -@RequiredArgsConstructor -public class ProductDomainService { - - private final ProductRepository productRepository; - private final BrandRepository brandRepository; - private final LikeRepository likeRepository; - - @Transactional(readOnly = true) - public ProductDetail getProductDetail(Long id) { - Product product = productRepository.findById(id) - .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "์ƒํ’ˆ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); - Brand brand = brandRepository.findById(product.getBrandId()) - .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "๋ธŒ๋žœ๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค")); - long likeCount = likeRepository.countByProductId(id); - - return ProductDetail.of(product, brand, likeCount); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductRepository.java deleted file mode 100644 index dadda62a0..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductRepository.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.loopers.domain.product; - -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; - -import java.util.Optional; - -/** - * packageName : com.loopers.domain.product - * fileName : ProductRepositroy - * author : byeonsungmun - * date : 2025. 11. 12. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 12. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -public interface ProductRepository { - Page findAll(Pageable pageable); - - Optional findById(Long id); - - void incrementLikeCount(Long productId); - - void decrementLikeCount(Long productId); - - Product save(Product product); -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java deleted file mode 100644 index 067f194ae..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.loopers.domain.product; - -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; - -/** - * packageName : com.loopers.domain.product - * fileName : ProductService - * author : byeonsungmun - * date : 2025. 11. 12. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 12. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ - -@Component -@RequiredArgsConstructor -public class ProductService { - - private final ProductRepository productRepository; - - @Transactional(readOnly = true) - public Page getProducts(String sort, Pageable pageable) { - Sort sortOption = switch (sort) { - case "price_asc" -> Sort.by("price").ascending(); - case "likes_desc" -> Sort.by("likeCount").descending(); - default -> Sort.by("createdAt").descending(); // latest - }; - - Pageable sortedPageable = PageRequest.of( - pageable.getPageNumber(), - pageable.getPageSize(), - sortOption - ); - - return productRepository.findAll(sortedPageable); - } - - public Product getProduct(Long productId) { - return productRepository.findById(productId) - .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "ํ•ด๋‹น ์ƒํ’ˆ์ด ์—†์Šต๋‹ˆ๋‹ค")); - } -} - diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java deleted file mode 100644 index 287b84cf8..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java +++ /dev/null @@ -1,82 +0,0 @@ -package com.loopers.domain.user; - -import com.loopers.domain.BaseEntity; -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.Table; -import lombok.Getter; - -import java.util.regex.Pattern; - -@Entity -@Table(name = "user") -@Getter -public class User extends BaseEntity { - - private static final Pattern USERID_PATTERN = Pattern.compile("^[a-zA-Z0-9]{1,10}$"); - private static final Pattern BIRTH_PATTERN = Pattern.compile("^\\d{4}-\\d{2}-\\d{2}$"); - private static final Pattern EMAIL_PATTERN = Pattern.compile("^[\\w.+-]+@[\\w.-]+\\.[a-zA-Z]{2,}$"); - - @Column(unique = true, nullable = false) - private String userId; - - @Column(nullable = false) - private String email; - - @Column(nullable = false) - private String birth; - - @Column(nullable = false) - private String gender; - - protected User() {} - - public User(String userId, String email, String birth, String gender) { - this.userId = requireValidUserId(userId); - this.email = requireValidEmail(email); - this.birth = requireValidBirthDate(birth); - this.gender = requireValidGender(gender); - } - - String requireValidUserId(String userId) { - if(userId == null || userId.isEmpty()) { - throw new CoreException(ErrorType.BAD_REQUEST, "์‚ฌ์šฉ์ž ID๊ฐ€ ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); - } - - if (!USERID_PATTERN.matcher(userId).matches()) { - throw new CoreException(ErrorType.BAD_REQUEST, "์‚ฌ์šฉ์ž ID๊ฐ€ ์˜๋ฌธ ๋ฐ ์ˆซ์ž 10์ž ์ด๋‚ด์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค."); - } - return userId; - } - - String requireValidEmail(String email) { - if(email == null || email.isBlank()) { - throw new CoreException(ErrorType.BAD_REQUEST, "์ด๋ฉ”์ผ์ด ๋น„์–ด ์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); - } - - if(!EMAIL_PATTERN.matcher(email).matches()) { - throw new CoreException(ErrorType.BAD_REQUEST, "์ด๋ฉ”์ผ ํ˜•์‹์— ๋งž์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ex)xx@yy.zz"); - } - return email; - } - - String requireValidBirthDate(String birth) { - if (birth == null || birth.isBlank()) { - throw new CoreException(ErrorType.BAD_REQUEST, "์ƒ๋…„์›”์ผ์ด ๋น„์–ด ์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); - } - - if(!BIRTH_PATTERN.matcher(birth).matches()) { - throw new CoreException(ErrorType.BAD_REQUEST, "์ƒ๋…„์›”์ผ ํ˜•์‹์ด ๋งž์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); - } - return birth; - } - - String requireValidGender(String gender) { - if(gender == null || gender.isBlank()) { - throw new CoreException(ErrorType.BAD_REQUEST, "์„ฑ๋ณ„์ด ๋น„์–ด์žˆ์Šต๋‹ˆ๋‹ค."); - } - return gender; - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java deleted file mode 100644 index f4b26266e..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.loopers.domain.user; - -import java.util.Optional; - -public interface UserRepository { - - Optional findByUserId(String userId); - - User save(User user); -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java deleted file mode 100644 index 3cc033076..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.loopers.domain.user; - -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; - -@Component -@RequiredArgsConstructor -public class UserService { - - private final UserRepository userRepository; - - @Transactional - public User register(String userId, String email, String birth, String gender) { - userRepository.findByUserId(userId).ifPresent(user -> { - throw new CoreException(ErrorType.CONFLICT, "์ด๋ฏธ ๊ฐ€์ž…๋œ ์‚ฌ์šฉ์ž ID ์ž…๋‹ˆ๋‹ค."); - }); - - User user = new User(userId, email, birth, gender); - return userRepository.save(user); - } - - @Transactional(readOnly = true) - public User findUserByUserId(String userId) { - return userRepository.findByUserId(userId).orElse(null); - } - -} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandJpaRepository.java deleted file mode 100644 index 759f3caf1..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandJpaRepository.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.loopers.infrastructure.brand; - -import com.loopers.domain.brand.Brand; -import org.springframework.data.jpa.repository.JpaRepository; - -/** - * packageName : com.loopers.infrastructure.brand - * fileName : BrandJpaRepository - * author : byeonsungmun - * date : 2025. 11. 12. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 12. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -public interface BrandJpaRepository extends JpaRepository { -} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandRepositoryImpl.java deleted file mode 100644 index f23e6e5d9..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandRepositoryImpl.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.loopers.infrastructure.brand; - -import com.loopers.domain.brand.Brand; -import com.loopers.domain.brand.BrandRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; - -import java.util.Optional; - -/** - * packageName : com.loopers.infrastructure.brand - * fileName : BrandRepositroyImpl - * author : byeonsungmun - * date : 2025. 11. 12. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 12. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -@RequiredArgsConstructor -@Component -public class BrandRepositoryImpl implements BrandRepository { - - private final BrandJpaRepository jpaRepository; - - @Override - public Optional findById(Long id) { - return jpaRepository.findById(id); - } - - @Override - public void save(Brand brand) { - jpaRepository.save(brand); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/example/ExampleJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/example/ExampleJpaRepository.java new file mode 100644 index 000000000..ce6d3ead0 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/example/ExampleJpaRepository.java @@ -0,0 +1,6 @@ +package com.loopers.infrastructure.example; + +import com.loopers.domain.example.ExampleModel; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ExampleJpaRepository extends JpaRepository {} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/example/ExampleRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/example/ExampleRepositoryImpl.java new file mode 100644 index 000000000..37f2272f0 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/example/ExampleRepositoryImpl.java @@ -0,0 +1,19 @@ +package com.loopers.infrastructure.example; + +import com.loopers.domain.example.ExampleModel; +import com.loopers.domain.example.ExampleRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +@RequiredArgsConstructor +@Component +public class ExampleRepositoryImpl implements ExampleRepository { + private final ExampleJpaRepository exampleJpaRepository; + + @Override + public Optional find(Long id) { + return exampleJpaRepository.findById(id); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeJpaRepository.java deleted file mode 100644 index 865a30db7..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeJpaRepository.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.loopers.infrastructure.like; - -import com.loopers.domain.like.Like; -import org.springframework.data.jpa.repository.JpaRepository; - -import java.util.Optional; - -/** - * packageName : com.loopers.infrastructure.like - * fileName : LikeJpaRepository - * author : byeonsungmun - * date : 2025. 11. 12. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 12. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -public interface LikeJpaRepository extends JpaRepository { - Optional findByUserIdAndProductId(String userId, Long productId); - - long countByProductId(Long productId); -} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeRepositoryImpl.java deleted file mode 100644 index e037b6efb..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeRepositoryImpl.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.loopers.infrastructure.like; - -import com.loopers.domain.like.Like; -import com.loopers.domain.like.LikeRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; - -import java.util.Optional; - -/** - * packageName : com.loopers.infrastructure.like - * fileName : LikeRepositoryImpl - * author : byeonsungmun - * date : 2025. 11. 12. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 12. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -@Component -@RequiredArgsConstructor -public class LikeRepositoryImpl implements LikeRepository { - - private final LikeJpaRepository likeJpaRepository; - - @Override - public Optional findByUserIdAndProductId(String userId, Long productId) { - return likeJpaRepository.findByUserIdAndProductId(userId, productId); - } - - @Override - public void save(Like like) { - likeJpaRepository.save(like); - } - - @Override - public void delete(Like like) { - likeJpaRepository.delete(like); - } - - @Override - public long countByProductId(Long productId) { - return likeJpaRepository.countByProductId(productId); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderJpaRepository.java deleted file mode 100644 index 39cfb136d..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderJpaRepository.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.loopers.infrastructure.order; - -import com.loopers.domain.order.Order; -import org.springframework.data.jpa.repository.JpaRepository; - -/** - * packageName : com.loopers.infrastructure.order - * fileName : OrderJpaRepository - * author : byeonsungmun - * date : 2025. 11. 14. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 14. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -public interface OrderJpaRepository extends JpaRepository { -} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderRepositoryImpl.java deleted file mode 100644 index f8c7b5b68..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderRepositoryImpl.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.loopers.infrastructure.order; - -import com.loopers.domain.order.Order; -import com.loopers.domain.order.OrderRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; - -import java.util.Optional; - -/** - * packageName : com.loopers.infrastructure.order - * fileName : OrderRepositroyImpl - * author : byeonsungmun - * date : 2025. 11. 14. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 14. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -@Component -@RequiredArgsConstructor -public class OrderRepositoryImpl implements OrderRepository { - - private final OrderJpaRepository orderJpaRepository; - - @Override - public Order save(Order order) { - return orderJpaRepository.save(order); - } - - @Override - public Optional findById(Long orderId) { - return orderJpaRepository.findById(orderId); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointJpaRepository.java deleted file mode 100644 index a35a56151..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointJpaRepository.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.loopers.infrastructure.point; - -import com.loopers.domain.point.Point; -import org.springframework.data.jpa.repository.JpaRepository; - -import java.util.Optional; - -public interface PointJpaRepository extends JpaRepository { - - Optional findByUserId(String userId); -} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointRepositoryImpl.java deleted file mode 100644 index 530191b66..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointRepositoryImpl.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.loopers.infrastructure.point; - -import com.loopers.domain.point.Point; -import com.loopers.domain.point.PointRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; - -import java.util.Optional; - -@RequiredArgsConstructor -@Component -public class PointRepositoryImpl implements PointRepository { - - private final PointJpaRepository pointJpaRepository; - - @Override - public Optional findByUserId(String userId) { - return pointJpaRepository.findByUserId(userId); - } - - @Override - public Point save(Point point) { - return pointJpaRepository.save(point); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.java deleted file mode 100644 index 5ceaae067..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.loopers.infrastructure.product; - -import com.loopers.domain.product.Product; -import org.springframework.data.jpa.repository.JpaRepository; - -/** - * packageName : com.loopers.infrastructure.product - * fileName : ProductJpaRepository - * author : byeonsungmun - * date : 2025. 11. 13. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 13. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -public interface ProductJpaRepository extends JpaRepository { - -} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java deleted file mode 100644 index dbad0d9d5..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.loopers.infrastructure.product; - -import com.loopers.domain.product.Product; -import com.loopers.domain.product.ProductRepository; -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Component; - -import java.util.Optional; - -/** - * packageName : com.loopers.infrastructure.product - * fileName : ProductRepositoryImpl - * author : byeonsungmun - * date : 2025. 11. 13. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 13. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -@Component -@RequiredArgsConstructor -public class ProductRepositoryImpl implements ProductRepository { - - private final ProductJpaRepository productJpaRepository; - - @Override - public Page findAll(Pageable pageable) { - return productJpaRepository.findAll(pageable); - } - - @Override - public Optional findById(Long id) { - return productJpaRepository.findById(id); - } - - @Override - public void incrementLikeCount(Long productId) { - Product product = productJpaRepository.findById(productId) - .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "์ƒํ’ˆ์„ ์ฐพ์„์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); - product.increaseLikeCount(); - } - - @Override - public void decrementLikeCount(Long productId) { - Product product = productJpaRepository.findById(productId) - .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "์ƒํ’ˆ์„ ์ฐพ์„์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); - product.decreaseLikeCount(); - } - - @Override - public Product save(Product product) { - return productJpaRepository.save(product); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserJpaRepository.java deleted file mode 100644 index f80a5bc52..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserJpaRepository.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.loopers.infrastructure.user; - -import com.loopers.domain.user.User; -import org.springframework.data.jpa.repository.JpaRepository; - -import java.util.Optional; - -public interface UserJpaRepository extends JpaRepository { - - Optional findByUserId(String userId); -} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java deleted file mode 100644 index 8fb6f7bdf..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.loopers.infrastructure.user; - -import com.loopers.domain.user.User; -import com.loopers.domain.user.UserRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; - -import java.util.Optional; - -@RequiredArgsConstructor -@Component -public class UserRepositoryImpl implements UserRepository { - - private final UserJpaRepository userJpaRepository; - - @Override - public Optional findByUserId(String userId) { - return userJpaRepository.findByUserId(userId); - } - - @Override - public User save(User user) { - return userJpaRepository.save(user); - } -} - diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1ApiSpec.java new file mode 100644 index 000000000..219e3101e --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1ApiSpec.java @@ -0,0 +1,19 @@ +package com.loopers.interfaces.api.example; + +import com.loopers.interfaces.api.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.tags.Tag; + +@Tag(name = "Example V1 API", description = "Loopers ์˜ˆ์‹œ API ์ž…๋‹ˆ๋‹ค.") +public interface ExampleV1ApiSpec { + + @Operation( + summary = "์˜ˆ์‹œ ์กฐํšŒ", + description = "ID๋กœ ์˜ˆ์‹œ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค." + ) + ApiResponse getExample( + @Schema(name = "์˜ˆ์‹œ ID", description = "์กฐํšŒํ•  ์˜ˆ์‹œ์˜ ID") + Long exampleId + ); +} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1Controller.java new file mode 100644 index 000000000..917376016 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1Controller.java @@ -0,0 +1,28 @@ +package com.loopers.interfaces.api.example; + +import com.loopers.application.example.ExampleFacade; +import com.loopers.application.example.ExampleInfo; +import com.loopers.interfaces.api.ApiResponse; +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; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/v1/examples") +public class ExampleV1Controller implements ExampleV1ApiSpec { + + private final ExampleFacade exampleFacade; + + @GetMapping("/{exampleId}") + @Override + public ApiResponse getExample( + @PathVariable(value = "exampleId") Long exampleId + ) { + ExampleInfo info = exampleFacade.getExample(exampleId); + ExampleV1Dto.ExampleResponse response = ExampleV1Dto.ExampleResponse.from(info); + return ApiResponse.success(response); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1Dto.java new file mode 100644 index 000000000..4ecf0eea5 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1Dto.java @@ -0,0 +1,15 @@ +package com.loopers.interfaces.api.example; + +import com.loopers.application.example.ExampleInfo; + +public class ExampleV1Dto { + public record ExampleResponse(Long id, String name, String description) { + public static ExampleResponse from(ExampleInfo info) { + return new ExampleResponse( + info.id(), + info.name(), + info.description() + ); + } + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1ApiSpec.java deleted file mode 100644 index 6f0458399..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1ApiSpec.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.loopers.interfaces.api.point; - -import com.loopers.interfaces.api.ApiResponse; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.tags.Tag; - -@Tag(name = "Point V1 API", description = "Point API ์ž…๋‹ˆ๋‹ค.") -public interface PointV1ApiSpec { - - @Operation( - summary = "ํฌ์ธํŠธ ํšŒ์› ์กฐํšŒ", - description = "ํšŒ์›์˜ ํฌ์ธํŠธ๋ฅผ ์กฐํšŒํ•œ๋‹ค." - ) - ApiResponse getPoint( - @Schema(name = "ํšŒ์› Id", description = "์กฐํšŒํ•  ํšŒ์› ID") - String userId - ); - - @Operation( - summary = "ํฌ์ธํŠธ ์ถฉ์ „", - description = "ํšŒ์›์˜ ํฌ์ธํŠธ๋ฅผ ์ถฉ์ „ํ•œ๋‹ค." - ) - ApiResponse chargePoint( - @Schema(name = "ํฌ์ธํŠธ ์ถฉ์ „ ์š”์ฒญ", description = "์ถฉ์ „ํ•  ํฌ์ธํŠธ ์ •๋ณด๋ฅผ ํฌํ•จํ•œ ์š”์ฒญ") - PointV1Dto.ChargePointRequest request - ); -} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Controller.java deleted file mode 100644 index 866fce9b3..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Controller.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.loopers.interfaces.api.point; - -import com.loopers.application.point.PointFacade; -import com.loopers.application.point.PointInfo; -import com.loopers.interfaces.api.ApiResponse; -import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.*; - -@RequiredArgsConstructor -@RestController -@RequestMapping("/api/v1/points") -public class PointV1Controller implements PointV1ApiSpec { - - private final PointFacade pointFacade; - - @Override - @GetMapping - public ApiResponse getPoint(@RequestHeader("X-USER-ID") String userId) { - PointInfo pointInfo = pointFacade.getPoint(userId); - PointV1Dto.PointResponse response = PointV1Dto.PointResponse.from(pointInfo); - return ApiResponse.success(response); - } - - @Override - @PatchMapping("/charge") - public ApiResponse chargePoint(@RequestBody PointV1Dto.ChargePointRequest request) { - PointInfo pointInfo = pointFacade.chargePoint(request); - PointV1Dto.PointResponse response = PointV1Dto.PointResponse.from(pointInfo); - return ApiResponse.success(response); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Dto.java deleted file mode 100644 index b0b3d050e..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Dto.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.loopers.interfaces.api.point; - -import com.loopers.application.point.PointInfo; - -public class PointV1Dto { - - public record ChargePointRequest(String userId, Long chargeAmount) { - } - - public record PointResponse(String userId, Long amount) { - public static PointResponse from(PointInfo info) { - return new PointResponse( - info.userId(), - info.amount() - ); - } - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1ApiSpec.java deleted file mode 100644 index 1bed68e62..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1ApiSpec.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.loopers.interfaces.api.user; - -import com.loopers.interfaces.api.ApiResponse; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.tags.Tag; - -@Tag(name = "Users V1 API", description = "Users API ์ž…๋‹ˆ๋‹ค.") -public interface UserV1ApiSpec { - - @Operation( - summary = "ํšŒ์› ๊ฐ€์ž…", - description = "ํšŒ์› ๊ฐ€์ž…์„ ํ•ฉ๋‹ˆ๋‹ค." - ) - ApiResponse register( - @Schema(name = "ํšŒ์› ๊ฐ€์ž… ์š”์ฒญ", description = "ํšŒ์›๊ฐ€์ž…") - UserV1Dto.RegisterRequest request - ); - - @Operation( - summary = "ํšŒ์› ์กฐํšŒ", - description = "ํ•ด๋‹น ํšŒ์›์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค." - ) - ApiResponse getUser( - @Schema(name = "ํšŒ์› ID", description = "์กฐํšŒํ•  ํšŒ์› ID") - String userId - ); -} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java deleted file mode 100644 index aed39ae1f..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.loopers.interfaces.api.user; - -import com.loopers.application.user.UserFacade; -import com.loopers.application.user.UserInfo; -import com.loopers.interfaces.api.ApiResponse; -import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.*; - -@RequiredArgsConstructor -@RestController -@RequestMapping("/api/v1/users") -public class UserV1Controller implements UserV1ApiSpec { - - private final UserFacade userFacade; - - @Override - @PostMapping("/register") - public ApiResponse register(@RequestBody UserV1Dto.RegisterRequest request) { - UserInfo userInfo = userFacade.register(request.userId(), request.mail(), request.birth(), request.gender()); - UserV1Dto.UserResponse response = UserV1Dto.UserResponse.from(userInfo); - return ApiResponse.success(response); - } - - @Override - @GetMapping("/{userId}") - public ApiResponse getUser(@PathVariable String userId) { - UserInfo userInfo = userFacade.getUser(userId); - UserV1Dto.UserResponse response = UserV1Dto.UserResponse.from(userInfo); - return ApiResponse.success(response); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java deleted file mode 100644 index 263214848..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.loopers.interfaces.api.user; - -import com.loopers.application.user.UserInfo; - -public class UserV1Dto { - public record RegisterRequest( - String userId, - String mail, - String birth, - String gender - ) { - } - - public record UserResponse(String userId, String email, String birth, String gender) { - public static UserResponse from(UserInfo info) { - return new UserResponse( - info.userId(), - info.email(), - info.birth(), - info.gender() - ); - } - } -} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandTest.java deleted file mode 100644 index 9541c11f4..000000000 --- a/apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandTest.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.loopers.domain.brand; - -import com.loopers.support.error.CoreException; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -/** - * packageName : com.loopers.domain.brand - * fileName : BrandTest - * author : byeonsungmun - * date : 2025. 11. 14. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 14. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -class BrandTest { - - @DisplayName("Brand ๋‹จ์œ„ ํ…Œ์ŠคํŠธ") - @Nested - class CreateBrandTest { - - @Test - @DisplayName("๋ธŒ๋žœ๋“œ ์ƒ์„ฑ ์„ฑ๊ณต") - void createBrandSuccess() { - Brand brand = Brand.create("Nike"); - assertThat(brand.getName()).isEqualTo("Nike"); - } - - @Test - @DisplayName("๋ธŒ๋žœ๋“œ ์ด๋ฆ„์ด ์—†์œผ๋ฉด ์˜ˆ์™ธ") - void createBrandFail() { - assertThatThrownBy(() -> Brand.create("")) - .isInstanceOf(CoreException.class); - } - } -} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/example/ExampleModelTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/example/ExampleModelTest.java new file mode 100644 index 000000000..44ca7576e --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/example/ExampleModelTest.java @@ -0,0 +1,65 @@ +package com.loopers.domain.example; + +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class ExampleModelTest { + @DisplayName("์˜ˆ์‹œ ๋ชจ๋ธ์„ ์ƒ์„ฑํ•  ๋•Œ, ") + @Nested + class Create { + @DisplayName("์ œ๋ชฉ๊ณผ ์„ค๋ช…์ด ๋ชจ๋‘ ์ฃผ์–ด์ง€๋ฉด, ์ •์ƒ์ ์œผ๋กœ ์ƒ์„ฑ๋œ๋‹ค.") + @Test + void createsExampleModel_whenNameAndDescriptionAreProvided() { + // arrange + String name = "์ œ๋ชฉ"; + String description = "์„ค๋ช…"; + + // act + ExampleModel exampleModel = new ExampleModel(name, description); + + // assert + assertAll( + () -> assertThat(exampleModel.getId()).isNotNull(), + () -> assertThat(exampleModel.getName()).isEqualTo(name), + () -> assertThat(exampleModel.getDescription()).isEqualTo(description) + ); + } + + @DisplayName("์ œ๋ชฉ์ด ๋นˆ์นธ์œผ๋กœ๋งŒ ์ด๋ฃจ์–ด์ ธ ์žˆ์œผ๋ฉด, BAD_REQUEST ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") + @Test + void throwsBadRequestException_whenTitleIsBlank() { + // arrange + String name = " "; + + // act + CoreException result = assertThrows(CoreException.class, () -> { + new ExampleModel(name, "์„ค๋ช…"); + }); + + // assert + assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); + } + + @DisplayName("์„ค๋ช…์ด ๋น„์–ด์žˆ์œผ๋ฉด, BAD_REQUEST ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") + @Test + void throwsBadRequestException_whenDescriptionIsEmpty() { + // arrange + String description = ""; + + // act + CoreException result = assertThrows(CoreException.class, () -> { + new ExampleModel("์ œ๋ชฉ", description); + }); + + // assert + assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); + } + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/example/ExampleServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/example/ExampleServiceIntegrationTest.java new file mode 100644 index 000000000..bbd5fdbe1 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/example/ExampleServiceIntegrationTest.java @@ -0,0 +1,72 @@ +package com.loopers.domain.example; + +import com.loopers.infrastructure.example.ExampleJpaRepository; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import com.loopers.utils.DatabaseCleanUp; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@SpringBootTest +class ExampleServiceIntegrationTest { + @Autowired + private ExampleService exampleService; + + @Autowired + private ExampleJpaRepository exampleJpaRepository; + + @Autowired + private DatabaseCleanUp databaseCleanUp; + + @AfterEach + void tearDown() { + databaseCleanUp.truncateAllTables(); + } + + @DisplayName("์˜ˆ์‹œ๋ฅผ ์กฐํšŒํ•  ๋•Œ,") + @Nested + class Get { + @DisplayName("์กด์žฌํ•˜๋Š” ์˜ˆ์‹œ ID๋ฅผ ์ฃผ๋ฉด, ํ•ด๋‹น ์˜ˆ์‹œ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnsExampleInfo_whenValidIdIsProvided() { + // arrange + ExampleModel exampleModel = exampleJpaRepository.save( + new ExampleModel("์˜ˆ์‹œ ์ œ๋ชฉ", "์˜ˆ์‹œ ์„ค๋ช…") + ); + + // act + ExampleModel result = exampleService.getExample(exampleModel.getId()); + + // assert + assertAll( + () -> assertThat(result).isNotNull(), + () -> assertThat(result.getId()).isEqualTo(exampleModel.getId()), + () -> assertThat(result.getName()).isEqualTo(exampleModel.getName()), + () -> assertThat(result.getDescription()).isEqualTo(exampleModel.getDescription()) + ); + } + + @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์˜ˆ์‹œ ID๋ฅผ ์ฃผ๋ฉด, NOT_FOUND ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") + @Test + void throwsException_whenInvalidIdIsProvided() { + // arrange + Long invalidId = 999L; // Assuming this ID does not exist + + // act + CoreException exception = assertThrows(CoreException.class, () -> { + exampleService.getExample(invalidId); + }); + + // assert + assertThat(exception.getErrorType()).isEqualTo(ErrorType.NOT_FOUND); + } + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeServiceIntegrationTest.java deleted file mode 100644 index 0be07a6fb..000000000 --- a/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeServiceIntegrationTest.java +++ /dev/null @@ -1,155 +0,0 @@ -package com.loopers.domain.like; - -import com.loopers.domain.product.Product; -import com.loopers.domain.product.ProductRepository; -import com.loopers.domain.user.User; -import com.loopers.domain.user.UserRepository; -import com.loopers.utils.DatabaseCleanUp; -import org.junit.jupiter.api.*; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.transaction.annotation.Transactional; - -import static org.assertj.core.api.Assertions.*; - -/** - * packageName : com.loopers.domain.like - * fileName : LikeServiceIntegrationTest - * author : byeonsungmun - * date : 2025. 11. 14. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 14. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -@SpringBootTest -class LikeServiceIntegrationTest { - - @Autowired - private LikeService likeService; - - @Autowired - private LikeRepository likeRepository; - - @Autowired - private ProductRepository productRepository; - - @Autowired - private UserRepository userRepository; - - @Autowired - private DatabaseCleanUp cleanUp; - - @AfterEach - void tearDown() { - cleanUp.truncateAllTables(); - } - - @Nested - @DisplayName("์ข‹์•„์š” ๊ธฐ๋Šฅ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ") - class LikeTests { - - @Test - @DisplayName("์ข‹์•„์š” ์ƒ์„ฑ ์„ฑ๊ณต โ†’ ์ข‹์•„์š” ์ €์žฅ + ์ƒํ’ˆ์˜ likeCount ์ฆ๊ฐ€") - @Transactional - void likeSuccess() { - // given - User user = userRepository.save(new User("user1", "u1@mail.com", "1990-01-01", "MALE")); - Product product = productRepository.save(Product.create(1L, "์ƒํ’ˆA", 1000L, 10L)); - - // when - likeService.like(user.getUserId(), product.getId()); - - // then - Like saved = likeRepository.findByUserIdAndProductId("user1", 1L).orElse(null); - assertThat(saved).isNotNull(); - - Product updated = productRepository.findById(1L).get(); - assertThat(updated.getLikeCount()).isEqualTo(1L); - } - - @Test - @DisplayName("์ค‘๋ณต ์ข‹์•„์š” ์‹œ likeCount ์ฆ๊ฐ€ ์•ˆ ํ•˜๊ณ  ์ €์žฅ๋„ ์•ˆ ๋จ") - @Transactional - void duplicateLike() { - // given - userRepository.save(new User("user1", "u1@mail.com", "1990-01-01", "MALE")); - productRepository.save(Product.create(1L, "์ƒํ’ˆA", 1000L, 10L)); - - likeService.like("user1", 1L); - - // when - likeService.like("user1", 1L); // ์ค‘๋ณต ํ˜ธ์ถœ - - // then - long likeCount = likeRepository.countByProductId(1L); - assertThat(likeCount).isEqualTo(1L); - - Product updated = productRepository.findById(1L).get(); - assertThat(updated.getLikeCount()).isEqualTo(1L); // ์ฆ๊ฐ€ X - } - - @Test - @DisplayName("์ข‹์•„์š” ์ทจ์†Œ ์„ฑ๊ณต โ†’ like ์‚ญ์ œ + ์ƒํ’ˆ์˜ likeCount ๊ฐ์†Œ") - @Transactional - void unlikeSuccess() { - // given - userRepository.save(new User("user1", "u1@mail.com", "1990-01-01", "MALE")); - productRepository.save(Product.create(1L, "์ƒํ’ˆA", 1000L, 10L)); - - likeService.like("user1", 1L); - - // when - likeService.unlike("user1", 1L); - - // then - Like like = likeRepository.findByUserIdAndProductId("user1", 1L).orElse(null); - assertThat(like).isNull(); - - Product updated = productRepository.findById(1L).get(); - assertThat(updated.getLikeCount()).isEqualTo(0L); - } - - @Test - @DisplayName("์—†๋Š” ์ข‹์•„์š” ์ทจ์†Œ ์‹œ likeCount ๊ฐ์†Œ ์•ˆ ํ•จ") - @Transactional - void unlikeNonExisting() { - // given - userRepository.save(new User("user1", "u1@mail.com", "1990-01-01", "MALE")); - Product product = Product.create(1L, "์ƒํ’ˆA", 1000L, 10L); - product.increaseLikeCount(); - product.increaseLikeCount(); - product.increaseLikeCount(); - product.increaseLikeCount(); - product.increaseLikeCount(); - - productRepository.save(product); - // when โ€” ํ˜ธ์ถœ์€ ํ•ด๋„ - likeService.unlike("user1", 1L); - - // then โ€” ๋ณ€ํ™” ์—†์Œ - Product updated = productRepository.findById(1L).get(); - assertThat(updated.getLikeCount()).isEqualTo(5L); - } - - @Test - @DisplayName("countByProductId ์ •์ƒ ์กฐํšŒ") - @Transactional - void countTest() { - // given - userRepository.save(new User("user1", "u1@mail.com", "1990-01-01", "MALE")); - userRepository.save(new User("user2", "u2@mail.com", "1991-01-01", "MALE")); - productRepository.save(Product.create(1L, "์ƒํ’ˆA", 1000L, 10L)); - - likeService.like("user1", 1L); - likeService.like("user2", 1L); - - // when - long count = likeService.countByProductId(1L); - - // then - assertThat(count).isEqualTo(2L); - } - } -} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeTest.java deleted file mode 100644 index d5b8bd851..000000000 --- a/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeTest.java +++ /dev/null @@ -1,91 +0,0 @@ -package com.loopers.domain.like; - -import com.loopers.support.error.CoreException; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -import java.time.LocalDateTime; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; - -/** - * packageName : com.loopers.domain.like - * fileName : LikeTest - * author : byeonsungmun - * date : 2025. 11. 13. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 13. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -class LikeTest { - - - @DisplayName("์ •์ƒ์ ์œผ๋กœ Like ์—”ํ‹ฐํ‹ฐ๋ฅผ ์ƒ์„ฑ์ˆ˜ ํ•  ์žˆ๋‹ค") - @Nested - class LikeCreate { - - @DisplayName("Like์ƒ์„ฑ์ž๋ฅผ ์ƒ์„ฑํ•œ๋‹ค") - @Test - void createLike_success() { - // given - String userId = "user-001"; - Long productId = 100L; - - // when - Like like = Like.create(userId, productId); - - // then - assertThat(like.getUserId()).isEqualTo(userId); - assertThat(like.getProductId()).isEqualTo(productId); - assertThat(like.getCreatedAt()).isBeforeOrEqualTo(LocalDateTime.now()); - } - - @Test - @DisplayName("userId๊ฐ€ null์ด๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค") - void createLike_invalidUserId_null() { - // given - String userId = null; - Long productId = 100L; - - // when & then - assertThrows(CoreException.class, () -> Like.create(userId, productId)); - } - - @Test - @DisplayName("userId๊ฐ€ ๋นˆ ๋ฌธ์ž์—ด์ด๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค") - void createLike_invalidUserId_empty() { - // given - String userId = ""; - Long productId = 100L; - - // when & then - assertThrows(CoreException.class, () -> Like.create(userId, productId)); - } - - @Test - @DisplayName("productId๊ฐ€ null์ด๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค") - void createLike_invalidProductId_null() { - // given - String userId = "user-001"; - Long productId = null; - - // when & then - assertThrows(CoreException.class, () -> Like.create(userId, productId)); - } - - @Test - @DisplayName("productId๊ฐ€ 0 ์ดํ•˜์ด๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค") - void createLike_invalidProductId_zeroOrNegative() { - // given - String userId = "user-001"; - Long productId = -1L; - - // when & then - assertThrows(CoreException.class, () -> Like.create(userId, productId)); - } - } -} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderServiceIntegrationTest.java deleted file mode 100644 index 149e71540..000000000 --- a/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderServiceIntegrationTest.java +++ /dev/null @@ -1,170 +0,0 @@ -package com.loopers.domain.order; - -import com.loopers.application.order.CreateOrderCommand; -import com.loopers.application.order.OrderFacade; -import com.loopers.application.order.OrderInfo; -import com.loopers.application.order.OrderItemCommand; -import com.loopers.domain.point.Point; -import com.loopers.domain.point.PointRepository; -import com.loopers.domain.product.Product; -import com.loopers.domain.product.ProductRepository; -import com.loopers.utils.DatabaseCleanUp; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.transaction.annotation.Transactional; - -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -/** - * packageName : com.loopers.domain.order - * fileName : OrderServiceIntegrationTest - * author : byeonsungmun - * date : 2025. 11. 14. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 14. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -@SpringBootTest -public class OrderServiceIntegrationTest { - - @Autowired - private OrderFacade orderFacade; - - @Autowired - private ProductRepository productRepository; - - @Autowired - private PointRepository pointRepository; - - @Autowired - private OrderRepository orderRepository; - - @Autowired - private DatabaseCleanUp databaseCleanUp; - - @AfterEach - void tearDown() { - databaseCleanUp.truncateAllTables(); - } - - @Nested - @DisplayName("์ฃผ๋ฌธ ์ƒ์„ฑ ์„ฑ๊ณต") - class OrderCreateSuccess { - - @Test - @Transactional - void createOrder_success() { - - // given - Product p1 = productRepository.save(Product.create(1L, "์•„๋ฉ”๋ฆฌ์นด๋…ธ", 3000L, 100L)); - Product p2 = productRepository.save(Product.create(1L, "๋ผ๋–ผ", 4000L, 200L)); - - pointRepository.save(Point.create("user1", 20000L)); - - CreateOrderCommand command = new CreateOrderCommand( - "user1", - List.of( - new OrderItemCommand(p1.getId(), 2L), // 6000์› - new OrderItemCommand(p2.getId(), 1L) // 4000์› - ) - ); - - // when - OrderInfo info = orderFacade.createOrder(command); - - // then - Order saved = orderRepository.findById(info.orderId()).orElseThrow(); - - assertThat(saved.getStatus()).isEqualTo(OrderStatus.COMPLETE); - assertThat(saved.getTotalAmount()).isEqualTo(10000L); - assertThat(saved.getOrderItems()).hasSize(2); - - // ์žฌ๊ณ  ๊ฐ์†Œ ํ™•์ธ - Product updated1 = productRepository.findById(p1.getId()).get(); - Product updated2 = productRepository.findById(p2.getId()).get(); - assertThat(updated1.getStock()).isEqualTo(98); - assertThat(updated2.getStock()).isEqualTo(199); - - // ํฌ์ธํŠธ ๊ฐ์†Œ ํ™•์ธ - Point point = pointRepository.findByUserId("user1").get(); - assertThat(point.getBalance()).isEqualTo(10000L); // 20000 - 10000 - - } - } - - @Nested - @DisplayName("์ฃผ๋ฌธ ์‹คํŒจ ์ผ€์ด์Šค") - class OrderCreateFail { - - @Test - @Transactional - @DisplayName("์žฌ๊ณ  ๋ถ€์กฑ์œผ๋กœ ์‹คํŒจ") - void insufficientStock_fail() { - Product item = productRepository.save(Product.create(1L, "์ƒํ’ˆ", 1000L, 1L)); - pointRepository.save(Point.create("user1", 5000L)); - - CreateOrderCommand command = new CreateOrderCommand( - "user1", - List.of(new OrderItemCommand(item.getId(), 5L)) - ); - - assertThatThrownBy(() -> orderFacade.createOrder(command)) - .isInstanceOf(RuntimeException.class); // ๋„ˆ์˜ ๋„๋ฉ”์ธ ์˜ˆ์™ธ ํƒ€์ž… ๋งž์ถฐ๋„ ๋จ - } - - @Test - @Transactional - @DisplayName("ํฌ์ธํŠธ ๋ถ€์กฑ์œผ๋กœ ์‹คํŒจ") - void insufficientPoint_fail() { - Product item = productRepository.save(Product.create(1L, "์ƒํ’ˆ", 1000L, 10L)); - pointRepository.save(Point.create("user1", 2000L)); // ๋ถ€์กฑ - - CreateOrderCommand command = new CreateOrderCommand( - "user1", - List.of(new OrderItemCommand(item.getId(), 5L)) // ์ด 5000์› - ); - - assertThatThrownBy(() -> orderFacade.createOrder(command)) - .hasMessageContaining("ํฌ์ธํŠธ"); // ๋ฉ”์‹œ์ง€ ๋งž์ถ”๋ฉด ๋” ์ •ํ™•ํ•˜๊ฒŒ ๊ฐ€๋Šฅ - } - - @Test - @Transactional - @DisplayName("์—†๋Š” ์ƒํ’ˆ ์ฃผ๋ฌธ ์‹œ ์‹คํŒจ") - void noProduct_fail() { - pointRepository.save(Point.create("user1", 10000L)); - - CreateOrderCommand command = new CreateOrderCommand( - "user1", - List.of(new OrderItemCommand(999L, 1L)) - ); - - assertThatThrownBy(() -> orderFacade.createOrder(command)) - .isInstanceOf(RuntimeException.class); - } - - @Test - @Transactional - @DisplayName("์œ ์ € ํฌ์ธํŠธ ์ •๋ณด ์—†์œผ๋ฉด ์‹คํŒจ") - void noUserPoint_fail() { - Product item = productRepository.save(Product.create(1L, "์ƒํ’ˆ", 1000L, 10L)); - - CreateOrderCommand command = new CreateOrderCommand( - "user1", - List.of(new OrderItemCommand(item.getId(), 1L)) - ); - - assertThatThrownBy(() -> orderFacade.createOrder(command)) - .isInstanceOf(RuntimeException.class); - } - } -} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderTest.java deleted file mode 100644 index 60ed16ecc..000000000 --- a/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderTest.java +++ /dev/null @@ -1,122 +0,0 @@ -package com.loopers.domain.order; - -import com.loopers.support.error.CoreException; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -/** - * packageName : com.loopers.domain.order - * fileName : OrderTest - * author : byeonsungmun - * date : 2025. 11. 14. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 14. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -class OrderTest { - - @Nested - @DisplayName("Order ์ƒ์„ฑ ํ…Œ์ŠคํŠธ") - class CreateOrderTest { - - @Test - @DisplayName("์ฃผ๋ฌธ ์ƒ์„ฑ ์„ฑ๊ณต") - void createOrderSuccess() { - // when - Order order = Order.create("user123"); - - // then - assertThat(order.getUserId()).isEqualTo("user123"); - assertThat(order.getStatus()).isEqualTo(OrderStatus.PENDING); - assertThat(order.getTotalAmount()).isEqualTo(0L); - assertThat(order.getCreatedAt()).isNotNull(); - assertThat(order.getOrderItems()).isEmpty(); - } - - @Test - @DisplayName("userId๊ฐ€ null์ด๋ฉด ์ƒ์„ฑ ์‹คํŒจ") - void createOrderFailUserIdNull() { - assertThatThrownBy(() -> Order.create(null)) - .isInstanceOf(CoreException.class) - .hasMessageContaining("์‚ฌ์šฉ์ž ID๋Š” ํ•„์ˆ˜"); - } - - @Test - @DisplayName("userId๊ฐ€ ๊ณต๋ฐฑ์ด๋ฉด ์ƒ์„ฑ ์‹คํŒจ") - void createOrderFailUserIdBlank() { - assertThatThrownBy(() -> Order.create("")) - .isInstanceOf(CoreException.class) - .hasMessageContaining("์‚ฌ์šฉ์ž ID๋Š” ํ•„์ˆ˜"); - } - } - - @Nested - @DisplayName("Order ์ƒํƒœ ์—…๋ฐ์ดํŠธ ํ…Œ์ŠคํŠธ") - class UpdateStatusTest { - - @Test - @DisplayName("์ฃผ๋ฌธ ์ƒํƒœ ์—…๋ฐ์ดํŠธ ์„ฑ๊ณต") - void updateStatusSuccess() { - // given - Order order = Order.create("user123"); - - // when - order.updateStatus(OrderStatus.COMPLETE); - - // then - assertThat(order.getStatus()).isEqualTo(OrderStatus.COMPLETE); - } - } - - @Nested - @DisplayName("์ด์•ก ์—…๋ฐ์ดํŠธ ํ…Œ์ŠคํŠธ") - class UpdateAmountTest { - - @Test - @DisplayName("์ด์•ก ์—…๋ฐ์ดํŠธ ์„ฑ๊ณต") - void updateTotalAmountSuccess() { - // given - Order order = Order.create("user123"); - - // when - order.updateTotalAmount(5000L); - - // then - assertThat(order.getTotalAmount()).isEqualTo(5000L); - } - } - - @Nested - @DisplayName("OrderItem ์ถ”๊ฐ€ ํ…Œ์ŠคํŠธ") - class AddOrderItemTest { - - @Test - @DisplayName("OrderItem ์ถ”๊ฐ€ ์„ฑ๊ณต") - void addOrderItemSuccess() { - // given - Order order = Order.create("user123"); - - OrderItem item = OrderItem.create( - 1L, - "์ƒํ’ˆ๋ช…", - 2L, - 1000L - ); - - // when - order.addOrderItem(item); - item.setOrder(order); - - // then - assertThat(order.getOrderItems()).hasSize(1); - assertThat(order.getOrderItems().getFirst().getProductName()).isEqualTo("์ƒํ’ˆ๋ช…"); - assertThat(item.getOrder()).isEqualTo(order); - } - } -} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java deleted file mode 100644 index b623bc9c7..000000000 --- a/apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java +++ /dev/null @@ -1,108 +0,0 @@ -package com.loopers.domain.point; - -import com.loopers.domain.user.User; -import com.loopers.domain.user.UserRepository; -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import com.loopers.utils.DatabaseCleanUp; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; - -@SpringBootTest -class PointServiceIntegrationTest { - - @Autowired - private PointRepository pointRepository; - - @Autowired - private UserRepository userRepository; - - @Autowired - private PointService pointService; - - @Autowired - private DatabaseCleanUp databaseCleanUp; - - @AfterEach - void tearDown() { - databaseCleanUp.truncateAllTables(); - } - - @DisplayName("ํฌ์ธํŠธ ์กฐํšŒ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ") - @Nested - class PointUser { - - @DisplayName("ํ•ด๋‹น ID์˜ ํšŒ์›์ด ์กด์žฌํ•  ๊ฒฝ์šฐ, ๋ณด์œ  ํฌ์ธํŠธ๊ฐ€ ๋ฐ˜ํ™˜๋œ๋‹ค.") - @Test - void returnPointInfo_whenValidIdIsProvided() { - //given - String id = "yh45g"; - String email = "yh45g@loopers.com"; - String birth = "1994-12-05"; - String gender = "MALE"; - - userRepository.save(new User(id, email, birth, gender)); - pointRepository.save(Point.create(id, 0L)); - - //when - Point result = pointService.findPointByUserId(id); - - //then - assertThat(result.getUserId()).isEqualTo(id); - assertThat(result.getBalance()).isEqualTo(0L); - } - - @DisplayName("ํšŒ์›์ด ์กด์žฌ ํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ, null ์ด ๋ฐ˜ํ™˜๋œ๋‹ค.") - @Test - void returnNull_whenInvalidUserIdIsProvided() { - //given - String id = "yh45g"; - - //when - Point point = pointService.findPointByUserId(id); - - //then - assertThat(point).isNull(); - } - } - - @DisplayName("ํฌ์ธํŠธ ์ถฉ์ „ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ") - @Nested - class Charge { - - @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ์ € ID ๋กœ ์ถฉ์ „์„ ์‹œ๋„ํ•œ ๊ฒฝ์šฐ, ์‹คํŒจํ•œ๋‹ค.") - @Test - void throwsChargeAmountFailException_whenUserIDIsNotProvided() { - //given - String id = "yh45g"; - - //when - CoreException exception = assertThrows(CoreException.class, () -> pointService.chargePoint(id, 1000L)); - - //then - assertThat(exception.getErrorType()).isEqualTo(ErrorType.NOT_FOUND); - } - - @Test - @DisplayName("ํšŒ์›์ด ์กด์žฌํ•˜๋ฉด ํฌ์ธํŠธ ์ถฉ์ „ ์„ฑ๊ณต") - void chargeSuccess() { - // given - String userId = "user2"; - userRepository.save(new User(userId, "yh45g@loopers.com", "1994-12-05", "MALE")); - pointRepository.save(Point.create(userId, 1000L)); - - // when - Point updated = pointService.chargePoint(userId, 500L); - - // then - assertThat(updated.getBalance()).isEqualTo(1500L); - } - } -} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/point/PointTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/point/PointTest.java deleted file mode 100644 index f33fb2821..000000000 --- a/apps/commerce-api/src/test/java/com/loopers/domain/point/PointTest.java +++ /dev/null @@ -1,117 +0,0 @@ -package com.loopers.domain.point; - -import com.loopers.support.error.CoreException; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; -import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; - -class PointTest { - - @Nested - @DisplayName("ํฌ์ธํŠธ ์ƒ์„ฑ ํ…Œ์ŠคํŠธ") - class CreatePointTest { - - @Test - @DisplayName("ํฌ์ธํŠธ ์ƒ์„ฑ ์„ฑ๊ณต") - void createPointSuccess() { - // when - Point point = Point.create("user123", 100L); - - // then - assertThat(point.getUserId()).isEqualTo("user123"); - assertThat(point.getBalance()).isEqualTo(100L); - } - - @Test - @DisplayName("userId๊ฐ€ null์ด๋ฉด ์ƒ์„ฑ ์‹คํŒจ") - void createPointFailUserIdNull() { - assertThatThrownBy(() -> Point.create(null, 100L)) - .isInstanceOf(CoreException.class) - .hasMessageContaining("์‚ฌ์šฉ์ž ID๊ฐ€ ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); - } - - @Test - @DisplayName("userId๊ฐ€ ๊ณต๋ฐฑ์ด๋ฉด ์ƒ์„ฑ ์‹คํŒจ") - void createPointFailUserIdBlank() { - assertThatThrownBy(() -> Point.create("", 100L)) - .isInstanceOf(CoreException.class) - .hasMessageContaining("์‚ฌ์šฉ์ž ID๊ฐ€ ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); - } - } - - @Nested - @DisplayName("ํฌ์ธํŠธ ์ถฉ์ „ ํ…Œ์ŠคํŠธ") - class ChargePointTest { - - @Test - @DisplayName("ํฌ์ธํŠธ ์ถฉ์ „ ์„ฑ๊ณต") - void chargeSuccess() { - // given - Point point = Point.create("user123", 100L); - - // when - point.charge(50L); - - // then - assertThat(point.getBalance()).isEqualTo(150L); - } - - @Test - @DisplayName("ํฌ์ธํŠธ ์ถฉ์ „ ์‹คํŒจ - 0 ์ดํ•˜ ์ž…๋ ฅ") - void chargeFailZeroOrNegative() { - Point point = Point.create("user123", 100L); - - assertThatThrownBy(() -> point.charge(0L)) - .isInstanceOf(CoreException.class) - .hasMessageContaining("์ถฉ์ „"); - - assertThatThrownBy(() -> point.charge(-10L)) - .isInstanceOf(CoreException.class); - } - } - - @Nested - @DisplayName("ํฌ์ธํŠธ ์‚ฌ์šฉ ํ…Œ์ŠคํŠธ") - class UsePointTest { - - @Test - @DisplayName("ํฌ์ธํŠธ ์‚ฌ์šฉ ์„ฑ๊ณต") - void useSuccess() { - // given - Point point = Point.create("user123", 100L); - - // when - point.use(40L); - - // then - assertThat(point.getBalance()).isEqualTo(60L); - } - - @Test - @DisplayName("ํฌ์ธํŠธ ์‚ฌ์šฉ ์‹คํŒจ - 0 ์ดํ•˜ ์ž…๋ ฅ") - void useFailZeroOrNegative() { - Point point = Point.create("user123", 100L); - - assertThatThrownBy(() -> point.use(0L)) - .isInstanceOf(CoreException.class) - .hasMessageContaining("์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); - - assertThatThrownBy(() -> point.use(-10L)) - .isInstanceOf(CoreException.class); - } - - @Test - @DisplayName("ํฌ์ธํŠธ ์‚ฌ์šฉ ์‹คํŒจ - ์ž”์•ก ๋ถ€์กฑ") - void useFailNotEnough() { - Point point = Point.create("user123", 50L); - - assertThatThrownBy(() -> point.use(100L)) - .isInstanceOf(CoreException.class) - .hasMessageContaining("ํฌ์ธํŠธ๊ฐ€ ๋ถ€์กฑ"); - } - } - -} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductServiceIntegrationTest.java deleted file mode 100644 index 8ad61a194..000000000 --- a/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductServiceIntegrationTest.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.loopers.domain.product; - -import com.loopers.utils.DatabaseCleanUp; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; - -/** - * packageName : com.loopers.domain.product - * fileName : ProductServiceIntegrationTest - * author : byeonsungmun - * date : 2025. 11. 13. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 13. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ - -@SpringBootTest -public class ProductServiceIntegrationTest { - - @Autowired - private ProductService productService; - - @Autowired - private DatabaseCleanUp databaseCleanUp; - - @AfterEach - void tearDown() { - databaseCleanUp.truncateAllTables(); - } - - @Nested - @DisplayName("์ƒํ’ˆ ๋ชฉ๋ก ์กฐํšŒ ํ…Œ์ŠคํŠธ") - class ProductListTests { - - Product product; - - } -} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductTest.java deleted file mode 100644 index c2c6fdd9b..000000000 --- a/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductTest.java +++ /dev/null @@ -1,95 +0,0 @@ -package com.loopers.domain.product; - -import com.loopers.support.error.CoreException; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -/** - * packageName : com.loopers.domain.product - * fileName : ProductTest - * author : byeonsungmun - * date : 2025. 11. 10. - * description : - * =========================================== - * DATE AUTHOR NOTE - * ------------------------------------------- - * 2025. 11. 10. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ - */ -class ProductTest { - @DisplayName("Product ์ข‹์•„์š” ์ˆ˜ ์ฆ๊ฐ ํ…Œ์ŠคํŠธ") - @Nested - class LikeCountChange { - - @DisplayName("์ข‹์•„์š” ์ˆ˜๋ฅผ ์ฆ๊ฐ€์‹œํ‚จ๋‹ค.") - @Test - void increaseLikeCount_incrementsLikeCount() { - // given - Product product = Product.create(11L, "๋‚˜์ดํ‚ค ์—์–ด2", 30000L, 10L); - - // when - product.increaseLikeCount(); - - // then - assertEquals(1L, product.getLikeCount()); - } - - @DisplayName("์ข‹์•„์š” ์ˆ˜๋ฅผ ๊ฐ์†Œ์‹œํ‚จ๋‹ค. 0 ๋ฏธ๋งŒ์œผ๋กœ๋Š” ๊ฐ์†Œํ•˜์ง€ ์•Š๋Š”๋‹ค.") - @Test - void decreaseLikeCount_decrementsLikeCountButNotBelowZero() { - // given - Product product = Product.create(11L, "๋‚˜์ดํ‚ค ์—์–ด2", 30000L, 1L); - - // when - product.decreaseLikeCount(); - - // then - assertEquals(0L, product.getLikeCount()); - - // when decrease again - product.decreaseLikeCount(); - - // then likeCount should not go below 0 - assertEquals(0L, product.getLikeCount()); - } - } - - @DisplayName("Product ์žฌ๊ณ  ์ฐจ๊ฐ ํ…Œ์ŠคํŠธ") - @Nested - class Stock { - - @DisplayName("์žฌ๊ณ ๋ฅผ ์ •์ƒ ์ฐจ๊ฐํ•œ๋‹ค.") - @Test - void decreaseStock_successfullyDecreasesStock() { - // given - Product product = Product.create(1L, "๋‚˜์ดํ‚ค ์—์–ด2", 30000L, 10L); - - // when - product.decreaseStock(3L); - - // then - assertEquals(7, product.getStock()); - } - - @DisplayName("์ฐจ๊ฐ ์ˆ˜๋Ÿ‰์ด 0 ์ดํ•˜์ด๋ฉด ์˜ˆ์™ธ ๋ฐœ์ƒ") - @Test - void decreaseStock_withInvalidQuantity_throwsException() { - Product product = Product.create(1L, "๋‚˜์ดํ‚ค ์—์–ด2", 30000L, 10L); - - assertThrows(CoreException.class, () -> product.decreaseStock(0L)); - assertThrows(CoreException.class, () -> product.decreaseStock(-1L)); - } - - @DisplayName("์žฌ๊ณ ๋ณด๋‹ค ํฐ ์ˆ˜๋Ÿ‰ ์ฐจ๊ฐ ์‹œ ์˜ˆ์™ธ ๋ฐœ์ƒ") - @Test - void decreaseStock_withInsufficientStock_throwsException() { - Product product = Product.create(1L, "๋‚˜์ดํ‚ค ์—์–ด2", 30000L, 10L); - - assertThrows(CoreException.class, () -> product.decreaseStock(11L)); - } - } -} - diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java deleted file mode 100644 index 71091883f..000000000 --- a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java +++ /dev/null @@ -1,112 +0,0 @@ -package com.loopers.domain.user; - -import com.loopers.support.error.CoreException; -import com.loopers.utils.DatabaseCleanUp; -import org.junit.jupiter.api.*; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertAll; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; - -@SpringBootTest -class UserServiceIntegrationTest { - - @Autowired - private UserRepository userRepository; - - @Autowired - private UserService userService; - - @Autowired - private DatabaseCleanUp databaseCleanUp; - - @AfterEach - void tearDown() { - databaseCleanUp.truncateAllTables(); - } - - @DisplayName("ํšŒ์› ๊ฐ€์ž… ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ") - @Nested - class UserRegister { - - @DisplayName("ํšŒ์› ๊ฐ€์ž…์‹œ User ์ €์žฅ์ด ์ˆ˜ํ–‰๋œ๋‹ค.") - @Test - void save_whenUserRegister() { - //given - String userId = "yh45g"; - String email = "yh45g@loopers.com"; - String brith = "1994-12-05"; - String gender = "Male"; - - UserRepository userRepositorySpy = spy(userRepository); - UserService userServiceSpy = new UserService(userRepositorySpy); - - //when - userServiceSpy.register(userId, email, brith, gender); - - //then - verify(userRepositorySpy).save(any(User.class)); - } - - @DisplayName("์ด๋ฏธ ๊ฐ€์ž…๋œ ID ๋กœ ํšŒ์›๊ฐ€์ž… ์‹œ๋„ ์‹œ, ์‹คํŒจํ•œ๋‹ค.") - @Test - void throwsException_whenDuplicateUserId() { - //given - String userId = "yh45g"; - String email = "yh45g@loopers.com"; - String brith = "1994-12-05"; - String gender = "Male"; - - //when - userService.register(userId, email, brith, gender); - - //then - Assertions.assertThrows(CoreException.class, () - -> userService.register(userId, email, brith, gender)); - } - } - - @DisplayName("๋‚ด ์ •๋ณด ์กฐํšŒ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ") - @Nested - class Get { - - @DisplayName("ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•  ๊ฒฝ์šฐ, ํšŒ์› ์ •๋ณด๊ฐ€ ๋ฐ˜ํ™˜๋œ๋‹ค.") - @Test - void returnsUser_whenValidIdIsProvided() { - //given - String userId = "yh45g"; - String email = "yh45g@loopers.com"; - String brith = "1994-12-05"; - String gender = "Male"; - - //when - userService.register(userId, email, brith, gender); - User user = userService.findUserByUserId(userId); - - //then - assertAll( - () -> assertThat(user.getUserId()).isEqualTo(userId), - () -> assertThat(user.getEmail()).isEqualTo(email), - () -> assertThat(user.getBirth()).isEqualTo(brith), - () -> assertThat(user.getGender()).isEqualTo(gender) - ); - } - - @DisplayName("ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ, null ์ด ๋ฐ˜ํ™˜๋œ๋‹ค.") - @Test - void returnNull_whenInvalidUserIdIsProvided() { - //given - String userId = "yh45g"; - - //when - User user = userService.findUserByUserId(userId); - - //then - assertThat(user).isNull(); - } - } -} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserTest.java deleted file mode 100644 index 7d74fdfe2..000000000 --- a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserTest.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.loopers.domain.user; - -import com.loopers.support.error.CoreException; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertThrows; - -class UserTest { - @DisplayName("User ๋‹จ์œ„ ํ…Œ์ŠคํŠธ") - @Nested - class Create { - @DisplayName("ID ๊ฐ€ ์˜๋ฌธ ๋ฐ ์ˆซ์ž 10์ž ์ด๋‚ด ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค.") - @Test - void throwsException_whenInvalidUserIdFormat() { - // given - String invalidUserId = "invalid_id_123"; // 10์ž ์ดˆ๊ณผ + ํŠน์ˆ˜๋ฌธ์ž ํฌํ•จ - String email = "valid@loopers.com"; - String birth = "1994-12-05"; - String gender = "MALE"; - - // when & then - assertThrows(CoreException.class, () -> new User(invalidUserId, email, birth, gender)); - } - - @DisplayName("์ด๋ฉ”์ผ์ด `xx@yy.zz` ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค.") - @Test - void throwsException_whenInvalidEmailFormat() { - // given - String userId = "yh45g"; - String invalidEmail = "invalid-email-format"; // '@' ์—†์Œ - String birth = "1994-12-05"; - String gender = "MALE"; - - // when & then - assertThrows(CoreException.class, () -> new User(userId, invalidEmail, birth, gender)); - } - - @DisplayName("์ƒ๋…„์›”์ผ์ด `yyyy-MM-dd` ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค.") - @Test - void throwsException_whenInvalidBirthFormat() { - // given - String userId = "yh45g"; - String email = "valid@loopers.com"; - String invalidBirth = "19941205"; // ํ˜•์‹ ์˜ค๋ฅ˜: ํ•˜์ดํ”ˆ ์—†์Œ - String gender = "MALE"; - - // when & then - assertThrows(CoreException.class, () -> new User(userId, email, invalidBirth, gender)); - } - } -} diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/ExampleV1ApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/ExampleV1ApiE2ETest.java new file mode 100644 index 000000000..1bb3dba65 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/ExampleV1ApiE2ETest.java @@ -0,0 +1,114 @@ +package com.loopers.interfaces.api; + +import com.loopers.domain.example.ExampleModel; +import com.loopers.infrastructure.example.ExampleJpaRepository; +import com.loopers.interfaces.api.example.ExampleV1Dto; +import com.loopers.utils.DatabaseCleanUp; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import java.util.function.Function; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class ExampleV1ApiE2ETest { + + private static final Function ENDPOINT_GET = id -> "/api/v1/examples/" + id; + + private final TestRestTemplate testRestTemplate; + private final ExampleJpaRepository exampleJpaRepository; + private final DatabaseCleanUp databaseCleanUp; + + @Autowired + public ExampleV1ApiE2ETest( + TestRestTemplate testRestTemplate, + ExampleJpaRepository exampleJpaRepository, + DatabaseCleanUp databaseCleanUp + ) { + this.testRestTemplate = testRestTemplate; + this.exampleJpaRepository = exampleJpaRepository; + this.databaseCleanUp = databaseCleanUp; + } + + @AfterEach + void tearDown() { + databaseCleanUp.truncateAllTables(); + } + + @DisplayName("GET /api/v1/examples/{id}") + @Nested + class Get { + @DisplayName("์กด์žฌํ•˜๋Š” ์˜ˆ์‹œ ID๋ฅผ ์ฃผ๋ฉด, ํ•ด๋‹น ์˜ˆ์‹œ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnsExampleInfo_whenValidIdIsProvided() { + // arrange + ExampleModel exampleModel = exampleJpaRepository.save( + new ExampleModel("์˜ˆ์‹œ ์ œ๋ชฉ", "์˜ˆ์‹œ ์„ค๋ช…") + ); + String requestUrl = ENDPOINT_GET.apply(exampleModel.getId()); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; + ResponseEntity> response = + testRestTemplate.exchange(requestUrl, HttpMethod.GET, new HttpEntity<>(null), responseType); + + // assert + assertAll( + () -> assertTrue(response.getStatusCode().is2xxSuccessful()), + () -> assertThat(response.getBody().data().id()).isEqualTo(exampleModel.getId()), + () -> assertThat(response.getBody().data().name()).isEqualTo(exampleModel.getName()), + () -> assertThat(response.getBody().data().description()).isEqualTo(exampleModel.getDescription()) + ); + } + + @DisplayName("์ˆซ์ž๊ฐ€ ์•„๋‹Œ ID ๋กœ ์š”์ฒญํ•˜๋ฉด, 400 BAD_REQUEST ์‘๋‹ต์„ ๋ฐ›๋Š”๋‹ค.") + @Test + void throwsBadRequest_whenIdIsNotProvided() { + // arrange + String requestUrl = "/api/v1/examples/๋‚˜๋‚˜"; + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; + ResponseEntity> response = + testRestTemplate.exchange(requestUrl, HttpMethod.GET, new HttpEntity<>(null), responseType); + + // assert + assertAll( + () -> assertTrue(response.getStatusCode().is4xxClientError()), + () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST) + ); + } + + @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์˜ˆ์‹œ ID๋ฅผ ์ฃผ๋ฉด, 404 NOT_FOUND ์‘๋‹ต์„ ๋ฐ›๋Š”๋‹ค.") + @Test + void throwsException_whenInvalidIdIsProvided() { + // arrange + Long invalidId = -1L; + String requestUrl = ENDPOINT_GET.apply(invalidId); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; + ResponseEntity> response = + testRestTemplate.exchange(requestUrl, HttpMethod.GET, new HttpEntity<>(null), responseType); + + // assert + assertAll( + () -> assertTrue(response.getStatusCode().is4xxClientError()), + () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND) + ); + } + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/point/PointV1ControllerTest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/point/PointV1ControllerTest.java deleted file mode 100644 index 7d7a2c18c..000000000 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/point/PointV1ControllerTest.java +++ /dev/null @@ -1,156 +0,0 @@ -package com.loopers.interfaces.api.point; - -import com.loopers.domain.point.Point; -import com.loopers.domain.point.PointRepository; -import com.loopers.domain.user.User; -import com.loopers.domain.user.UserRepository; -import com.loopers.interfaces.api.ApiResponse; -import com.loopers.utils.DatabaseCleanUp; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.web.client.TestRestTemplate; -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.http.*; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertAll; -import static org.junit.jupiter.api.Assertions.assertTrue; - -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -class PointV1ControllerTest { - - private static final String GET_USER_POINT_ENDPOINT = "/api/v1/points"; - private static final String POST_USER_POINT_ENDPOINT = "/api/v1/points/charge"; - - @Autowired - private PointRepository pointRepository; - - @Autowired - private UserRepository userRepository; - - @Autowired - private DatabaseCleanUp databaseCleanUp; - - @Autowired - private TestRestTemplate testRestTemplate; - - @AfterEach - void tearDown() { - databaseCleanUp.truncateAllTables(); - } - - @DisplayName("GET /api/v1/points") - @Nested - class UserPoint { - - @DisplayName("ํฌ์ธํŠธ ์กฐํšŒ์— ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ๋ณด์œ  ํฌ์ธํŠธ๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnPoint_whenValidUserIdIsProvided() { - //given - String id = "yh45g"; - String email = "yh45g@loopers.com"; - String birth = "1994-12-05"; - String gender = "MALE"; - Long amount = 1000L; - - userRepository.save(new User(id, email, birth, gender)); - pointRepository.save(Point.create(id, amount)); - - HttpHeaders headers = new HttpHeaders(); - headers.add("X-USER-ID", id); - - //when - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; - ResponseEntity> response = - testRestTemplate.exchange(GET_USER_POINT_ENDPOINT, HttpMethod.GET, new HttpEntity<>(null, headers), responseType); - - //then - assertAll( - () -> assertTrue(response.getStatusCode().is2xxSuccessful()), - () -> assertThat(response.getBody().data().userId()).isEqualTo(id), - () -> assertThat(response.getBody().data().amount()).isEqualTo(1000L) - ); - } - - @DisplayName("ํ•ด๋‹น ID์˜ ํšŒ์›์ด ์กด์žฌํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ, null์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnNull_whenUserIdExists() { - //given - String id = "yh45g"; - - HttpHeaders headers = new HttpHeaders(); - headers.add("X-USER-ID", id); - - //when - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; - ResponseEntity> response = - testRestTemplate.exchange(GET_USER_POINT_ENDPOINT, HttpMethod.GET, new HttpEntity<>(null, headers), responseType); - - //then - assertAll( - () -> assertTrue(response.getStatusCode().is4xxClientError()), - () -> assertThat(response.getBody().data()).isNull() - ); - } - } - - @DisplayName("POST /api/v1/points/charge") - @Nested - class Charge { - - @DisplayName("์กด์žฌํ•˜๋Š” ์œ ์ €๊ฐ€ 1000์›์„ ์ถฉ์ „ํ•  ๊ฒฝ์šฐ, ์ถฉ์ „๋œ ๋ณด์œ  ์ด๋Ÿ‰์„ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnsTotalPoint_whenChargeUserPoint() { - //given - String id = "yh45g"; - String email = "yh45g@loopers.com"; - String birth = "1994-12-05"; - String gender = "MALE"; - - userRepository.save(new User(id, email, birth, gender)); - pointRepository.save(Point.create(id, 0L)); - - PointV1Dto.ChargePointRequest request = new PointV1Dto.ChargePointRequest(id, 1000L); - - HttpHeaders headers = new HttpHeaders(); - headers.add("X-USER-ID", id); - - //when - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; - ResponseEntity> response = - testRestTemplate.exchange(POST_USER_POINT_ENDPOINT, HttpMethod.PATCH, new HttpEntity<>(request, headers), responseType); - - //then - assertAll( - () -> assertTrue(response.getStatusCode().is2xxSuccessful()), - () -> assertThat(response.getBody().data().userId()).isEqualTo(id), - () -> assertThat(response.getBody().data().amount()).isEqualTo(1000L) - ); - } - - @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ์ €๋กœ ์š”์ฒญํ•  ๊ฒฝ์šฐ, 404 Not Found ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void throwsException_whenInvalidUserIdIsProvided() { - //given - String id = "yh45g"; - - HttpHeaders headers = new HttpHeaders(); - headers.add("X-USER-ID", id); - - //when - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; - ResponseEntity> response = - testRestTemplate.exchange(POST_USER_POINT_ENDPOINT, HttpMethod.PATCH, new HttpEntity<>(null, headers), responseType); - - //then - assertAll( - () -> assertTrue(response.getStatusCode().is4xxClientError()), - () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND) - ); - } - } -} diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/user/UserV1ControllerTest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/user/UserV1ControllerTest.java deleted file mode 100644 index defe2fcd5..000000000 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/user/UserV1ControllerTest.java +++ /dev/null @@ -1,148 +0,0 @@ -package com.loopers.interfaces.api.user; - -import com.loopers.domain.user.User; -import com.loopers.infrastructure.user.UserJpaRepository; -import com.loopers.interfaces.api.ApiResponse; -import com.loopers.utils.DatabaseCleanUp; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.web.client.TestRestTemplate; -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpMethod; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; - -import java.util.function.Function; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertAll; -import static org.junit.jupiter.api.Assertions.assertTrue; - -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -class UserV1ControllerTest { - - private static final String USER_REGISTER_ENDPOINT = "/api/v1/users/register"; - private static final Function GET_USER_ENDPOINT = id -> "/api/v1/users/" + id; - - @Autowired - private TestRestTemplate testRestTemplate; - - @Autowired - private UserJpaRepository userJpaRepository; - - @Autowired - private DatabaseCleanUp databaseCleanUp; - - @AfterEach - void tearDown() { - databaseCleanUp.truncateAllTables(); - } - - @DisplayName("POST /api/v1/users") - @Nested - class RegisterUser { - @DisplayName("ํšŒ์› ๊ฐ€์ž…์ด ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ์ƒ์„ฑ๋œ ์œ ์ € ์ •๋ณด๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void registerUser_whenSuccessResponseUser() { - //given - String userId = "yh45g"; - String email = "yh45g@loopers.com"; - String birth = "1994-12-05"; - String gender = "MALE"; - - UserV1Dto.RegisterRequest request = new UserV1Dto.RegisterRequest(userId, email, birth, gender); - - //when - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; - ResponseEntity> response = - testRestTemplate.exchange(USER_REGISTER_ENDPOINT, HttpMethod.POST, new HttpEntity<>(request), responseType); - - //then - assertAll( - () -> assertTrue(response.getStatusCode().is2xxSuccessful()), - () -> assertThat(response.getBody().data().userId()).isEqualTo(userId), - () -> assertThat(response.getBody().data().email()).isEqualTo(email), - () -> assertThat(response.getBody().data().birth()).isEqualTo(birth), - () -> assertThat(response.getBody().data().gender()).isEqualTo(gender) - ); - } - @DisplayName("ํšŒ์› ๊ฐ€์ž… ์‹œ์— ์„ฑ๋ณ„์ด ์—†์„ ๊ฒฝ์šฐ, 400 BAD_REQUEST ์‘๋‹ต์„ ๋ฐ›๋Š”๋‹ค.") - @Test - void throwsBadRequest_whenGenderIsNotProvided() { - //given - String userId = "yh45g"; - String email = "yh45g@loopers.com"; - String birth = "1994-12-05"; - String gender = null; - - UserV1Dto.RegisterRequest request = new UserV1Dto.RegisterRequest(userId, email, birth, gender); - - //when - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; - ResponseEntity> response = - testRestTemplate.exchange(USER_REGISTER_ENDPOINT, HttpMethod.POST, new HttpEntity<>(request), responseType); - - //then - assertAll( - () -> assertTrue(response.getStatusCode().is4xxClientError()), - () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST) - ); - } - } - - @DisplayName("GET /api/v1/users/{userId}") - @Nested - class GetUserById { - @DisplayName("๋‚ด ์ •๋ณด ์กฐํšŒ์— ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ํ•ด๋‹นํ•˜๋Š” ์œ ์ € ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void getUserById_whenSuccessResponseUser() { - //given - String userId = "yh45g"; - String email = "yh45g@loopers.com"; - String birth = "1994-12-05"; - String gender = "MALE"; - - userJpaRepository.save(new User(userId, email, birth, gender)); - - String requestUrl = GET_USER_ENDPOINT.apply(userId); - - //when - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; - ResponseEntity> response = - testRestTemplate.exchange(requestUrl, HttpMethod.GET, new HttpEntity<>(null), responseType); - - //then - assertAll( - () -> assertTrue(response.getStatusCode().is2xxSuccessful()), - () -> assertThat(response.getBody().data().userId()).isEqualTo(userId), - () -> assertThat(response.getBody().data().email()).isEqualTo(email), - () -> assertThat(response.getBody().data().birth()).isEqualTo(birth), - () -> assertThat(response.getBody().data().gender()).isEqualTo(gender) - ); - } - - @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ID ๋กœ ์กฐํšŒํ•  ๊ฒฝ์šฐ, 404 Not Found ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void throwsException_whenInvalidUserIdIsProvided() { - //given - String userId = "notUserId"; - String requestUrl = GET_USER_ENDPOINT.apply(userId); - - //when - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; - ResponseEntity> response = - testRestTemplate.exchange(requestUrl, HttpMethod.GET, new HttpEntity<>(null), responseType); - - //then - assertAll( - () -> assertTrue(response.getStatusCode().is4xxClientError()), - () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND) - ); - } - } -} diff --git a/docs/1round/1round.md b/docs/1round/1round.md deleted file mode 100644 index 106d6c809..000000000 --- a/docs/1round/1round.md +++ /dev/null @@ -1,67 +0,0 @@ -## ๐Ÿงช Implementation Quest - -> ์ง€์ •๋œ **๋‹จ์œ„ ํ…Œ์ŠคํŠธ / ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ / E2E ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค**๋ฅผ ํ•„์ˆ˜๋กœ ๊ตฌํ˜„ํ•˜๊ณ , ๋ชจ๋“  ํ…Œ์ŠคํŠธ๋ฅผ ํ†ต๊ณผ์‹œํ‚ค๋Š” ๊ฒƒ์„ ๋ชฉํ‘œ๋กœ ํ•ฉ๋‹ˆ๋‹ค. -> - -### ํšŒ์› ๊ฐ€์ž… - -**๐Ÿงฑ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ** - -- [x] ID ๊ฐ€ `์˜๋ฌธ ๋ฐ ์ˆซ์ž 10์ž ์ด๋‚ด` ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. -- [x] ์ด๋ฉ”์ผ์ด `xx@yy.zz` ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. -- [x] ์ƒ๋…„์›”์ผ์ด `yyyy-MM-dd` ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. - -**๐Ÿ”— ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ** - -- [x] ํšŒ์› ๊ฐ€์ž…์‹œ User ์ €์žฅ์ด ์ˆ˜ํ–‰๋œ๋‹ค. ( spy ๊ฒ€์ฆ ) -- [x] ์ด๋ฏธ ๊ฐ€์ž…๋œ ID ๋กœ ํšŒ์›๊ฐ€์ž… ์‹œ๋„ ์‹œ, ์‹คํŒจํ•œ๋‹ค. - -**๐ŸŒ E2E ํ…Œ์ŠคํŠธ** - -- [x] ํšŒ์› ๊ฐ€์ž…์ด ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ์ƒ์„ฑ๋œ ์œ ์ € ์ •๋ณด๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค. -- [x] ํšŒ์› ๊ฐ€์ž… ์‹œ์— ์„ฑ๋ณ„์ด ์—†์„ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. - -### ๋‚ด ์ •๋ณด ์กฐํšŒ - -**๐Ÿ”— ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ** - -- [x] ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•  ๊ฒฝ์šฐ, ํšŒ์› ์ •๋ณด๊ฐ€ ๋ฐ˜ํ™˜๋œ๋‹ค. -- [x] ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ, null ์ด ๋ฐ˜ํ™˜๋œ๋‹ค. - -**๐ŸŒ E2E ํ…Œ์ŠคํŠธ** - -- [x] ๋‚ด ์ •๋ณด ์กฐํšŒ์— ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ํ•ด๋‹นํ•˜๋Š” ์œ ์ € ์ •๋ณด๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค. -- [x] ์กด์žฌํ•˜์ง€ ์•Š๋Š” ID ๋กœ ์กฐํšŒํ•  ๊ฒฝ์šฐ, `404 Not Found` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. - -### ํฌ์ธํŠธ ์กฐํšŒ - -**๐Ÿ”— ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ** - -- [x] ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•  ๊ฒฝ์šฐ, ๋ณด์œ  ํฌ์ธํŠธ๊ฐ€ ๋ฐ˜ํ™˜๋œ๋‹ค. -- [x] ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ, null ์ด ๋ฐ˜ํ™˜๋œ๋‹ค. - -**๐ŸŒ E2E ํ…Œ์ŠคํŠธ** - -- [x] ํฌ์ธํŠธ ์กฐํšŒ์— ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ๋ณด์œ  ํฌ์ธํŠธ๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค. -- [x] `X-USER-ID` ํ—ค๋”๊ฐ€ ์—†์„ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. - -### ํฌ์ธํŠธ ์ถฉ์ „ - -**๐Ÿงฑ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ** - -- [X] 0 ์ดํ•˜์˜ ์ •์ˆ˜๋กœ ํฌ์ธํŠธ๋ฅผ ์ถฉ์ „ ์‹œ ์‹คํŒจํ•œ๋‹ค. - -**๐Ÿ”— ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ** - -- [X] ์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ์ € ID ๋กœ ์ถฉ์ „์„ ์‹œ๋„ํ•œ ๊ฒฝ์šฐ, ์‹คํŒจํ•œ๋‹ค. - -**๐ŸŒ E2E ํ…Œ์ŠคํŠธ** - -- [X] ์กด์žฌํ•˜๋Š” ์œ ์ €๊ฐ€ 1000์›์„ ์ถฉ์ „ํ•  ๊ฒฝ์šฐ, ์ถฉ์ „๋œ ๋ณด์œ  ์ด๋Ÿ‰์„ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค. -- [X] ์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ์ €๋กœ ์š”์ฒญํ•  ๊ฒฝ์šฐ, `404 Not Found` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. - -## โœ… Checklist - -- [X] ํ…Œ์ŠคํŠธ๋Š” ๋ชจ๋‘ ํ†ต๊ณผํ•ด์•ผ ํ•˜๋ฉฐ, `@Test` ๊ธฐ๋ฐ˜์œผ๋กœ ๋ช…์‹œ์ ์œผ๋กœ ์ž‘์„ฑ -- [X] ๊ฐ ํ…Œ์ŠคํŠธ๋Š” ํ…Œ์ŠคํŠธ ๋ช…/์„ค๋ช…/์ž…๋ ฅ/์˜ˆ์ƒ ๊ฒฐ๊ณผ๊ฐ€ ๋ถ„๋ช…ํ•ด์•ผ ํ•จ -- [X] E2E ํ…Œ์ŠคํŠธ๋Š” ์‹ค์ œ HTTP ์š”์ฒญ์„ ํ†ตํ•ด ํ๋ฆ„์„ ๊ฒ€์ฆํ•  ๊ฒƒ \ No newline at end of file diff --git a/docs/2round/01-requirements.md b/docs/2round/01-requirements.md deleted file mode 100644 index 3296c21c6..000000000 --- a/docs/2round/01-requirements.md +++ /dev/null @@ -1,104 +0,0 @@ -# ์œ ์ €-์‹œ๋‚˜๋ฆฌ์˜ค - -## ์ƒํ’ˆ ๋ชฉ๋ก -1. ์‚ฌ์šฉ์ž๊ฐ€ ๋ชจ๋“  ์ƒํ’ˆ์— ๋Œ€ํ•ด์„œ ๋ณผ์ˆ˜ ์žˆ๋‹ค. -2. ํŒ๋งค์ค‘์ธ ์ƒํ’ˆ์— ๋Œ€ํ•œ ํŒ๋งค๋ช…, ํŒ๋งค๊ธˆ์•ก, ํŒ๋งค๋ธŒ๋žœ๋“œ, ์ด๋ฏธ์ง€, ์ข‹์•„์š”ํ•œ ์ƒํ’ˆ์„ ๋ณผ์ˆ˜์žˆ๋‹ค. -3. ์‚ฌ์šฉ์ž๋Š” ์›ํ•˜๋Š” ๋ธŒ๋žœ๋“œ๋ณ„๋กœ ์ƒํ’ˆ์„ ๋ณผ์ˆ˜ ์žˆ๋‹ค. -4. ์‚ฌ์šฉ์ž๋Š” ์›ํ•˜๋Š” ์ƒํ’ˆ์—๋Œ€ํ•ด์„œ ์ข‹์•„์š”๋ฅผ ๋ˆ„๋ฅผ์ˆ˜์žˆ๋‹ค. -5. ์‚ฌ์šฉ์ž๋Š” ์›ํ•˜๋Š” ์กฐ๊ฑด์— ๋”ฐ๋ผ ์ƒํ’ˆ์„ ๋ณผ์ˆ˜์žˆ๋‹ค. -6. ์‚ฌ์šฉ์ž๋Š” ์›ํ•˜๋Š” ํŽ˜์ด์ง€๋กœ ์ด๋™ํ• ์ˆ˜์žˆ๋‹ค. - --[๊ธฐ๋Šฅ] -1. ์ „์ฒด ์ƒํ’ˆ ๋ชฉ๋ก ์กฐํšŒ -2. ๋ธŒ๋žœ๋“œ ์ƒํ’ˆ ๋ชฉ๋ก ์กฐํšŒ -3. ์ƒํ’ˆ ์ข‹์•„์š” ๋“ฑ๋ก -4. ํ•„ํ„ฐ๋ง (์ตœ์‹ ์ˆœ, ํŒ๋งค์ˆœ, ์ข‹์•„์š” ์ˆœ, ๋‚จ์ž/์—ฌ์ž) -5. ํŽ˜์ด์ง• - --[์ œ์•ฝ] -1. ๋กœ๊ทธ์ธ์€ X-USER-ID๋กœ ํŒ๋‹จ, ๋น„ํšŒ์›์ผ๊ฒฝ์šฐ "๋กœ๊ทธ์ธ ํ›„ ์ข‹์•„์š” ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค" ๋ฉ”์‹œ์ง€ ์ „๋‹ฌํ•œ๋‹ค -2. ์ƒํ’ˆ์ด ์—†์„๊ฒฝ์šฐ 404_NOTFOUND ์ œ๊ณตํ•œ๋‹ค. - ---- -## ์ƒํ’ˆ ์ƒ์„ธ -1. ์‚ฌ์šฉ์ž๋Š” ์›ํ•˜๋Š” ํŒ๋งค์ค‘ ์ƒํ’ˆ(ํŒ๋งค๋ช…,ํŒ๋งค๊ธˆ์•ก,ํŒ๋งค๋ธŒ๋žœ๋“œ,์ด๋ฏธ์ง€,์ข‹์•„์š”ํ•œ ์ƒํ’ˆ์„ ๋ณผ์ˆ˜์žˆ๋‹ค. - -[๊ธฐ๋Šฅ] -1. ์‚ฌ์šฉ์ž๊ฐ€ ์›ํ•˜๋Š” ์ƒํ’ˆ๋ฒˆํ˜ธ๋กœ ์กฐํšŒ -2. ์ƒํ’ˆ ์ข‹์•„์š” ๋“ฑ๋ก / ์ทจ์†Œ - -[์ œ์•ฝ] -1. ๋กœ๊ทธ์ธ์€ X-USER-ID๋กœ ํŒ๋‹จ, ๋น„ํšŒ์›์ผ๊ฒฝ์šฐ "๋กœ๊ทธ์ธ ํ›„ ์ข‹์•„์š” ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค" ๋ฉ”์‹œ์ง€ ์ „๋‹ฌํ•œ๋‹ค -2. ์ƒํ’ˆ์ด ์กด์žฌํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ 404_NOTFOUND ์ œ๊ณตํ•œ๋‹ค. - ---- -## ์ข‹์•„์š” -1. ์‚ฌ์šฉ์ž๊ฐ€ ์›ํ•˜๋Š” ์ƒํ’ˆ์— ๋Œ€ํ•ด์„œ ์ข‹์•„์š”๋ฅผ ๋ˆ„๋ฅผ ์ˆ˜ ์žˆ๋‹ค. -2. ์‚ฌ์šฉ์ž๊ฐ€ ์›ํ•˜๋Š” ์ƒํ’ˆ์— ๋Œ€ํ•ด์„œ ์ข‹์•„์š”๋ฅผ ์ทจ์†Œ ํ•  ์ˆ˜ ์žˆ๋‹ค. -2. ์‚ฌ์šฉ์ž๊ฐ€ ์ข‹์•„ํ•˜๋Š” ์ƒํ’ˆ์— ๋Œ€ํ•ด ๋ชฉ๋ก์„ ๋ณผ์ˆ˜์žˆ๋‹ค. - -[๊ธฐ๋Šฅ] -1. ์ข‹์•„์š” ๋ˆ„๋ฅธ ์ƒํ’ˆ์—๋Œ€ํ•ด ๋ชฉ๋ก ์กฐํšŒ -2. ์‚ฌ์šฉ๊ฐ€ ์›ํ•˜๋Š” ์ƒํ’ˆ์— ๋Œ€ํ•œ ๋“ฑ๋ก/์ทจ์†Œ, ๋‹จ ๋“ฑ๋ก/ํ•ด์ œ (๋ฉฑ๋“ฑ์„ฑ) - -[์ œ์•ฝ] -1. ๋กœ๊ทธ์ธ์€ X-USER-ID๋กœ ํŒ๋‹จ, ๋น„ํšŒ์›์ผ๊ฒฝ์šฐ "๋กœ๊ทธ์ธ ํ›„ ์ข‹์•„์š” ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค" ๋ฉ”์‹œ์ง€ ์ „๋‹ฌํ•œ๋‹ค -2. ํ•ด๋‹น ์ƒํ’ˆ์— ์ข‹์•„์š”๋ฅผ ์ฒ˜์Œ ๋“ฑ๋ก ํ• ๋•Œ๋Š” 201_Created ์ œ๊ณตํ•œ๋‹ค -3. ํ•ด๋‹น ์ƒํ’ˆ์— ์ข‹์•„์š”๋ฅผ ํ•œ๋ฒˆ๋” ๋“ฑ๋ก ํ• ๋•Œ๋Š” 200_OK ์ œ๊ณตํ•œ๋‹ค -4. ํ•ด๋‹น ์ƒํ’ˆ์— ์ข‹์•„์š”๊ฐ€ ๋“ฑ๋ก ๋œ ์ƒํƒœ์—์„œ ์ทจ์†Œ ํ• ๋•Œ๋Š” 204 No Content ์ œ๊ณตํ•œ๋‹ค -5. ํ•ด๋‹น ์ƒํ’ˆ์— ์ข‹์•„์š”๊ฐ€ ์ทจ์†Œ๊ฐ€ ๋œ ์ƒํƒœ์—์„œ ํ•œ๋ฒˆ๋” ์ทจ์†Œ ํ• ๋•Œ๋Š” 204 No Content ์ œ๊ณตํ•œ๋‹ค ---- -## ๋ธŒ๋žœ๋“œ -1. ์‚ฌ์šฉ์ž๋Š” ๋ชจ๋“  ๋ธŒ๋žœ๋“œ์˜ ์ƒํ’ˆ์„ ๋ณผ์ˆ˜์žˆ๋‹ค. -2. ์‚ฌ์šฉ์ž๋Š” ํŠน์ • ๋ธŒ๋žœ๋“œ์— ๋Œ€ํ•œ ์ƒํ’ˆ๋งŒ ๋ณผ์ˆ˜์žˆ๋‹ค. -3. ์‚ฌ์šฉ์ž๋Š” ํŠน์ • ๊ธฐ์ค€์œผ๋กœ ์ƒํ’ˆ์„ ๋ณผ์ˆ˜์žˆ๋‹ค. (์ตœ์‹ ์ˆœ, ํŒ๋งค์ˆœ, ์ข‹์•„์š” ์ˆœ) -4. ์‚ฌ์šฉ์ž๋Š” ์›ํ•˜๋Š” ํŽ˜์ด์ง€๋กœ ์ด๋™ํ• ์ˆ˜์žˆ๋‹ค. - [๊ธฐ๋Šฅ] -1. ๋ชจ๋“  ๋ธŒ๋žœ๋“œ ์ƒํ’ˆ ์กฐํšŒ -2. ํŠน์ • ๋ธŒ๋žœ๋“œ ์ƒํ’ˆ ์กฐํšŒ -3. ํ•„ํ„ฐ๋ง (์ตœ์‹ ์ˆœ, ํŒ๋งค์ˆœ, ์ข‹์•„์š” ์ˆœ, ๋‚จ์ž/์—ฌ์ž) -4. ํŽ˜์ด์ง• - [์ œ์•ฝ] -1. ๋ธŒ๋žœ๋“œ๊ฐ€ ์—†์„์‹œ 404_NOTFOUND๋ฅผ ์ œ๊ณตํ•œ๋‹ค ---- -## ์ฃผ๋ฌธ -1. ์‚ฌ์šฉ์ž๊ฐ€ ์ƒํ’ˆ ๋ชฉ๋ก์—์„œ ์›ํ•˜๋Š” ์ƒํ’ˆ์„ ์„ ํƒํ•˜์—ฌ ์ฃผ๋ฌธํ•  ์ˆ˜ ์žˆ๋‹ค. -2. ํ•œ๊ฐœ์˜ ์ฃผ๋ฌธ์€ ์—ฌ๋Ÿฌ ์ƒํ’ˆ์„ ํฌํ•จํ•  ์ˆ˜ ์žˆ๋‹ค. -3. ์‚ฌ์šฉ์ž๊ฐ€ ์ฃผ๋ฌธ ๋‚ด์—ญ์„ ์กฐํšŒํ•ด ์–ด๋–ค ์ƒํ’ˆ์„ ์ฃผ๋ฌธํ–ˆ๋Š”์ง€ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค. -4. ์‚ฌ์šฉ์ž๋Š” ๊ฒฐ์ œ ์ „์ด๋ผ๋ฉด ์ฃผ๋ฌธ์„ ์ทจ์†Œํ•  ์ˆ˜ ์žˆ๋‹ค. -5. ์‚ฌ์šฉ์ž๋Š” ์ฃผ๋ฌธ ์ƒ์„ธ ํ™”๋ฉด์—์„œ ์ƒํ’ˆ ์ •๋ณด, ์ˆ˜๋Ÿ‰, ๊ฒฐ์ œ ๊ธˆ์•ก, ์ƒํƒœ ๋“ฑ์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค. - [๊ธฐ๋Šฅ] -1. ์ฃผ๋ฌธ ์ƒ์„ฑ -2. ์ฃผ๋ฌธ ๋ชฉ๋ก ์กฐํšŒ -3. ์ฃผ๋ฌธ ์ƒ์„ธ ์กฐํšŒ -4. ์ฃผ๋ฌธ ์ทจ์†Œ -5. ์ฃผ๋ฌธ์— ๋Œ€ํ•œ ์ƒํƒœ๊ด€๋ฆฌ - [์ œ์•ฝ] -1. ์ฃผ๋ฌธ ์‹œ ์ƒํ’ˆ ์žฌ๊ณ  ํ™•์ธ ๋ฐ ๋ถ€์กฑ ์‹œ ์˜ˆ์™ธ ๋ฐœ์ƒ -2. ํฌ์ธํŠธ ์ž”์•ก ๋ถ€์กฑ ์‹œ ์ฃผ๋ฌธ ๋ถˆ๊ฐ€ -3. ๋™์ผํ•œ ์ฃผ๋ฌธ ์š”์ฒญ์ด ์ค‘๋ณต์œผ๋กœ ๋“ค์–ด์™€๋„ ํ•œ ๋ฒˆ๋งŒ ์ฒ˜๋ฆฌ ---- -## ๊ฒฐ์ œ -1. ์‚ฌ์šฉ์ž๋Š” ์ฃผ๋ฌธํ•œ ์ƒํ’ˆ์— ๋Œ€ํ•ด ํฌ์ธํŠธ๋กœ ๊ฒฐ์ œํ•  ์ˆ˜ ์žˆ๋‹ค. -2. ๊ฒฐ์ œ๊ฐ€ ์™„๋ฃŒ๋˜๋ฉด ์ฃผ๋ฌธ ์ƒํƒœ๊ฐ€ ๊ฒฐ์ œ ์™„๋ฃŒ๋กœ ๋ณ€๊ฒฝ๋œ๋‹ค. -3. ๊ฒฐ์ œ ์ค‘ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ๊ฒฐ์ œ ์‹คํŒจ๋กœ ์ฒ˜๋ฆฌ๋˜๋ฉฐ ํฌ์ธํŠธ์™€ ์žฌ๊ณ ๋Š” ๋ณต๊ตฌ๋œ๋‹ค. -4. ๊ฒฐ์ œ ์™„๋ฃŒ ํ›„์—๋Š” ์ฃผ๋ฌธ ์ทจ์†Œ๊ฐ€ ๋ถˆ๊ฐ€๋Šฅํ•˜๋‹ค. - [๊ธฐ๋Šฅ] -1. ๊ฒฐ์ œ์š”์ฒญ -2. ๊ฒฐ์ œ ๊ฒฐ๊ณผ ๋ฐ˜์˜ -3. ๊ฒฐ์ œ ์‹คํŒจ ์ฒ˜๋ฆฌ -4. ๊ฒฐ์ œ ๋‚ด์—ญ ์กฐํšŒ - [์ œ์•ฝ] -1. ๋™์ผ ์ฃผ๋ฌธ์— ๋Œ€ํ•ด ์ค‘๋ณต ๊ฒฐ์ œ ์š”์ฒญ ์‹œ ํ•œ ๋ฒˆ๋งŒ ์ฒ˜๋ฆฌ -2. ํฌ์ธํŠธ ์ฐจ๊ฐ์‹คํŒจ ์‹œ ๋ณต๊ตฌ -3. ์™ธ๋ถ€๊ฒฐ์ œ ์‹œ์Šคํ…œ ๊ฒฐ์ œ ์‹œ์Šคํ…œ ์ฒ˜๋ฆฌ ์‹คํŒจ์‹œ ์˜ˆ์™ธ์ฒ˜๋ฆฌ - ----- -## Ubiquitous -| ํ•œ๊ตญ์–ด | ์˜์–ด | -|--------|------| -| ์‚ฌ์šฉ์ž | User | -| ํฌ์ธํŠธ | Point | -| ์ƒํ’ˆ | Product | -| ๋ธŒ๋žœ๋“œ | Brand | -| ์ข‹์•„์š” | Like | -| ์ฃผ๋ฌธ | Order | -| ์žฌ๊ณ  | Stock | -| ๊ฐ€๊ฒฉ | Price | -| ๊ฒฐ์ œ | Payment | \ No newline at end of file diff --git a/docs/2round/02-sequence-diagrams.md b/docs/2round/02-sequence-diagrams.md deleted file mode 100644 index 5264a4dc0..000000000 --- a/docs/2round/02-sequence-diagrams.md +++ /dev/null @@ -1,164 +0,0 @@ -# ์‹œํ€€์Šค ๋‹ค์ด์–ด๊ทธ๋žจ - -### 1. ์ƒํ’ˆ ๋ชฉ๋ก ์กฐํšŒ -```mermaid -sequenceDiagram - participant User - participant ProductController - participant ProductService - participant ProductRepository - participant BrandRepository - participant LikeRepository - - User->>ProductController: GET /api/v1/products - ProductController->>ProductService: getProductList - ProductService->>ProductRepository: findAllWithPaging - ProductService->>BrandRepository: findBrandInfoForProducts() - ProductService->>LikeRepository: countLikesForProducts() - ProductRepository-->>ProductService: productList - ProductService-->>ProductController: productListResponse - ProductController-->>User: 200 OK (์ƒํ’ˆ ๋ชฉ๋ก + ๋ธŒ๋žœ๋“œ + ์ข‹์•„์š” ์ˆ˜) -``` ---- -### 2. ์ƒํ’ˆ ์ƒ์„ธ ์กฐํšŒ -```mermaid -sequenceDiagram - participant User - participant ProductController - participant ProductService - participant ProductRepository - participant BrandRepository - participant LikeRepository - - User->>ProductController: GET /api/v1/products/{productId} - ProductController->>ProductService: getProductDetail(productId, userId) - ProductService->>ProductRepository: findById(productId) - ProductService->>BrandRepository: findBrandInfo(brandId) - ProductService->>LikeRepository: existsByUserIdAndProductId(userId, productId) - ProductRepository-->>ProductService: productDetail - ProductService-->>ProductController: productDetailResponse - ProductController-->>User: 200 OK (์ƒํ’ˆ ์ƒ์„ธ ์ •๋ณด) -``` ---- -### 3. ์ƒํ’ˆ ์ข‹์•„์š” ๋“ฑ๋ก/์ทจ์†Œ -```mermaid -sequenceDiagram - participant User - participant LikeController - participant LikeService - participant LikeRepository - - User->>LikeController: POST /api/v1/like/products/{productId} - LikeController->>LikeService: toggleLike(userId, productId) - LikeService->>LikeRepository: existsByUserIdAndProductId(userId, productId) - alt ์ข‹์•„์š”๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Œ - LikeService->>LikeRepository: save(userId, productId) - LikeService-->>LikeController: 201 Created - else ์ด๋ฏธ ์ข‹์•„์š” ๋˜์–ด์žˆ์Œ - LikeService->>LikeRepository: delete(userId, productId) - LikeService-->>LikeController: 204 No Content - end - LikeController-->>User: ์‘๋‹ต (์ƒํƒœ์ฝ”๋“œ์— ๋”ฐ๋ผ ๋‹ค๋ฆ„) -``` ---- - -### 4. ๋ธŒ๋žœ๋“œ๋ณ„ ์ƒํ’ˆ ์กฐํšŒ -```mermaid -sequenceDiagram - participant User - participant BrandController - participant BrandService - participant ProductRepository - participant BrandRepository - - User->>BrandController: GET /api/v1/brands/{brandId}/products - BrandController->>BrandService: getProductsByBrand(brandId, sort, page) - BrandService->>BrandRepository: findById(brandId) - BrandService->>ProductRepository: findByBrandId(brandId, sort, page) - BrandRepository-->>BrandService: brandInfo - ProductRepository-->>BrandService: productList - BrandService-->>BrandController: productListResponse - BrandController-->>User: 200 OK (๋ธŒ๋žœ๋“œ ์ƒํ’ˆ ๋ชฉ๋ก) -``` ---- -### 5. ์ฃผ๋ฌธ ์ƒ์„ฑ -```mermaid -sequenceDiagram - participant User - participant OrderController - participant OrderService - participant ProductReader - participant StockService - participant PointService - participant OrderRepository - - User->>OrderController: POST /api/v1/orders (items[]) - OrderController->>OrderService: createOrder(userId, items) - OrderService->>ProductReader: getProductsByIds(productIds) - loop ๊ฐ ์ƒํ’ˆ์— ๋Œ€ํ•ด - OrderService->>StockService: checkAndDecreaseStock(productId, quantity) - end - OrderService->>PointService: deductPoint(userId, totalPrice) - alt ์žฌ๊ณ  ๋˜๋Š” ํฌ์ธํŠธ ๋ถ€์กฑ - OrderService-->>OrderController: throw Exception - OrderController-->>User: 400 Bad Request - else ์ •์ƒ - OrderService->>OrderRepository: save(order, orderItems) - OrderService-->>OrderController: OrderResponse - OrderController-->>User: 201 Created (์ฃผ๋ฌธ ์™„๋ฃŒ) - end -``` ---- -### 6. ์ฃผ๋ฌธ ๋ชฉ๋ก ๋ฐ ์ƒ์„ธ ์กฐํšŒ -```mermaid -sequenceDiagram - participant User - participant OrderController - participant OrderService - participant OrderRepository - participant ProductRepository - - User->>OrderController: GET /api/v1/orders - OrderController->>OrderService: getOrderList(userId) - OrderService->>OrderRepository: findByUserId(userId) - OrderRepository-->>OrderService: orderList - OrderService-->>OrderController: orderListResponse - OrderController-->>User: 200 OK (์ฃผ๋ฌธ ๋ชฉ๋ก) - - User->>OrderController: GET /api/v1/orders/{orderId} - OrderController->>OrderService: getOrderDetail(orderId, userId) - OrderService->>OrderRepository: findById(orderId) - OrderService->>ProductRepository: findProductsInOrder(orderId) - OrderRepository-->>OrderService: orderDetail - OrderService-->>OrderController: orderDetailResponse - OrderController-->>User: 200 OK (์ฃผ๋ฌธ ์ƒ์„ธ) -``` ---- -### 7. ๊ฒฐ์ œ ์ฒ˜๋ฆฌ -```mermaid -sequenceDiagram - participant User - participant PaymentController - participant PaymentService - participant PaymentGateway - participant OrderRepository - participant PointService - participant StockService - - User->>PaymentController: POST /api/v1/payments (orderId) - PaymentController->>PaymentService: processPayment(orderId, userId) - PaymentService->>OrderRepository: findById(orderId) - PaymentService->>PaymentGateway: requestPayment(orderId, amount) - alt ๊ฒฐ์ œ ์„ฑ๊ณต - PaymentGateway-->>PaymentService: SUCCESS - PaymentService->>OrderRepository: updateStatus(orderId, PAID) - PaymentService-->>PaymentController: successResponse - PaymentController-->>User: 200 OK (๊ฒฐ์ œ ์™„๋ฃŒ) - else ๊ฒฐ์ œ ์‹คํŒจ - PaymentGateway-->>PaymentService: FAILED - PaymentService->>PointService: rollbackPoint(userId, amount) - PaymentService->>StockService: restoreStock(orderId) - PaymentService->>OrderRepository: updateStatus(orderId, FAILED) - PaymentController-->>User: 500 Internal Server Error (๊ฒฐ์ œ ์‹คํŒจ) - end -``` diff --git a/docs/2round/03-class-diagram.md b/docs/2round/03-class-diagram.md deleted file mode 100644 index 8d39cfd0a..000000000 --- a/docs/2round/03-class-diagram.md +++ /dev/null @@ -1,78 +0,0 @@ -# ํด๋ž˜์Šค ๋‹ค์ด์–ด๊ทธ๋žจ - -```mermaid -classDiagram -direction TB - -class User { - Long id - String userId - String name - String email - String gender -} - -class Point { - Long id - String userId - Long balance -} - -class Brand { - Long id - String name -} - -class Product { - Long id - Long brandId - String name - Long price - Long likeCount; - Long stock -} - -class Like { - Long id - String userId - Long productId - LocalDateTime createdAt -} - -class Order { - Long id - String userId - Long totalPrice - OrderStatus status - LocalDateTime createdAt - List orderItems -} - -class OrderItem { - Long id - Order order - Long productId - String productName - Long quantity - Long price -} - -class Payment { - Long id - Long orderId - String status - String paymentRequestId - LocalDateTime createdAt -} - -%% ๊ด€๊ณ„ ์„ค์ • -User --> Point -Brand --> Product -Product --> Like -User --> Like -User --> Order -Order --> OrderItem -Order --> Payment -OrderItem --> Product - -``` \ No newline at end of file diff --git a/docs/2round/04-erd.md b/docs/2round/04-erd.md deleted file mode 100644 index 6389b2202..000000000 --- a/docs/2round/04-erd.md +++ /dev/null @@ -1,74 +0,0 @@ -# erd - -```mermaid -erDiagram - USER { - bigint id PK - varchar user_id - varchar name - varchar email - varchar gender - } - - POINT { - bigint id PK - varchar user_id FK - bigint balance - } - - BRAND { - bigint id PK - varchar name - } - - PRODUCT { - bigint id PK - bigint brand_id FK - varchar name - bigint price - bigint like_count - bigint stock - } - - LIKE { - bigint id PK - varchar user_id FK - bigint product_id FK - datetime created_at - } - - ORDERS { - bigint id PK - varchar user_id FK - bigint total_amount - varchar status - datetime created_at - } - - ORDER_ITEM { - bigint id PK - bigint order_id FK - bigint product_id FK - varchar product_name - bigint quantity - bigint price - } - - PAYMENT { - bigint id PK - bigint order_id FK - varchar status - varchar payment_request_id - datetime created_at - } - - %% ๊ด€๊ณ„ (cardinality) - USER ||--|| POINT : "1:1" - BRAND ||--o{ PRODUCT : "1:N" - PRODUCT ||--o{ LIKE : "1:N" - USER ||--o{ LIKE : "1:N" - USER ||--o{ ORDERS : "1:N" - ORDERS ||--o{ ORDER_ITEM : "1:N" - ORDER_ITEM }o--|| PRODUCT : "N:1" - ORDERS ||--|| PAYMENT : "1:1" -``` \ No newline at end of file diff --git a/docs/2round/2round.md b/docs/2round/2round.md deleted file mode 100644 index 84fdc982c..000000000 --- a/docs/2round/2round.md +++ /dev/null @@ -1,37 +0,0 @@ -## โœ๏ธ Design Quest - -> **์ด์ปค๋จธ์Šค ๋„๋ฉ”์ธ(์ƒํ’ˆ, ๋ธŒ๋žœ๋“œ, ์ข‹์•„์š”, ์ฃผ๋ฌธ ๋“ฑ)์— ๋Œ€ํ•œ ์„ค๊ณ„**๋ฅผ ์™„๋ฃŒํ•˜๊ณ , ๋‹ค์Œ ์ฃผ๋ถ€ํ„ฐ ๊ฐœ๋ฐœ ๊ฐ€๋Šฅํ•œ ์ˆ˜์ค€์˜ ์„ค๊ณ„ ๋ฌธ์„œ๋ฅผ ์ •๋ฆฌํ•˜์—ฌ PR๋กœ ์ œ์ถœํ•ฉ๋‹ˆ๋‹ค. -> - -### ๐Ÿ“‹ ๊ณผ์ œ ์ •๋ณด - -- **์„ค๊ณ„ ๋ฒ”์œ„** - - ์ƒํ’ˆ ๋ชฉ๋ก / ์ƒํ’ˆ ์ƒ์„ธ / ๋ธŒ๋žœ๋“œ ์กฐํšŒ - - ์ƒํ’ˆ ์ข‹์•„์š” ๋“ฑ๋ก/์ทจ์†Œ (๋ฉฑ๋“ฑ ๋™์ž‘) - - ์ฃผ๋ฌธ ์ƒ์„ฑ ๋ฐ ๊ฒฐ์ œ ํ๋ฆ„ (์žฌ๊ณ  ์ฐจ๊ฐ, ํฌ์ธํŠธ ์ฐจ๊ฐ, ์™ธ๋ถ€ ์‹œ์Šคํ…œ ์—ฐ๋™) -- **์ œ์™ธ ๋„๋ฉ”์ธ** - - ํšŒ์›๊ฐ€์ž…, ํฌ์ธํŠธ ์ถฉ์ „ (1์ฃผ์ฐจ ๊ตฌํ˜„ ์™„๋ฃŒ ๊ธฐ์ค€) -- **์š”๊ตฌ์‚ฌํ•ญ ๊ธฐ๋ฐ˜** - - ๋ฃจํ”„ํŒฉ ์ด์ปค๋จธ์Šค ์‹œ๋‚˜๋ฆฌ์˜ค ๋ฌธ์„œ๋ฅผ ๊ธฐ์ค€์œผ๋กœ ๊ธฐ๋Šฅ/์ œ์•ฝ์‚ฌํ•ญ์„ ์„ค๊ณ„์— ๋ฐ˜์˜ํ•ฉ๋‹ˆ๋‹ค. -- **์ œ์ถœ ๋ฐฉ์‹** - 1. ์•„๋ž˜ ํŒŒ์ผ๋“ค์„ ํ”„๋กœ์ ํŠธ ๋‚ด `docs/week2/` ํด๋”์— `.md`๋กœ ์ €์žฅ - 2. Github PR๋กœ ์ œ์ถœ - - PR ์ œ๋ชฉ: `[2์ฃผ์ฐจ] ์„ค๊ณ„ ๋ฌธ์„œ ์ œ์ถœ - ํ™๊ธธ๋™` - - PR ๋ณธ๋ฌธ์— ๋ฆฌ๋ทฐ ํฌ์ธํŠธ ํฌํ•จ (์˜ˆ: ๊ณ ๋ฏผํ•œ ์ง€์  ๋“ฑ) - -### โœ… ์ œ์ถœ ํŒŒ์ผ ๋ชฉ๋ก (.docs/design ๋””๋ ‰ํ† ๋ฆฌ ๋‚ด) - -| ํŒŒ์ผ๋ช… | ๋‚ด์šฉ | -| --- | --- | -| `01-requirements.md` | ์œ ์ € ์‹œ๋‚˜๋ฆฌ์˜ค ๊ธฐ๋ฐ˜ ๊ธฐ๋Šฅ ์ •์˜, ์š”๊ตฌ์‚ฌํ•ญ ๋ช…์„ธ | -| `02-sequence-diagrams.md` | ์‹œํ€€์Šค ๋‹ค์ด์–ด๊ทธ๋žจ ์ตœ์†Œ 2๊ฐœ ์ด์ƒ (Mermaid ๊ธฐ๋ฐ˜ ์ž‘์„ฑ ๊ถŒ์žฅ) | -| `03-class-diagram.md` | ๋„๋ฉ”์ธ ๊ฐ์ฒด ์„ค๊ณ„ (ํด๋ž˜์Šค ๋‹ค์ด์–ด๊ทธ๋žจ or ์„ค๋ช… ์ค‘์‹ฌ) | -| `04-erd.md` | ์ „์ฒด ํ…Œ์ด๋ธ” ๊ตฌ์กฐ ๋ฐ ๊ด€๊ณ„ ์ •๋ฆฌ (ERD Mermaid ์ž‘์„ฑ ๊ฐ€๋Šฅ) | - -## โœ… Checklist - -- [ ] ์ƒํ’ˆ/๋ธŒ๋žœ๋“œ/์ข‹์•„์š”/์ฃผ๋ฌธ ๋„๋ฉ”์ธ์ด ๋ชจ๋‘ ํฌํ•จ๋˜์–ด ์žˆ๋Š”๊ฐ€? -- [ ] ๊ธฐ๋Šฅ ์š”๊ตฌ์‚ฌํ•ญ์ด ์œ ์ € ์ค‘์‹ฌ์œผ๋กœ ์ •๋ฆฌ๋˜์–ด ์žˆ๋Š”๊ฐ€? -- [ ] ์‹œํ€€์Šค ๋‹ค์ด์–ด๊ทธ๋žจ์—์„œ ์ฑ…์ž„ ๊ฐ์ฒด๊ฐ€ ๋“œ๋Ÿฌ๋‚˜๋Š”๊ฐ€? -- [ ] ํด๋ž˜์Šค ๊ตฌ์กฐ๊ฐ€ ๋„๋ฉ”์ธ ์„ค๊ณ„๋ฅผ ์ž˜ ํ‘œํ˜„ํ•˜๊ณ  ์žˆ๋Š”๊ฐ€? -- [ ] ERD ์„ค๊ณ„ ์‹œ ๋ฐ์ดํ„ฐ ์ •ํ•ฉ์„ฑ์„ ๊ณ ๋ คํ•˜์—ฌ ๊ตฌ์„ฑํ•˜์˜€๋Š”๊ฐ€? \ No newline at end of file diff --git a/docs/3round/3round.md b/docs/3round/3round.md deleted file mode 100644 index b9f333cca..000000000 --- a/docs/3round/3round.md +++ /dev/null @@ -1,60 +0,0 @@ -# ๐Ÿ“ Round 3 Quests - ---- - -## ๐Ÿ’ป Implementation Quest - -> *** ๋„๋ฉ”์ธ ๋ชจ๋ธ๋ง**์„ ํ†ตํ•ด Product, Brand, Like, Order ๊ธฐ๋Šฅ์˜ ํ•ต์‹ฌ ๊ฐœ๋…์„ **Entity, Value Object, Domain Service ๋“ฑ ์ ํ•ฉํ•œ** **์ฝ”๋“œ**๋กœ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค. -* ๋ ˆ์ด์–ด๋“œ ์•„ํ‚คํ…์ฒ˜ + DIP ๋ฅผ ์ ์šฉํ•ด ์œ ์—ฐํ•˜๊ณ  ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅํ•œ ๊ตฌ์กฐ๋กœ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค. -* **Application Layer๋ฅผ ๊ฒฝ๋Ÿ‰ ์ˆ˜์ค€**์œผ๋กœ ๊ตฌ์„ฑํ•˜์—ฌ, ๋„๋ฉ”์ธ ๊ฐ์ฒด๋ฅผ ์กฐํ•ฉํ•˜๋Š” ํ๋ฆ„์„ ์‹ค์ œ ๊ตฌํ˜„ํ•ด๋ด…๋‹ˆ๋‹ค. -* **๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑ**ํ•˜์—ฌ ๋„๋ฉ”์ธ ๋กœ์ง์˜ ์ •ํ•ฉ์„ฑ๊ณผ ๊ทœ์น™์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. -> - -### ๐Ÿ“‹ ๊ณผ์ œ ์ •๋ณด - -- ์ƒํ’ˆ, ๋ธŒ๋žœ๋“œ, ์ข‹์•„์š”, ์ฃผ๋ฌธ ๊ธฐ๋Šฅ์˜ **๋„๋ฉ”์ธ ๋ชจ๋ธ ๋ฐ ๋„๋ฉ”์ธ ์„œ๋น„์Šค**๋ฅผ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค. -- ๋„๋ฉ”์ธ ๊ฐ„ ํ˜‘๋ ฅ ํ๋ฆ„์„ ์„ค๊ณ„ํ•˜๊ณ , ํ•„์š”ํ•œ ๋กœ์ง์„ **๋„๋ฉ”์ธ ์„œ๋น„์Šค**๋กœ ๋ถ„๋ฆฌํ•ฉ๋‹ˆ๋‹ค. -- Application Layer์—์„œ ๋„๋ฉ”์ธ ๊ฐ์ฒด๋ฅผ ์กฐํ•ฉํ•˜๋Š” ํ๋ฆ„์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค. - (์˜ˆ: `ProductFacade.getProductDetail(productId)` โ†’ `Product + Brand + Like ์กฐํ•ฉ`) -- Repository Interface ์™€ ๊ตฌํ˜„์ฒด๋Š” ๋ถ„๋ฆฌํ•˜๊ณ , ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅ์„ฑ์„ ๊ณ ๋ คํ•œ ๊ตฌ์กฐ๋ฅผ ์„ค๊ณ„ํ•ฉ๋‹ˆ๋‹ค. -- ๋ชจ๋“  ํ•ต์‹ฌ ๋„๋ฉ”์ธ ๋กœ์ง์— ๋Œ€ํ•ด ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•˜๊ณ , ์˜ˆ์™ธ/๊ฒฝ๊ณ„ ์ผ€์ด์Šค๋„ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. - -### ๐Ÿท Product / Brand ๋„๋ฉ”์ธ - -## โœ… Checklist - -- [x] ์ƒํ’ˆ ์ •๋ณด ๊ฐ์ฒด๋Š” ๋ธŒ๋žœ๋“œ ์ •๋ณด, ์ข‹์•„์š” ์ˆ˜๋ฅผ ํฌํ•จํ•œ๋‹ค. -- [x] ์ƒํ’ˆ์˜ ์ •๋ ฌ ์กฐ๊ฑด(`latest`, `price_asc`, `likes_desc`) ์„ ๊ณ ๋ คํ•œ ์กฐํšŒ ๊ธฐ๋Šฅ์„ ์„ค๊ณ„ํ–ˆ๋‹ค -- [x] ์ƒํ’ˆ์€ ์žฌ๊ณ ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๊ณ , ์ฃผ๋ฌธ ์‹œ ์ฐจ๊ฐํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•œ๋‹ค -- [x] ์žฌ๊ณ ๋Š” ๊ฐ์†Œ๋งŒ ๊ฐ€๋Šฅํ•˜๋ฉฐ ์Œ์ˆ˜ ๋ฐฉ์ง€๋Š” ๋„๋ฉ”์ธ ๋ ˆ๋ฒจ์—์„œ ์ฒ˜๋ฆฌ๋œ๋‹ค - -### ๐Ÿ‘ Like ๋„๋ฉ”์ธ - -- [x] ์ข‹์•„์š”๋Š” ์œ ์ €์™€ ์ƒํ’ˆ ๊ฐ„์˜ ๊ด€๊ณ„๋กœ ๋ณ„๋„ ๋„๋ฉ”์ธ์œผ๋กœ ๋ถ„๋ฆฌํ–ˆ๋‹ค -- [x] ์ค‘๋ณต ์ข‹์•„์š” ๋ฐฉ์ง€๋ฅผ ์œ„ํ•œ ๋ฉฑ๋“ฑ์„ฑ ์ฒ˜๋ฆฌ๊ฐ€ ๊ตฌํ˜„๋˜์—ˆ๋‹ค -- [x] ์ƒํ’ˆ์˜ ์ข‹์•„์š” ์ˆ˜๋Š” ์ƒํ’ˆ ์ƒ์„ธ/๋ชฉ๋ก ์กฐํšŒ์—์„œ ํ•จ๊ป˜ ์ œ๊ณต๋œ๋‹ค -- [x] ๋‹จ์œ„ ํ…Œ์ŠคํŠธ์—์„œ ์ข‹์•„์š” ๋“ฑ๋ก/์ทจ์†Œ/์ค‘๋ณต ๋ฐฉ์ง€ ํ๋ฆ„์„ ๊ฒ€์ฆํ–ˆ๋‹ค - -### ๐Ÿ›’ Order ๋„๋ฉ”์ธ - -- [x] ์ฃผ๋ฌธ์€ ์—ฌ๋Ÿฌ ์ƒํ’ˆ์„ ํฌํ•จํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ๊ฐ ์ƒํ’ˆ์˜ ์ˆ˜๋Ÿ‰์„ ๋ช…์‹œํ•œ๋‹ค -- [x] ์ฃผ๋ฌธ ์‹œ ์ƒํ’ˆ์˜ ์žฌ๊ณ  ์ฐจ๊ฐ, ์œ ์ € ํฌ์ธํŠธ ์ฐจ๊ฐ ๋“ฑ์„ ์ˆ˜ํ–‰ํ•œ๋‹ค -- [x] ์žฌ๊ณ  ๋ถ€์กฑ, ํฌ์ธํŠธ ๋ถ€์กฑ ๋“ฑ ์˜ˆ์™ธ ํ๋ฆ„์„ ๊ณ ๋ คํ•ด ์„ค๊ณ„๋˜์—ˆ๋‹ค -- [x] ๋‹จ์œ„ ํ…Œ์ŠคํŠธ์—์„œ ์ •์ƒ ์ฃผ๋ฌธ / ์˜ˆ์™ธ ์ฃผ๋ฌธ ํ๋ฆ„์„ ๋ชจ๋‘ ๊ฒ€์ฆํ–ˆ๋‹ค - -### ๐Ÿงฉ ๋„๋ฉ”์ธ ์„œ๋น„์Šค - -- [x] ๋„๋ฉ”์ธ ๊ฐ„ ํ˜‘๋ ฅ ๋กœ์ง์€ Domain Service์— ์œ„์น˜์‹œ์ผฐ๋‹ค -- [x] ์ƒํ’ˆ ์ƒ์„ธ ์กฐํšŒ ์‹œ Product + Brand ์ •๋ณด ์กฐํ•ฉ์€ ๋„๋ฉ”์ธ ์„œ๋น„์Šค์—์„œ ์ฒ˜๋ฆฌํ–ˆ๋‹ค -- [x] ๋ณตํ•ฉ ์œ ์Šค์ผ€์ด์Šค๋Š” Application Layer์— ์กด์žฌํ•˜๊ณ , ๋„๋ฉ”์ธ ๋กœ์ง์€ ์œ„์ž„๋˜์—ˆ๋‹ค -- [x] ๋„๋ฉ”์ธ ์„œ๋น„์Šค๋Š” ์ƒํƒœ ์—†์ด, ๋„๋ฉ”์ธ ๊ฐ์ฒด์˜ ํ˜‘๋ ฅ ์ค‘์‹ฌ์œผ๋กœ ์„ค๊ณ„๋˜์—ˆ๋‹ค - -### **๐Ÿงฑ ์†Œํ”„ํŠธ์›จ์–ด ์•„ํ‚คํ…์ฒ˜ & ์„ค๊ณ„** - -- [x] ์ „์ฒด ํ”„๋กœ์ ํŠธ์˜ ๊ตฌ์„ฑ์€ ์•„๋ž˜ ์•„ํ‚คํ…์ฒ˜๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๊ตฌ์„ฑ๋˜์—ˆ๋‹ค - - Application โ†’ **Domain** โ† Infrastructure -- [x] Application Layer๋Š” ๋„๋ฉ”์ธ ๊ฐ์ฒด๋ฅผ ์กฐํ•ฉํ•ด ํ๋ฆ„์„ orchestration ํ–ˆ๋‹ค -- [x] ํ•ต์‹ฌ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์€ Entity, VO, Domain Service ์— ์œ„์น˜ํ•œ๋‹ค -- [x] Repository Interface๋Š” Domain Layer ์— ์ •์˜๋˜๊ณ , ๊ตฌํ˜„์ฒด๋Š” Infra์— ์œ„์น˜ํ•œ๋‹ค -- [x] ํŒจํ‚ค์ง€๋Š” ๊ณ„์ธต + ๋„๋ฉ”์ธ ๊ธฐ์ค€์œผ๋กœ ๊ตฌ์„ฑ๋˜์—ˆ๋‹ค (`/domain/order`, `/application/like` ๋“ฑ) -- [x] ํ…Œ์ŠคํŠธ๋Š” ์™ธ๋ถ€ ์˜์กด์„ฑ์„ ๋ถ„๋ฆฌํ•˜๊ณ , Fake/Stub ๋“ฑ์„ ์‚ฌ์šฉํ•ด ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๊ฐ€ ๊ฐ€๋Šฅํ•˜๊ฒŒ ๊ตฌ์„ฑ๋˜์—ˆ๋‹ค \ No newline at end of file From b84525a82190d877a7751ba156bca4492b240ede Mon Sep 17 00:00:00 2001 From: BOB <56067193+adminhelper@users.noreply.github.com> Date: Fri, 21 Nov 2025 16:58:59 +0900 Subject: [PATCH 63/85] =?UTF-8?q?Revert=20"Revert=20"[volume-3]=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=20=EB=AA=A8=EB=8D=B8=EB=A7=81=20?= =?UTF-8?q?=EB=B0=8F=20=EA=B5=AC=ED=98=84""?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 0074ea918de0aee3995308e4ab57f0fba05428a5. --- .../application/example/ExampleFacade.java | 17 -- .../application/example/ExampleInfo.java | 13 -- .../loopers/application/like/LikeFacade.java | 32 ++++ .../application/order/CreateOrderCommand.java | 19 ++ .../application/order/OrderFacade.java | 81 +++++++++ .../loopers/application/order/OrderInfo.java | 42 +++++ .../application/order/OrderItemCommand.java | 17 ++ .../application/order/OrderItemInfo.java | 32 ++++ .../application/point/PointFacade.java | 28 +++ .../loopers/application/point/PointInfo.java | 13 ++ .../product/ProductDetailInfo.java | 32 ++++ .../application/product/ProductFacade.java | 47 +++++ .../application/product/ProductInfo.java | 33 ++++ .../loopers/application/user/UserFacade.java | 27 +++ .../loopers/application/user/UserInfo.java | 14 ++ .../java/com/loopers/domain/brand/Brand.java | 47 +++++ .../loopers/domain/brand/BrandRepository.java | 20 +++ .../loopers/domain/brand/BrandService.java | 35 ++++ .../loopers/domain/example/ExampleModel.java | 44 ----- .../domain/example/ExampleRepository.java | 7 - .../domain/example/ExampleService.java | 20 --- .../java/com/loopers/domain/like/Like.java | 63 +++++++ .../loopers/domain/like/LikeRepository.java | 25 +++ .../com/loopers/domain/like/LikeService.java | 49 +++++ .../java/com/loopers/domain/order/Order.java | 86 +++++++++ .../com/loopers/domain/order/OrderItem.java | 91 ++++++++++ .../loopers/domain/order/OrderRepository.java | 21 +++ .../loopers/domain/order/OrderService.java | 28 +++ .../com/loopers/domain/order/OrderStatus.java | 42 +++++ .../java/com/loopers/domain/point/Point.java | 56 ++++++ .../loopers/domain/point/PointRepository.java | 10 ++ .../loopers/domain/point/PointService.java | 43 +++++ .../com/loopers/domain/product/Product.java | 120 +++++++++++++ .../loopers/domain/product/ProductDetail.java | 45 +++++ .../domain/product/ProductDomainService.java | 41 +++++ .../domain/product/ProductRepository.java | 29 +++ .../domain/product/ProductService.java | 53 ++++++ .../java/com/loopers/domain/user/User.java | 82 +++++++++ .../loopers/domain/user/UserRepository.java | 10 ++ .../com/loopers/domain/user/UserService.java | 30 ++++ .../brand/BrandJpaRepository.java | 18 ++ .../brand/BrandRepositoryImpl.java | 36 ++++ .../example/ExampleJpaRepository.java | 6 - .../example/ExampleRepositoryImpl.java | 19 -- .../like/LikeJpaRepository.java | 23 +++ .../like/LikeRepositoryImpl.java | 46 +++++ .../order/OrderJpaRepository.java | 18 ++ .../order/OrderRepositoryImpl.java | 36 ++++ .../point/PointJpaRepository.java | 11 ++ .../point/PointRepositoryImpl.java | 25 +++ .../product/ProductJpaRepository.java | 19 ++ .../product/ProductRepositoryImpl.java | 59 ++++++ .../user/UserJpaRepository.java | 11 ++ .../user/UserRepositoryImpl.java | 26 +++ .../api/example/ExampleV1ApiSpec.java | 19 -- .../api/example/ExampleV1Controller.java | 28 --- .../interfaces/api/example/ExampleV1Dto.java | 15 -- .../interfaces/api/point/PointV1ApiSpec.java | 28 +++ .../api/point/PointV1Controller.java | 31 ++++ .../interfaces/api/point/PointV1Dto.java | 18 ++ .../interfaces/api/user/UserV1ApiSpec.java | 28 +++ .../interfaces/api/user/UserV1Controller.java | 31 ++++ .../interfaces/api/user/UserV1Dto.java | 24 +++ .../com/loopers/domain/brand/BrandTest.java | 42 +++++ .../domain/example/ExampleModelTest.java | 65 ------- .../ExampleServiceIntegrationTest.java | 72 -------- .../like/LikeServiceIntegrationTest.java | 155 ++++++++++++++++ .../com/loopers/domain/like/LikeTest.java | 91 ++++++++++ .../order/OrderServiceIntegrationTest.java | 170 ++++++++++++++++++ .../com/loopers/domain/order/OrderTest.java | 122 +++++++++++++ .../point/PointServiceIntegrationTest.java | 108 +++++++++++ .../com/loopers/domain/point/PointTest.java | 117 ++++++++++++ .../ProductServiceIntegrationTest.java | 43 +++++ .../loopers/domain/product/ProductTest.java | 95 ++++++++++ .../user/UserServiceIntegrationTest.java | 112 ++++++++++++ .../com/loopers/domain/user/UserTest.java | 53 ++++++ .../interfaces/api/ExampleV1ApiE2ETest.java | 114 ------------ .../api/point/PointV1ControllerTest.java | 156 ++++++++++++++++ .../api/user/UserV1ControllerTest.java | 148 +++++++++++++++ docs/1round/1round.md | 67 +++++++ docs/2round/01-requirements.md | 104 +++++++++++ docs/2round/02-sequence-diagrams.md | 164 +++++++++++++++++ docs/2round/03-class-diagram.md | 78 ++++++++ docs/2round/04-erd.md | 74 ++++++++ docs/2round/2round.md | 37 ++++ docs/3round/3round.md | 60 +++++++ 86 files changed, 3927 insertions(+), 439 deletions(-) delete mode 100644 apps/commerce-api/src/main/java/com/loopers/application/example/ExampleFacade.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/application/example/ExampleInfo.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/like/LikeFacade.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/order/CreateOrderCommand.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/order/OrderInfo.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/order/OrderItemCommand.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/order/OrderItemInfo.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/point/PointInfo.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/product/ProductDetailInfo.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/product/ProductInfo.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/user/UserInfo.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/brand/Brand.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandRepository.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandService.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleModel.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleRepository.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleService.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/like/Like.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/like/LikeRepository.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/like/LikeService.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/order/Order.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItem.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/order/OrderRepository.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/order/OrderStatus.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/point/PointRepository.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/product/ProductDetail.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/product/ProductDomainService.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/product/ProductRepository.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/user/User.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandJpaRepository.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandRepositoryImpl.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/example/ExampleJpaRepository.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/example/ExampleRepositoryImpl.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeJpaRepository.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeRepositoryImpl.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderJpaRepository.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderRepositoryImpl.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointJpaRepository.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointRepositoryImpl.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserJpaRepository.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1ApiSpec.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1Controller.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1Dto.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1ApiSpec.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Controller.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Dto.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1ApiSpec.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandTest.java delete mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/example/ExampleModelTest.java delete mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/example/ExampleServiceIntegrationTest.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/like/LikeServiceIntegrationTest.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/like/LikeTest.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/order/OrderServiceIntegrationTest.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/order/OrderTest.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/point/PointTest.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/product/ProductServiceIntegrationTest.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/product/ProductTest.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/user/UserTest.java delete mode 100644 apps/commerce-api/src/test/java/com/loopers/interfaces/api/ExampleV1ApiE2ETest.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/interfaces/api/point/PointV1ControllerTest.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/interfaces/api/user/UserV1ControllerTest.java create mode 100644 docs/1round/1round.md create mode 100644 docs/2round/01-requirements.md create mode 100644 docs/2round/02-sequence-diagrams.md create mode 100644 docs/2round/03-class-diagram.md create mode 100644 docs/2round/04-erd.md create mode 100644 docs/2round/2round.md create mode 100644 docs/3round/3round.md diff --git a/apps/commerce-api/src/main/java/com/loopers/application/example/ExampleFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/example/ExampleFacade.java deleted file mode 100644 index 552a9ad62..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/application/example/ExampleFacade.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.loopers.application.example; - -import com.loopers.domain.example.ExampleModel; -import com.loopers.domain.example.ExampleService; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; - -@RequiredArgsConstructor -@Component -public class ExampleFacade { - private final ExampleService exampleService; - - public ExampleInfo getExample(Long id) { - ExampleModel example = exampleService.getExample(id); - return ExampleInfo.from(example); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/example/ExampleInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/example/ExampleInfo.java deleted file mode 100644 index 877aba96c..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/application/example/ExampleInfo.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.loopers.application.example; - -import com.loopers.domain.example.ExampleModel; - -public record ExampleInfo(Long id, String name, String description) { - public static ExampleInfo from(ExampleModel model) { - return new ExampleInfo( - model.getId(), - model.getName(), - model.getDescription() - ); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/like/LikeFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/like/LikeFacade.java new file mode 100644 index 000000000..d9dd33205 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/like/LikeFacade.java @@ -0,0 +1,32 @@ +package com.loopers.application.like; + +import com.loopers.domain.like.LikeService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +/** + * packageName : com.loopers.application.like + * fileName : LikeFacade + * author : byeonsungmun + * date : 2025. 11. 14. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 14. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +@Component +@RequiredArgsConstructor +public class LikeFacade { + + private final LikeService likeService; + + public void createLike(String userId, Long productId) { + likeService.like(userId, productId); + } + + public void deleteLike(String userId, Long productId) { + likeService.unlike(userId, productId); + } +} + diff --git a/apps/commerce-api/src/main/java/com/loopers/application/order/CreateOrderCommand.java b/apps/commerce-api/src/main/java/com/loopers/application/order/CreateOrderCommand.java new file mode 100644 index 000000000..683e39cdd --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/order/CreateOrderCommand.java @@ -0,0 +1,19 @@ +package com.loopers.application.order; + +import java.util.List; + +/** + * packageName : com.loopers.application.order + * fileName : CreateOrderCommand + * author : byeonsungmun + * date : 2025. 11. 14. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 14. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +public record CreateOrderCommand( + String userId, + List items +) {} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java new file mode 100644 index 000000000..2fba4b4aa --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java @@ -0,0 +1,81 @@ +package com.loopers.application.order; + +import com.loopers.domain.order.Order; +import com.loopers.domain.order.OrderItem; +import com.loopers.domain.order.OrderService; +import com.loopers.domain.order.OrderStatus; +import com.loopers.domain.point.PointService; +import com.loopers.domain.product.Product; +import com.loopers.domain.product.ProductService; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +/** + * packageName : com.loopers.application.order + * fileName : OrderFacade + * author : byeonsungmun + * date : 2025. 11. 13. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 13. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ + +@Slf4j +@Component +@RequiredArgsConstructor +public class OrderFacade { + + private final OrderService orderService; + private final ProductService productService; + private final PointService pointService; + + @Transactional + public OrderInfo createOrder(CreateOrderCommand command) { + + if (command == null || command.items() == null || command.items().isEmpty()) { + throw new CoreException(ErrorType.BAD_REQUEST, "์ƒํ’ˆ ์ •๋ณด๊ฐ€ ๋น„์–ด์žˆ์Šต๋‹ˆ๋‹ค"); + } + + Order order = Order.create(command.userId()); + + for (OrderItemCommand itemCommand : command.items()) { + + //์ƒํ’ˆ๊ฐ€์ ธ์˜ค๊ณ  + Product product = productService.getProduct(itemCommand.productId()); + + // ์žฌ๊ณ ๊ฐ์†Œ + product.decreaseStock(itemCommand.quantity()); + + // OrderItem์ƒ์„ฑ + OrderItem orderItem = OrderItem.create( + product.getId(), + product.getName(), + itemCommand.quantity(), + product.getPrice()); + + order.addOrderItem(orderItem); + orderItem.setOrder(order); + } + + //์ด ๊ฐ€๊ฒฉ๊ตฌํ•˜๊ณ  + long totalAmount = order.getOrderItems().stream() + .mapToLong(OrderItem::getAmount) + .sum(); + + order.updateTotalAmount(totalAmount); + + pointService.usePoint(command.userId(), totalAmount); + + //์ €์žฅ + Order saved = orderService.createOrder(order); + saved.updateStatus(OrderStatus.COMPLETE); + + return OrderInfo.from(saved); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderInfo.java new file mode 100644 index 000000000..70028c27c --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderInfo.java @@ -0,0 +1,42 @@ +package com.loopers.application.order; + +import com.loopers.domain.order.Order; +import com.loopers.domain.order.OrderStatus; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * packageName : com.loopers.application.order + * fileName : OrderInfo + * author : byeonsungmun + * date : 2025. 11. 14. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 14. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +public record OrderInfo( + Long orderId, + String userId, + Long totalAmount, + OrderStatus status, + LocalDateTime createdAt, + List items +) { + public static OrderInfo from(Order order) { + List itemInfos = order.getOrderItems().stream() + .map(OrderItemInfo::from) + .toList(); + + return new OrderInfo( + order.getId(), + order.getUserId(), + order.getTotalAmount(), + order.getStatus(), + order.getCreatedAt(), + itemInfos + ); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderItemCommand.java b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderItemCommand.java new file mode 100644 index 000000000..1ac46862f --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderItemCommand.java @@ -0,0 +1,17 @@ +package com.loopers.application.order; + +/** + * packageName : com.loopers.application.order + * fileName : OrderItemCommand + * author : byeonsungmun + * date : 2025. 11. 14. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 14. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +public record OrderItemCommand( + Long productId, + Long quantity +) {} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderItemInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderItemInfo.java new file mode 100644 index 000000000..b3f2359c6 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderItemInfo.java @@ -0,0 +1,32 @@ +package com.loopers.application.order; + +import com.loopers.domain.order.OrderItem; + +/** + * packageName : com.loopers.application.order + * fileName : OrderInfo + * author : byeonsungmun + * date : 2025. 11. 13. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 13. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +public record OrderItemInfo( + Long productId, + String productName, + Long quantity, + Long price, + Long amount +) { + public static OrderItemInfo from(OrderItem item) { + return new OrderItemInfo( + item.getProductId(), + item.getProductName(), + item.getQuantity(), + item.getPrice(), + item.getAmount() + ); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java new file mode 100644 index 000000000..009be1cec --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java @@ -0,0 +1,28 @@ +package com.loopers.application.point; + +import com.loopers.domain.point.Point; +import com.loopers.domain.point.PointService; +import com.loopers.interfaces.api.point.PointV1Dto; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class PointFacade { + private final PointService pointService; + + public PointInfo getPoint(String userId) { + Point point = pointService.findPointByUserId(userId); + + if (point == null) { + throw new CoreException(ErrorType.NOT_FOUND, "์‚ฌ์šฉ์ž ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); + } + return PointInfo.from(point); + } + + public PointInfo chargePoint(PointV1Dto.ChargePointRequest request) { + return PointInfo.from(pointService.chargePoint(request.userId(), request.chargeAmount())); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/point/PointInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/point/PointInfo.java new file mode 100644 index 000000000..65497297b --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/point/PointInfo.java @@ -0,0 +1,13 @@ +package com.loopers.application.point; + +import com.loopers.domain.point.Point; + +public record PointInfo(String userId, Long amount) { + public static PointInfo from(Point info) { + return new PointInfo( + info.getUserId(), + info.getBalance() + ); + } + +} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/product/ProductDetailInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductDetailInfo.java new file mode 100644 index 000000000..2a9ecee27 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductDetailInfo.java @@ -0,0 +1,32 @@ +package com.loopers.application.product; + +import com.loopers.domain.product.ProductDetail; + +/** + * packageName : com.loopers.application.product + * fileName : ProductDetail + * author : byeonsungmun + * date : 2025. 11. 13. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 13. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +public record ProductDetailInfo( + Long id, + String name, + String brandName, + Long price, + Long likeCount +) { + public static ProductDetailInfo from(ProductDetail productDetail) { + return new ProductDetailInfo( + productDetail.getId(), + productDetail.getName(), + productDetail.getBrandName(), + productDetail.getPrice(), + productDetail.getLikeCount() + ); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java new file mode 100644 index 000000000..e6a25de23 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java @@ -0,0 +1,47 @@ +package com.loopers.application.product; + +import com.loopers.domain.brand.Brand; +import com.loopers.domain.brand.BrandService; +import com.loopers.domain.like.LikeService; +import com.loopers.domain.product.ProductDetail; +import com.loopers.domain.product.ProductDomainService; +import com.loopers.domain.product.ProductService; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Component; + +/** + * packageName : com.loopers.application.product + * fileName : ProdcutFacade + * author : byeonsungmun + * date : 2025. 11. 10. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 10. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +@Component +@RequiredArgsConstructor +public class ProductFacade { + + private final ProductService productService; + private final BrandService brandService; + private final LikeService likeService; + private final ProductDomainService productDomainService; + + public Page getProducts(String sort, Pageable pageable) { + return productService.getProducts(sort ,pageable) + .map(product -> { + Brand brand = brandService.getBrand(product.getBrandId()); + long likeCount = likeService.countByProductId(product.getId()); + return ProductInfo.of(product, brand, likeCount); + }); + } + + public ProductDetailInfo getProduct(Long id) { + ProductDetail productDetail = productDomainService.getProductDetail(id); + return ProductDetailInfo.from(productDetail); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/product/ProductInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductInfo.java new file mode 100644 index 000000000..8bcd93dd8 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductInfo.java @@ -0,0 +1,33 @@ +package com.loopers.application.product; + +import com.loopers.domain.brand.Brand; +import com.loopers.domain.product.Product; + +/** + * packageName : com.loopers.application.product + * fileName : ProductInfo + * author : byeonsungmun + * date : 2025. 11. 10. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 10. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +public record ProductInfo( + Long id, + String name, + String brandName, + Long price, + Long likeCount +) { + public static ProductInfo of(Product product, Brand brand, Long likeCount) { + return new ProductInfo( + product.getId(), + product.getName(), + brand.getName(), + product.getPrice(), + likeCount + ); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java new file mode 100644 index 000000000..f42bd5206 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java @@ -0,0 +1,27 @@ +package com.loopers.application.user; + +import com.loopers.domain.user.User; +import com.loopers.domain.user.UserService; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class UserFacade { + private final UserService userService; + + public UserInfo register(String userId, String email, String birth, String gender) { + User user = userService.register(userId, email, birth, gender); + return UserInfo.from(user); + } + + public UserInfo getUser(String userId) { + User user = userService.findUserByUserId(userId); + if (user == null) { + throw new CoreException(ErrorType.NOT_FOUND, "์‚ฌ์šฉ์ž ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); + } + return UserInfo.from(user); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/user/UserInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/user/UserInfo.java new file mode 100644 index 000000000..08f5cea43 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/user/UserInfo.java @@ -0,0 +1,14 @@ +package com.loopers.application.user; + +import com.loopers.domain.user.User; + +public record UserInfo(String userId, String email, String birth, String gender) { + public static UserInfo from(User user) { + return new UserInfo( + user.getUserId(), + user.getEmail(), + user.getBirth(), + user.getGender() + ); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/brand/Brand.java b/apps/commerce-api/src/main/java/com/loopers/domain/brand/Brand.java new file mode 100644 index 000000000..d334ccebf --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/brand/Brand.java @@ -0,0 +1,47 @@ +package com.loopers.domain.brand; + +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import jakarta.persistence.*; +import lombok.Getter; + +/** + * packageName : com.loopers.domain.brand + * fileName : Brand + * author : byeonsungmun + * date : 2025. 11. 11. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 11. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +@Entity +@Table(name = "brand") +@Getter +public class Brand { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String name; + + protected Brand() {} + + private Brand(String name) { + this.name = requireValidName(name); + } + + public static Brand create(String name) { + return new Brand(name); + } + + + private String requireValidName(String name) { + if (name == null || name.isEmpty()) { + throw new CoreException(ErrorType.BAD_REQUEST, "๋ธŒ๋žœ๋“œ๋ช…์€ ๋น„์–ด ์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + return name.trim(); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandRepository.java new file mode 100644 index 000000000..c558b23fc --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandRepository.java @@ -0,0 +1,20 @@ +package com.loopers.domain.brand; + +import java.util.Optional; + +/** + * packageName : com.loopers.domain.brand + * fileName : BrandRepository + * author : byeonsungmun + * date : 2025. 11. 12. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 12. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +public interface BrandRepository { + Optional findById(Long id); + + void save(Brand brand); +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandService.java b/apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandService.java new file mode 100644 index 000000000..e0f58c77b --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandService.java @@ -0,0 +1,35 @@ +package com.loopers.domain.brand; + +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +/** + * packageName : com.loopers.domain.brand + * fileName : BrandService + * author : byeonsungmun + * date : 2025. 11. 12. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 12. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +@Component +@RequiredArgsConstructor +public class BrandService { + + private final BrandRepository brandRepository; + + public void save(Brand brand) { + brandRepository.save(brand); + } + + @Transactional(readOnly = true) + public Brand getBrand(Long id) { + return brandRepository.findById(id) + .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "๋ธŒ๋žœ๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleModel.java b/apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleModel.java deleted file mode 100644 index c588c4a8a..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleModel.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.loopers.domain.example; - -import com.loopers.domain.BaseEntity; -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import jakarta.persistence.Entity; -import jakarta.persistence.Table; - -@Entity -@Table(name = "example") -public class ExampleModel extends BaseEntity { - - private String name; - private String description; - - protected ExampleModel() {} - - public ExampleModel(String name, String description) { - if (name == null || name.isBlank()) { - throw new CoreException(ErrorType.BAD_REQUEST, "์ด๋ฆ„์€ ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); - } - if (description == null || description.isBlank()) { - throw new CoreException(ErrorType.BAD_REQUEST, "์„ค๋ช…์€ ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); - } - - this.name = name; - this.description = description; - } - - public String getName() { - return name; - } - - public String getDescription() { - return description; - } - - public void update(String newDescription) { - if (newDescription == null || newDescription.isBlank()) { - throw new CoreException(ErrorType.BAD_REQUEST, "์„ค๋ช…์€ ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); - } - this.description = newDescription; - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleRepository.java deleted file mode 100644 index 3625e5662..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.loopers.domain.example; - -import java.util.Optional; - -public interface ExampleRepository { - Optional find(Long id); -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleService.java b/apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleService.java deleted file mode 100644 index c0e8431e8..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleService.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.loopers.domain.example; - -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; - -@RequiredArgsConstructor -@Component -public class ExampleService { - - private final ExampleRepository exampleRepository; - - @Transactional(readOnly = true) - public ExampleModel getExample(Long id) { - return exampleRepository.find(id) - .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "[id = " + id + "] ์˜ˆ์‹œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/like/Like.java b/apps/commerce-api/src/main/java/com/loopers/domain/like/Like.java new file mode 100644 index 000000000..4430b496a --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/like/Like.java @@ -0,0 +1,63 @@ +package com.loopers.domain.like; + +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import jakarta.persistence.*; +import lombok.Getter; + +import java.time.LocalDateTime; + +/** + * packageName : com.loopers.domain.like + * fileName : Like + * author : byeonsungmun + * date : 2025. 11. 11. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 11. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +@Entity +@Table(name = "product_like") +@Getter +public class Like { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "ref_user_id", nullable = false) + private String userId; + + @Column(name = "ref_product_id", nullable = false) + private Long productId; + + @Column(nullable = false) + private LocalDateTime createdAt; + + protected Like() {} + + private Like(String userId, Long productId) { + this.userId = requireValidUserId(userId); + this.productId = requireValidProductId(productId); + this.createdAt = LocalDateTime.now(); + } + + public static Like create(String userId, Long productId) { + return new Like(userId, productId); + } + + private String requireValidUserId(String userId) { + if (userId == null || userId.isEmpty()) { + throw new CoreException(ErrorType.BAD_REQUEST, "์‚ฌ์šฉ์ž ID๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."); + } + return userId; + } + + private Long requireValidProductId(Long productId) { + if (productId == null || productId <= 0) { + throw new CoreException(ErrorType.BAD_REQUEST, "์ƒํ’ˆ ID๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."); + } + return productId; + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/like/LikeRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/like/LikeRepository.java new file mode 100644 index 000000000..945b10235 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/like/LikeRepository.java @@ -0,0 +1,25 @@ +package com.loopers.domain.like; + +import java.util.Optional; + +/** + * packageName : com.loopers.domain.like + * fileName : LikeRepository + * author : byeonsungmun + * date : 2025. 11. 12. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 12. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +public interface LikeRepository { + + Optional findByUserIdAndProductId(String userId, Long productId); + + void save(Like like); + + void delete(Like like); + + long countByProductId(Long productId); +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/like/LikeService.java b/apps/commerce-api/src/main/java/com/loopers/domain/like/LikeService.java new file mode 100644 index 000000000..41ae90b6a --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/like/LikeService.java @@ -0,0 +1,49 @@ +package com.loopers.domain.like; + +import com.loopers.domain.product.ProductRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +/** + * packageName : com.loopers.application.like + * fileName : LikeService + * author : byeonsungmun + * date : 2025. 11. 12. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 12. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +@Component +@RequiredArgsConstructor +public class LikeService { + + private final LikeRepository likeRepository; + private final ProductRepository productRepository; + + @Transactional + public void like(String userId, Long productId) { + if (likeRepository.findByUserIdAndProductId(userId, productId).isPresent()) return; + + Like like = Like.create(userId, productId); + likeRepository.save(like); + productRepository.incrementLikeCount(productId); + } + + @Transactional + public void unlike(String userId, Long productId) { + likeRepository.findByUserIdAndProductId(userId, productId) + .ifPresent(like -> { + likeRepository.delete(like); + productRepository.decrementLikeCount(productId); + }); + } + + @Transactional(readOnly = true) + public long countByProductId(Long productId) { + return likeRepository.countByProductId(productId); + } + +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/Order.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/Order.java new file mode 100644 index 000000000..84f299c6b --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/order/Order.java @@ -0,0 +1,86 @@ +package com.loopers.domain.order; + +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import jakarta.persistence.*; +import lombok.Getter; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +/** + * packageName : com.loopers.domain.order + * fileName : Order + * author : byeonsungmun + * date : 2025. 11. 11. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 11. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +@Entity +@Table(name = "orders") +@Getter +public class Order { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "ref_user_id", nullable = false) + private String userId; + + @Column(nullable = false) + private Long totalAmount; + + @Enumerated(EnumType.STRING) + private OrderStatus status; + + @Column(nullable = false) + private LocalDateTime createdAt; + + @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true) + private List orderItems = new ArrayList<>(); + + protected Order() {} + + private Order(String userId, OrderStatus status) { + this.userId = requiredValidUserId(userId); + this.totalAmount = 0L; + this.status = requiredValidStatus(status); + this.createdAt = LocalDateTime.now(); + } + + public static Order create(String userId) { + return new Order(userId, OrderStatus.PENDING); + } + + public void addOrderItem(OrderItem orderItem) { + orderItem.setOrder(this); + this.orderItems.add(orderItem); + } + + private OrderStatus requiredValidStatus(OrderStatus status) { + if (status == null) { + throw new CoreException(ErrorType.BAD_REQUEST, "์ฃผ๋ฌธ ์ƒํƒœ๋Š” ํ•„์ˆ˜ ์ž…๋‹ˆ๋‹ค."); + } + return status; + } + + private String requiredValidUserId(String userId) { + if (userId == null || userId.isEmpty()) { + throw new CoreException(ErrorType.BAD_REQUEST, "์‚ฌ์šฉ์ž ID๋Š” ํ•„์ˆ˜ ์ž…๋‹ˆ๋‹ค."); + } + return userId; + } + + public void updateTotalAmount(long totalAmount) { + this.totalAmount = totalAmount; + } + + public void updateStatus(OrderStatus status) { + this.status = status; + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItem.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItem.java new file mode 100644 index 000000000..dce97a44a --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItem.java @@ -0,0 +1,91 @@ +package com.loopers.domain.order; + +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + +/** + * packageName : com.loopers.domain.order + * fileName : OrderItem + * author : byeonsungmun + * date : 2025. 11. 11. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 11. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ + +@Entity +@Table(name = "order_item") +@Getter +public class OrderItem { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Setter + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "order_id", nullable = false) + private Order order; + + @Column(name = "ref_product_id", nullable = false) + private Long productId; + + @Column(name = "ref_product_name", nullable = false) + private String productName; + + @Column(nullable = false) + private Long quantity; + + @Column(nullable = false) + private Long price; + + protected OrderItem() {} + + private OrderItem(Long productId, String productName, Long quantity, Long price) { + this.productId = requiredValidProductId(productId); + this.productName = requiredValidProductName(productName); + this.quantity = requiredQuantity(quantity); + this.price = requiredPrice(price); + } + + public static OrderItem create(Long productId, String productName, Long quantity, Long price) { + return new OrderItem(productId, productName, quantity, price); + } + + public Long getAmount() { + return quantity * price; + } + + private Long requiredValidProductId(Long productId) { + if (productId == null || productId <= 0) { + throw new CoreException(ErrorType.BAD_REQUEST, "์ƒํ’ˆ ID๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."); + } + return productId; + } + + private String requiredValidProductName(String productName) { + if (productName == null || productName.isEmpty()) { + throw new CoreException(ErrorType.BAD_REQUEST, "์ƒํ’ˆ๋ช…์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."); + } + return productName; + } + + private Long requiredQuantity(Long quantity) { + if (quantity == null || quantity <= 0) { + throw new CoreException(ErrorType.BAD_REQUEST, "์ˆ˜๋Ÿ‰์€ 1๊ฐœ ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + } + return quantity; + } + + private Long requiredPrice(Long price) { + if (price == null || price < 0) { + throw new CoreException(ErrorType.BAD_REQUEST, "๊ฐ€๊ฒฉ์€ 0์› ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + } + return price; + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderRepository.java new file mode 100644 index 000000000..c80262041 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderRepository.java @@ -0,0 +1,21 @@ +package com.loopers.domain.order; + +import java.util.Optional; + +/** + * packageName : com.loopers.domain.order + * fileName : OrderRepository + * author : byeonsungmun + * date : 2025. 11. 13. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 13. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +public interface OrderRepository { + + Order save(Order order); + + Optional findById(Long orderId); +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java new file mode 100644 index 000000000..a66be03d3 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java @@ -0,0 +1,28 @@ +package com.loopers.domain.order; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +/** + * packageName : com.loopers.domain.order + * fileName : OrderService + * author : byeonsungmun + * date : 2025. 11. 13. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 13. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +@Component +@RequiredArgsConstructor +public class OrderService { + + private final OrderRepository orderRepository; + + @Transactional + public Order createOrder(Order order) { + return orderRepository.save(order); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderStatus.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderStatus.java new file mode 100644 index 000000000..14ea592ef --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderStatus.java @@ -0,0 +1,42 @@ +package com.loopers.domain.order; + +/** + * packageName : com.loopers.domain.order + * fileName : OrderStatus + * author : byeonsungmun + * date : 2025. 11. 11. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 11. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +public enum OrderStatus { + + COMPLETE("๊ฒฐ์ œ์„ฑ๊ณต"), + CANCEL("๊ฒฐ์ œ์ทจ์†Œ"), + FAIL("๊ฒฐ์ œ์‹คํŒจ"), + PENDING("๊ฒฐ์ œ์ค‘"); + + private final String description; + + OrderStatus(String description) { + this.description = description; + } + + public boolean isCompleted() { + return this == COMPLETE; + } + + public boolean isPending() { + return this == PENDING; + } + + public boolean isCanceled() { + return this == CANCEL; + } + + public String description() { + return description; + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java b/apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java new file mode 100644 index 000000000..bc28a902a --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java @@ -0,0 +1,56 @@ +package com.loopers.domain.point; + +import com.loopers.domain.BaseEntity; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import jakarta.persistence.*; +import lombok.Getter; + +@Entity +@Table(name = "point") +@Getter +public class Point extends BaseEntity { + + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Id + private Long id; + + private String userId; + + private Long balance; + + protected Point() {} + + private Point(String userId, Long balance) { + this.userId = requireValidUserId(userId); + this.balance = balance; + } + + public static Point create(String userId, Long balance) { + return new Point(userId, balance); + } + + String requireValidUserId(String userId) { + if(userId == null || userId.isEmpty()) { + throw new CoreException(ErrorType.BAD_REQUEST, "์‚ฌ์šฉ์ž ID๊ฐ€ ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + return userId; + } + + public void charge(Long chargeAmount) { + if (chargeAmount == null || chargeAmount <= 0) { + throw new CoreException(ErrorType.BAD_REQUEST, "0์› ์ดํ•˜๋กœ ํฌ์ธํŠธ๋ฅผ ์ถฉ์ „ ํ• ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + this.balance += chargeAmount; + } + + public void use(Long useAmount) { + if (useAmount == null || useAmount <= 0) { + throw new CoreException(ErrorType.BAD_REQUEST, "0์› ์ดํ•˜๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + if (this.balance < useAmount) { + throw new CoreException(ErrorType.BAD_REQUEST, "ํฌ์ธํŠธ๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค."); + } + this.balance -= useAmount; + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/point/PointRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/point/PointRepository.java new file mode 100644 index 000000000..314022491 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/point/PointRepository.java @@ -0,0 +1,10 @@ +package com.loopers.domain.point; + +import java.util.Optional; + +public interface PointRepository { + + Optional findByUserId(String userId); + + Point save(Point point); +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java b/apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java new file mode 100644 index 000000000..9c9570615 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java @@ -0,0 +1,43 @@ +package com.loopers.domain.point; + +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Component +public class PointService { + + private final PointRepository pointRepository; + + @Transactional(readOnly = true) + public Point findPointByUserId(String userId) { + return pointRepository.findByUserId(userId).orElse(null); + } + + @Transactional + public Point chargePoint(String userId, Long chargeAmount) { + Point point = pointRepository.findByUserId(userId).orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "ํฌ์ธํŠธ๋ฅผ ์ถฉ์ „ํ• ์ˆ˜ ์—†๋Š” ์‚ฌ์šฉ์ž์ž…๋‹ˆ๋‹ค.")); + point.charge(chargeAmount); + return pointRepository.save(point); + } + + @Transactional + public Point usePoint(String userId, Long useAmount) { + Point point = pointRepository.findByUserId(userId) + .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "ํฌ์ธํŠธ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); + + if (useAmount == null || useAmount <= 0) { + throw new CoreException(ErrorType.BAD_REQUEST, "์ฐจ๊ฐํ•  ํฌ์ธํŠธ๋Š” 1 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + } + + if (point.getBalance() < useAmount) { + throw new CoreException(ErrorType.BAD_REQUEST, "ํฌ์ธํŠธ๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค."); + } + + point.use(useAmount); + return pointRepository.save(point); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java new file mode 100644 index 000000000..29968402f --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java @@ -0,0 +1,120 @@ +package com.loopers.domain.product; + +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import jakarta.persistence.*; +import lombok.Getter; + +/** + * packageName : com.loopers.domain.product + * fileName : Product + * author : byeonsungmun + * date : 2025. 11. 10. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 10. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ + +@Entity +@Table(name = "product") +@Getter +public class Product { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "ref_brand_id", nullable = false) + private Long brandId; + + @Column(nullable = false) + private String name; + + @Column(nullable = false) + private Long price; + + @Column + private Long likeCount; + + @Column(nullable = false) + private Long stock; + + protected Product() {} + + private Product(Long brandId, String name, Long price, Long likeCount, Long stock) { + this.brandId = requireValidBrandId(brandId); + this.name = requireValidName(name); + this.price = requireValidPrice(price); + this.likeCount = requireValidLikeCount(likeCount); + this.stock = requireValidStock(stock); + } + + public static Product create(Long brandId, String name, Long price, Long stock) { + return new Product( + brandId, + name, + price, + 0L, + stock + ); + } + + private Long requireValidBrandId(Long brandId) { + if (brandId == null || brandId <= 0) { + throw new CoreException(ErrorType.BAD_REQUEST, "๋ธŒ๋žœ๋“œ ID๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."); + } + + return brandId; + } + + private String requireValidName(String name) { + if (name == null || name.isEmpty()) { + throw new CoreException(ErrorType.BAD_REQUEST, "์ƒํ’ˆ๋ช…์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."); + } + return name; + } + + private Long requireValidPrice(Long price) { + if (price == null || price < 0) { + throw new CoreException(ErrorType.BAD_REQUEST, "์ƒํ’ˆ ๊ฐ€๊ฒฉ์€ 0์› ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + } + return price; + } + + private Long requireValidLikeCount(Long likeCount) { + if (likeCount == null || likeCount < 0) { + throw new CoreException(ErrorType.BAD_REQUEST, "์ข‹์•„์š” ๊ฐœ์ˆ˜๋Š” 0๊ฐœ ๋ฏธ๋งŒ์œผ๋กœ ์„ค์ •ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + return likeCount; + } + + private Long requireValidStock(Long stock) { + if (stock == null) { + throw new CoreException(ErrorType.BAD_REQUEST, "์ƒํ’ˆ ์žฌ๊ณ ๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."); + } + if (stock < 0) { + throw new CoreException(ErrorType.BAD_REQUEST, "์ƒํ’ˆ ์žฌ๊ณ ๋Š” 0 ๋ฏธ๋งŒ์œผ๋กœ ์„ค์ •ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + return stock; + } + + public void increaseLikeCount() { + this.likeCount++; + } + + public void decreaseLikeCount() { + if (this.likeCount > 0) this.likeCount--; + } + + public void decreaseStock(Long quantity) { + if (quantity <= 0) { + throw new CoreException(ErrorType.BAD_REQUEST, "์ฐจ๊ฐ ์ˆ˜๋Ÿ‰์€ 0๋ณด๋‹ค ์ปค์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + } + if (this.stock - quantity < 0) { + throw new CoreException(ErrorType.BAD_REQUEST, "์žฌ๊ณ ๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค."); + } + this.stock -= quantity; + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductDetail.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductDetail.java new file mode 100644 index 000000000..808bff196 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductDetail.java @@ -0,0 +1,45 @@ +package com.loopers.domain.product; + +import com.loopers.domain.brand.Brand; +import lombok.Getter; + +/** + * packageName : com.loopers.domain.product + * fileName : ProductDetail + * author : byeonsungmun + * date : 2025. 11. 13. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 13. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +@Getter +public class ProductDetail { + + private Long id; + private String name; + private String brandName; + private Long price; + private Long likeCount; + + protected ProductDetail() {} + + private ProductDetail(Long id, String name, String brandName, Long price, Long likeCount) { + this.id = id; + this.name = name; + this.brandName = brandName; + this.price = price; + this.likeCount = likeCount; + } + + public static ProductDetail of(Product product, Brand brand, Long likeCount) { + return new ProductDetail( + product.getId(), + product.getName(), + brand.getName(), + product.getPrice(), + likeCount + ); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductDomainService.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductDomainService.java new file mode 100644 index 000000000..166aff66b --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductDomainService.java @@ -0,0 +1,41 @@ +package com.loopers.domain.product; + +import com.loopers.domain.brand.Brand; +import com.loopers.domain.brand.BrandRepository; +import com.loopers.domain.like.LikeRepository; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +/** + * packageName : com.loopers.domain.product + * fileName : ProductDetailService + * author : byeonsungmun + * date : 2025. 11. 13. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 13. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +@Component +@RequiredArgsConstructor +public class ProductDomainService { + + private final ProductRepository productRepository; + private final BrandRepository brandRepository; + private final LikeRepository likeRepository; + + @Transactional(readOnly = true) + public ProductDetail getProductDetail(Long id) { + Product product = productRepository.findById(id) + .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "์ƒํ’ˆ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); + Brand brand = brandRepository.findById(product.getBrandId()) + .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "๋ธŒ๋žœ๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค")); + long likeCount = likeRepository.countByProductId(id); + + return ProductDetail.of(product, brand, likeCount); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductRepository.java new file mode 100644 index 000000000..dadda62a0 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductRepository.java @@ -0,0 +1,29 @@ +package com.loopers.domain.product; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.Optional; + +/** + * packageName : com.loopers.domain.product + * fileName : ProductRepositroy + * author : byeonsungmun + * date : 2025. 11. 12. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 12. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +public interface ProductRepository { + Page findAll(Pageable pageable); + + Optional findById(Long id); + + void incrementLikeCount(Long productId); + + void decrementLikeCount(Long productId); + + Product save(Product product); +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java new file mode 100644 index 000000000..067f194ae --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java @@ -0,0 +1,53 @@ +package com.loopers.domain.product; + +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +/** + * packageName : com.loopers.domain.product + * fileName : ProductService + * author : byeonsungmun + * date : 2025. 11. 12. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 12. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ + +@Component +@RequiredArgsConstructor +public class ProductService { + + private final ProductRepository productRepository; + + @Transactional(readOnly = true) + public Page getProducts(String sort, Pageable pageable) { + Sort sortOption = switch (sort) { + case "price_asc" -> Sort.by("price").ascending(); + case "likes_desc" -> Sort.by("likeCount").descending(); + default -> Sort.by("createdAt").descending(); // latest + }; + + Pageable sortedPageable = PageRequest.of( + pageable.getPageNumber(), + pageable.getPageSize(), + sortOption + ); + + return productRepository.findAll(sortedPageable); + } + + public Product getProduct(Long productId) { + return productRepository.findById(productId) + .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "ํ•ด๋‹น ์ƒํ’ˆ์ด ์—†์Šต๋‹ˆ๋‹ค")); + } +} + diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java new file mode 100644 index 000000000..287b84cf8 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java @@ -0,0 +1,82 @@ +package com.loopers.domain.user; + +import com.loopers.domain.BaseEntity; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import lombok.Getter; + +import java.util.regex.Pattern; + +@Entity +@Table(name = "user") +@Getter +public class User extends BaseEntity { + + private static final Pattern USERID_PATTERN = Pattern.compile("^[a-zA-Z0-9]{1,10}$"); + private static final Pattern BIRTH_PATTERN = Pattern.compile("^\\d{4}-\\d{2}-\\d{2}$"); + private static final Pattern EMAIL_PATTERN = Pattern.compile("^[\\w.+-]+@[\\w.-]+\\.[a-zA-Z]{2,}$"); + + @Column(unique = true, nullable = false) + private String userId; + + @Column(nullable = false) + private String email; + + @Column(nullable = false) + private String birth; + + @Column(nullable = false) + private String gender; + + protected User() {} + + public User(String userId, String email, String birth, String gender) { + this.userId = requireValidUserId(userId); + this.email = requireValidEmail(email); + this.birth = requireValidBirthDate(birth); + this.gender = requireValidGender(gender); + } + + String requireValidUserId(String userId) { + if(userId == null || userId.isEmpty()) { + throw new CoreException(ErrorType.BAD_REQUEST, "์‚ฌ์šฉ์ž ID๊ฐ€ ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + + if (!USERID_PATTERN.matcher(userId).matches()) { + throw new CoreException(ErrorType.BAD_REQUEST, "์‚ฌ์šฉ์ž ID๊ฐ€ ์˜๋ฌธ ๋ฐ ์ˆซ์ž 10์ž ์ด๋‚ด์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + } + return userId; + } + + String requireValidEmail(String email) { + if(email == null || email.isBlank()) { + throw new CoreException(ErrorType.BAD_REQUEST, "์ด๋ฉ”์ผ์ด ๋น„์–ด ์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + + if(!EMAIL_PATTERN.matcher(email).matches()) { + throw new CoreException(ErrorType.BAD_REQUEST, "์ด๋ฉ”์ผ ํ˜•์‹์— ๋งž์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ex)xx@yy.zz"); + } + return email; + } + + String requireValidBirthDate(String birth) { + if (birth == null || birth.isBlank()) { + throw new CoreException(ErrorType.BAD_REQUEST, "์ƒ๋…„์›”์ผ์ด ๋น„์–ด ์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + + if(!BIRTH_PATTERN.matcher(birth).matches()) { + throw new CoreException(ErrorType.BAD_REQUEST, "์ƒ๋…„์›”์ผ ํ˜•์‹์ด ๋งž์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); + } + return birth; + } + + String requireValidGender(String gender) { + if(gender == null || gender.isBlank()) { + throw new CoreException(ErrorType.BAD_REQUEST, "์„ฑ๋ณ„์ด ๋น„์–ด์žˆ์Šต๋‹ˆ๋‹ค."); + } + return gender; + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java new file mode 100644 index 000000000..f4b26266e --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java @@ -0,0 +1,10 @@ +package com.loopers.domain.user; + +import java.util.Optional; + +public interface UserRepository { + + Optional findByUserId(String userId); + + User save(User user); +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java new file mode 100644 index 000000000..3cc033076 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java @@ -0,0 +1,30 @@ +package com.loopers.domain.user; + +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Component +@RequiredArgsConstructor +public class UserService { + + private final UserRepository userRepository; + + @Transactional + public User register(String userId, String email, String birth, String gender) { + userRepository.findByUserId(userId).ifPresent(user -> { + throw new CoreException(ErrorType.CONFLICT, "์ด๋ฏธ ๊ฐ€์ž…๋œ ์‚ฌ์šฉ์ž ID ์ž…๋‹ˆ๋‹ค."); + }); + + User user = new User(userId, email, birth, gender); + return userRepository.save(user); + } + + @Transactional(readOnly = true) + public User findUserByUserId(String userId) { + return userRepository.findByUserId(userId).orElse(null); + } + +} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandJpaRepository.java new file mode 100644 index 000000000..759f3caf1 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandJpaRepository.java @@ -0,0 +1,18 @@ +package com.loopers.infrastructure.brand; + +import com.loopers.domain.brand.Brand; +import org.springframework.data.jpa.repository.JpaRepository; + +/** + * packageName : com.loopers.infrastructure.brand + * fileName : BrandJpaRepository + * author : byeonsungmun + * date : 2025. 11. 12. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 12. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +public interface BrandJpaRepository extends JpaRepository { +} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandRepositoryImpl.java new file mode 100644 index 000000000..f23e6e5d9 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandRepositoryImpl.java @@ -0,0 +1,36 @@ +package com.loopers.infrastructure.brand; + +import com.loopers.domain.brand.Brand; +import com.loopers.domain.brand.BrandRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +/** + * packageName : com.loopers.infrastructure.brand + * fileName : BrandRepositroyImpl + * author : byeonsungmun + * date : 2025. 11. 12. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 12. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +@RequiredArgsConstructor +@Component +public class BrandRepositoryImpl implements BrandRepository { + + private final BrandJpaRepository jpaRepository; + + @Override + public Optional findById(Long id) { + return jpaRepository.findById(id); + } + + @Override + public void save(Brand brand) { + jpaRepository.save(brand); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/example/ExampleJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/example/ExampleJpaRepository.java deleted file mode 100644 index ce6d3ead0..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/example/ExampleJpaRepository.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.loopers.infrastructure.example; - -import com.loopers.domain.example.ExampleModel; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface ExampleJpaRepository extends JpaRepository {} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/example/ExampleRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/example/ExampleRepositoryImpl.java deleted file mode 100644 index 37f2272f0..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/example/ExampleRepositoryImpl.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.loopers.infrastructure.example; - -import com.loopers.domain.example.ExampleModel; -import com.loopers.domain.example.ExampleRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; - -import java.util.Optional; - -@RequiredArgsConstructor -@Component -public class ExampleRepositoryImpl implements ExampleRepository { - private final ExampleJpaRepository exampleJpaRepository; - - @Override - public Optional find(Long id) { - return exampleJpaRepository.findById(id); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeJpaRepository.java new file mode 100644 index 000000000..865a30db7 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeJpaRepository.java @@ -0,0 +1,23 @@ +package com.loopers.infrastructure.like; + +import com.loopers.domain.like.Like; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +/** + * packageName : com.loopers.infrastructure.like + * fileName : LikeJpaRepository + * author : byeonsungmun + * date : 2025. 11. 12. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 12. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +public interface LikeJpaRepository extends JpaRepository { + Optional findByUserIdAndProductId(String userId, Long productId); + + long countByProductId(Long productId); +} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeRepositoryImpl.java new file mode 100644 index 000000000..e037b6efb --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeRepositoryImpl.java @@ -0,0 +1,46 @@ +package com.loopers.infrastructure.like; + +import com.loopers.domain.like.Like; +import com.loopers.domain.like.LikeRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +/** + * packageName : com.loopers.infrastructure.like + * fileName : LikeRepositoryImpl + * author : byeonsungmun + * date : 2025. 11. 12. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 12. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +@Component +@RequiredArgsConstructor +public class LikeRepositoryImpl implements LikeRepository { + + private final LikeJpaRepository likeJpaRepository; + + @Override + public Optional findByUserIdAndProductId(String userId, Long productId) { + return likeJpaRepository.findByUserIdAndProductId(userId, productId); + } + + @Override + public void save(Like like) { + likeJpaRepository.save(like); + } + + @Override + public void delete(Like like) { + likeJpaRepository.delete(like); + } + + @Override + public long countByProductId(Long productId) { + return likeJpaRepository.countByProductId(productId); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderJpaRepository.java new file mode 100644 index 000000000..39cfb136d --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderJpaRepository.java @@ -0,0 +1,18 @@ +package com.loopers.infrastructure.order; + +import com.loopers.domain.order.Order; +import org.springframework.data.jpa.repository.JpaRepository; + +/** + * packageName : com.loopers.infrastructure.order + * fileName : OrderJpaRepository + * author : byeonsungmun + * date : 2025. 11. 14. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 14. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +public interface OrderJpaRepository extends JpaRepository { +} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderRepositoryImpl.java new file mode 100644 index 000000000..f8c7b5b68 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderRepositoryImpl.java @@ -0,0 +1,36 @@ +package com.loopers.infrastructure.order; + +import com.loopers.domain.order.Order; +import com.loopers.domain.order.OrderRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +/** + * packageName : com.loopers.infrastructure.order + * fileName : OrderRepositroyImpl + * author : byeonsungmun + * date : 2025. 11. 14. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 14. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +@Component +@RequiredArgsConstructor +public class OrderRepositoryImpl implements OrderRepository { + + private final OrderJpaRepository orderJpaRepository; + + @Override + public Order save(Order order) { + return orderJpaRepository.save(order); + } + + @Override + public Optional findById(Long orderId) { + return orderJpaRepository.findById(orderId); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointJpaRepository.java new file mode 100644 index 000000000..a35a56151 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointJpaRepository.java @@ -0,0 +1,11 @@ +package com.loopers.infrastructure.point; + +import com.loopers.domain.point.Point; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface PointJpaRepository extends JpaRepository { + + Optional findByUserId(String userId); +} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointRepositoryImpl.java new file mode 100644 index 000000000..530191b66 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointRepositoryImpl.java @@ -0,0 +1,25 @@ +package com.loopers.infrastructure.point; + +import com.loopers.domain.point.Point; +import com.loopers.domain.point.PointRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +@RequiredArgsConstructor +@Component +public class PointRepositoryImpl implements PointRepository { + + private final PointJpaRepository pointJpaRepository; + + @Override + public Optional findByUserId(String userId) { + return pointJpaRepository.findByUserId(userId); + } + + @Override + public Point save(Point point) { + return pointJpaRepository.save(point); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.java new file mode 100644 index 000000000..5ceaae067 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.java @@ -0,0 +1,19 @@ +package com.loopers.infrastructure.product; + +import com.loopers.domain.product.Product; +import org.springframework.data.jpa.repository.JpaRepository; + +/** + * packageName : com.loopers.infrastructure.product + * fileName : ProductJpaRepository + * author : byeonsungmun + * date : 2025. 11. 13. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 13. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +public interface ProductJpaRepository extends JpaRepository { + +} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java new file mode 100644 index 000000000..dbad0d9d5 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java @@ -0,0 +1,59 @@ +package com.loopers.infrastructure.product; + +import com.loopers.domain.product.Product; +import com.loopers.domain.product.ProductRepository; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +/** + * packageName : com.loopers.infrastructure.product + * fileName : ProductRepositoryImpl + * author : byeonsungmun + * date : 2025. 11. 13. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 13. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +@Component +@RequiredArgsConstructor +public class ProductRepositoryImpl implements ProductRepository { + + private final ProductJpaRepository productJpaRepository; + + @Override + public Page findAll(Pageable pageable) { + return productJpaRepository.findAll(pageable); + } + + @Override + public Optional findById(Long id) { + return productJpaRepository.findById(id); + } + + @Override + public void incrementLikeCount(Long productId) { + Product product = productJpaRepository.findById(productId) + .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "์ƒํ’ˆ์„ ์ฐพ์„์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); + product.increaseLikeCount(); + } + + @Override + public void decrementLikeCount(Long productId) { + Product product = productJpaRepository.findById(productId) + .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "์ƒํ’ˆ์„ ์ฐพ์„์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); + product.decreaseLikeCount(); + } + + @Override + public Product save(Product product) { + return productJpaRepository.save(product); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserJpaRepository.java new file mode 100644 index 000000000..f80a5bc52 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserJpaRepository.java @@ -0,0 +1,11 @@ +package com.loopers.infrastructure.user; + +import com.loopers.domain.user.User; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface UserJpaRepository extends JpaRepository { + + Optional findByUserId(String userId); +} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java new file mode 100644 index 000000000..8fb6f7bdf --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java @@ -0,0 +1,26 @@ +package com.loopers.infrastructure.user; + +import com.loopers.domain.user.User; +import com.loopers.domain.user.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +@RequiredArgsConstructor +@Component +public class UserRepositoryImpl implements UserRepository { + + private final UserJpaRepository userJpaRepository; + + @Override + public Optional findByUserId(String userId) { + return userJpaRepository.findByUserId(userId); + } + + @Override + public User save(User user) { + return userJpaRepository.save(user); + } +} + diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1ApiSpec.java deleted file mode 100644 index 219e3101e..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1ApiSpec.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.loopers.interfaces.api.example; - -import com.loopers.interfaces.api.ApiResponse; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.tags.Tag; - -@Tag(name = "Example V1 API", description = "Loopers ์˜ˆ์‹œ API ์ž…๋‹ˆ๋‹ค.") -public interface ExampleV1ApiSpec { - - @Operation( - summary = "์˜ˆ์‹œ ์กฐํšŒ", - description = "ID๋กœ ์˜ˆ์‹œ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค." - ) - ApiResponse getExample( - @Schema(name = "์˜ˆ์‹œ ID", description = "์กฐํšŒํ•  ์˜ˆ์‹œ์˜ ID") - Long exampleId - ); -} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1Controller.java deleted file mode 100644 index 917376016..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1Controller.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.loopers.interfaces.api.example; - -import com.loopers.application.example.ExampleFacade; -import com.loopers.application.example.ExampleInfo; -import com.loopers.interfaces.api.ApiResponse; -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; - -@RequiredArgsConstructor -@RestController -@RequestMapping("/api/v1/examples") -public class ExampleV1Controller implements ExampleV1ApiSpec { - - private final ExampleFacade exampleFacade; - - @GetMapping("/{exampleId}") - @Override - public ApiResponse getExample( - @PathVariable(value = "exampleId") Long exampleId - ) { - ExampleInfo info = exampleFacade.getExample(exampleId); - ExampleV1Dto.ExampleResponse response = ExampleV1Dto.ExampleResponse.from(info); - return ApiResponse.success(response); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1Dto.java deleted file mode 100644 index 4ecf0eea5..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1Dto.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.loopers.interfaces.api.example; - -import com.loopers.application.example.ExampleInfo; - -public class ExampleV1Dto { - public record ExampleResponse(Long id, String name, String description) { - public static ExampleResponse from(ExampleInfo info) { - return new ExampleResponse( - info.id(), - info.name(), - info.description() - ); - } - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1ApiSpec.java new file mode 100644 index 000000000..6f0458399 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1ApiSpec.java @@ -0,0 +1,28 @@ +package com.loopers.interfaces.api.point; + +import com.loopers.interfaces.api.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.tags.Tag; + +@Tag(name = "Point V1 API", description = "Point API ์ž…๋‹ˆ๋‹ค.") +public interface PointV1ApiSpec { + + @Operation( + summary = "ํฌ์ธํŠธ ํšŒ์› ์กฐํšŒ", + description = "ํšŒ์›์˜ ํฌ์ธํŠธ๋ฅผ ์กฐํšŒํ•œ๋‹ค." + ) + ApiResponse getPoint( + @Schema(name = "ํšŒ์› Id", description = "์กฐํšŒํ•  ํšŒ์› ID") + String userId + ); + + @Operation( + summary = "ํฌ์ธํŠธ ์ถฉ์ „", + description = "ํšŒ์›์˜ ํฌ์ธํŠธ๋ฅผ ์ถฉ์ „ํ•œ๋‹ค." + ) + ApiResponse chargePoint( + @Schema(name = "ํฌ์ธํŠธ ์ถฉ์ „ ์š”์ฒญ", description = "์ถฉ์ „ํ•  ํฌ์ธํŠธ ์ •๋ณด๋ฅผ ํฌํ•จํ•œ ์š”์ฒญ") + PointV1Dto.ChargePointRequest request + ); +} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Controller.java new file mode 100644 index 000000000..866fce9b3 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Controller.java @@ -0,0 +1,31 @@ +package com.loopers.interfaces.api.point; + +import com.loopers.application.point.PointFacade; +import com.loopers.application.point.PointInfo; +import com.loopers.interfaces.api.ApiResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/v1/points") +public class PointV1Controller implements PointV1ApiSpec { + + private final PointFacade pointFacade; + + @Override + @GetMapping + public ApiResponse getPoint(@RequestHeader("X-USER-ID") String userId) { + PointInfo pointInfo = pointFacade.getPoint(userId); + PointV1Dto.PointResponse response = PointV1Dto.PointResponse.from(pointInfo); + return ApiResponse.success(response); + } + + @Override + @PatchMapping("/charge") + public ApiResponse chargePoint(@RequestBody PointV1Dto.ChargePointRequest request) { + PointInfo pointInfo = pointFacade.chargePoint(request); + PointV1Dto.PointResponse response = PointV1Dto.PointResponse.from(pointInfo); + return ApiResponse.success(response); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Dto.java new file mode 100644 index 000000000..b0b3d050e --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Dto.java @@ -0,0 +1,18 @@ +package com.loopers.interfaces.api.point; + +import com.loopers.application.point.PointInfo; + +public class PointV1Dto { + + public record ChargePointRequest(String userId, Long chargeAmount) { + } + + public record PointResponse(String userId, Long amount) { + public static PointResponse from(PointInfo info) { + return new PointResponse( + info.userId(), + info.amount() + ); + } + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1ApiSpec.java new file mode 100644 index 000000000..1bed68e62 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1ApiSpec.java @@ -0,0 +1,28 @@ +package com.loopers.interfaces.api.user; + +import com.loopers.interfaces.api.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.tags.Tag; + +@Tag(name = "Users V1 API", description = "Users API ์ž…๋‹ˆ๋‹ค.") +public interface UserV1ApiSpec { + + @Operation( + summary = "ํšŒ์› ๊ฐ€์ž…", + description = "ํšŒ์› ๊ฐ€์ž…์„ ํ•ฉ๋‹ˆ๋‹ค." + ) + ApiResponse register( + @Schema(name = "ํšŒ์› ๊ฐ€์ž… ์š”์ฒญ", description = "ํšŒ์›๊ฐ€์ž…") + UserV1Dto.RegisterRequest request + ); + + @Operation( + summary = "ํšŒ์› ์กฐํšŒ", + description = "ํ•ด๋‹น ํšŒ์›์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค." + ) + ApiResponse getUser( + @Schema(name = "ํšŒ์› ID", description = "์กฐํšŒํ•  ํšŒ์› ID") + String userId + ); +} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java new file mode 100644 index 000000000..aed39ae1f --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java @@ -0,0 +1,31 @@ +package com.loopers.interfaces.api.user; + +import com.loopers.application.user.UserFacade; +import com.loopers.application.user.UserInfo; +import com.loopers.interfaces.api.ApiResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/v1/users") +public class UserV1Controller implements UserV1ApiSpec { + + private final UserFacade userFacade; + + @Override + @PostMapping("/register") + public ApiResponse register(@RequestBody UserV1Dto.RegisterRequest request) { + UserInfo userInfo = userFacade.register(request.userId(), request.mail(), request.birth(), request.gender()); + UserV1Dto.UserResponse response = UserV1Dto.UserResponse.from(userInfo); + return ApiResponse.success(response); + } + + @Override + @GetMapping("/{userId}") + public ApiResponse getUser(@PathVariable String userId) { + UserInfo userInfo = userFacade.getUser(userId); + UserV1Dto.UserResponse response = UserV1Dto.UserResponse.from(userInfo); + return ApiResponse.success(response); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java new file mode 100644 index 000000000..263214848 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java @@ -0,0 +1,24 @@ +package com.loopers.interfaces.api.user; + +import com.loopers.application.user.UserInfo; + +public class UserV1Dto { + public record RegisterRequest( + String userId, + String mail, + String birth, + String gender + ) { + } + + public record UserResponse(String userId, String email, String birth, String gender) { + public static UserResponse from(UserInfo info) { + return new UserResponse( + info.userId(), + info.email(), + info.birth(), + info.gender() + ); + } + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandTest.java new file mode 100644 index 000000000..9541c11f4 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandTest.java @@ -0,0 +1,42 @@ +package com.loopers.domain.brand; + +import com.loopers.support.error.CoreException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * packageName : com.loopers.domain.brand + * fileName : BrandTest + * author : byeonsungmun + * date : 2025. 11. 14. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 14. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +class BrandTest { + + @DisplayName("Brand ๋‹จ์œ„ ํ…Œ์ŠคํŠธ") + @Nested + class CreateBrandTest { + + @Test + @DisplayName("๋ธŒ๋žœ๋“œ ์ƒ์„ฑ ์„ฑ๊ณต") + void createBrandSuccess() { + Brand brand = Brand.create("Nike"); + assertThat(brand.getName()).isEqualTo("Nike"); + } + + @Test + @DisplayName("๋ธŒ๋žœ๋“œ ์ด๋ฆ„์ด ์—†์œผ๋ฉด ์˜ˆ์™ธ") + void createBrandFail() { + assertThatThrownBy(() -> Brand.create("")) + .isInstanceOf(CoreException.class); + } + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/example/ExampleModelTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/example/ExampleModelTest.java deleted file mode 100644 index 44ca7576e..000000000 --- a/apps/commerce-api/src/test/java/com/loopers/domain/example/ExampleModelTest.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.loopers.domain.example; - -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertAll; -import static org.junit.jupiter.api.Assertions.assertThrows; - -class ExampleModelTest { - @DisplayName("์˜ˆ์‹œ ๋ชจ๋ธ์„ ์ƒ์„ฑํ•  ๋•Œ, ") - @Nested - class Create { - @DisplayName("์ œ๋ชฉ๊ณผ ์„ค๋ช…์ด ๋ชจ๋‘ ์ฃผ์–ด์ง€๋ฉด, ์ •์ƒ์ ์œผ๋กœ ์ƒ์„ฑ๋œ๋‹ค.") - @Test - void createsExampleModel_whenNameAndDescriptionAreProvided() { - // arrange - String name = "์ œ๋ชฉ"; - String description = "์„ค๋ช…"; - - // act - ExampleModel exampleModel = new ExampleModel(name, description); - - // assert - assertAll( - () -> assertThat(exampleModel.getId()).isNotNull(), - () -> assertThat(exampleModel.getName()).isEqualTo(name), - () -> assertThat(exampleModel.getDescription()).isEqualTo(description) - ); - } - - @DisplayName("์ œ๋ชฉ์ด ๋นˆ์นธ์œผ๋กœ๋งŒ ์ด๋ฃจ์–ด์ ธ ์žˆ์œผ๋ฉด, BAD_REQUEST ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") - @Test - void throwsBadRequestException_whenTitleIsBlank() { - // arrange - String name = " "; - - // act - CoreException result = assertThrows(CoreException.class, () -> { - new ExampleModel(name, "์„ค๋ช…"); - }); - - // assert - assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); - } - - @DisplayName("์„ค๋ช…์ด ๋น„์–ด์žˆ์œผ๋ฉด, BAD_REQUEST ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") - @Test - void throwsBadRequestException_whenDescriptionIsEmpty() { - // arrange - String description = ""; - - // act - CoreException result = assertThrows(CoreException.class, () -> { - new ExampleModel("์ œ๋ชฉ", description); - }); - - // assert - assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); - } - } -} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/example/ExampleServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/example/ExampleServiceIntegrationTest.java deleted file mode 100644 index bbd5fdbe1..000000000 --- a/apps/commerce-api/src/test/java/com/loopers/domain/example/ExampleServiceIntegrationTest.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.loopers.domain.example; - -import com.loopers.infrastructure.example.ExampleJpaRepository; -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import com.loopers.utils.DatabaseCleanUp; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertAll; -import static org.junit.jupiter.api.Assertions.assertThrows; - -@SpringBootTest -class ExampleServiceIntegrationTest { - @Autowired - private ExampleService exampleService; - - @Autowired - private ExampleJpaRepository exampleJpaRepository; - - @Autowired - private DatabaseCleanUp databaseCleanUp; - - @AfterEach - void tearDown() { - databaseCleanUp.truncateAllTables(); - } - - @DisplayName("์˜ˆ์‹œ๋ฅผ ์กฐํšŒํ•  ๋•Œ,") - @Nested - class Get { - @DisplayName("์กด์žฌํ•˜๋Š” ์˜ˆ์‹œ ID๋ฅผ ์ฃผ๋ฉด, ํ•ด๋‹น ์˜ˆ์‹œ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnsExampleInfo_whenValidIdIsProvided() { - // arrange - ExampleModel exampleModel = exampleJpaRepository.save( - new ExampleModel("์˜ˆ์‹œ ์ œ๋ชฉ", "์˜ˆ์‹œ ์„ค๋ช…") - ); - - // act - ExampleModel result = exampleService.getExample(exampleModel.getId()); - - // assert - assertAll( - () -> assertThat(result).isNotNull(), - () -> assertThat(result.getId()).isEqualTo(exampleModel.getId()), - () -> assertThat(result.getName()).isEqualTo(exampleModel.getName()), - () -> assertThat(result.getDescription()).isEqualTo(exampleModel.getDescription()) - ); - } - - @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์˜ˆ์‹œ ID๋ฅผ ์ฃผ๋ฉด, NOT_FOUND ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") - @Test - void throwsException_whenInvalidIdIsProvided() { - // arrange - Long invalidId = 999L; // Assuming this ID does not exist - - // act - CoreException exception = assertThrows(CoreException.class, () -> { - exampleService.getExample(invalidId); - }); - - // assert - assertThat(exception.getErrorType()).isEqualTo(ErrorType.NOT_FOUND); - } - } -} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeServiceIntegrationTest.java new file mode 100644 index 000000000..0be07a6fb --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeServiceIntegrationTest.java @@ -0,0 +1,155 @@ +package com.loopers.domain.like; + +import com.loopers.domain.product.Product; +import com.loopers.domain.product.ProductRepository; +import com.loopers.domain.user.User; +import com.loopers.domain.user.UserRepository; +import com.loopers.utils.DatabaseCleanUp; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; + +import static org.assertj.core.api.Assertions.*; + +/** + * packageName : com.loopers.domain.like + * fileName : LikeServiceIntegrationTest + * author : byeonsungmun + * date : 2025. 11. 14. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 14. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +@SpringBootTest +class LikeServiceIntegrationTest { + + @Autowired + private LikeService likeService; + + @Autowired + private LikeRepository likeRepository; + + @Autowired + private ProductRepository productRepository; + + @Autowired + private UserRepository userRepository; + + @Autowired + private DatabaseCleanUp cleanUp; + + @AfterEach + void tearDown() { + cleanUp.truncateAllTables(); + } + + @Nested + @DisplayName("์ข‹์•„์š” ๊ธฐ๋Šฅ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ") + class LikeTests { + + @Test + @DisplayName("์ข‹์•„์š” ์ƒ์„ฑ ์„ฑ๊ณต โ†’ ์ข‹์•„์š” ์ €์žฅ + ์ƒํ’ˆ์˜ likeCount ์ฆ๊ฐ€") + @Transactional + void likeSuccess() { + // given + User user = userRepository.save(new User("user1", "u1@mail.com", "1990-01-01", "MALE")); + Product product = productRepository.save(Product.create(1L, "์ƒํ’ˆA", 1000L, 10L)); + + // when + likeService.like(user.getUserId(), product.getId()); + + // then + Like saved = likeRepository.findByUserIdAndProductId("user1", 1L).orElse(null); + assertThat(saved).isNotNull(); + + Product updated = productRepository.findById(1L).get(); + assertThat(updated.getLikeCount()).isEqualTo(1L); + } + + @Test + @DisplayName("์ค‘๋ณต ์ข‹์•„์š” ์‹œ likeCount ์ฆ๊ฐ€ ์•ˆ ํ•˜๊ณ  ์ €์žฅ๋„ ์•ˆ ๋จ") + @Transactional + void duplicateLike() { + // given + userRepository.save(new User("user1", "u1@mail.com", "1990-01-01", "MALE")); + productRepository.save(Product.create(1L, "์ƒํ’ˆA", 1000L, 10L)); + + likeService.like("user1", 1L); + + // when + likeService.like("user1", 1L); // ์ค‘๋ณต ํ˜ธ์ถœ + + // then + long likeCount = likeRepository.countByProductId(1L); + assertThat(likeCount).isEqualTo(1L); + + Product updated = productRepository.findById(1L).get(); + assertThat(updated.getLikeCount()).isEqualTo(1L); // ์ฆ๊ฐ€ X + } + + @Test + @DisplayName("์ข‹์•„์š” ์ทจ์†Œ ์„ฑ๊ณต โ†’ like ์‚ญ์ œ + ์ƒํ’ˆ์˜ likeCount ๊ฐ์†Œ") + @Transactional + void unlikeSuccess() { + // given + userRepository.save(new User("user1", "u1@mail.com", "1990-01-01", "MALE")); + productRepository.save(Product.create(1L, "์ƒํ’ˆA", 1000L, 10L)); + + likeService.like("user1", 1L); + + // when + likeService.unlike("user1", 1L); + + // then + Like like = likeRepository.findByUserIdAndProductId("user1", 1L).orElse(null); + assertThat(like).isNull(); + + Product updated = productRepository.findById(1L).get(); + assertThat(updated.getLikeCount()).isEqualTo(0L); + } + + @Test + @DisplayName("์—†๋Š” ์ข‹์•„์š” ์ทจ์†Œ ์‹œ likeCount ๊ฐ์†Œ ์•ˆ ํ•จ") + @Transactional + void unlikeNonExisting() { + // given + userRepository.save(new User("user1", "u1@mail.com", "1990-01-01", "MALE")); + Product product = Product.create(1L, "์ƒํ’ˆA", 1000L, 10L); + product.increaseLikeCount(); + product.increaseLikeCount(); + product.increaseLikeCount(); + product.increaseLikeCount(); + product.increaseLikeCount(); + + productRepository.save(product); + // when โ€” ํ˜ธ์ถœ์€ ํ•ด๋„ + likeService.unlike("user1", 1L); + + // then โ€” ๋ณ€ํ™” ์—†์Œ + Product updated = productRepository.findById(1L).get(); + assertThat(updated.getLikeCount()).isEqualTo(5L); + } + + @Test + @DisplayName("countByProductId ์ •์ƒ ์กฐํšŒ") + @Transactional + void countTest() { + // given + userRepository.save(new User("user1", "u1@mail.com", "1990-01-01", "MALE")); + userRepository.save(new User("user2", "u2@mail.com", "1991-01-01", "MALE")); + productRepository.save(Product.create(1L, "์ƒํ’ˆA", 1000L, 10L)); + + likeService.like("user1", 1L); + likeService.like("user2", 1L); + + // when + long count = likeService.countByProductId(1L); + + // then + assertThat(count).isEqualTo(2L); + } + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeTest.java new file mode 100644 index 000000000..d5b8bd851 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeTest.java @@ -0,0 +1,91 @@ +package com.loopers.domain.like; + +import com.loopers.support.error.CoreException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * packageName : com.loopers.domain.like + * fileName : LikeTest + * author : byeonsungmun + * date : 2025. 11. 13. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 13. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +class LikeTest { + + + @DisplayName("์ •์ƒ์ ์œผ๋กœ Like ์—”ํ‹ฐํ‹ฐ๋ฅผ ์ƒ์„ฑ์ˆ˜ ํ•  ์žˆ๋‹ค") + @Nested + class LikeCreate { + + @DisplayName("Like์ƒ์„ฑ์ž๋ฅผ ์ƒ์„ฑํ•œ๋‹ค") + @Test + void createLike_success() { + // given + String userId = "user-001"; + Long productId = 100L; + + // when + Like like = Like.create(userId, productId); + + // then + assertThat(like.getUserId()).isEqualTo(userId); + assertThat(like.getProductId()).isEqualTo(productId); + assertThat(like.getCreatedAt()).isBeforeOrEqualTo(LocalDateTime.now()); + } + + @Test + @DisplayName("userId๊ฐ€ null์ด๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค") + void createLike_invalidUserId_null() { + // given + String userId = null; + Long productId = 100L; + + // when & then + assertThrows(CoreException.class, () -> Like.create(userId, productId)); + } + + @Test + @DisplayName("userId๊ฐ€ ๋นˆ ๋ฌธ์ž์—ด์ด๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค") + void createLike_invalidUserId_empty() { + // given + String userId = ""; + Long productId = 100L; + + // when & then + assertThrows(CoreException.class, () -> Like.create(userId, productId)); + } + + @Test + @DisplayName("productId๊ฐ€ null์ด๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค") + void createLike_invalidProductId_null() { + // given + String userId = "user-001"; + Long productId = null; + + // when & then + assertThrows(CoreException.class, () -> Like.create(userId, productId)); + } + + @Test + @DisplayName("productId๊ฐ€ 0 ์ดํ•˜์ด๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค") + void createLike_invalidProductId_zeroOrNegative() { + // given + String userId = "user-001"; + Long productId = -1L; + + // when & then + assertThrows(CoreException.class, () -> Like.create(userId, productId)); + } + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderServiceIntegrationTest.java new file mode 100644 index 000000000..149e71540 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderServiceIntegrationTest.java @@ -0,0 +1,170 @@ +package com.loopers.domain.order; + +import com.loopers.application.order.CreateOrderCommand; +import com.loopers.application.order.OrderFacade; +import com.loopers.application.order.OrderInfo; +import com.loopers.application.order.OrderItemCommand; +import com.loopers.domain.point.Point; +import com.loopers.domain.point.PointRepository; +import com.loopers.domain.product.Product; +import com.loopers.domain.product.ProductRepository; +import com.loopers.utils.DatabaseCleanUp; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * packageName : com.loopers.domain.order + * fileName : OrderServiceIntegrationTest + * author : byeonsungmun + * date : 2025. 11. 14. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 14. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +@SpringBootTest +public class OrderServiceIntegrationTest { + + @Autowired + private OrderFacade orderFacade; + + @Autowired + private ProductRepository productRepository; + + @Autowired + private PointRepository pointRepository; + + @Autowired + private OrderRepository orderRepository; + + @Autowired + private DatabaseCleanUp databaseCleanUp; + + @AfterEach + void tearDown() { + databaseCleanUp.truncateAllTables(); + } + + @Nested + @DisplayName("์ฃผ๋ฌธ ์ƒ์„ฑ ์„ฑ๊ณต") + class OrderCreateSuccess { + + @Test + @Transactional + void createOrder_success() { + + // given + Product p1 = productRepository.save(Product.create(1L, "์•„๋ฉ”๋ฆฌ์นด๋…ธ", 3000L, 100L)); + Product p2 = productRepository.save(Product.create(1L, "๋ผ๋–ผ", 4000L, 200L)); + + pointRepository.save(Point.create("user1", 20000L)); + + CreateOrderCommand command = new CreateOrderCommand( + "user1", + List.of( + new OrderItemCommand(p1.getId(), 2L), // 6000์› + new OrderItemCommand(p2.getId(), 1L) // 4000์› + ) + ); + + // when + OrderInfo info = orderFacade.createOrder(command); + + // then + Order saved = orderRepository.findById(info.orderId()).orElseThrow(); + + assertThat(saved.getStatus()).isEqualTo(OrderStatus.COMPLETE); + assertThat(saved.getTotalAmount()).isEqualTo(10000L); + assertThat(saved.getOrderItems()).hasSize(2); + + // ์žฌ๊ณ  ๊ฐ์†Œ ํ™•์ธ + Product updated1 = productRepository.findById(p1.getId()).get(); + Product updated2 = productRepository.findById(p2.getId()).get(); + assertThat(updated1.getStock()).isEqualTo(98); + assertThat(updated2.getStock()).isEqualTo(199); + + // ํฌ์ธํŠธ ๊ฐ์†Œ ํ™•์ธ + Point point = pointRepository.findByUserId("user1").get(); + assertThat(point.getBalance()).isEqualTo(10000L); // 20000 - 10000 + + } + } + + @Nested + @DisplayName("์ฃผ๋ฌธ ์‹คํŒจ ์ผ€์ด์Šค") + class OrderCreateFail { + + @Test + @Transactional + @DisplayName("์žฌ๊ณ  ๋ถ€์กฑ์œผ๋กœ ์‹คํŒจ") + void insufficientStock_fail() { + Product item = productRepository.save(Product.create(1L, "์ƒํ’ˆ", 1000L, 1L)); + pointRepository.save(Point.create("user1", 5000L)); + + CreateOrderCommand command = new CreateOrderCommand( + "user1", + List.of(new OrderItemCommand(item.getId(), 5L)) + ); + + assertThatThrownBy(() -> orderFacade.createOrder(command)) + .isInstanceOf(RuntimeException.class); // ๋„ˆ์˜ ๋„๋ฉ”์ธ ์˜ˆ์™ธ ํƒ€์ž… ๋งž์ถฐ๋„ ๋จ + } + + @Test + @Transactional + @DisplayName("ํฌ์ธํŠธ ๋ถ€์กฑ์œผ๋กœ ์‹คํŒจ") + void insufficientPoint_fail() { + Product item = productRepository.save(Product.create(1L, "์ƒํ’ˆ", 1000L, 10L)); + pointRepository.save(Point.create("user1", 2000L)); // ๋ถ€์กฑ + + CreateOrderCommand command = new CreateOrderCommand( + "user1", + List.of(new OrderItemCommand(item.getId(), 5L)) // ์ด 5000์› + ); + + assertThatThrownBy(() -> orderFacade.createOrder(command)) + .hasMessageContaining("ํฌ์ธํŠธ"); // ๋ฉ”์‹œ์ง€ ๋งž์ถ”๋ฉด ๋” ์ •ํ™•ํ•˜๊ฒŒ ๊ฐ€๋Šฅ + } + + @Test + @Transactional + @DisplayName("์—†๋Š” ์ƒํ’ˆ ์ฃผ๋ฌธ ์‹œ ์‹คํŒจ") + void noProduct_fail() { + pointRepository.save(Point.create("user1", 10000L)); + + CreateOrderCommand command = new CreateOrderCommand( + "user1", + List.of(new OrderItemCommand(999L, 1L)) + ); + + assertThatThrownBy(() -> orderFacade.createOrder(command)) + .isInstanceOf(RuntimeException.class); + } + + @Test + @Transactional + @DisplayName("์œ ์ € ํฌ์ธํŠธ ์ •๋ณด ์—†์œผ๋ฉด ์‹คํŒจ") + void noUserPoint_fail() { + Product item = productRepository.save(Product.create(1L, "์ƒํ’ˆ", 1000L, 10L)); + + CreateOrderCommand command = new CreateOrderCommand( + "user1", + List.of(new OrderItemCommand(item.getId(), 1L)) + ); + + assertThatThrownBy(() -> orderFacade.createOrder(command)) + .isInstanceOf(RuntimeException.class); + } + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderTest.java new file mode 100644 index 000000000..60ed16ecc --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderTest.java @@ -0,0 +1,122 @@ +package com.loopers.domain.order; + +import com.loopers.support.error.CoreException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * packageName : com.loopers.domain.order + * fileName : OrderTest + * author : byeonsungmun + * date : 2025. 11. 14. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 14. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +class OrderTest { + + @Nested + @DisplayName("Order ์ƒ์„ฑ ํ…Œ์ŠคํŠธ") + class CreateOrderTest { + + @Test + @DisplayName("์ฃผ๋ฌธ ์ƒ์„ฑ ์„ฑ๊ณต") + void createOrderSuccess() { + // when + Order order = Order.create("user123"); + + // then + assertThat(order.getUserId()).isEqualTo("user123"); + assertThat(order.getStatus()).isEqualTo(OrderStatus.PENDING); + assertThat(order.getTotalAmount()).isEqualTo(0L); + assertThat(order.getCreatedAt()).isNotNull(); + assertThat(order.getOrderItems()).isEmpty(); + } + + @Test + @DisplayName("userId๊ฐ€ null์ด๋ฉด ์ƒ์„ฑ ์‹คํŒจ") + void createOrderFailUserIdNull() { + assertThatThrownBy(() -> Order.create(null)) + .isInstanceOf(CoreException.class) + .hasMessageContaining("์‚ฌ์šฉ์ž ID๋Š” ํ•„์ˆ˜"); + } + + @Test + @DisplayName("userId๊ฐ€ ๊ณต๋ฐฑ์ด๋ฉด ์ƒ์„ฑ ์‹คํŒจ") + void createOrderFailUserIdBlank() { + assertThatThrownBy(() -> Order.create("")) + .isInstanceOf(CoreException.class) + .hasMessageContaining("์‚ฌ์šฉ์ž ID๋Š” ํ•„์ˆ˜"); + } + } + + @Nested + @DisplayName("Order ์ƒํƒœ ์—…๋ฐ์ดํŠธ ํ…Œ์ŠคํŠธ") + class UpdateStatusTest { + + @Test + @DisplayName("์ฃผ๋ฌธ ์ƒํƒœ ์—…๋ฐ์ดํŠธ ์„ฑ๊ณต") + void updateStatusSuccess() { + // given + Order order = Order.create("user123"); + + // when + order.updateStatus(OrderStatus.COMPLETE); + + // then + assertThat(order.getStatus()).isEqualTo(OrderStatus.COMPLETE); + } + } + + @Nested + @DisplayName("์ด์•ก ์—…๋ฐ์ดํŠธ ํ…Œ์ŠคํŠธ") + class UpdateAmountTest { + + @Test + @DisplayName("์ด์•ก ์—…๋ฐ์ดํŠธ ์„ฑ๊ณต") + void updateTotalAmountSuccess() { + // given + Order order = Order.create("user123"); + + // when + order.updateTotalAmount(5000L); + + // then + assertThat(order.getTotalAmount()).isEqualTo(5000L); + } + } + + @Nested + @DisplayName("OrderItem ์ถ”๊ฐ€ ํ…Œ์ŠคํŠธ") + class AddOrderItemTest { + + @Test + @DisplayName("OrderItem ์ถ”๊ฐ€ ์„ฑ๊ณต") + void addOrderItemSuccess() { + // given + Order order = Order.create("user123"); + + OrderItem item = OrderItem.create( + 1L, + "์ƒํ’ˆ๋ช…", + 2L, + 1000L + ); + + // when + order.addOrderItem(item); + item.setOrder(order); + + // then + assertThat(order.getOrderItems()).hasSize(1); + assertThat(order.getOrderItems().getFirst().getProductName()).isEqualTo("์ƒํ’ˆ๋ช…"); + assertThat(item.getOrder()).isEqualTo(order); + } + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java new file mode 100644 index 000000000..b623bc9c7 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java @@ -0,0 +1,108 @@ +package com.loopers.domain.point; + +import com.loopers.domain.user.User; +import com.loopers.domain.user.UserRepository; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import com.loopers.utils.DatabaseCleanUp; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@SpringBootTest +class PointServiceIntegrationTest { + + @Autowired + private PointRepository pointRepository; + + @Autowired + private UserRepository userRepository; + + @Autowired + private PointService pointService; + + @Autowired + private DatabaseCleanUp databaseCleanUp; + + @AfterEach + void tearDown() { + databaseCleanUp.truncateAllTables(); + } + + @DisplayName("ํฌ์ธํŠธ ์กฐํšŒ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ") + @Nested + class PointUser { + + @DisplayName("ํ•ด๋‹น ID์˜ ํšŒ์›์ด ์กด์žฌํ•  ๊ฒฝ์šฐ, ๋ณด์œ  ํฌ์ธํŠธ๊ฐ€ ๋ฐ˜ํ™˜๋œ๋‹ค.") + @Test + void returnPointInfo_whenValidIdIsProvided() { + //given + String id = "yh45g"; + String email = "yh45g@loopers.com"; + String birth = "1994-12-05"; + String gender = "MALE"; + + userRepository.save(new User(id, email, birth, gender)); + pointRepository.save(Point.create(id, 0L)); + + //when + Point result = pointService.findPointByUserId(id); + + //then + assertThat(result.getUserId()).isEqualTo(id); + assertThat(result.getBalance()).isEqualTo(0L); + } + + @DisplayName("ํšŒ์›์ด ์กด์žฌ ํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ, null ์ด ๋ฐ˜ํ™˜๋œ๋‹ค.") + @Test + void returnNull_whenInvalidUserIdIsProvided() { + //given + String id = "yh45g"; + + //when + Point point = pointService.findPointByUserId(id); + + //then + assertThat(point).isNull(); + } + } + + @DisplayName("ํฌ์ธํŠธ ์ถฉ์ „ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ") + @Nested + class Charge { + + @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ์ € ID ๋กœ ์ถฉ์ „์„ ์‹œ๋„ํ•œ ๊ฒฝ์šฐ, ์‹คํŒจํ•œ๋‹ค.") + @Test + void throwsChargeAmountFailException_whenUserIDIsNotProvided() { + //given + String id = "yh45g"; + + //when + CoreException exception = assertThrows(CoreException.class, () -> pointService.chargePoint(id, 1000L)); + + //then + assertThat(exception.getErrorType()).isEqualTo(ErrorType.NOT_FOUND); + } + + @Test + @DisplayName("ํšŒ์›์ด ์กด์žฌํ•˜๋ฉด ํฌ์ธํŠธ ์ถฉ์ „ ์„ฑ๊ณต") + void chargeSuccess() { + // given + String userId = "user2"; + userRepository.save(new User(userId, "yh45g@loopers.com", "1994-12-05", "MALE")); + pointRepository.save(Point.create(userId, 1000L)); + + // when + Point updated = pointService.chargePoint(userId, 500L); + + // then + assertThat(updated.getBalance()).isEqualTo(1500L); + } + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/point/PointTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/point/PointTest.java new file mode 100644 index 000000000..f33fb2821 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/point/PointTest.java @@ -0,0 +1,117 @@ +package com.loopers.domain.point; + +import com.loopers.support.error.CoreException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; + +class PointTest { + + @Nested + @DisplayName("ํฌ์ธํŠธ ์ƒ์„ฑ ํ…Œ์ŠคํŠธ") + class CreatePointTest { + + @Test + @DisplayName("ํฌ์ธํŠธ ์ƒ์„ฑ ์„ฑ๊ณต") + void createPointSuccess() { + // when + Point point = Point.create("user123", 100L); + + // then + assertThat(point.getUserId()).isEqualTo("user123"); + assertThat(point.getBalance()).isEqualTo(100L); + } + + @Test + @DisplayName("userId๊ฐ€ null์ด๋ฉด ์ƒ์„ฑ ์‹คํŒจ") + void createPointFailUserIdNull() { + assertThatThrownBy(() -> Point.create(null, 100L)) + .isInstanceOf(CoreException.class) + .hasMessageContaining("์‚ฌ์šฉ์ž ID๊ฐ€ ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + + @Test + @DisplayName("userId๊ฐ€ ๊ณต๋ฐฑ์ด๋ฉด ์ƒ์„ฑ ์‹คํŒจ") + void createPointFailUserIdBlank() { + assertThatThrownBy(() -> Point.create("", 100L)) + .isInstanceOf(CoreException.class) + .hasMessageContaining("์‚ฌ์šฉ์ž ID๊ฐ€ ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + } + + @Nested + @DisplayName("ํฌ์ธํŠธ ์ถฉ์ „ ํ…Œ์ŠคํŠธ") + class ChargePointTest { + + @Test + @DisplayName("ํฌ์ธํŠธ ์ถฉ์ „ ์„ฑ๊ณต") + void chargeSuccess() { + // given + Point point = Point.create("user123", 100L); + + // when + point.charge(50L); + + // then + assertThat(point.getBalance()).isEqualTo(150L); + } + + @Test + @DisplayName("ํฌ์ธํŠธ ์ถฉ์ „ ์‹คํŒจ - 0 ์ดํ•˜ ์ž…๋ ฅ") + void chargeFailZeroOrNegative() { + Point point = Point.create("user123", 100L); + + assertThatThrownBy(() -> point.charge(0L)) + .isInstanceOf(CoreException.class) + .hasMessageContaining("์ถฉ์ „"); + + assertThatThrownBy(() -> point.charge(-10L)) + .isInstanceOf(CoreException.class); + } + } + + @Nested + @DisplayName("ํฌ์ธํŠธ ์‚ฌ์šฉ ํ…Œ์ŠคํŠธ") + class UsePointTest { + + @Test + @DisplayName("ํฌ์ธํŠธ ์‚ฌ์šฉ ์„ฑ๊ณต") + void useSuccess() { + // given + Point point = Point.create("user123", 100L); + + // when + point.use(40L); + + // then + assertThat(point.getBalance()).isEqualTo(60L); + } + + @Test + @DisplayName("ํฌ์ธํŠธ ์‚ฌ์šฉ ์‹คํŒจ - 0 ์ดํ•˜ ์ž…๋ ฅ") + void useFailZeroOrNegative() { + Point point = Point.create("user123", 100L); + + assertThatThrownBy(() -> point.use(0L)) + .isInstanceOf(CoreException.class) + .hasMessageContaining("์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + + assertThatThrownBy(() -> point.use(-10L)) + .isInstanceOf(CoreException.class); + } + + @Test + @DisplayName("ํฌ์ธํŠธ ์‚ฌ์šฉ ์‹คํŒจ - ์ž”์•ก ๋ถ€์กฑ") + void useFailNotEnough() { + Point point = Point.create("user123", 50L); + + assertThatThrownBy(() -> point.use(100L)) + .isInstanceOf(CoreException.class) + .hasMessageContaining("ํฌ์ธํŠธ๊ฐ€ ๋ถ€์กฑ"); + } + } + +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductServiceIntegrationTest.java new file mode 100644 index 000000000..8ad61a194 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductServiceIntegrationTest.java @@ -0,0 +1,43 @@ +package com.loopers.domain.product; + +import com.loopers.utils.DatabaseCleanUp; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +/** + * packageName : com.loopers.domain.product + * fileName : ProductServiceIntegrationTest + * author : byeonsungmun + * date : 2025. 11. 13. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 13. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ + +@SpringBootTest +public class ProductServiceIntegrationTest { + + @Autowired + private ProductService productService; + + @Autowired + private DatabaseCleanUp databaseCleanUp; + + @AfterEach + void tearDown() { + databaseCleanUp.truncateAllTables(); + } + + @Nested + @DisplayName("์ƒํ’ˆ ๋ชฉ๋ก ์กฐํšŒ ํ…Œ์ŠคํŠธ") + class ProductListTests { + + Product product; + + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductTest.java new file mode 100644 index 000000000..c2c6fdd9b --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductTest.java @@ -0,0 +1,95 @@ +package com.loopers.domain.product; + +import com.loopers.support.error.CoreException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * packageName : com.loopers.domain.product + * fileName : ProductTest + * author : byeonsungmun + * date : 2025. 11. 10. + * description : + * =========================================== + * DATE AUTHOR NOTE + * ------------------------------------------- + * 2025. 11. 10. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ + */ +class ProductTest { + @DisplayName("Product ์ข‹์•„์š” ์ˆ˜ ์ฆ๊ฐ ํ…Œ์ŠคํŠธ") + @Nested + class LikeCountChange { + + @DisplayName("์ข‹์•„์š” ์ˆ˜๋ฅผ ์ฆ๊ฐ€์‹œํ‚จ๋‹ค.") + @Test + void increaseLikeCount_incrementsLikeCount() { + // given + Product product = Product.create(11L, "๋‚˜์ดํ‚ค ์—์–ด2", 30000L, 10L); + + // when + product.increaseLikeCount(); + + // then + assertEquals(1L, product.getLikeCount()); + } + + @DisplayName("์ข‹์•„์š” ์ˆ˜๋ฅผ ๊ฐ์†Œ์‹œํ‚จ๋‹ค. 0 ๋ฏธ๋งŒ์œผ๋กœ๋Š” ๊ฐ์†Œํ•˜์ง€ ์•Š๋Š”๋‹ค.") + @Test + void decreaseLikeCount_decrementsLikeCountButNotBelowZero() { + // given + Product product = Product.create(11L, "๋‚˜์ดํ‚ค ์—์–ด2", 30000L, 1L); + + // when + product.decreaseLikeCount(); + + // then + assertEquals(0L, product.getLikeCount()); + + // when decrease again + product.decreaseLikeCount(); + + // then likeCount should not go below 0 + assertEquals(0L, product.getLikeCount()); + } + } + + @DisplayName("Product ์žฌ๊ณ  ์ฐจ๊ฐ ํ…Œ์ŠคํŠธ") + @Nested + class Stock { + + @DisplayName("์žฌ๊ณ ๋ฅผ ์ •์ƒ ์ฐจ๊ฐํ•œ๋‹ค.") + @Test + void decreaseStock_successfullyDecreasesStock() { + // given + Product product = Product.create(1L, "๋‚˜์ดํ‚ค ์—์–ด2", 30000L, 10L); + + // when + product.decreaseStock(3L); + + // then + assertEquals(7, product.getStock()); + } + + @DisplayName("์ฐจ๊ฐ ์ˆ˜๋Ÿ‰์ด 0 ์ดํ•˜์ด๋ฉด ์˜ˆ์™ธ ๋ฐœ์ƒ") + @Test + void decreaseStock_withInvalidQuantity_throwsException() { + Product product = Product.create(1L, "๋‚˜์ดํ‚ค ์—์–ด2", 30000L, 10L); + + assertThrows(CoreException.class, () -> product.decreaseStock(0L)); + assertThrows(CoreException.class, () -> product.decreaseStock(-1L)); + } + + @DisplayName("์žฌ๊ณ ๋ณด๋‹ค ํฐ ์ˆ˜๋Ÿ‰ ์ฐจ๊ฐ ์‹œ ์˜ˆ์™ธ ๋ฐœ์ƒ") + @Test + void decreaseStock_withInsufficientStock_throwsException() { + Product product = Product.create(1L, "๋‚˜์ดํ‚ค ์—์–ด2", 30000L, 10L); + + assertThrows(CoreException.class, () -> product.decreaseStock(11L)); + } + } +} + diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java new file mode 100644 index 000000000..71091883f --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java @@ -0,0 +1,112 @@ +package com.loopers.domain.user; + +import com.loopers.support.error.CoreException; +import com.loopers.utils.DatabaseCleanUp; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +@SpringBootTest +class UserServiceIntegrationTest { + + @Autowired + private UserRepository userRepository; + + @Autowired + private UserService userService; + + @Autowired + private DatabaseCleanUp databaseCleanUp; + + @AfterEach + void tearDown() { + databaseCleanUp.truncateAllTables(); + } + + @DisplayName("ํšŒ์› ๊ฐ€์ž… ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ") + @Nested + class UserRegister { + + @DisplayName("ํšŒ์› ๊ฐ€์ž…์‹œ User ์ €์žฅ์ด ์ˆ˜ํ–‰๋œ๋‹ค.") + @Test + void save_whenUserRegister() { + //given + String userId = "yh45g"; + String email = "yh45g@loopers.com"; + String brith = "1994-12-05"; + String gender = "Male"; + + UserRepository userRepositorySpy = spy(userRepository); + UserService userServiceSpy = new UserService(userRepositorySpy); + + //when + userServiceSpy.register(userId, email, brith, gender); + + //then + verify(userRepositorySpy).save(any(User.class)); + } + + @DisplayName("์ด๋ฏธ ๊ฐ€์ž…๋œ ID ๋กœ ํšŒ์›๊ฐ€์ž… ์‹œ๋„ ์‹œ, ์‹คํŒจํ•œ๋‹ค.") + @Test + void throwsException_whenDuplicateUserId() { + //given + String userId = "yh45g"; + String email = "yh45g@loopers.com"; + String brith = "1994-12-05"; + String gender = "Male"; + + //when + userService.register(userId, email, brith, gender); + + //then + Assertions.assertThrows(CoreException.class, () + -> userService.register(userId, email, brith, gender)); + } + } + + @DisplayName("๋‚ด ์ •๋ณด ์กฐํšŒ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ") + @Nested + class Get { + + @DisplayName("ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•  ๊ฒฝ์šฐ, ํšŒ์› ์ •๋ณด๊ฐ€ ๋ฐ˜ํ™˜๋œ๋‹ค.") + @Test + void returnsUser_whenValidIdIsProvided() { + //given + String userId = "yh45g"; + String email = "yh45g@loopers.com"; + String brith = "1994-12-05"; + String gender = "Male"; + + //when + userService.register(userId, email, brith, gender); + User user = userService.findUserByUserId(userId); + + //then + assertAll( + () -> assertThat(user.getUserId()).isEqualTo(userId), + () -> assertThat(user.getEmail()).isEqualTo(email), + () -> assertThat(user.getBirth()).isEqualTo(brith), + () -> assertThat(user.getGender()).isEqualTo(gender) + ); + } + + @DisplayName("ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ, null ์ด ๋ฐ˜ํ™˜๋œ๋‹ค.") + @Test + void returnNull_whenInvalidUserIdIsProvided() { + //given + String userId = "yh45g"; + + //when + User user = userService.findUserByUserId(userId); + + //then + assertThat(user).isNull(); + } + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserTest.java new file mode 100644 index 000000000..7d74fdfe2 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserTest.java @@ -0,0 +1,53 @@ +package com.loopers.domain.user; + +import com.loopers.support.error.CoreException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +class UserTest { + @DisplayName("User ๋‹จ์œ„ ํ…Œ์ŠคํŠธ") + @Nested + class Create { + @DisplayName("ID ๊ฐ€ ์˜๋ฌธ ๋ฐ ์ˆซ์ž 10์ž ์ด๋‚ด ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค.") + @Test + void throwsException_whenInvalidUserIdFormat() { + // given + String invalidUserId = "invalid_id_123"; // 10์ž ์ดˆ๊ณผ + ํŠน์ˆ˜๋ฌธ์ž ํฌํ•จ + String email = "valid@loopers.com"; + String birth = "1994-12-05"; + String gender = "MALE"; + + // when & then + assertThrows(CoreException.class, () -> new User(invalidUserId, email, birth, gender)); + } + + @DisplayName("์ด๋ฉ”์ผ์ด `xx@yy.zz` ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค.") + @Test + void throwsException_whenInvalidEmailFormat() { + // given + String userId = "yh45g"; + String invalidEmail = "invalid-email-format"; // '@' ์—†์Œ + String birth = "1994-12-05"; + String gender = "MALE"; + + // when & then + assertThrows(CoreException.class, () -> new User(userId, invalidEmail, birth, gender)); + } + + @DisplayName("์ƒ๋…„์›”์ผ์ด `yyyy-MM-dd` ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค.") + @Test + void throwsException_whenInvalidBirthFormat() { + // given + String userId = "yh45g"; + String email = "valid@loopers.com"; + String invalidBirth = "19941205"; // ํ˜•์‹ ์˜ค๋ฅ˜: ํ•˜์ดํ”ˆ ์—†์Œ + String gender = "MALE"; + + // when & then + assertThrows(CoreException.class, () -> new User(userId, email, invalidBirth, gender)); + } + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/ExampleV1ApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/ExampleV1ApiE2ETest.java deleted file mode 100644 index 1bb3dba65..000000000 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/ExampleV1ApiE2ETest.java +++ /dev/null @@ -1,114 +0,0 @@ -package com.loopers.interfaces.api; - -import com.loopers.domain.example.ExampleModel; -import com.loopers.infrastructure.example.ExampleJpaRepository; -import com.loopers.interfaces.api.example.ExampleV1Dto; -import com.loopers.utils.DatabaseCleanUp; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.web.client.TestRestTemplate; -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpMethod; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; - -import java.util.function.Function; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertAll; -import static org.junit.jupiter.api.Assertions.assertTrue; - -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -class ExampleV1ApiE2ETest { - - private static final Function ENDPOINT_GET = id -> "/api/v1/examples/" + id; - - private final TestRestTemplate testRestTemplate; - private final ExampleJpaRepository exampleJpaRepository; - private final DatabaseCleanUp databaseCleanUp; - - @Autowired - public ExampleV1ApiE2ETest( - TestRestTemplate testRestTemplate, - ExampleJpaRepository exampleJpaRepository, - DatabaseCleanUp databaseCleanUp - ) { - this.testRestTemplate = testRestTemplate; - this.exampleJpaRepository = exampleJpaRepository; - this.databaseCleanUp = databaseCleanUp; - } - - @AfterEach - void tearDown() { - databaseCleanUp.truncateAllTables(); - } - - @DisplayName("GET /api/v1/examples/{id}") - @Nested - class Get { - @DisplayName("์กด์žฌํ•˜๋Š” ์˜ˆ์‹œ ID๋ฅผ ์ฃผ๋ฉด, ํ•ด๋‹น ์˜ˆ์‹œ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnsExampleInfo_whenValidIdIsProvided() { - // arrange - ExampleModel exampleModel = exampleJpaRepository.save( - new ExampleModel("์˜ˆ์‹œ ์ œ๋ชฉ", "์˜ˆ์‹œ ์„ค๋ช…") - ); - String requestUrl = ENDPOINT_GET.apply(exampleModel.getId()); - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; - ResponseEntity> response = - testRestTemplate.exchange(requestUrl, HttpMethod.GET, new HttpEntity<>(null), responseType); - - // assert - assertAll( - () -> assertTrue(response.getStatusCode().is2xxSuccessful()), - () -> assertThat(response.getBody().data().id()).isEqualTo(exampleModel.getId()), - () -> assertThat(response.getBody().data().name()).isEqualTo(exampleModel.getName()), - () -> assertThat(response.getBody().data().description()).isEqualTo(exampleModel.getDescription()) - ); - } - - @DisplayName("์ˆซ์ž๊ฐ€ ์•„๋‹Œ ID ๋กœ ์š”์ฒญํ•˜๋ฉด, 400 BAD_REQUEST ์‘๋‹ต์„ ๋ฐ›๋Š”๋‹ค.") - @Test - void throwsBadRequest_whenIdIsNotProvided() { - // arrange - String requestUrl = "/api/v1/examples/๋‚˜๋‚˜"; - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; - ResponseEntity> response = - testRestTemplate.exchange(requestUrl, HttpMethod.GET, new HttpEntity<>(null), responseType); - - // assert - assertAll( - () -> assertTrue(response.getStatusCode().is4xxClientError()), - () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST) - ); - } - - @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์˜ˆ์‹œ ID๋ฅผ ์ฃผ๋ฉด, 404 NOT_FOUND ์‘๋‹ต์„ ๋ฐ›๋Š”๋‹ค.") - @Test - void throwsException_whenInvalidIdIsProvided() { - // arrange - Long invalidId = -1L; - String requestUrl = ENDPOINT_GET.apply(invalidId); - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; - ResponseEntity> response = - testRestTemplate.exchange(requestUrl, HttpMethod.GET, new HttpEntity<>(null), responseType); - - // assert - assertAll( - () -> assertTrue(response.getStatusCode().is4xxClientError()), - () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND) - ); - } - } -} diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/point/PointV1ControllerTest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/point/PointV1ControllerTest.java new file mode 100644 index 000000000..7d7a2c18c --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/point/PointV1ControllerTest.java @@ -0,0 +1,156 @@ +package com.loopers.interfaces.api.point; + +import com.loopers.domain.point.Point; +import com.loopers.domain.point.PointRepository; +import com.loopers.domain.user.User; +import com.loopers.domain.user.UserRepository; +import com.loopers.interfaces.api.ApiResponse; +import com.loopers.utils.DatabaseCleanUp; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.*; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class PointV1ControllerTest { + + private static final String GET_USER_POINT_ENDPOINT = "/api/v1/points"; + private static final String POST_USER_POINT_ENDPOINT = "/api/v1/points/charge"; + + @Autowired + private PointRepository pointRepository; + + @Autowired + private UserRepository userRepository; + + @Autowired + private DatabaseCleanUp databaseCleanUp; + + @Autowired + private TestRestTemplate testRestTemplate; + + @AfterEach + void tearDown() { + databaseCleanUp.truncateAllTables(); + } + + @DisplayName("GET /api/v1/points") + @Nested + class UserPoint { + + @DisplayName("ํฌ์ธํŠธ ์กฐํšŒ์— ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ๋ณด์œ  ํฌ์ธํŠธ๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnPoint_whenValidUserIdIsProvided() { + //given + String id = "yh45g"; + String email = "yh45g@loopers.com"; + String birth = "1994-12-05"; + String gender = "MALE"; + Long amount = 1000L; + + userRepository.save(new User(id, email, birth, gender)); + pointRepository.save(Point.create(id, amount)); + + HttpHeaders headers = new HttpHeaders(); + headers.add("X-USER-ID", id); + + //when + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; + ResponseEntity> response = + testRestTemplate.exchange(GET_USER_POINT_ENDPOINT, HttpMethod.GET, new HttpEntity<>(null, headers), responseType); + + //then + assertAll( + () -> assertTrue(response.getStatusCode().is2xxSuccessful()), + () -> assertThat(response.getBody().data().userId()).isEqualTo(id), + () -> assertThat(response.getBody().data().amount()).isEqualTo(1000L) + ); + } + + @DisplayName("ํ•ด๋‹น ID์˜ ํšŒ์›์ด ์กด์žฌํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ, null์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnNull_whenUserIdExists() { + //given + String id = "yh45g"; + + HttpHeaders headers = new HttpHeaders(); + headers.add("X-USER-ID", id); + + //when + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; + ResponseEntity> response = + testRestTemplate.exchange(GET_USER_POINT_ENDPOINT, HttpMethod.GET, new HttpEntity<>(null, headers), responseType); + + //then + assertAll( + () -> assertTrue(response.getStatusCode().is4xxClientError()), + () -> assertThat(response.getBody().data()).isNull() + ); + } + } + + @DisplayName("POST /api/v1/points/charge") + @Nested + class Charge { + + @DisplayName("์กด์žฌํ•˜๋Š” ์œ ์ €๊ฐ€ 1000์›์„ ์ถฉ์ „ํ•  ๊ฒฝ์šฐ, ์ถฉ์ „๋œ ๋ณด์œ  ์ด๋Ÿ‰์„ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnsTotalPoint_whenChargeUserPoint() { + //given + String id = "yh45g"; + String email = "yh45g@loopers.com"; + String birth = "1994-12-05"; + String gender = "MALE"; + + userRepository.save(new User(id, email, birth, gender)); + pointRepository.save(Point.create(id, 0L)); + + PointV1Dto.ChargePointRequest request = new PointV1Dto.ChargePointRequest(id, 1000L); + + HttpHeaders headers = new HttpHeaders(); + headers.add("X-USER-ID", id); + + //when + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; + ResponseEntity> response = + testRestTemplate.exchange(POST_USER_POINT_ENDPOINT, HttpMethod.PATCH, new HttpEntity<>(request, headers), responseType); + + //then + assertAll( + () -> assertTrue(response.getStatusCode().is2xxSuccessful()), + () -> assertThat(response.getBody().data().userId()).isEqualTo(id), + () -> assertThat(response.getBody().data().amount()).isEqualTo(1000L) + ); + } + + @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ์ €๋กœ ์š”์ฒญํ•  ๊ฒฝ์šฐ, 404 Not Found ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void throwsException_whenInvalidUserIdIsProvided() { + //given + String id = "yh45g"; + + HttpHeaders headers = new HttpHeaders(); + headers.add("X-USER-ID", id); + + //when + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; + ResponseEntity> response = + testRestTemplate.exchange(POST_USER_POINT_ENDPOINT, HttpMethod.PATCH, new HttpEntity<>(null, headers), responseType); + + //then + assertAll( + () -> assertTrue(response.getStatusCode().is4xxClientError()), + () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND) + ); + } + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/user/UserV1ControllerTest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/user/UserV1ControllerTest.java new file mode 100644 index 000000000..defe2fcd5 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/user/UserV1ControllerTest.java @@ -0,0 +1,148 @@ +package com.loopers.interfaces.api.user; + +import com.loopers.domain.user.User; +import com.loopers.infrastructure.user.UserJpaRepository; +import com.loopers.interfaces.api.ApiResponse; +import com.loopers.utils.DatabaseCleanUp; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import java.util.function.Function; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class UserV1ControllerTest { + + private static final String USER_REGISTER_ENDPOINT = "/api/v1/users/register"; + private static final Function GET_USER_ENDPOINT = id -> "/api/v1/users/" + id; + + @Autowired + private TestRestTemplate testRestTemplate; + + @Autowired + private UserJpaRepository userJpaRepository; + + @Autowired + private DatabaseCleanUp databaseCleanUp; + + @AfterEach + void tearDown() { + databaseCleanUp.truncateAllTables(); + } + + @DisplayName("POST /api/v1/users") + @Nested + class RegisterUser { + @DisplayName("ํšŒ์› ๊ฐ€์ž…์ด ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ์ƒ์„ฑ๋œ ์œ ์ € ์ •๋ณด๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void registerUser_whenSuccessResponseUser() { + //given + String userId = "yh45g"; + String email = "yh45g@loopers.com"; + String birth = "1994-12-05"; + String gender = "MALE"; + + UserV1Dto.RegisterRequest request = new UserV1Dto.RegisterRequest(userId, email, birth, gender); + + //when + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; + ResponseEntity> response = + testRestTemplate.exchange(USER_REGISTER_ENDPOINT, HttpMethod.POST, new HttpEntity<>(request), responseType); + + //then + assertAll( + () -> assertTrue(response.getStatusCode().is2xxSuccessful()), + () -> assertThat(response.getBody().data().userId()).isEqualTo(userId), + () -> assertThat(response.getBody().data().email()).isEqualTo(email), + () -> assertThat(response.getBody().data().birth()).isEqualTo(birth), + () -> assertThat(response.getBody().data().gender()).isEqualTo(gender) + ); + } + @DisplayName("ํšŒ์› ๊ฐ€์ž… ์‹œ์— ์„ฑ๋ณ„์ด ์—†์„ ๊ฒฝ์šฐ, 400 BAD_REQUEST ์‘๋‹ต์„ ๋ฐ›๋Š”๋‹ค.") + @Test + void throwsBadRequest_whenGenderIsNotProvided() { + //given + String userId = "yh45g"; + String email = "yh45g@loopers.com"; + String birth = "1994-12-05"; + String gender = null; + + UserV1Dto.RegisterRequest request = new UserV1Dto.RegisterRequest(userId, email, birth, gender); + + //when + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; + ResponseEntity> response = + testRestTemplate.exchange(USER_REGISTER_ENDPOINT, HttpMethod.POST, new HttpEntity<>(request), responseType); + + //then + assertAll( + () -> assertTrue(response.getStatusCode().is4xxClientError()), + () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST) + ); + } + } + + @DisplayName("GET /api/v1/users/{userId}") + @Nested + class GetUserById { + @DisplayName("๋‚ด ์ •๋ณด ์กฐํšŒ์— ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ํ•ด๋‹นํ•˜๋Š” ์œ ์ € ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void getUserById_whenSuccessResponseUser() { + //given + String userId = "yh45g"; + String email = "yh45g@loopers.com"; + String birth = "1994-12-05"; + String gender = "MALE"; + + userJpaRepository.save(new User(userId, email, birth, gender)); + + String requestUrl = GET_USER_ENDPOINT.apply(userId); + + //when + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; + ResponseEntity> response = + testRestTemplate.exchange(requestUrl, HttpMethod.GET, new HttpEntity<>(null), responseType); + + //then + assertAll( + () -> assertTrue(response.getStatusCode().is2xxSuccessful()), + () -> assertThat(response.getBody().data().userId()).isEqualTo(userId), + () -> assertThat(response.getBody().data().email()).isEqualTo(email), + () -> assertThat(response.getBody().data().birth()).isEqualTo(birth), + () -> assertThat(response.getBody().data().gender()).isEqualTo(gender) + ); + } + + @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ID ๋กœ ์กฐํšŒํ•  ๊ฒฝ์šฐ, 404 Not Found ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void throwsException_whenInvalidUserIdIsProvided() { + //given + String userId = "notUserId"; + String requestUrl = GET_USER_ENDPOINT.apply(userId); + + //when + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; + ResponseEntity> response = + testRestTemplate.exchange(requestUrl, HttpMethod.GET, new HttpEntity<>(null), responseType); + + //then + assertAll( + () -> assertTrue(response.getStatusCode().is4xxClientError()), + () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND) + ); + } + } +} diff --git a/docs/1round/1round.md b/docs/1round/1round.md new file mode 100644 index 000000000..106d6c809 --- /dev/null +++ b/docs/1round/1round.md @@ -0,0 +1,67 @@ +## ๐Ÿงช Implementation Quest + +> ์ง€์ •๋œ **๋‹จ์œ„ ํ…Œ์ŠคํŠธ / ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ / E2E ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค**๋ฅผ ํ•„์ˆ˜๋กœ ๊ตฌํ˜„ํ•˜๊ณ , ๋ชจ๋“  ํ…Œ์ŠคํŠธ๋ฅผ ํ†ต๊ณผ์‹œํ‚ค๋Š” ๊ฒƒ์„ ๋ชฉํ‘œ๋กœ ํ•ฉ๋‹ˆ๋‹ค. +> + +### ํšŒ์› ๊ฐ€์ž… + +**๐Ÿงฑ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ** + +- [x] ID ๊ฐ€ `์˜๋ฌธ ๋ฐ ์ˆซ์ž 10์ž ์ด๋‚ด` ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. +- [x] ์ด๋ฉ”์ผ์ด `xx@yy.zz` ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. +- [x] ์ƒ๋…„์›”์ผ์ด `yyyy-MM-dd` ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. + +**๐Ÿ”— ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ** + +- [x] ํšŒ์› ๊ฐ€์ž…์‹œ User ์ €์žฅ์ด ์ˆ˜ํ–‰๋œ๋‹ค. ( spy ๊ฒ€์ฆ ) +- [x] ์ด๋ฏธ ๊ฐ€์ž…๋œ ID ๋กœ ํšŒ์›๊ฐ€์ž… ์‹œ๋„ ์‹œ, ์‹คํŒจํ•œ๋‹ค. + +**๐ŸŒ E2E ํ…Œ์ŠคํŠธ** + +- [x] ํšŒ์› ๊ฐ€์ž…์ด ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ์ƒ์„ฑ๋œ ์œ ์ € ์ •๋ณด๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค. +- [x] ํšŒ์› ๊ฐ€์ž… ์‹œ์— ์„ฑ๋ณ„์ด ์—†์„ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. + +### ๋‚ด ์ •๋ณด ์กฐํšŒ + +**๐Ÿ”— ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ** + +- [x] ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•  ๊ฒฝ์šฐ, ํšŒ์› ์ •๋ณด๊ฐ€ ๋ฐ˜ํ™˜๋œ๋‹ค. +- [x] ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ, null ์ด ๋ฐ˜ํ™˜๋œ๋‹ค. + +**๐ŸŒ E2E ํ…Œ์ŠคํŠธ** + +- [x] ๋‚ด ์ •๋ณด ์กฐํšŒ์— ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ํ•ด๋‹นํ•˜๋Š” ์œ ์ € ์ •๋ณด๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค. +- [x] ์กด์žฌํ•˜์ง€ ์•Š๋Š” ID ๋กœ ์กฐํšŒํ•  ๊ฒฝ์šฐ, `404 Not Found` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. + +### ํฌ์ธํŠธ ์กฐํšŒ + +**๐Ÿ”— ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ** + +- [x] ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•  ๊ฒฝ์šฐ, ๋ณด์œ  ํฌ์ธํŠธ๊ฐ€ ๋ฐ˜ํ™˜๋œ๋‹ค. +- [x] ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ, null ์ด ๋ฐ˜ํ™˜๋œ๋‹ค. + +**๐ŸŒ E2E ํ…Œ์ŠคํŠธ** + +- [x] ํฌ์ธํŠธ ์กฐํšŒ์— ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ๋ณด์œ  ํฌ์ธํŠธ๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค. +- [x] `X-USER-ID` ํ—ค๋”๊ฐ€ ์—†์„ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. + +### ํฌ์ธํŠธ ์ถฉ์ „ + +**๐Ÿงฑ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ** + +- [X] 0 ์ดํ•˜์˜ ์ •์ˆ˜๋กœ ํฌ์ธํŠธ๋ฅผ ์ถฉ์ „ ์‹œ ์‹คํŒจํ•œ๋‹ค. + +**๐Ÿ”— ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ** + +- [X] ์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ์ € ID ๋กœ ์ถฉ์ „์„ ์‹œ๋„ํ•œ ๊ฒฝ์šฐ, ์‹คํŒจํ•œ๋‹ค. + +**๐ŸŒ E2E ํ…Œ์ŠคํŠธ** + +- [X] ์กด์žฌํ•˜๋Š” ์œ ์ €๊ฐ€ 1000์›์„ ์ถฉ์ „ํ•  ๊ฒฝ์šฐ, ์ถฉ์ „๋œ ๋ณด์œ  ์ด๋Ÿ‰์„ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค. +- [X] ์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ์ €๋กœ ์š”์ฒญํ•  ๊ฒฝ์šฐ, `404 Not Found` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. + +## โœ… Checklist + +- [X] ํ…Œ์ŠคํŠธ๋Š” ๋ชจ๋‘ ํ†ต๊ณผํ•ด์•ผ ํ•˜๋ฉฐ, `@Test` ๊ธฐ๋ฐ˜์œผ๋กœ ๋ช…์‹œ์ ์œผ๋กœ ์ž‘์„ฑ +- [X] ๊ฐ ํ…Œ์ŠคํŠธ๋Š” ํ…Œ์ŠคํŠธ ๋ช…/์„ค๋ช…/์ž…๋ ฅ/์˜ˆ์ƒ ๊ฒฐ๊ณผ๊ฐ€ ๋ถ„๋ช…ํ•ด์•ผ ํ•จ +- [X] E2E ํ…Œ์ŠคํŠธ๋Š” ์‹ค์ œ HTTP ์š”์ฒญ์„ ํ†ตํ•ด ํ๋ฆ„์„ ๊ฒ€์ฆํ•  ๊ฒƒ \ No newline at end of file diff --git a/docs/2round/01-requirements.md b/docs/2round/01-requirements.md new file mode 100644 index 000000000..3296c21c6 --- /dev/null +++ b/docs/2round/01-requirements.md @@ -0,0 +1,104 @@ +# ์œ ์ €-์‹œ๋‚˜๋ฆฌ์˜ค + +## ์ƒํ’ˆ ๋ชฉ๋ก +1. ์‚ฌ์šฉ์ž๊ฐ€ ๋ชจ๋“  ์ƒํ’ˆ์— ๋Œ€ํ•ด์„œ ๋ณผ์ˆ˜ ์žˆ๋‹ค. +2. ํŒ๋งค์ค‘์ธ ์ƒํ’ˆ์— ๋Œ€ํ•œ ํŒ๋งค๋ช…, ํŒ๋งค๊ธˆ์•ก, ํŒ๋งค๋ธŒ๋žœ๋“œ, ์ด๋ฏธ์ง€, ์ข‹์•„์š”ํ•œ ์ƒํ’ˆ์„ ๋ณผ์ˆ˜์žˆ๋‹ค. +3. ์‚ฌ์šฉ์ž๋Š” ์›ํ•˜๋Š” ๋ธŒ๋žœ๋“œ๋ณ„๋กœ ์ƒํ’ˆ์„ ๋ณผ์ˆ˜ ์žˆ๋‹ค. +4. ์‚ฌ์šฉ์ž๋Š” ์›ํ•˜๋Š” ์ƒํ’ˆ์—๋Œ€ํ•ด์„œ ์ข‹์•„์š”๋ฅผ ๋ˆ„๋ฅผ์ˆ˜์žˆ๋‹ค. +5. ์‚ฌ์šฉ์ž๋Š” ์›ํ•˜๋Š” ์กฐ๊ฑด์— ๋”ฐ๋ผ ์ƒํ’ˆ์„ ๋ณผ์ˆ˜์žˆ๋‹ค. +6. ์‚ฌ์šฉ์ž๋Š” ์›ํ•˜๋Š” ํŽ˜์ด์ง€๋กœ ์ด๋™ํ• ์ˆ˜์žˆ๋‹ค. + +-[๊ธฐ๋Šฅ] +1. ์ „์ฒด ์ƒํ’ˆ ๋ชฉ๋ก ์กฐํšŒ +2. ๋ธŒ๋žœ๋“œ ์ƒํ’ˆ ๋ชฉ๋ก ์กฐํšŒ +3. ์ƒํ’ˆ ์ข‹์•„์š” ๋“ฑ๋ก +4. ํ•„ํ„ฐ๋ง (์ตœ์‹ ์ˆœ, ํŒ๋งค์ˆœ, ์ข‹์•„์š” ์ˆœ, ๋‚จ์ž/์—ฌ์ž) +5. ํŽ˜์ด์ง• + +-[์ œ์•ฝ] +1. ๋กœ๊ทธ์ธ์€ X-USER-ID๋กœ ํŒ๋‹จ, ๋น„ํšŒ์›์ผ๊ฒฝ์šฐ "๋กœ๊ทธ์ธ ํ›„ ์ข‹์•„์š” ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค" ๋ฉ”์‹œ์ง€ ์ „๋‹ฌํ•œ๋‹ค +2. ์ƒํ’ˆ์ด ์—†์„๊ฒฝ์šฐ 404_NOTFOUND ์ œ๊ณตํ•œ๋‹ค. + +--- +## ์ƒํ’ˆ ์ƒ์„ธ +1. ์‚ฌ์šฉ์ž๋Š” ์›ํ•˜๋Š” ํŒ๋งค์ค‘ ์ƒํ’ˆ(ํŒ๋งค๋ช…,ํŒ๋งค๊ธˆ์•ก,ํŒ๋งค๋ธŒ๋žœ๋“œ,์ด๋ฏธ์ง€,์ข‹์•„์š”ํ•œ ์ƒํ’ˆ์„ ๋ณผ์ˆ˜์žˆ๋‹ค. + -[๊ธฐ๋Šฅ] +1. ์‚ฌ์šฉ์ž๊ฐ€ ์›ํ•˜๋Š” ์ƒํ’ˆ๋ฒˆํ˜ธ๋กœ ์กฐํšŒ +2. ์ƒํ’ˆ ์ข‹์•„์š” ๋“ฑ๋ก / ์ทจ์†Œ + -[์ œ์•ฝ] +1. ๋กœ๊ทธ์ธ์€ X-USER-ID๋กœ ํŒ๋‹จ, ๋น„ํšŒ์›์ผ๊ฒฝ์šฐ "๋กœ๊ทธ์ธ ํ›„ ์ข‹์•„์š” ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค" ๋ฉ”์‹œ์ง€ ์ „๋‹ฌํ•œ๋‹ค +2. ์ƒํ’ˆ์ด ์กด์žฌํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ 404_NOTFOUND ์ œ๊ณตํ•œ๋‹ค. + +--- +## ์ข‹์•„์š” +1. ์‚ฌ์šฉ์ž๊ฐ€ ์›ํ•˜๋Š” ์ƒํ’ˆ์— ๋Œ€ํ•ด์„œ ์ข‹์•„์š”๋ฅผ ๋ˆ„๋ฅผ ์ˆ˜ ์žˆ๋‹ค. +2. ์‚ฌ์šฉ์ž๊ฐ€ ์›ํ•˜๋Š” ์ƒํ’ˆ์— ๋Œ€ํ•ด์„œ ์ข‹์•„์š”๋ฅผ ์ทจ์†Œ ํ•  ์ˆ˜ ์žˆ๋‹ค. +2. ์‚ฌ์šฉ์ž๊ฐ€ ์ข‹์•„ํ•˜๋Š” ์ƒํ’ˆ์— ๋Œ€ํ•ด ๋ชฉ๋ก์„ ๋ณผ์ˆ˜์žˆ๋‹ค. + -[๊ธฐ๋Šฅ] +1. ์ข‹์•„์š” ๋ˆ„๋ฅธ ์ƒํ’ˆ์—๋Œ€ํ•ด ๋ชฉ๋ก ์กฐํšŒ +2. ์‚ฌ์šฉ๊ฐ€ ์›ํ•˜๋Š” ์ƒํ’ˆ์— ๋Œ€ํ•œ ๋“ฑ๋ก/์ทจ์†Œ, ๋‹จ ๋“ฑ๋ก/ํ•ด์ œ (๋ฉฑ๋“ฑ์„ฑ) + -[์ œ์•ฝ] +1. ๋กœ๊ทธ์ธ์€ X-USER-ID๋กœ ํŒ๋‹จ, ๋น„ํšŒ์›์ผ๊ฒฝ์šฐ "๋กœ๊ทธ์ธ ํ›„ ์ข‹์•„์š” ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค" ๋ฉ”์‹œ์ง€ ์ „๋‹ฌํ•œ๋‹ค +2. ํ•ด๋‹น ์ƒํ’ˆ์— ์ข‹์•„์š”๋ฅผ ์ฒ˜์Œ ๋“ฑ๋ก ํ• ๋•Œ๋Š” 201_Created ์ œ๊ณตํ•œ๋‹ค +3. ํ•ด๋‹น ์ƒํ’ˆ์— ์ข‹์•„์š”๋ฅผ ํ•œ๋ฒˆ๋” ๋“ฑ๋ก ํ• ๋•Œ๋Š” 200_OK ์ œ๊ณตํ•œ๋‹ค +4. ํ•ด๋‹น ์ƒํ’ˆ์— ์ข‹์•„์š”๊ฐ€ ๋“ฑ๋ก ๋œ ์ƒํƒœ์—์„œ ์ทจ์†Œ ํ• ๋•Œ๋Š” 204 No Content ์ œ๊ณตํ•œ๋‹ค +5. ํ•ด๋‹น ์ƒํ’ˆ์— ์ข‹์•„์š”๊ฐ€ ์ทจ์†Œ๊ฐ€ ๋œ ์ƒํƒœ์—์„œ ํ•œ๋ฒˆ๋” ์ทจ์†Œ ํ• ๋•Œ๋Š” 204 No Content ์ œ๊ณตํ•œ๋‹ค +--- +## ๋ธŒ๋žœ๋“œ +1. ์‚ฌ์šฉ์ž๋Š” ๋ชจ๋“  ๋ธŒ๋žœ๋“œ์˜ ์ƒํ’ˆ์„ ๋ณผ์ˆ˜์žˆ๋‹ค. +2. ์‚ฌ์šฉ์ž๋Š” ํŠน์ • ๋ธŒ๋žœ๋“œ์— ๋Œ€ํ•œ ์ƒํ’ˆ๋งŒ ๋ณผ์ˆ˜์žˆ๋‹ค. +3. ์‚ฌ์šฉ์ž๋Š” ํŠน์ • ๊ธฐ์ค€์œผ๋กœ ์ƒํ’ˆ์„ ๋ณผ์ˆ˜์žˆ๋‹ค. (์ตœ์‹ ์ˆœ, ํŒ๋งค์ˆœ, ์ข‹์•„์š” ์ˆœ) +4. ์‚ฌ์šฉ์ž๋Š” ์›ํ•˜๋Š” ํŽ˜์ด์ง€๋กœ ์ด๋™ํ• ์ˆ˜์žˆ๋‹ค. + [๊ธฐ๋Šฅ] +1. ๋ชจ๋“  ๋ธŒ๋žœ๋“œ ์ƒํ’ˆ ์กฐํšŒ +2. ํŠน์ • ๋ธŒ๋žœ๋“œ ์ƒํ’ˆ ์กฐํšŒ +3. ํ•„ํ„ฐ๋ง (์ตœ์‹ ์ˆœ, ํŒ๋งค์ˆœ, ์ข‹์•„์š” ์ˆœ, ๋‚จ์ž/์—ฌ์ž) +4. ํŽ˜์ด์ง• + [์ œ์•ฝ] +1. ๋ธŒ๋žœ๋“œ๊ฐ€ ์—†์„์‹œ 404_NOTFOUND๋ฅผ ์ œ๊ณตํ•œ๋‹ค +--- +## ์ฃผ๋ฌธ +1. ์‚ฌ์šฉ์ž๊ฐ€ ์ƒํ’ˆ ๋ชฉ๋ก์—์„œ ์›ํ•˜๋Š” ์ƒํ’ˆ์„ ์„ ํƒํ•˜์—ฌ ์ฃผ๋ฌธํ•  ์ˆ˜ ์žˆ๋‹ค. +2. ํ•œ๊ฐœ์˜ ์ฃผ๋ฌธ์€ ์—ฌ๋Ÿฌ ์ƒํ’ˆ์„ ํฌํ•จํ•  ์ˆ˜ ์žˆ๋‹ค. +3. ์‚ฌ์šฉ์ž๊ฐ€ ์ฃผ๋ฌธ ๋‚ด์—ญ์„ ์กฐํšŒํ•ด ์–ด๋–ค ์ƒํ’ˆ์„ ์ฃผ๋ฌธํ–ˆ๋Š”์ง€ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค. +4. ์‚ฌ์šฉ์ž๋Š” ๊ฒฐ์ œ ์ „์ด๋ผ๋ฉด ์ฃผ๋ฌธ์„ ์ทจ์†Œํ•  ์ˆ˜ ์žˆ๋‹ค. +5. ์‚ฌ์šฉ์ž๋Š” ์ฃผ๋ฌธ ์ƒ์„ธ ํ™”๋ฉด์—์„œ ์ƒํ’ˆ ์ •๋ณด, ์ˆ˜๋Ÿ‰, ๊ฒฐ์ œ ๊ธˆ์•ก, ์ƒํƒœ ๋“ฑ์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค. + [๊ธฐ๋Šฅ] +1. ์ฃผ๋ฌธ ์ƒ์„ฑ +2. ์ฃผ๋ฌธ ๋ชฉ๋ก ์กฐํšŒ +3. ์ฃผ๋ฌธ ์ƒ์„ธ ์กฐํšŒ +4. ์ฃผ๋ฌธ ์ทจ์†Œ +5. ์ฃผ๋ฌธ์— ๋Œ€ํ•œ ์ƒํƒœ๊ด€๋ฆฌ + [์ œ์•ฝ] +1. ์ฃผ๋ฌธ ์‹œ ์ƒํ’ˆ ์žฌ๊ณ  ํ™•์ธ ๋ฐ ๋ถ€์กฑ ์‹œ ์˜ˆ์™ธ ๋ฐœ์ƒ +2. ํฌ์ธํŠธ ์ž”์•ก ๋ถ€์กฑ ์‹œ ์ฃผ๋ฌธ ๋ถˆ๊ฐ€ +3. ๋™์ผํ•œ ์ฃผ๋ฌธ ์š”์ฒญ์ด ์ค‘๋ณต์œผ๋กœ ๋“ค์–ด์™€๋„ ํ•œ ๋ฒˆ๋งŒ ์ฒ˜๋ฆฌ +--- +## ๊ฒฐ์ œ +1. ์‚ฌ์šฉ์ž๋Š” ์ฃผ๋ฌธํ•œ ์ƒํ’ˆ์— ๋Œ€ํ•ด ํฌ์ธํŠธ๋กœ ๊ฒฐ์ œํ•  ์ˆ˜ ์žˆ๋‹ค. +2. ๊ฒฐ์ œ๊ฐ€ ์™„๋ฃŒ๋˜๋ฉด ์ฃผ๋ฌธ ์ƒํƒœ๊ฐ€ ๊ฒฐ์ œ ์™„๋ฃŒ๋กœ ๋ณ€๊ฒฝ๋œ๋‹ค. +3. ๊ฒฐ์ œ ์ค‘ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ๊ฒฐ์ œ ์‹คํŒจ๋กœ ์ฒ˜๋ฆฌ๋˜๋ฉฐ ํฌ์ธํŠธ์™€ ์žฌ๊ณ ๋Š” ๋ณต๊ตฌ๋œ๋‹ค. +4. ๊ฒฐ์ œ ์™„๋ฃŒ ํ›„์—๋Š” ์ฃผ๋ฌธ ์ทจ์†Œ๊ฐ€ ๋ถˆ๊ฐ€๋Šฅํ•˜๋‹ค. + [๊ธฐ๋Šฅ] +1. ๊ฒฐ์ œ์š”์ฒญ +2. ๊ฒฐ์ œ ๊ฒฐ๊ณผ ๋ฐ˜์˜ +3. ๊ฒฐ์ œ ์‹คํŒจ ์ฒ˜๋ฆฌ +4. ๊ฒฐ์ œ ๋‚ด์—ญ ์กฐํšŒ + [์ œ์•ฝ] +1. ๋™์ผ ์ฃผ๋ฌธ์— ๋Œ€ํ•ด ์ค‘๋ณต ๊ฒฐ์ œ ์š”์ฒญ ์‹œ ํ•œ ๋ฒˆ๋งŒ ์ฒ˜๋ฆฌ +2. ํฌ์ธํŠธ ์ฐจ๊ฐ์‹คํŒจ ์‹œ ๋ณต๊ตฌ +3. ์™ธ๋ถ€๊ฒฐ์ œ ์‹œ์Šคํ…œ ๊ฒฐ์ œ ์‹œ์Šคํ…œ ์ฒ˜๋ฆฌ ์‹คํŒจ์‹œ ์˜ˆ์™ธ์ฒ˜๋ฆฌ + +---- +## Ubiquitous +| ํ•œ๊ตญ์–ด | ์˜์–ด | +|--------|------| +| ์‚ฌ์šฉ์ž | User | +| ํฌ์ธํŠธ | Point | +| ์ƒํ’ˆ | Product | +| ๋ธŒ๋žœ๋“œ | Brand | +| ์ข‹์•„์š” | Like | +| ์ฃผ๋ฌธ | Order | +| ์žฌ๊ณ  | Stock | +| ๊ฐ€๊ฒฉ | Price | +| ๊ฒฐ์ œ | Payment | \ No newline at end of file diff --git a/docs/2round/02-sequence-diagrams.md b/docs/2round/02-sequence-diagrams.md new file mode 100644 index 000000000..5264a4dc0 --- /dev/null +++ b/docs/2round/02-sequence-diagrams.md @@ -0,0 +1,164 @@ +# ์‹œํ€€์Šค ๋‹ค์ด์–ด๊ทธ๋žจ + +### 1. ์ƒํ’ˆ ๋ชฉ๋ก ์กฐํšŒ +```mermaid +sequenceDiagram + participant User + participant ProductController + participant ProductService + participant ProductRepository + participant BrandRepository + participant LikeRepository + + User->>ProductController: GET /api/v1/products + ProductController->>ProductService: getProductList + ProductService->>ProductRepository: findAllWithPaging + ProductService->>BrandRepository: findBrandInfoForProducts() + ProductService->>LikeRepository: countLikesForProducts() + ProductRepository-->>ProductService: productList + ProductService-->>ProductController: productListResponse + ProductController-->>User: 200 OK (์ƒํ’ˆ ๋ชฉ๋ก + ๋ธŒ๋žœ๋“œ + ์ข‹์•„์š” ์ˆ˜) +``` +--- +### 2. ์ƒํ’ˆ ์ƒ์„ธ ์กฐํšŒ +```mermaid +sequenceDiagram + participant User + participant ProductController + participant ProductService + participant ProductRepository + participant BrandRepository + participant LikeRepository + + User->>ProductController: GET /api/v1/products/{productId} + ProductController->>ProductService: getProductDetail(productId, userId) + ProductService->>ProductRepository: findById(productId) + ProductService->>BrandRepository: findBrandInfo(brandId) + ProductService->>LikeRepository: existsByUserIdAndProductId(userId, productId) + ProductRepository-->>ProductService: productDetail + ProductService-->>ProductController: productDetailResponse + ProductController-->>User: 200 OK (์ƒํ’ˆ ์ƒ์„ธ ์ •๋ณด) +``` +--- +### 3. ์ƒํ’ˆ ์ข‹์•„์š” ๋“ฑ๋ก/์ทจ์†Œ +```mermaid +sequenceDiagram + participant User + participant LikeController + participant LikeService + participant LikeRepository + + User->>LikeController: POST /api/v1/like/products/{productId} + LikeController->>LikeService: toggleLike(userId, productId) + LikeService->>LikeRepository: existsByUserIdAndProductId(userId, productId) + alt ์ข‹์•„์š”๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Œ + LikeService->>LikeRepository: save(userId, productId) + LikeService-->>LikeController: 201 Created + else ์ด๋ฏธ ์ข‹์•„์š” ๋˜์–ด์žˆ์Œ + LikeService->>LikeRepository: delete(userId, productId) + LikeService-->>LikeController: 204 No Content + end + LikeController-->>User: ์‘๋‹ต (์ƒํƒœ์ฝ”๋“œ์— ๋”ฐ๋ผ ๋‹ค๋ฆ„) +``` +--- + +### 4. ๋ธŒ๋žœ๋“œ๋ณ„ ์ƒํ’ˆ ์กฐํšŒ +```mermaid +sequenceDiagram + participant User + participant BrandController + participant BrandService + participant ProductRepository + participant BrandRepository + + User->>BrandController: GET /api/v1/brands/{brandId}/products + BrandController->>BrandService: getProductsByBrand(brandId, sort, page) + BrandService->>BrandRepository: findById(brandId) + BrandService->>ProductRepository: findByBrandId(brandId, sort, page) + BrandRepository-->>BrandService: brandInfo + ProductRepository-->>BrandService: productList + BrandService-->>BrandController: productListResponse + BrandController-->>User: 200 OK (๋ธŒ๋žœ๋“œ ์ƒํ’ˆ ๋ชฉ๋ก) +``` +--- +### 5. ์ฃผ๋ฌธ ์ƒ์„ฑ +```mermaid +sequenceDiagram + participant User + participant OrderController + participant OrderService + participant ProductReader + participant StockService + participant PointService + participant OrderRepository + + User->>OrderController: POST /api/v1/orders (items[]) + OrderController->>OrderService: createOrder(userId, items) + OrderService->>ProductReader: getProductsByIds(productIds) + loop ๊ฐ ์ƒํ’ˆ์— ๋Œ€ํ•ด + OrderService->>StockService: checkAndDecreaseStock(productId, quantity) + end + OrderService->>PointService: deductPoint(userId, totalPrice) + alt ์žฌ๊ณ  ๋˜๋Š” ํฌ์ธํŠธ ๋ถ€์กฑ + OrderService-->>OrderController: throw Exception + OrderController-->>User: 400 Bad Request + else ์ •์ƒ + OrderService->>OrderRepository: save(order, orderItems) + OrderService-->>OrderController: OrderResponse + OrderController-->>User: 201 Created (์ฃผ๋ฌธ ์™„๋ฃŒ) + end +``` +--- +### 6. ์ฃผ๋ฌธ ๋ชฉ๋ก ๋ฐ ์ƒ์„ธ ์กฐํšŒ +```mermaid +sequenceDiagram + participant User + participant OrderController + participant OrderService + participant OrderRepository + participant ProductRepository + + User->>OrderController: GET /api/v1/orders + OrderController->>OrderService: getOrderList(userId) + OrderService->>OrderRepository: findByUserId(userId) + OrderRepository-->>OrderService: orderList + OrderService-->>OrderController: orderListResponse + OrderController-->>User: 200 OK (์ฃผ๋ฌธ ๋ชฉ๋ก) + + User->>OrderController: GET /api/v1/orders/{orderId} + OrderController->>OrderService: getOrderDetail(orderId, userId) + OrderService->>OrderRepository: findById(orderId) + OrderService->>ProductRepository: findProductsInOrder(orderId) + OrderRepository-->>OrderService: orderDetail + OrderService-->>OrderController: orderDetailResponse + OrderController-->>User: 200 OK (์ฃผ๋ฌธ ์ƒ์„ธ) +``` +--- +### 7. ๊ฒฐ์ œ ์ฒ˜๋ฆฌ +```mermaid +sequenceDiagram + participant User + participant PaymentController + participant PaymentService + participant PaymentGateway + participant OrderRepository + participant PointService + participant StockService + + User->>PaymentController: POST /api/v1/payments (orderId) + PaymentController->>PaymentService: processPayment(orderId, userId) + PaymentService->>OrderRepository: findById(orderId) + PaymentService->>PaymentGateway: requestPayment(orderId, amount) + alt ๊ฒฐ์ œ ์„ฑ๊ณต + PaymentGateway-->>PaymentService: SUCCESS + PaymentService->>OrderRepository: updateStatus(orderId, PAID) + PaymentService-->>PaymentController: successResponse + PaymentController-->>User: 200 OK (๊ฒฐ์ œ ์™„๋ฃŒ) + else ๊ฒฐ์ œ ์‹คํŒจ + PaymentGateway-->>PaymentService: FAILED + PaymentService->>PointService: rollbackPoint(userId, amount) + PaymentService->>StockService: restoreStock(orderId) + PaymentService->>OrderRepository: updateStatus(orderId, FAILED) + PaymentController-->>User: 500 Internal Server Error (๊ฒฐ์ œ ์‹คํŒจ) + end +``` diff --git a/docs/2round/03-class-diagram.md b/docs/2round/03-class-diagram.md new file mode 100644 index 000000000..8d39cfd0a --- /dev/null +++ b/docs/2round/03-class-diagram.md @@ -0,0 +1,78 @@ +# ํด๋ž˜์Šค ๋‹ค์ด์–ด๊ทธ๋žจ + +```mermaid +classDiagram +direction TB + +class User { + Long id + String userId + String name + String email + String gender +} + +class Point { + Long id + String userId + Long balance +} + +class Brand { + Long id + String name +} + +class Product { + Long id + Long brandId + String name + Long price + Long likeCount; + Long stock +} + +class Like { + Long id + String userId + Long productId + LocalDateTime createdAt +} + +class Order { + Long id + String userId + Long totalPrice + OrderStatus status + LocalDateTime createdAt + List orderItems +} + +class OrderItem { + Long id + Order order + Long productId + String productName + Long quantity + Long price +} + +class Payment { + Long id + Long orderId + String status + String paymentRequestId + LocalDateTime createdAt +} + +%% ๊ด€๊ณ„ ์„ค์ • +User --> Point +Brand --> Product +Product --> Like +User --> Like +User --> Order +Order --> OrderItem +Order --> Payment +OrderItem --> Product + +``` \ No newline at end of file diff --git a/docs/2round/04-erd.md b/docs/2round/04-erd.md new file mode 100644 index 000000000..6389b2202 --- /dev/null +++ b/docs/2round/04-erd.md @@ -0,0 +1,74 @@ +# erd + +```mermaid +erDiagram + USER { + bigint id PK + varchar user_id + varchar name + varchar email + varchar gender + } + + POINT { + bigint id PK + varchar user_id FK + bigint balance + } + + BRAND { + bigint id PK + varchar name + } + + PRODUCT { + bigint id PK + bigint brand_id FK + varchar name + bigint price + bigint like_count + bigint stock + } + + LIKE { + bigint id PK + varchar user_id FK + bigint product_id FK + datetime created_at + } + + ORDERS { + bigint id PK + varchar user_id FK + bigint total_amount + varchar status + datetime created_at + } + + ORDER_ITEM { + bigint id PK + bigint order_id FK + bigint product_id FK + varchar product_name + bigint quantity + bigint price + } + + PAYMENT { + bigint id PK + bigint order_id FK + varchar status + varchar payment_request_id + datetime created_at + } + + %% ๊ด€๊ณ„ (cardinality) + USER ||--|| POINT : "1:1" + BRAND ||--o{ PRODUCT : "1:N" + PRODUCT ||--o{ LIKE : "1:N" + USER ||--o{ LIKE : "1:N" + USER ||--o{ ORDERS : "1:N" + ORDERS ||--o{ ORDER_ITEM : "1:N" + ORDER_ITEM }o--|| PRODUCT : "N:1" + ORDERS ||--|| PAYMENT : "1:1" +``` \ No newline at end of file diff --git a/docs/2round/2round.md b/docs/2round/2round.md new file mode 100644 index 000000000..84fdc982c --- /dev/null +++ b/docs/2round/2round.md @@ -0,0 +1,37 @@ +## โœ๏ธ Design Quest + +> **์ด์ปค๋จธ์Šค ๋„๋ฉ”์ธ(์ƒํ’ˆ, ๋ธŒ๋žœ๋“œ, ์ข‹์•„์š”, ์ฃผ๋ฌธ ๋“ฑ)์— ๋Œ€ํ•œ ์„ค๊ณ„**๋ฅผ ์™„๋ฃŒํ•˜๊ณ , ๋‹ค์Œ ์ฃผ๋ถ€ํ„ฐ ๊ฐœ๋ฐœ ๊ฐ€๋Šฅํ•œ ์ˆ˜์ค€์˜ ์„ค๊ณ„ ๋ฌธ์„œ๋ฅผ ์ •๋ฆฌํ•˜์—ฌ PR๋กœ ์ œ์ถœํ•ฉ๋‹ˆ๋‹ค. +> + +### ๐Ÿ“‹ ๊ณผ์ œ ์ •๋ณด + +- **์„ค๊ณ„ ๋ฒ”์œ„** + - ์ƒํ’ˆ ๋ชฉ๋ก / ์ƒํ’ˆ ์ƒ์„ธ / ๋ธŒ๋žœ๋“œ ์กฐํšŒ + - ์ƒํ’ˆ ์ข‹์•„์š” ๋“ฑ๋ก/์ทจ์†Œ (๋ฉฑ๋“ฑ ๋™์ž‘) + - ์ฃผ๋ฌธ ์ƒ์„ฑ ๋ฐ ๊ฒฐ์ œ ํ๋ฆ„ (์žฌ๊ณ  ์ฐจ๊ฐ, ํฌ์ธํŠธ ์ฐจ๊ฐ, ์™ธ๋ถ€ ์‹œ์Šคํ…œ ์—ฐ๋™) +- **์ œ์™ธ ๋„๋ฉ”์ธ** + - ํšŒ์›๊ฐ€์ž…, ํฌ์ธํŠธ ์ถฉ์ „ (1์ฃผ์ฐจ ๊ตฌํ˜„ ์™„๋ฃŒ ๊ธฐ์ค€) +- **์š”๊ตฌ์‚ฌํ•ญ ๊ธฐ๋ฐ˜** + - ๋ฃจํ”„ํŒฉ ์ด์ปค๋จธ์Šค ์‹œ๋‚˜๋ฆฌ์˜ค ๋ฌธ์„œ๋ฅผ ๊ธฐ์ค€์œผ๋กœ ๊ธฐ๋Šฅ/์ œ์•ฝ์‚ฌํ•ญ์„ ์„ค๊ณ„์— ๋ฐ˜์˜ํ•ฉ๋‹ˆ๋‹ค. +- **์ œ์ถœ ๋ฐฉ์‹** + 1. ์•„๋ž˜ ํŒŒ์ผ๋“ค์„ ํ”„๋กœ์ ํŠธ ๋‚ด `docs/week2/` ํด๋”์— `.md`๋กœ ์ €์žฅ + 2. Github PR๋กœ ์ œ์ถœ + - PR ์ œ๋ชฉ: `[2์ฃผ์ฐจ] ์„ค๊ณ„ ๋ฌธ์„œ ์ œ์ถœ - ํ™๊ธธ๋™` + - PR ๋ณธ๋ฌธ์— ๋ฆฌ๋ทฐ ํฌ์ธํŠธ ํฌํ•จ (์˜ˆ: ๊ณ ๋ฏผํ•œ ์ง€์  ๋“ฑ) + +### โœ… ์ œ์ถœ ํŒŒ์ผ ๋ชฉ๋ก (.docs/design ๋””๋ ‰ํ† ๋ฆฌ ๋‚ด) + +| ํŒŒ์ผ๋ช… | ๋‚ด์šฉ | +| --- | --- | +| `01-requirements.md` | ์œ ์ € ์‹œ๋‚˜๋ฆฌ์˜ค ๊ธฐ๋ฐ˜ ๊ธฐ๋Šฅ ์ •์˜, ์š”๊ตฌ์‚ฌํ•ญ ๋ช…์„ธ | +| `02-sequence-diagrams.md` | ์‹œํ€€์Šค ๋‹ค์ด์–ด๊ทธ๋žจ ์ตœ์†Œ 2๊ฐœ ์ด์ƒ (Mermaid ๊ธฐ๋ฐ˜ ์ž‘์„ฑ ๊ถŒ์žฅ) | +| `03-class-diagram.md` | ๋„๋ฉ”์ธ ๊ฐ์ฒด ์„ค๊ณ„ (ํด๋ž˜์Šค ๋‹ค์ด์–ด๊ทธ๋žจ or ์„ค๋ช… ์ค‘์‹ฌ) | +| `04-erd.md` | ์ „์ฒด ํ…Œ์ด๋ธ” ๊ตฌ์กฐ ๋ฐ ๊ด€๊ณ„ ์ •๋ฆฌ (ERD Mermaid ์ž‘์„ฑ ๊ฐ€๋Šฅ) | + +## โœ… Checklist + +- [ ] ์ƒํ’ˆ/๋ธŒ๋žœ๋“œ/์ข‹์•„์š”/์ฃผ๋ฌธ ๋„๋ฉ”์ธ์ด ๋ชจ๋‘ ํฌํ•จ๋˜์–ด ์žˆ๋Š”๊ฐ€? +- [ ] ๊ธฐ๋Šฅ ์š”๊ตฌ์‚ฌํ•ญ์ด ์œ ์ € ์ค‘์‹ฌ์œผ๋กœ ์ •๋ฆฌ๋˜์–ด ์žˆ๋Š”๊ฐ€? +- [ ] ์‹œํ€€์Šค ๋‹ค์ด์–ด๊ทธ๋žจ์—์„œ ์ฑ…์ž„ ๊ฐ์ฒด๊ฐ€ ๋“œ๋Ÿฌ๋‚˜๋Š”๊ฐ€? +- [ ] ํด๋ž˜์Šค ๊ตฌ์กฐ๊ฐ€ ๋„๋ฉ”์ธ ์„ค๊ณ„๋ฅผ ์ž˜ ํ‘œํ˜„ํ•˜๊ณ  ์žˆ๋Š”๊ฐ€? +- [ ] ERD ์„ค๊ณ„ ์‹œ ๋ฐ์ดํ„ฐ ์ •ํ•ฉ์„ฑ์„ ๊ณ ๋ คํ•˜์—ฌ ๊ตฌ์„ฑํ•˜์˜€๋Š”๊ฐ€? \ No newline at end of file diff --git a/docs/3round/3round.md b/docs/3round/3round.md new file mode 100644 index 000000000..b9f333cca --- /dev/null +++ b/docs/3round/3round.md @@ -0,0 +1,60 @@ +# ๐Ÿ“ Round 3 Quests + +--- + +## ๐Ÿ’ป Implementation Quest + +> *** ๋„๋ฉ”์ธ ๋ชจ๋ธ๋ง**์„ ํ†ตํ•ด Product, Brand, Like, Order ๊ธฐ๋Šฅ์˜ ํ•ต์‹ฌ ๊ฐœ๋…์„ **Entity, Value Object, Domain Service ๋“ฑ ์ ํ•ฉํ•œ** **์ฝ”๋“œ**๋กœ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค. +* ๋ ˆ์ด์–ด๋“œ ์•„ํ‚คํ…์ฒ˜ + DIP ๋ฅผ ์ ์šฉํ•ด ์œ ์—ฐํ•˜๊ณ  ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅํ•œ ๊ตฌ์กฐ๋กœ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค. +* **Application Layer๋ฅผ ๊ฒฝ๋Ÿ‰ ์ˆ˜์ค€**์œผ๋กœ ๊ตฌ์„ฑํ•˜์—ฌ, ๋„๋ฉ”์ธ ๊ฐ์ฒด๋ฅผ ์กฐํ•ฉํ•˜๋Š” ํ๋ฆ„์„ ์‹ค์ œ ๊ตฌํ˜„ํ•ด๋ด…๋‹ˆ๋‹ค. +* **๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑ**ํ•˜์—ฌ ๋„๋ฉ”์ธ ๋กœ์ง์˜ ์ •ํ•ฉ์„ฑ๊ณผ ๊ทœ์น™์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. +> + +### ๐Ÿ“‹ ๊ณผ์ œ ์ •๋ณด + +- ์ƒํ’ˆ, ๋ธŒ๋žœ๋“œ, ์ข‹์•„์š”, ์ฃผ๋ฌธ ๊ธฐ๋Šฅ์˜ **๋„๋ฉ”์ธ ๋ชจ๋ธ ๋ฐ ๋„๋ฉ”์ธ ์„œ๋น„์Šค**๋ฅผ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค. +- ๋„๋ฉ”์ธ ๊ฐ„ ํ˜‘๋ ฅ ํ๋ฆ„์„ ์„ค๊ณ„ํ•˜๊ณ , ํ•„์š”ํ•œ ๋กœ์ง์„ **๋„๋ฉ”์ธ ์„œ๋น„์Šค**๋กœ ๋ถ„๋ฆฌํ•ฉ๋‹ˆ๋‹ค. +- Application Layer์—์„œ ๋„๋ฉ”์ธ ๊ฐ์ฒด๋ฅผ ์กฐํ•ฉํ•˜๋Š” ํ๋ฆ„์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค. + (์˜ˆ: `ProductFacade.getProductDetail(productId)` โ†’ `Product + Brand + Like ์กฐํ•ฉ`) +- Repository Interface ์™€ ๊ตฌํ˜„์ฒด๋Š” ๋ถ„๋ฆฌํ•˜๊ณ , ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅ์„ฑ์„ ๊ณ ๋ คํ•œ ๊ตฌ์กฐ๋ฅผ ์„ค๊ณ„ํ•ฉ๋‹ˆ๋‹ค. +- ๋ชจ๋“  ํ•ต์‹ฌ ๋„๋ฉ”์ธ ๋กœ์ง์— ๋Œ€ํ•ด ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•˜๊ณ , ์˜ˆ์™ธ/๊ฒฝ๊ณ„ ์ผ€์ด์Šค๋„ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. + +### ๐Ÿท Product / Brand ๋„๋ฉ”์ธ + +## โœ… Checklist + +- [x] ์ƒํ’ˆ ์ •๋ณด ๊ฐ์ฒด๋Š” ๋ธŒ๋žœ๋“œ ์ •๋ณด, ์ข‹์•„์š” ์ˆ˜๋ฅผ ํฌํ•จํ•œ๋‹ค. +- [x] ์ƒํ’ˆ์˜ ์ •๋ ฌ ์กฐ๊ฑด(`latest`, `price_asc`, `likes_desc`) ์„ ๊ณ ๋ คํ•œ ์กฐํšŒ ๊ธฐ๋Šฅ์„ ์„ค๊ณ„ํ–ˆ๋‹ค +- [x] ์ƒํ’ˆ์€ ์žฌ๊ณ ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๊ณ , ์ฃผ๋ฌธ ์‹œ ์ฐจ๊ฐํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•œ๋‹ค +- [x] ์žฌ๊ณ ๋Š” ๊ฐ์†Œ๋งŒ ๊ฐ€๋Šฅํ•˜๋ฉฐ ์Œ์ˆ˜ ๋ฐฉ์ง€๋Š” ๋„๋ฉ”์ธ ๋ ˆ๋ฒจ์—์„œ ์ฒ˜๋ฆฌ๋œ๋‹ค + +### ๐Ÿ‘ Like ๋„๋ฉ”์ธ + +- [x] ์ข‹์•„์š”๋Š” ์œ ์ €์™€ ์ƒํ’ˆ ๊ฐ„์˜ ๊ด€๊ณ„๋กœ ๋ณ„๋„ ๋„๋ฉ”์ธ์œผ๋กœ ๋ถ„๋ฆฌํ–ˆ๋‹ค +- [x] ์ค‘๋ณต ์ข‹์•„์š” ๋ฐฉ์ง€๋ฅผ ์œ„ํ•œ ๋ฉฑ๋“ฑ์„ฑ ์ฒ˜๋ฆฌ๊ฐ€ ๊ตฌํ˜„๋˜์—ˆ๋‹ค +- [x] ์ƒํ’ˆ์˜ ์ข‹์•„์š” ์ˆ˜๋Š” ์ƒํ’ˆ ์ƒ์„ธ/๋ชฉ๋ก ์กฐํšŒ์—์„œ ํ•จ๊ป˜ ์ œ๊ณต๋œ๋‹ค +- [x] ๋‹จ์œ„ ํ…Œ์ŠคํŠธ์—์„œ ์ข‹์•„์š” ๋“ฑ๋ก/์ทจ์†Œ/์ค‘๋ณต ๋ฐฉ์ง€ ํ๋ฆ„์„ ๊ฒ€์ฆํ–ˆ๋‹ค + +### ๐Ÿ›’ Order ๋„๋ฉ”์ธ + +- [x] ์ฃผ๋ฌธ์€ ์—ฌ๋Ÿฌ ์ƒํ’ˆ์„ ํฌํ•จํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ๊ฐ ์ƒํ’ˆ์˜ ์ˆ˜๋Ÿ‰์„ ๋ช…์‹œํ•œ๋‹ค +- [x] ์ฃผ๋ฌธ ์‹œ ์ƒํ’ˆ์˜ ์žฌ๊ณ  ์ฐจ๊ฐ, ์œ ์ € ํฌ์ธํŠธ ์ฐจ๊ฐ ๋“ฑ์„ ์ˆ˜ํ–‰ํ•œ๋‹ค +- [x] ์žฌ๊ณ  ๋ถ€์กฑ, ํฌ์ธํŠธ ๋ถ€์กฑ ๋“ฑ ์˜ˆ์™ธ ํ๋ฆ„์„ ๊ณ ๋ คํ•ด ์„ค๊ณ„๋˜์—ˆ๋‹ค +- [x] ๋‹จ์œ„ ํ…Œ์ŠคํŠธ์—์„œ ์ •์ƒ ์ฃผ๋ฌธ / ์˜ˆ์™ธ ์ฃผ๋ฌธ ํ๋ฆ„์„ ๋ชจ๋‘ ๊ฒ€์ฆํ–ˆ๋‹ค + +### ๐Ÿงฉ ๋„๋ฉ”์ธ ์„œ๋น„์Šค + +- [x] ๋„๋ฉ”์ธ ๊ฐ„ ํ˜‘๋ ฅ ๋กœ์ง์€ Domain Service์— ์œ„์น˜์‹œ์ผฐ๋‹ค +- [x] ์ƒํ’ˆ ์ƒ์„ธ ์กฐํšŒ ์‹œ Product + Brand ์ •๋ณด ์กฐํ•ฉ์€ ๋„๋ฉ”์ธ ์„œ๋น„์Šค์—์„œ ์ฒ˜๋ฆฌํ–ˆ๋‹ค +- [x] ๋ณตํ•ฉ ์œ ์Šค์ผ€์ด์Šค๋Š” Application Layer์— ์กด์žฌํ•˜๊ณ , ๋„๋ฉ”์ธ ๋กœ์ง์€ ์œ„์ž„๋˜์—ˆ๋‹ค +- [x] ๋„๋ฉ”์ธ ์„œ๋น„์Šค๋Š” ์ƒํƒœ ์—†์ด, ๋„๋ฉ”์ธ ๊ฐ์ฒด์˜ ํ˜‘๋ ฅ ์ค‘์‹ฌ์œผ๋กœ ์„ค๊ณ„๋˜์—ˆ๋‹ค + +### **๐Ÿงฑ ์†Œํ”„ํŠธ์›จ์–ด ์•„ํ‚คํ…์ฒ˜ & ์„ค๊ณ„** + +- [x] ์ „์ฒด ํ”„๋กœ์ ํŠธ์˜ ๊ตฌ์„ฑ์€ ์•„๋ž˜ ์•„ํ‚คํ…์ฒ˜๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๊ตฌ์„ฑ๋˜์—ˆ๋‹ค + - Application โ†’ **Domain** โ† Infrastructure +- [x] Application Layer๋Š” ๋„๋ฉ”์ธ ๊ฐ์ฒด๋ฅผ ์กฐํ•ฉํ•ด ํ๋ฆ„์„ orchestration ํ–ˆ๋‹ค +- [x] ํ•ต์‹ฌ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์€ Entity, VO, Domain Service ์— ์œ„์น˜ํ•œ๋‹ค +- [x] Repository Interface๋Š” Domain Layer ์— ์ •์˜๋˜๊ณ , ๊ตฌํ˜„์ฒด๋Š” Infra์— ์œ„์น˜ํ•œ๋‹ค +- [x] ํŒจํ‚ค์ง€๋Š” ๊ณ„์ธต + ๋„๋ฉ”์ธ ๊ธฐ์ค€์œผ๋กœ ๊ตฌ์„ฑ๋˜์—ˆ๋‹ค (`/domain/order`, `/application/like` ๋“ฑ) +- [x] ํ…Œ์ŠคํŠธ๋Š” ์™ธ๋ถ€ ์˜์กด์„ฑ์„ ๋ถ„๋ฆฌํ•˜๊ณ , Fake/Stub ๋“ฑ์„ ์‚ฌ์šฉํ•ด ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๊ฐ€ ๊ฐ€๋Šฅํ•˜๊ฒŒ ๊ตฌ์„ฑ๋˜์—ˆ๋‹ค \ No newline at end of file From 4ca321e6e8bf7b9d69231a23d46ab44a893e5d93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A0=95=EB=B0=B0?= Date: Tue, 25 Nov 2025 11:28:12 +0900 Subject: [PATCH 64/85] =?UTF-8?q?round3:=20=EC=BD=94=EB=93=9C=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0,=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=98=A4=EB=A5=98?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/order/OrderFacade.java | 32 ++----- .../application/order/OrderItemRequest.java | 7 ++ .../application/order/OrderRequest.java | 8 ++ .../loopers/domain/order/OrderService.java | 20 ++++ .../interfaces/api/order/OrderV1ApiSpec.java | 4 +- .../api/order/OrderV1Controller.java | 3 +- .../interfaces/api/order/OrderV1Dto.java | 9 -- .../order/OrderFacadeIntegrationTest.java | 57 ++++++----- .../order/OrderServiceIntegrationTest.java | 28 +++--- .../interfaces/api/OrderV1ApiE2ETest.java | 96 ++++++++++--------- 10 files changed, 140 insertions(+), 124 deletions(-) create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/order/OrderItemRequest.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/order/OrderRequest.java diff --git a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java index e4725ada1..893428a79 100644 --- a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java @@ -1,7 +1,6 @@ package com.loopers.application.order; import com.loopers.domain.order.Order; -import com.loopers.domain.order.OrderItem; import com.loopers.domain.order.OrderService; import com.loopers.domain.point.PointService; import com.loopers.domain.product.Product; @@ -9,7 +8,6 @@ import com.loopers.domain.supply.SupplyService; import com.loopers.domain.user.User; import com.loopers.domain.user.UserService; -import com.loopers.interfaces.api.order.OrderV1Dto; import com.loopers.support.error.CoreException; import com.loopers.support.error.ErrorType; import lombok.RequiredArgsConstructor; @@ -18,7 +16,6 @@ import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; -import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -31,6 +28,7 @@ public class OrderFacade { private final PointService pointService; private final SupplyService supplyService; + @Transactional(readOnly = true) public OrderInfo getOrderInfo(String userId, Long orderId) { User user = userService.findByUserId(userId).orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); Order order = orderService.getOrderByIdAndUserId(orderId, user.getId()); @@ -46,38 +44,22 @@ public Page getOrderList(String userId, Pageable pageable) { } @Transactional - public OrderInfo createOrder(String userId, OrderV1Dto.OrderRequest request) { + public OrderInfo createOrder(String userId, OrderRequest request) { User user = userService.findByUserId(userId).orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); - // request์—์„œ productId - quantity ๋งต ์ƒ์„ฑ - Map productQuantityMap = request.items().stream() - .collect(Collectors.toMap( - OrderV1Dto.OrderRequest.OrderItemRequest::productId, - OrderV1Dto.OrderRequest.OrderItemRequest::quantity - )); + Map productIdQuantityMap = request.items().stream() + .collect(Collectors.toMap(OrderItemRequest::productId, OrderItemRequest::quantity)); - Map productMap = productService.getProductMapByIds(productQuantityMap.keySet()); + Map productMap = productService.getProductMapByIds(productIdQuantityMap.keySet()); request.items().forEach(item -> { supplyService.checkAndDecreaseStock(item.productId(), item.quantity()); }); - Integer totalAmount = productService.calculateTotalAmount(productQuantityMap); - + Integer totalAmount = productService.calculateTotalAmount(productIdQuantityMap); pointService.checkAndDeductPoint(user.getId(), totalAmount); - List orderItems = request.items() - .stream() - .map(item -> OrderItem.create( - item.productId(), - productMap.get(item.productId()).getName(), - item.quantity(), - productMap.get(item.productId()).getPrice() - )) - .toList(); - Order order = Order.create(user.getId(), orderItems); - - orderService.save(order); + Order order = orderService.createOrder(request.items(), productMap, user.getId()); return OrderInfo.from(order); } diff --git a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderItemRequest.java b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderItemRequest.java new file mode 100644 index 000000000..cf4984f10 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderItemRequest.java @@ -0,0 +1,7 @@ +package com.loopers.application.order; + +public record OrderItemRequest( + Long productId, + Integer quantity +) { +} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderRequest.java b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderRequest.java new file mode 100644 index 000000000..63f746a8f --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderRequest.java @@ -0,0 +1,8 @@ +package com.loopers.application.order; + +import java.util.List; + +public record OrderRequest( + List items +) { +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java index 7628720f4..979d19fbd 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java @@ -1,5 +1,7 @@ package com.loopers.domain.order; +import com.loopers.application.order.OrderItemRequest; +import com.loopers.domain.product.Product; import com.loopers.support.error.CoreException; import com.loopers.support.error.ErrorType; import lombok.RequiredArgsConstructor; @@ -9,6 +11,9 @@ import org.springframework.data.domain.Sort; import org.springframework.stereotype.Component; +import java.util.List; +import java.util.Map; + @RequiredArgsConstructor @Component public class OrderService { @@ -18,6 +23,21 @@ public Order save(Order order) { return orderRepository.save(order); } + public Order createOrder(List OrderItems, Map productMap, Long userId) { + List orderItems = OrderItems + .stream() + .map(item -> OrderItem.create( + item.productId(), + productMap.get(item.productId()).getName(), + item.quantity(), + productMap.get(item.productId()).getPrice() + )) + .toList(); + Order order = Order.create(userId, orderItems); + + return orderRepository.save(order); + } + public Order getOrderByIdAndUserId(Long orderId, Long userId) { return orderRepository.findByIdAndUserId(orderId, userId) .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "์ฃผ๋ฌธ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1ApiSpec.java index 57197cb68..37259802d 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1ApiSpec.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1ApiSpec.java @@ -1,10 +1,10 @@ package com.loopers.interfaces.api.order; +import com.loopers.application.order.OrderRequest; import com.loopers.interfaces.api.ApiResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.tags.Tag; -import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; import org.springframework.web.bind.annotation.RequestHeader; @@ -23,7 +23,7 @@ ApiResponse createOrder( name = "์ฃผ๋ฌธ ์š”์ฒญ ์ •๋ณด", description = "์ฃผ๋ฌธ ์ƒ์„ฑ์— ํ•„์š”ํ•œ ์ •๋ณด" ) - OrderV1Dto.OrderRequest request + OrderRequest request ); // /api/v1/orders - GET diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Controller.java index ae8f75ae3..0daeabd69 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Controller.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Controller.java @@ -2,6 +2,7 @@ import com.loopers.application.order.OrderFacade; import com.loopers.application.order.OrderInfo; +import com.loopers.application.order.OrderRequest; import com.loopers.interfaces.api.ApiResponse; import com.loopers.support.error.CoreException; import com.loopers.support.error.ErrorType; @@ -22,7 +23,7 @@ public class OrderV1Controller implements OrderV1ApiSpec { @Override public ApiResponse createOrder( @RequestHeader(value = "X-USER-ID", required = false) String userId, - @RequestBody OrderV1Dto.OrderRequest request + @RequestBody OrderRequest request ) { if (StringUtils.isBlank(userId)) { throw new CoreException(ErrorType.BAD_REQUEST); diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Dto.java index 01b97d12e..9b5312231 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Dto.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Dto.java @@ -7,15 +7,6 @@ import java.util.List; public class OrderV1Dto { - public record OrderRequest( - List items - ) { - public record OrderItemRequest( - Long productId, - Integer quantity - ) { - } - } public record OrderResponse( Long orderId, diff --git a/apps/commerce-api/src/test/java/com/loopers/application/order/OrderFacadeIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/application/order/OrderFacadeIntegrationTest.java index 7adbb731d..280d05573 100644 --- a/apps/commerce-api/src/test/java/com/loopers/application/order/OrderFacadeIntegrationTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/application/order/OrderFacadeIntegrationTest.java @@ -14,7 +14,6 @@ import com.loopers.infrastructure.product.ProductJpaRepository; import com.loopers.infrastructure.supply.SupplyJpaRepository; import com.loopers.infrastructure.user.UserJpaRepository; -import com.loopers.interfaces.api.order.OrderV1Dto; import com.loopers.support.error.CoreException; import com.loopers.support.error.ErrorType; import com.loopers.utils.DatabaseCleanUp; @@ -128,10 +127,10 @@ class CreateOrder { @Test void should_createOrder_when_validRequest() { // arrange - OrderV1Dto.OrderRequest request = new OrderV1Dto.OrderRequest( + OrderRequest request = new OrderRequest( List.of( - new OrderV1Dto.OrderRequest.OrderItemRequest(productId1, 2), - new OrderV1Dto.OrderRequest.OrderItemRequest(productId2, 1) + new OrderItemRequest(productId1, 2), + new OrderItemRequest(productId2, 1) ) ); @@ -150,9 +149,9 @@ void should_createOrder_when_validRequest() { void should_throwException_when_productIdDoesNotExist() { // arrange Long nonExistentProductId = 99999L; - OrderV1Dto.OrderRequest request = new OrderV1Dto.OrderRequest( + OrderRequest request = new OrderRequest( List.of( - new OrderV1Dto.OrderRequest.OrderItemRequest(nonExistentProductId, 1) + new OrderItemRequest(nonExistentProductId, 1) ) ); @@ -166,9 +165,9 @@ void should_throwException_when_productIdDoesNotExist() { @Test void should_throwException_when_singleProductStockInsufficient() { // arrange - OrderV1Dto.OrderRequest request = new OrderV1Dto.OrderRequest( + OrderRequest request = new OrderRequest( List.of( - new OrderV1Dto.OrderRequest.OrderItemRequest(productId1, 99999) + new OrderItemRequest(productId1, 99999) ) ); @@ -184,10 +183,10 @@ void should_throwException_when_partialStockInsufficient() { // arrange // productId1: ์žฌ๊ณ  100, productId2: ์žฌ๊ณ  50 // productId1์€ ์ถฉ๋ถ„ํ•˜์ง€๋งŒ productId2๋Š” ๋ถ€์กฑ - OrderV1Dto.OrderRequest request = new OrderV1Dto.OrderRequest( + OrderRequest request = new OrderRequest( List.of( - new OrderV1Dto.OrderRequest.OrderItemRequest(productId1, 10), // ์žฌ๊ณ  ์ถฉ๋ถ„ - new OrderV1Dto.OrderRequest.OrderItemRequest(productId2, 99999) // ์žฌ๊ณ  ๋ถ€์กฑ + new OrderItemRequest(productId1, 10), // ์žฌ๊ณ  ์ถฉ๋ถ„ + new OrderItemRequest(productId2, 99999) // ์žฌ๊ณ  ๋ถ€์กฑ ) ); @@ -203,10 +202,10 @@ void should_throwException_when_partialStockInsufficient() { @Test void should_throwException_when_allProductsStockInsufficient() { // arrange - OrderV1Dto.OrderRequest request = new OrderV1Dto.OrderRequest( + OrderRequest request = new OrderRequest( List.of( - new OrderV1Dto.OrderRequest.OrderItemRequest(productId1, 99999), - new OrderV1Dto.OrderRequest.OrderItemRequest(productId2, 99999) + new OrderItemRequest(productId1, 99999), + new OrderItemRequest(productId2, 99999) ) ); @@ -228,9 +227,9 @@ void should_throwException_when_supplyDoesNotExist() { productMetricsJpaRepository.save(metrics); // Supply๋Š” ์ƒ์„ฑํ•˜์ง€ ์•Š์Œ - OrderV1Dto.OrderRequest request = new OrderV1Dto.OrderRequest( + OrderRequest request = new OrderRequest( List.of( - new OrderV1Dto.OrderRequest.OrderItemRequest(savedProduct.getId(), 1) + new OrderItemRequest(savedProduct.getId(), 1) ) ); @@ -245,17 +244,17 @@ void should_throwException_when_supplyDoesNotExist() { void should_throwException_when_pointInsufficient() { // arrange // ํฌ์ธํŠธ๋ฅผ ๋ชจ๋‘ ์‚ฌ์šฉ - OrderV1Dto.OrderRequest firstOrder = new OrderV1Dto.OrderRequest( + OrderRequest firstOrder = new OrderRequest( List.of( - new OrderV1Dto.OrderRequest.OrderItemRequest(productId1, 10) + new OrderItemRequest(productId1, 10) ) ); orderFacade.createOrder(userId, firstOrder); // ํฌ์ธํŠธ ๋ถ€์กฑํ•œ ์ฃผ๋ฌธ ์‹œ๋„ - OrderV1Dto.OrderRequest request = new OrderV1Dto.OrderRequest( + OrderRequest request = new OrderRequest( List.of( - new OrderV1Dto.OrderRequest.OrderItemRequest(productId2, 1) // 20000์› ํ•„์š” (๋ถ€์กฑ) + new OrderItemRequest(productId2, 1) // 20000์› ํ•„์š” (๋ถ€์กฑ) ) ); @@ -271,18 +270,18 @@ void should_throwException_when_pointInsufficient() { void should_createOrder_when_pointExactlyMatches() { // arrange // ํฌ์ธํŠธ๋ฅผ ๊ฑฐ์˜ ๋ชจ๋‘ ์‚ฌ์šฉ - OrderV1Dto.OrderRequest firstOrder = new OrderV1Dto.OrderRequest( + OrderRequest firstOrder = new OrderRequest( List.of( - new OrderV1Dto.OrderRequest.OrderItemRequest(productId1, 9) // 90000์› ์‚ฌ์šฉ + new OrderItemRequest(productId1, 9) // 90000์› ์‚ฌ์šฉ ) ); orderFacade.createOrder(userId, firstOrder); // ๋‚จ์€ ํฌ์ธํŠธ: 10000์› // ์ •ํ™•ํžˆ ์ผ์น˜ํ•˜๋Š” ์ฃผ๋ฌธ - OrderV1Dto.OrderRequest request = new OrderV1Dto.OrderRequest( + OrderRequest request = new OrderRequest( List.of( - new OrderV1Dto.OrderRequest.OrderItemRequest(productId1, 1) // ์ •ํ™•ํžˆ 10000์› + new OrderItemRequest(productId1, 1) // ์ •ํ™•ํžˆ 10000์› ) ); @@ -300,10 +299,10 @@ void should_throwException_when_duplicateProducts() { // arrange // ๊ฐ™์€ ์ƒํ’ˆ์„ ์—ฌ๋Ÿฌ ๋ฒˆ ์ฃผ๋ฌธ ํ•ญ๋ชฉ์— ํฌํ•จ // Note: Collectors.toMap()์€ ์ค‘๋ณต ํ‚ค๊ฐ€ ์žˆ์œผ๋ฉด IllegalStateException์„ ๋ฐœ์ƒ์‹œํ‚ด - OrderV1Dto.OrderRequest request = new OrderV1Dto.OrderRequest( + OrderRequest request = new OrderRequest( List.of( - new OrderV1Dto.OrderRequest.OrderItemRequest(productId1, 2), - new OrderV1Dto.OrderRequest.OrderItemRequest(productId1, 3) // ์ค‘๋ณต + new OrderItemRequest(productId1, 2), + new OrderItemRequest(productId1, 3) // ์ค‘๋ณต ) ); @@ -320,9 +319,9 @@ void should_throwException_when_duplicateProducts() { void should_throwException_when_userDoesNotExist() { // arrange String nonExistentUserId = "nonexist"; - OrderV1Dto.OrderRequest request = new OrderV1Dto.OrderRequest( + OrderRequest request = new OrderRequest( List.of( - new OrderV1Dto.OrderRequest.OrderItemRequest(productId1, 1) + new OrderItemRequest(productId1, 1) ) ); diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderServiceIntegrationTest.java index 1e524bdf0..b3c481603 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderServiceIntegrationTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderServiceIntegrationTest.java @@ -1,6 +1,8 @@ package com.loopers.domain.order; +import com.loopers.application.order.OrderItemRequest; import com.loopers.domain.common.vo.Price; +import com.loopers.domain.product.Product; import com.loopers.support.error.CoreException; import com.loopers.support.error.ErrorType; import jakarta.transaction.Transactional; @@ -12,6 +14,7 @@ import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; import java.util.List; +import java.util.Map; import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; @@ -39,15 +42,17 @@ class SaveOrder { void should_saveOrder_when_validOrder() { // arrange Long userId = 1L; - List orderItems = List.of( - OrderItem.create(1L, "์ƒํ’ˆ1", 2, new Price(10000)), - OrderItem.create(2L, "์ƒํ’ˆ2", 1, new Price(20000)) + List orderItemRequests = List.of( + new OrderItemRequest(1L, 2), + new OrderItemRequest(2L, 1) + ); + Map productMap = Map.of( + 1L, Product.create("์ƒํ’ˆ1", 1L, new Price(10000)), + 2L, Product.create("์ƒํ’ˆ2", 1L, new Price(20000)) ); - Order order = Order.create(userId, orderItems); -// when(spyOrderRepository.save(any(Order.class))).thenReturn(order); // act - Order result = orderService.save(order); + Order result = orderService.createOrder(orderItemRequests, productMap, userId); // assert verify(spyOrderRepository).save(any(Order.class)); @@ -61,14 +66,15 @@ void should_saveOrder_when_validOrder() { void should_saveOrder_when_singleOrderItem() { // arrange Long userId = 1L; - List orderItems = List.of( - OrderItem.create(1L, "์ƒํ’ˆ1", 1, new Price(10000)) + List orderItemRequests = List.of( + new OrderItemRequest(1L, 1) + ); + Map productMap = Map.of( + 1L, Product.create("์ƒํ’ˆ1", 1L, new Price(15000)) ); - Order order = Order.create(userId, orderItems); -// when(spyOrderRepository.save(any(Order.class))).thenReturn(order); // act - Order result = orderService.save(order); + Order result = orderService.createOrder(orderItemRequests, productMap, userId); // assert verify(spyOrderRepository).save(any(Order.class)); diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/OrderV1ApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/OrderV1ApiE2ETest.java index cbfaf772d..48e79e710 100644 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/OrderV1ApiE2ETest.java +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/OrderV1ApiE2ETest.java @@ -1,5 +1,7 @@ package com.loopers.interfaces.api; +import com.loopers.application.order.OrderItemRequest; +import com.loopers.application.order.OrderRequest; import com.loopers.domain.brand.Brand; import com.loopers.domain.common.vo.Price; import com.loopers.domain.metrics.product.ProductMetrics; @@ -155,10 +157,10 @@ class PostOrder { @Test void returnOrderInfo_whenCreateOrderSuccess() { // arrange - OrderV1Dto.OrderRequest request = new OrderV1Dto.OrderRequest( + OrderRequest request = new OrderRequest( List.of( - new OrderV1Dto.OrderRequest.OrderItemRequest(productId1, 2), - new OrderV1Dto.OrderRequest.OrderItemRequest(productId2, 1) + new OrderItemRequest(productId1, 2), + new OrderItemRequest(productId2, 1) ) ); HttpHeaders headers = createHeaders(); @@ -184,9 +186,9 @@ void returnOrderInfo_whenCreateOrderSuccess() { @Test void returnBadRequest_whenStockInsufficient() { // arrange - OrderV1Dto.OrderRequest request = new OrderV1Dto.OrderRequest( + OrderRequest request = new OrderRequest( List.of( - new OrderV1Dto.OrderRequest.OrderItemRequest(productId1, 99999) + new OrderItemRequest(productId1, 99999) ) ); HttpHeaders headers = createHeaders(); @@ -206,9 +208,9 @@ void returnBadRequest_whenStockInsufficient() { void returnNotFoundOrBadRequest_whenProductIdDoesNotExist() { // arrange Long nonExistentProductId = 99999L; - OrderV1Dto.OrderRequest request = new OrderV1Dto.OrderRequest( + OrderRequest request = new OrderRequest( List.of( - new OrderV1Dto.OrderRequest.OrderItemRequest(nonExistentProductId, 1) + new OrderItemRequest(nonExistentProductId, 1) ) ); HttpHeaders headers = createHeaders(); @@ -232,9 +234,9 @@ void returnBadRequest_whenPointInsufficient() { // arrange // ํฌ์ธํŠธ๋ฅผ ๋ชจ๋‘ ์‚ฌ์šฉ HttpHeaders headers = createHeaders(); - OrderV1Dto.OrderRequest firstOrder = new OrderV1Dto.OrderRequest( + OrderRequest firstOrder = new OrderRequest( List.of( - new OrderV1Dto.OrderRequest.OrderItemRequest(productId1, 10) + new OrderItemRequest(productId1, 10) ) ); ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { @@ -242,9 +244,9 @@ void returnBadRequest_whenPointInsufficient() { testRestTemplate.exchange(ENDPOINT_ORDERS, HttpMethod.POST, new HttpEntity<>(firstOrder, headers), responseType); // ํฌ์ธํŠธ ๋ถ€์กฑํ•œ ์ฃผ๋ฌธ ์‹œ๋„ - OrderV1Dto.OrderRequest request = new OrderV1Dto.OrderRequest( + OrderRequest request = new OrderRequest( List.of( - new OrderV1Dto.OrderRequest.OrderItemRequest(productId2, 99999) + new OrderItemRequest(productId2, 99999) ) ); @@ -260,9 +262,9 @@ void returnBadRequest_whenPointInsufficient() { @Test void returnBadRequest_whenXUserIdHeaderIsMissing() { // arrange - OrderV1Dto.OrderRequest request = new OrderV1Dto.OrderRequest( + OrderRequest request = new OrderRequest( List.of( - new OrderV1Dto.OrderRequest.OrderItemRequest(productId1, 1) + new OrderItemRequest(productId1, 1) ) ); @@ -280,9 +282,9 @@ void returnBadRequest_whenXUserIdHeaderIsMissing() { @Test void returnBadRequest_whenXUserIdHeaderIsEmpty() { // arrange - OrderV1Dto.OrderRequest request = new OrderV1Dto.OrderRequest( + OrderRequest request = new OrderRequest( List.of( - new OrderV1Dto.OrderRequest.OrderItemRequest(productId1, 1) + new OrderItemRequest(productId1, 1) ) ); HttpHeaders headers = new HttpHeaders(); @@ -302,9 +304,9 @@ void returnBadRequest_whenXUserIdHeaderIsEmpty() { @Test void returnBadRequest_whenXUserIdHeaderIsBlank() { // arrange - OrderV1Dto.OrderRequest request = new OrderV1Dto.OrderRequest( + OrderRequest request = new OrderRequest( List.of( - new OrderV1Dto.OrderRequest.OrderItemRequest(productId1, 1) + new OrderItemRequest(productId1, 1) ) ); HttpHeaders headers = new HttpHeaders(); @@ -324,9 +326,9 @@ void returnBadRequest_whenXUserIdHeaderIsBlank() { @Test void returnNotFound_whenUserIdDoesNotExist() { // arrange - OrderV1Dto.OrderRequest request = new OrderV1Dto.OrderRequest( + OrderRequest request = new OrderRequest( List.of( - new OrderV1Dto.OrderRequest.OrderItemRequest(productId1, 1) + new OrderItemRequest(productId1, 1) ) ); HttpHeaders headers = new HttpHeaders(); @@ -347,9 +349,9 @@ void returnNotFound_whenUserIdDoesNotExist() { void returnNotFound_whenProductIdDoesNotExist() { // arrange Long nonExistentProductId = 99999L; - OrderV1Dto.OrderRequest request = new OrderV1Dto.OrderRequest( + OrderRequest request = new OrderRequest( List.of( - new OrderV1Dto.OrderRequest.OrderItemRequest(nonExistentProductId, 1) + new OrderItemRequest(nonExistentProductId, 1) ) ); HttpHeaders headers = createHeaders(); @@ -370,10 +372,10 @@ void returnBadRequest_whenPartialStockInsufficient() { // arrange // productId1: ์žฌ๊ณ  100, productId2: ์žฌ๊ณ  50 // productId1์€ ์ถฉ๋ถ„ํ•˜์ง€๋งŒ productId2๋Š” ๋ถ€์กฑ - OrderV1Dto.OrderRequest request = new OrderV1Dto.OrderRequest( + OrderRequest request = new OrderRequest( List.of( - new OrderV1Dto.OrderRequest.OrderItemRequest(productId1, 10), // ์žฌ๊ณ  ์ถฉ๋ถ„ - new OrderV1Dto.OrderRequest.OrderItemRequest(productId2, 99999) // ์žฌ๊ณ  ๋ถ€์กฑ + new OrderItemRequest(productId1, 10), // ์žฌ๊ณ  ์ถฉ๋ถ„ + new OrderItemRequest(productId2, 99999) // ์žฌ๊ณ  ๋ถ€์กฑ ) ); HttpHeaders headers = createHeaders(); @@ -394,10 +396,10 @@ void returnBadRequest_whenPartialStockInsufficient() { @Test void returnBadRequest_whenAllProductsStockInsufficient() { // arrange - OrderV1Dto.OrderRequest request = new OrderV1Dto.OrderRequest( + OrderRequest request = new OrderRequest( List.of( - new OrderV1Dto.OrderRequest.OrderItemRequest(productId1, 99999), - new OrderV1Dto.OrderRequest.OrderItemRequest(productId2, 99999) + new OrderItemRequest(productId1, 99999), + new OrderItemRequest(productId2, 99999) ) ); HttpHeaders headers = createHeaders(); @@ -419,9 +421,9 @@ void returnOrderInfo_whenPointExactlyMatches() { // arrange // ํฌ์ธํŠธ๋ฅผ ๊ฑฐ์˜ ๋ชจ๋‘ ์‚ฌ์šฉ HttpHeaders headers = createHeaders(); - OrderV1Dto.OrderRequest firstOrder = new OrderV1Dto.OrderRequest( + OrderRequest firstOrder = new OrderRequest( List.of( - new OrderV1Dto.OrderRequest.OrderItemRequest(productId1, 9) // 90000์› ์‚ฌ์šฉ + new OrderItemRequest(productId1, 9) // 90000์› ์‚ฌ์šฉ ) ); ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { @@ -430,9 +432,9 @@ void returnOrderInfo_whenPointExactlyMatches() { // ๋‚จ์€ ํฌ์ธํŠธ: 10000์› // ์ •ํ™•ํžˆ ์ผ์น˜ํ•˜๋Š” ์ฃผ๋ฌธ - OrderV1Dto.OrderRequest request = new OrderV1Dto.OrderRequest( + OrderRequest request = new OrderRequest( List.of( - new OrderV1Dto.OrderRequest.OrderItemRequest(productId1, 1) // ์ •ํ™•ํžˆ 10000์› + new OrderItemRequest(productId1, 1) // ์ •ํ™•ํžˆ 10000์› ) ); @@ -455,10 +457,10 @@ void returnError_whenDuplicateProducts() { // arrange // ๊ฐ™์€ ์ƒํ’ˆ์„ ์—ฌ๋Ÿฌ ๋ฒˆ ์ฃผ๋ฌธ ํ•ญ๋ชฉ์— ํฌํ•จ // Note: Collectors.toMap()์€ ์ค‘๋ณต ํ‚ค๊ฐ€ ์žˆ์œผ๋ฉด IllegalStateException์„ ๋ฐœ์ƒ์‹œํ‚ด - OrderV1Dto.OrderRequest request = new OrderV1Dto.OrderRequest( + OrderRequest request = new OrderRequest( List.of( - new OrderV1Dto.OrderRequest.OrderItemRequest(productId1, 2), - new OrderV1Dto.OrderRequest.OrderItemRequest(productId1, 3) // ์ค‘๋ณต + new OrderItemRequest(productId1, 2), + new OrderItemRequest(productId1, 3) // ์ค‘๋ณต ) ); HttpHeaders headers = createHeaders(); @@ -496,9 +498,9 @@ void returnNotFoundAndRollbackStock_whenPointDoesNotExist() { Supply initialSupply = supplyJpaRepository.findByProductId(productId1).orElseThrow(); int initialStock = initialSupply.getStock().quantity(); - OrderV1Dto.OrderRequest request = new OrderV1Dto.OrderRequest( + OrderRequest request = new OrderRequest( List.of( - new OrderV1Dto.OrderRequest.OrderItemRequest(productId1, 1) + new OrderItemRequest(productId1, 1) ) ); HttpHeaders headers = new HttpHeaders(); @@ -529,9 +531,9 @@ void should_rollbackStock_whenPointInsufficientAfterStockDecrease() { // ํฌ์ธํŠธ๋ฅผ ๊ฑฐ์˜ ๋ชจ๋‘ ์‚ฌ์šฉ HttpHeaders headers = createHeaders(); - OrderV1Dto.OrderRequest firstOrder = new OrderV1Dto.OrderRequest( + OrderRequest firstOrder = new OrderRequest( List.of( - new OrderV1Dto.OrderRequest.OrderItemRequest(productId1, 9) // 90000์› ์‚ฌ์šฉ + new OrderItemRequest(productId1, 9) // 90000์› ์‚ฌ์šฉ ) ); ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { @@ -540,9 +542,9 @@ void should_rollbackStock_whenPointInsufficientAfterStockDecrease() { // ๋‚จ์€ ํฌ์ธํŠธ: 10000์› // ํฌ์ธํŠธ ๋ถ€์กฑํ•œ ์ฃผ๋ฌธ ์‹œ๋„ (์žฌ๊ณ ๋Š” ์ถฉ๋ถ„) - OrderV1Dto.OrderRequest request = new OrderV1Dto.OrderRequest( + OrderRequest request = new OrderRequest( List.of( - new OrderV1Dto.OrderRequest.OrderItemRequest(productId1, 2) // 20000์› ํ•„์š” (๋ถ€์กฑ) + new OrderItemRequest(productId1, 2) // 20000์› ํ•„์š” (๋ถ€์กฑ) ) ); @@ -570,10 +572,10 @@ void should_rollbackStock_whenPartialStockInsufficient() { int initialStock2 = initialSupply2.getStock().quantity(); // productId1์€ ์ถฉ๋ถ„ํ•˜์ง€๋งŒ productId2๋Š” ๋ถ€์กฑํ•œ ์ฃผ๋ฌธ - OrderV1Dto.OrderRequest request = new OrderV1Dto.OrderRequest( + OrderRequest request = new OrderRequest( List.of( - new OrderV1Dto.OrderRequest.OrderItemRequest(productId1, 10), // ์žฌ๊ณ  ์ถฉ๋ถ„ - new OrderV1Dto.OrderRequest.OrderItemRequest(productId2, 99999) // ์žฌ๊ณ  ๋ถ€์กฑ + new OrderItemRequest(productId1, 10), // ์žฌ๊ณ  ์ถฉ๋ถ„ + new OrderItemRequest(productId2, 99999) // ์žฌ๊ณ  ๋ถ€์กฑ ) ); HttpHeaders headers = createHeaders(); @@ -606,9 +608,9 @@ void returnOrderList_whenGetOrderListSuccess() { // arrange HttpHeaders headers = createHeaders(); // ์ฃผ๋ฌธ ์ƒ์„ฑ - OrderV1Dto.OrderRequest orderRequest = new OrderV1Dto.OrderRequest( + OrderRequest orderRequest = new OrderRequest( List.of( - new OrderV1Dto.OrderRequest.OrderItemRequest(productId1, 1) + new OrderItemRequest(productId1, 1) ) ); ParameterizedTypeReference> orderResponseType = new ParameterizedTypeReference<>() { @@ -702,9 +704,9 @@ class GetOrderDetail { void setupOrder() { // ์ฃผ๋ฌธ ์ƒ์„ฑ HttpHeaders headers = createHeaders(); - OrderV1Dto.OrderRequest orderRequest = new OrderV1Dto.OrderRequest( + OrderRequest orderRequest = new OrderRequest( List.of( - new OrderV1Dto.OrderRequest.OrderItemRequest(productId1, 1) + new OrderItemRequest(productId1, 1) ) ); ParameterizedTypeReference> orderResponseType = new ParameterizedTypeReference<>() { From d321b49deaa3e09f3e69d9b7b98c2d4f790a774c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A0=95=EB=B0=B0?= Date: Fri, 28 Nov 2025 15:48:32 +0900 Subject: [PATCH 65/85] Revert "Round3: Product, Brand, Like, Order" --- apps/commerce-api/build.gradle.kts | 4 +- .../com/loopers/CommerceApiApplication.java | 2 - .../like/product/LikeProductFacade.java | 83 -- .../like/product/LikeProductInfo.java | 11 - .../application/order/OrderFacade.java | 66 -- .../loopers/application/order/OrderInfo.java | 21 - .../application/order/OrderItemInfo.java | 27 - .../application/order/OrderItemRequest.java | 7 - .../application/order/OrderRequest.java | 8 - .../application/point/PointFacade.java | 32 - .../application/product/ProductFacade.java | 128 --- .../application/product/ProductInfo.java | 11 - .../loopers/application/user/UserFacade.java | 31 - .../loopers/application/user/UserInfo.java | 14 - .../java/com/loopers/domain/brand/Brand.java | 30 - .../loopers/domain/brand/BrandRepository.java | 10 - .../loopers/domain/brand/BrandService.java | 26 - .../com/loopers/domain/common/vo/Price.java | 26 - .../domain/like/product/LikeProduct.java | 37 - .../like/product/LikeProductRepository.java | 16 - .../like/product/LikeProductService.java | 31 - .../metrics/product/ProductMetrics.java | 33 - .../product/ProductMetricsRepository.java | 15 - .../product/ProductMetricsService.java | 39 - .../java/com/loopers/domain/order/Order.java | 44 - .../com/loopers/domain/order/OrderItem.java | 50 -- .../loopers/domain/order/OrderRepository.java | 14 - .../loopers/domain/order/OrderService.java | 52 -- .../java/com/loopers/domain/point/Point.java | 48 - .../loopers/domain/point/PointRepository.java | 9 - .../loopers/domain/point/PointService.java | 44 - .../com/loopers/domain/product/Product.java | 46 - .../domain/product/ProductRepository.java | 18 - .../domain/product/ProductService.java | 55 -- .../com/loopers/domain/supply/Supply.java | 43 - .../domain/supply/SupplyRepository.java | 15 - .../loopers/domain/supply/SupplyService.java | 40 - .../com/loopers/domain/supply/vo/Stock.java | 40 - .../java/com/loopers/domain/user/User.java | 52 -- .../loopers/domain/user/UserRepository.java | 13 - .../com/loopers/domain/user/UserService.java | 37 - .../brand/BrandJpaRepository.java | 8 - .../brand/BrandRepositoryImpl.java | 25 - .../like/LikeProductJpaRepository.java | 17 - .../like/LikeProductRepositoryImpl.java | 37 - .../product/ProductMetricsJpaRepository.java | 10 - .../product/ProductMetricsRepositoryImpl.java | 32 - .../order/OrderJpaRepository.java | 16 - .../order/OrderRepositoryImpl.java | 31 - .../point/PointJpaRepository.java | 11 - .../point/PointRepositoryImpl.java | 24 - .../product/ProductJpaRepository.java | 7 - .../product/ProductRepositoryImpl.java | 38 - .../supply/SupplyJpaRepository.java | 21 - .../supply/SupplyRepositoryImpl.java | 36 - .../user/UserJpaRepository.java | 19 - .../user/UserRepositoryImpl.java | 34 - .../like/product/LikeProductV1ApiSpec.java | 49 -- .../like/product/LikeProductV1Controller.java | 59 -- .../api/like/product/LikeProductV1Dto.java | 48 - .../interfaces/api/order/OrderV1ApiSpec.java | 54 -- .../api/order/OrderV1Controller.java | 61 -- .../interfaces/api/order/OrderV1Dto.java | 64 -- .../interfaces/api/point/PointV1ApiSpec.java | 44 - .../api/point/PointV1Controller.java | 41 - .../interfaces/api/point/PointV1Dto.java | 19 - .../api/product/ProductV1ApiSpec.java | 40 - .../api/product/ProductV1Controller.java | 39 - .../interfaces/api/product/ProductV1Dto.java | 46 - .../interfaces/api/user/UserV1ApiSpec.java | 37 - .../interfaces/api/user/UserV1Controller.java | 41 - .../interfaces/api/user/UserV1Dto.java | 27 - .../src/main/resources/application.yml | 2 +- .../order/OrderFacadeIntegrationTest.java | 339 ------- .../com/loopers/domain/brand/BrandTest.java | 127 --- .../loopers/domain/common/vo/PriceTest.java | 62 -- .../LikeProductServiceIntegrationTest.java | 232 ----- .../domain/like/product/LikeProductTest.java | 202 ----- .../loopers/domain/order/OrderItemTest.java | 276 ------ .../order/OrderServiceIntegrationTest.java | 151 ---- .../com/loopers/domain/order/OrderTest.java | 176 ---- .../loopers/domain/point/PointModelTest.java | 30 - .../point/PointServiceIntegrationTest.java | 78 -- .../ProductServiceIntegrationTest.java | 362 -------- .../com/loopers/domain/supply/SupplyTest.java | 130 --- .../loopers/domain/supply/vo/StockTest.java | 183 ---- .../loopers/domain/user/UserModelTest.java | 139 --- .../user/UserServiceIntegrationTest.java | 108 --- .../api/LikeProductV1ApiE2ETest.java | 485 ---------- .../interfaces/api/OrderV1ApiE2ETest.java | 830 ------------------ .../interfaces/api/PointV1ApiE2ETest.java | 246 ------ .../interfaces/api/ProductV1ApiE2ETest.java | 265 ------ .../interfaces/api/UserV1ApiE2ETest.java | 202 ----- docker/infra-compose.yml | 174 ++-- settings.gradle.kts | 6 +- 95 files changed, 93 insertions(+), 7075 deletions(-) delete mode 100644 apps/commerce-api/src/main/java/com/loopers/application/like/product/LikeProductFacade.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/application/like/product/LikeProductInfo.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/application/order/OrderInfo.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/application/order/OrderItemInfo.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/application/order/OrderItemRequest.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/application/order/OrderRequest.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/application/product/ProductInfo.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/application/user/UserInfo.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/brand/Brand.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandRepository.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandService.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/common/vo/Price.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/like/product/LikeProduct.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/like/product/LikeProductRepository.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/like/product/LikeProductService.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/metrics/product/ProductMetrics.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/metrics/product/ProductMetricsRepository.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/metrics/product/ProductMetricsService.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/order/Order.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItem.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/order/OrderRepository.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/point/PointRepository.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/product/ProductRepository.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/supply/Supply.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/supply/SupplyRepository.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/supply/SupplyService.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/supply/vo/Stock.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/user/User.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandJpaRepository.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandRepositoryImpl.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeProductJpaRepository.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeProductRepositoryImpl.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/metrics/product/ProductMetricsJpaRepository.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/metrics/product/ProductMetricsRepositoryImpl.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderJpaRepository.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderRepositoryImpl.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointJpaRepository.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointRepositoryImpl.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/supply/SupplyJpaRepository.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/supply/SupplyRepositoryImpl.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserJpaRepository.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/product/LikeProductV1ApiSpec.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/product/LikeProductV1Controller.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/product/LikeProductV1Dto.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1ApiSpec.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Controller.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Dto.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1ApiSpec.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Controller.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Dto.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1ApiSpec.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Dto.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1ApiSpec.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java delete mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java delete mode 100644 apps/commerce-api/src/test/java/com/loopers/application/order/OrderFacadeIntegrationTest.java delete mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandTest.java delete mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/common/vo/PriceTest.java delete mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/like/product/LikeProductServiceIntegrationTest.java delete mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/like/product/LikeProductTest.java delete mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/order/OrderItemTest.java delete mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/order/OrderServiceIntegrationTest.java delete mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/order/OrderTest.java delete mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/point/PointModelTest.java delete mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java delete mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/product/ProductServiceIntegrationTest.java delete mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/supply/SupplyTest.java delete mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/supply/vo/StockTest.java delete mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/user/UserModelTest.java delete mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java delete mode 100644 apps/commerce-api/src/test/java/com/loopers/interfaces/api/LikeProductV1ApiE2ETest.java delete mode 100644 apps/commerce-api/src/test/java/com/loopers/interfaces/api/OrderV1ApiE2ETest.java delete mode 100644 apps/commerce-api/src/test/java/com/loopers/interfaces/api/PointV1ApiE2ETest.java delete mode 100644 apps/commerce-api/src/test/java/com/loopers/interfaces/api/ProductV1ApiE2ETest.java delete mode 100644 apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java diff --git a/apps/commerce-api/build.gradle.kts b/apps/commerce-api/build.gradle.kts index e6d28d4ed..03ce68f02 100644 --- a/apps/commerce-api/build.gradle.kts +++ b/apps/commerce-api/build.gradle.kts @@ -1,7 +1,7 @@ dependencies { // add-ons implementation(project(":modules:jpa")) -// implementation(project(":modules:redis")) + implementation(project(":modules:redis")) implementation(project(":supports:jackson")) implementation(project(":supports:logging")) implementation(project(":supports:monitoring")) @@ -18,5 +18,5 @@ dependencies { // test-fixtures testImplementation(testFixtures(project(":modules:jpa"))) -// testImplementation(testFixtures(project(":modules:redis"))) + testImplementation(testFixtures(project(":modules:redis"))) } diff --git a/apps/commerce-api/src/main/java/com/loopers/CommerceApiApplication.java b/apps/commerce-api/src/main/java/com/loopers/CommerceApiApplication.java index 62efd22b3..9027b51bf 100644 --- a/apps/commerce-api/src/main/java/com/loopers/CommerceApiApplication.java +++ b/apps/commerce-api/src/main/java/com/loopers/CommerceApiApplication.java @@ -4,8 +4,6 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.ConfigurationPropertiesScan; -import org.springframework.data.jpa.repository.config.EnableJpaRepositories; - import java.util.TimeZone; @ConfigurationPropertiesScan diff --git a/apps/commerce-api/src/main/java/com/loopers/application/like/product/LikeProductFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/like/product/LikeProductFacade.java deleted file mode 100644 index c70fa18fa..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/application/like/product/LikeProductFacade.java +++ /dev/null @@ -1,83 +0,0 @@ -package com.loopers.application.like.product; - -import com.loopers.domain.brand.Brand; -import com.loopers.domain.brand.BrandService; -import com.loopers.domain.like.product.LikeProduct; -import com.loopers.domain.like.product.LikeProductService; -import com.loopers.domain.metrics.product.ProductMetrics; -import com.loopers.domain.metrics.product.ProductMetricsService; -import com.loopers.domain.product.Product; -import com.loopers.domain.product.ProductService; -import com.loopers.domain.supply.Supply; -import com.loopers.domain.supply.SupplyService; -import com.loopers.domain.user.User; -import com.loopers.domain.user.UserService; -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Component; - -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; - -@RequiredArgsConstructor -@Component -public class LikeProductFacade { - private final LikeProductService likeProductService; - private final UserService userService; - private final ProductService productService; - private final ProductMetricsService productMetricsService; - private final BrandService brandService; - private final SupplyService supplyService; - - public void likeProduct(String userId, Long productId) { - User user = userService.findByUserId(userId).orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); - if (!productService.existsById(productId)) { - throw new CoreException(ErrorType.NOT_FOUND, "์ƒํ’ˆ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); - } - likeProductService.likeProduct(user.getId(), productId); - } - - public void unlikeProduct(String userId, Long productId) { - User user = userService.findByUserId(userId).orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); - if (!productService.existsById(productId)) { - throw new CoreException(ErrorType.NOT_FOUND, "์ƒํ’ˆ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); - } - likeProductService.unlikeProduct(user.getId(), productId); - } - - public Page getLikedProducts(String userId, Pageable pageable) { - User user = userService.findByUserId(userId).orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); - Page likedProducts = likeProductService.getLikedProducts(user.getId(), pageable); - - List productIds = likedProducts.map(LikeProduct::getProductId).toList(); - Map productMap = productService.getProductMapByIds(productIds); - - Set brandIds = productMap.values().stream().map(Product::getBrandId).collect(Collectors.toSet()); - - Map metricsMap = productMetricsService.getMetricsMapByProductIds(productIds); - Map supplyMap = supplyService.getSupplyMapByProductIds(productIds); - Map brandMap = brandService.getBrandMapByBrandIds(brandIds); - - return likedProducts.map(likeProduct -> { - Product product = productMap.get(likeProduct.getProductId()); - ProductMetrics metrics = metricsMap.get(product.getId()); - Brand brand = brandMap.get(product.getBrandId()); - Supply supply = supplyMap.get(product.getId()); - - return new LikeProductInfo( - product.getId(), - product.getName(), - brand.getName(), - product.getPrice().amount(), - metrics.getLikeCount(), - supply.getStock().quantity() - ); - }); - - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/like/product/LikeProductInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/like/product/LikeProductInfo.java deleted file mode 100644 index ce8b4928c..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/application/like/product/LikeProductInfo.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.loopers.application.like.product; - -public record LikeProductInfo( - Long id, - String name, - String brand, - int price, - int likes, - int stock -) { -} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java deleted file mode 100644 index 893428a79..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java +++ /dev/null @@ -1,66 +0,0 @@ -package com.loopers.application.order; - -import com.loopers.domain.order.Order; -import com.loopers.domain.order.OrderService; -import com.loopers.domain.point.PointService; -import com.loopers.domain.product.Product; -import com.loopers.domain.product.ProductService; -import com.loopers.domain.supply.SupplyService; -import com.loopers.domain.user.User; -import com.loopers.domain.user.UserService; -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; - -import java.util.Map; -import java.util.stream.Collectors; - -@RequiredArgsConstructor -@Component -public class OrderFacade { - private final UserService userService; - private final OrderService orderService; - private final ProductService productService; - private final PointService pointService; - private final SupplyService supplyService; - - @Transactional(readOnly = true) - public OrderInfo getOrderInfo(String userId, Long orderId) { - User user = userService.findByUserId(userId).orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); - Order order = orderService.getOrderByIdAndUserId(orderId, user.getId()); - - return OrderInfo.from(order); - } - - @Transactional(readOnly = true) - public Page getOrderList(String userId, Pageable pageable) { - User user = userService.findByUserId(userId).orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); - Page orders = orderService.getOrdersByUserId(user.getId(), pageable); - return orders.map(OrderInfo::from); - } - - @Transactional - public OrderInfo createOrder(String userId, OrderRequest request) { - User user = userService.findByUserId(userId).orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); - - Map productIdQuantityMap = request.items().stream() - .collect(Collectors.toMap(OrderItemRequest::productId, OrderItemRequest::quantity)); - - Map productMap = productService.getProductMapByIds(productIdQuantityMap.keySet()); - - request.items().forEach(item -> { - supplyService.checkAndDecreaseStock(item.productId(), item.quantity()); - }); - - Integer totalAmount = productService.calculateTotalAmount(productIdQuantityMap); - pointService.checkAndDeductPoint(user.getId(), totalAmount); - - Order order = orderService.createOrder(request.items(), productMap, user.getId()); - - return OrderInfo.from(order); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderInfo.java deleted file mode 100644 index c75047e66..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderInfo.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.loopers.application.order; - -import com.loopers.domain.order.Order; - -import java.util.List; - -public record OrderInfo( - Long orderId, - Long userId, - Integer totalPrice, - List items -) { - public static OrderInfo from(Order order) { - return new OrderInfo( - order.getId(), - order.getUserId(), - order.getTotalPrice().amount(), - OrderItemInfo.fromList(order.getOrderItems()) - ); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderItemInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderItemInfo.java deleted file mode 100644 index 99c53a78e..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderItemInfo.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.loopers.application.order; - -import com.loopers.domain.order.OrderItem; - -import java.util.List; - -public record OrderItemInfo( - Long productId, - String productName, - Integer quantity, - Integer totalPrice -) { - public static OrderItemInfo from(OrderItem orderItem) { - return new OrderItemInfo( - orderItem.getProductId(), - orderItem.getProductName(), - orderItem.getQuantity(), - orderItem.getTotalPrice() - ); - } - - public static List fromList(List items) { - return items.stream() - .map(OrderItemInfo::from) - .toList(); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderItemRequest.java b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderItemRequest.java deleted file mode 100644 index cf4984f10..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderItemRequest.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.loopers.application.order; - -public record OrderItemRequest( - Long productId, - Integer quantity -) { -} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderRequest.java b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderRequest.java deleted file mode 100644 index 63f746a8f..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderRequest.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.loopers.application.order; - -import java.util.List; - -public record OrderRequest( - List items -) { -} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java deleted file mode 100644 index 126a528de..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.loopers.application.point; - -import com.loopers.domain.point.PointService; -import com.loopers.domain.user.User; -import com.loopers.domain.user.UserService; -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; - -@RequiredArgsConstructor -@Component -public class PointFacade { - private final PointService pointService; - private final UserService userService; - - @Transactional(readOnly = true) - public Long getCurrentPoint(String userId) { - User user = userService.findByUserId(userId).orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); - - return pointService.getCurrentPoint(user.getId()) - .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); - } - - @Transactional - public Long chargePoint(String userId, int amount) { - User user = userService.findByUserIdForUpdate(userId).orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); - - return pointService.chargePoint(user.getId(), amount); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java deleted file mode 100644 index b87280ca3..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java +++ /dev/null @@ -1,128 +0,0 @@ -package com.loopers.application.product; - -import com.loopers.domain.brand.Brand; -import com.loopers.domain.brand.BrandService; -import com.loopers.domain.metrics.product.ProductMetrics; -import com.loopers.domain.metrics.product.ProductMetricsService; -import com.loopers.domain.product.Product; -import com.loopers.domain.product.ProductService; -import com.loopers.domain.supply.Supply; -import com.loopers.domain.supply.SupplyService; -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import lombok.RequiredArgsConstructor; -import org.apache.commons.lang3.StringUtils; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; - -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; - -@RequiredArgsConstructor -@Component -public class ProductFacade { - private final ProductService productService; - private final ProductMetricsService productMetricsService; - private final BrandService brandService; - private final SupplyService supplyService; - - @Transactional(readOnly = true) - public Page getProductList(Pageable pageable) { - String sortStr = pageable.getSort().toString().split(":")[0]; - if (StringUtils.equals(sortStr, "like_desc")) { - int page = pageable.getPageNumber(); - int size = pageable.getPageSize(); - Sort sort = Sort.by(Sort.Direction.DESC, "likeCount"); - return getProductsByLikeCount(PageRequest.of(page, size, sort)); - } - - Page products = productService.getProducts(pageable); - - List productIds = products.map(Product::getId).toList(); - Set brandIds = products.map(Product::getBrandId).toSet(); - - Map metricsMap = productMetricsService.getMetricsMapByProductIds(productIds); - Map supplyMap = supplyService.getSupplyMapByProductIds(productIds); - Map brandMap = brandService.getBrandMapByBrandIds(brandIds); - - return products.map(product -> { - ProductMetrics metrics = metricsMap.get(product.getId()); - if (metrics == null) { - throw new CoreException(ErrorType.NOT_FOUND, "ํ•ด๋‹น ์ƒํ’ˆ์˜ ๋ฉ”ํŠธ๋ฆญ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); - } - Brand brand = brandMap.get(product.getBrandId()); - if (brand == null) { - throw new CoreException(ErrorType.NOT_FOUND, "ํ•ด๋‹น ์ƒํ’ˆ์˜ ๋ธŒ๋žœ๋“œ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); - } - Supply supply = supplyMap.get(product.getId()); - if (supply == null) { - throw new CoreException(ErrorType.NOT_FOUND, "ํ•ด๋‹น ์ƒํ’ˆ์˜ ์žฌ๊ณ  ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); - } - - return new ProductInfo( - product.getId(), - product.getName(), - brand.getName(), - product.getPrice().amount(), - metrics.getLikeCount(), - supply.getStock().quantity() - ); - }); - } - - public Page getProductsByLikeCount(Pageable pageable) { - Page metricsPage = productMetricsService.getMetrics(pageable); - List productIds = metricsPage.map(ProductMetrics::getProductId).toList(); - Map productMap = productService.getProductMapByIds(productIds); - Set brandIds = productMap.values().stream().map(Product::getBrandId).collect(Collectors.toSet()); - Map brandMap = brandService.getBrandMapByBrandIds(brandIds); - Map supplyMap = supplyService.getSupplyMapByProductIds(productIds); - - return metricsPage.map(metrics -> { - Product product = productMap.get(metrics.getProductId()); - if (product == null) { - throw new CoreException(ErrorType.NOT_FOUND, "ํ•ด๋‹น ์ƒํ’ˆ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); - } - Brand brand = brandMap.get(product.getBrandId()); - if (brand == null) { - throw new CoreException(ErrorType.NOT_FOUND, "ํ•ด๋‹น ์ƒํ’ˆ์˜ ๋ธŒ๋žœ๋“œ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); - } - Supply supply = supplyMap.get(product.getId()); - if (supply == null) { - throw new CoreException(ErrorType.NOT_FOUND, "ํ•ด๋‹น ์ƒํ’ˆ์˜ ์žฌ๊ณ  ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); - } - - return new ProductInfo( - product.getId(), - product.getName(), - brand.getName(), - product.getPrice().amount(), - metrics.getLikeCount(), - supply.getStock().quantity() - ); - }); - } - - @Transactional(readOnly = true) - public ProductInfo getProductDetail(Long productId) { - Product product = productService.getProductById(productId); - ProductMetrics metrics = productMetricsService.getMetricsByProductId(productId); - Brand brand = brandService.getBrandById(product.getBrandId()); - Supply supply = supplyService.getSupplyByProductId(productId); - - return new ProductInfo( - productId, - product.getName(), - brand.getName(), - product.getPrice().amount(), - metrics.getLikeCount(), - supply.getStock().quantity() - ); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/product/ProductInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductInfo.java deleted file mode 100644 index b5286ed99..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/application/product/ProductInfo.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.loopers.application.product; - -public record ProductInfo( - Long id, - String name, - String brand, - int price, - int likes, - int stock -) { -} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java deleted file mode 100644 index 030d014b6..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.loopers.application.user; - -import com.loopers.domain.point.PointService; -import com.loopers.domain.user.User; -import com.loopers.domain.user.UserService; -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; - -import java.util.Optional; - -@RequiredArgsConstructor -@Component -public class UserFacade { - private final UserService userService; - private final PointService pointService; - - @Transactional - public UserInfo registerUser(String userId, String email, String birthday, String gender) { - User registeredUser = userService.registerUser(userId, email, birthday, gender); - pointService.createPoint(registeredUser.getId()); - return UserInfo.from(registeredUser); - } - - public UserInfo getUserInfo(String userId) { - Optional user = userService.findByUserId(userId); - return UserInfo.from(user.orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."))); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/user/UserInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/user/UserInfo.java deleted file mode 100644 index 84cda840f..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/application/user/UserInfo.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.loopers.application.user; - -import com.loopers.domain.user.User; - -public record UserInfo(String id, String email, String birthday, String gender) { - public static UserInfo from(User user) { - return new UserInfo( - user.getUserId(), - user.getEmail(), - user.getBirthday(), - user.getGender() - ); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/brand/Brand.java b/apps/commerce-api/src/main/java/com/loopers/domain/brand/Brand.java deleted file mode 100644 index a55ccbd33..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/brand/Brand.java +++ /dev/null @@ -1,30 +0,0 @@ - package com.loopers.domain.brand; - -import com.loopers.domain.BaseEntity; -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import jakarta.persistence.Entity; -import jakarta.persistence.Table; -import lombok.Getter; -import org.apache.commons.lang3.StringUtils; - -@Entity -@Table(name = "tb_brand") -@Getter -public class Brand extends BaseEntity { - private String name; - - protected Brand() { - } - - private Brand(String name) { - this.name = name; - } - - public static Brand create(String name) { - if (StringUtils.isBlank(name)) { - throw new CoreException(ErrorType.BAD_REQUEST, "๋ธŒ๋žœ๋“œ ์ด๋ฆ„์€ ํ•„์ˆ˜์ด๋ฉฐ 1์ž ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); - } - return new Brand(name); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandRepository.java deleted file mode 100644 index c51be9399..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandRepository.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.loopers.domain.brand; - -import java.util.Collection; -import java.util.Optional; - -public interface BrandRepository { - Optional findById(Long id); - - Collection findAllByIdIn(Collection ids); -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandService.java b/apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandService.java deleted file mode 100644 index 3fba0f915..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandService.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.loopers.domain.brand; - -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; - -import java.util.Collection; -import java.util.Map; - -@RequiredArgsConstructor -@Component -public class BrandService { - private final BrandRepository brandRepository; - - public Brand getBrandById(Long brandId) { - return brandRepository.findById(brandId) - .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "๋ธŒ๋žœ๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); - } - - public Map getBrandMapByBrandIds(Collection brandIds) { - return brandRepository.findAllByIdIn(brandIds) - .stream() - .collect(java.util.stream.Collectors.toMap(Brand::getId, brand -> brand)); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/common/vo/Price.java b/apps/commerce-api/src/main/java/com/loopers/domain/common/vo/Price.java deleted file mode 100644 index 58eee0043..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/common/vo/Price.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.loopers.domain.common.vo; - -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import jakarta.persistence.AttributeConverter; - -public record Price(int amount) { - public Price { - if (amount < 0) { - throw new CoreException(ErrorType.BAD_REQUEST, "๊ฐ€๊ฒฉ์€ ์Œ์ˆ˜๊ฐ€ ๋  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); - } - } - - public static class Converter implements AttributeConverter { - - @Override - public Integer convertToDatabaseColumn(Price attribute) { - return attribute.amount(); - } - - @Override - public Price convertToEntityAttribute(Integer dbData) { - return new Price(dbData); - } - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/like/product/LikeProduct.java b/apps/commerce-api/src/main/java/com/loopers/domain/like/product/LikeProduct.java deleted file mode 100644 index ee8d09c49..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/like/product/LikeProduct.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.loopers.domain.like.product; - -import com.loopers.domain.BaseEntity; -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.Table; -import lombok.Getter; - -@Entity -@Table(name = "tb_like_product") -@Getter -public class LikeProduct extends BaseEntity { - @Column(name = "user_id", nullable = false, updatable = false) - private Long userId; - @Column(name = "product_id", nullable = false, updatable = false) - private Long productId; - - protected LikeProduct() { - } - - private LikeProduct(Long userId, Long productId) { - this.userId = userId; - this.productId = productId; - } - - public static LikeProduct create(Long userId, Long productId) { - if (userId == null || userId <= 0) { - throw new CoreException(ErrorType.BAD_REQUEST, "์‚ฌ์šฉ์ž ID๋Š” 1 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); - } - if (productId == null || productId <= 0) { - throw new CoreException(ErrorType.BAD_REQUEST, "์ƒํ’ˆ ID๋Š” 1 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); - } - return new LikeProduct(userId, productId); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/like/product/LikeProductRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/like/product/LikeProductRepository.java deleted file mode 100644 index 525176f10..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/like/product/LikeProductRepository.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.loopers.domain.like.product; - -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; - -import java.util.Optional; - -public interface LikeProductRepository { - boolean existsByUserIdAndProductId(Long userId, Long productId); - - Optional findByUserIdAndProductId(Long userId, Long productId); - - void save(LikeProduct likeProduct); - - Page getLikeProductsByUserIdAndDeletedAtIsNull(Long userId, Pageable pageable); -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/like/product/LikeProductService.java b/apps/commerce-api/src/main/java/com/loopers/domain/like/product/LikeProductService.java deleted file mode 100644 index f9cacef65..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/like/product/LikeProductService.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.loopers.domain.like.product; - -import com.loopers.domain.BaseEntity; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Service; - -@RequiredArgsConstructor -@Service -public class LikeProductService { - private final LikeProductRepository likeProductRepository; - - public void likeProduct(Long userId, Long productId) { - likeProductRepository.findByUserIdAndProductId(userId, productId) - .ifPresentOrElse(BaseEntity::restore, () -> { - LikeProduct likeProduct = LikeProduct.create(userId, productId); - likeProductRepository.save(likeProduct); - }); - } - - public void unlikeProduct(Long userId, Long productId) { - likeProductRepository.findByUserIdAndProductId(userId, productId) - .ifPresent(BaseEntity::delete); - } - - public Page getLikedProducts(Long userId, Pageable pageable) { - return likeProductRepository.getLikeProductsByUserIdAndDeletedAtIsNull(userId, PageRequest.of(pageable.getPageNumber(), pageable.getPageSize())); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/metrics/product/ProductMetrics.java b/apps/commerce-api/src/main/java/com/loopers/domain/metrics/product/ProductMetrics.java deleted file mode 100644 index d815fd878..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/metrics/product/ProductMetrics.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.loopers.domain.metrics.product; - -import com.loopers.domain.BaseEntity; -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import jakarta.persistence.Entity; -import jakarta.persistence.Table; -import lombok.Getter; - -@Entity -@Table(name = "tb_product_metrics") -@Getter -public class ProductMetrics extends BaseEntity { - // ํ˜„์žฌ๋Š” ์ƒํ’ˆ์˜ ์ข‹์•„์š” ์ˆ˜๋งŒ ๊ด€๋ฆฌํ•˜์ง€๋งŒ, ์ถ”ํ›„์— ๋‹ค๋ฅธ ๋ฉ”ํŠธ๋ฆญ๋“ค๋„ ์ถ”๊ฐ€๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. - private Long productId; - private Integer likeCount; - - protected ProductMetrics() { - } - - public static ProductMetrics create(Long productId, Integer likeCount) { - if (productId == null || productId <= 0) { - throw new CoreException(ErrorType.BAD_REQUEST, "์ƒํ’ˆ ID๋Š” 1 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); - } - if (likeCount == null || likeCount < 0) { - throw new CoreException(ErrorType.BAD_REQUEST, "์ข‹์•„์š” ์ˆ˜๋Š” 0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); - } - ProductMetrics metrics = new ProductMetrics(); - metrics.productId = productId; - metrics.likeCount = likeCount; - return metrics; - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/metrics/product/ProductMetricsRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/metrics/product/ProductMetricsRepository.java deleted file mode 100644 index c9f236182..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/metrics/product/ProductMetricsRepository.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.loopers.domain.metrics.product; - -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; - -import java.util.Collection; -import java.util.Optional; - -public interface ProductMetricsRepository { - Optional findByProductId(Long productId); - - Collection findByProductIds(Collection productIds); - - Page findAll(Pageable pageable); -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/metrics/product/ProductMetricsService.java b/apps/commerce-api/src/main/java/com/loopers/domain/metrics/product/ProductMetricsService.java deleted file mode 100644 index d39f95c9f..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/metrics/product/ProductMetricsService.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.loopers.domain.metrics.product; - -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Component; - -import java.util.Collection; -import java.util.Map; -import java.util.stream.Collectors; - -@RequiredArgsConstructor -@Component -public class ProductMetricsService { - private final ProductMetricsRepository productMetricsRepository; - - public ProductMetrics getMetricsByProductId(Long productId) { - return productMetricsRepository.findByProductId(productId) - .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "ํ•ด๋‹น ์ƒํ’ˆ์˜ ๋ฉ”ํŠธ๋ฆญ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); - } - - public Map getMetricsMapByProductIds(Collection productIds) { - return productMetricsRepository.findByProductIds(productIds) - .stream() - .collect(Collectors.toMap(ProductMetrics::getProductId, metrics -> metrics)); - } - - // pageable like_count ์š”๊ฑด์— ๋”ฐ๋ผ ์ •๋ ฌ๋œ ์ƒ์œ„ N๊ฐœ ์ƒํ’ˆ ๋ฉ”ํŠธ๋ฆญ ์กฐํšŒ - public Page getMetrics(Pageable pageable) { - // ํ˜„์žฌ๋Š” like_count, desc๋งŒ ๊ฐ€์ง€๋ฏ€๋กœ, ์˜ˆ์™ธ์ฒ˜๋ฆฌ ํ•„์š” - String sortString = pageable.getSort().toString(); - if (!sortString.equals("likeCount: DESC")) { - throw new CoreException(ErrorType.BAD_REQUEST, "์ง€์›ํ•˜์ง€ ์•Š๋Š” ์ •๋ ฌ ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค."); - } - return productMetricsRepository.findAll(pageable); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/Order.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/Order.java deleted file mode 100644 index c9af5dba3..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/order/Order.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.loopers.domain.order; - -import com.loopers.domain.BaseEntity; -import com.loopers.domain.common.vo.Price; -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import jakarta.persistence.*; -import lombok.Getter; - -import java.util.List; - -@Entity -@Table(name = "tb_order") -@Getter -public class Order extends BaseEntity { - private Long userId; - @ElementCollection - @CollectionTable( - name = "tb_order_item", - joinColumns = @JoinColumn(name = "order_id") - ) - private List orderItems; - @Convert(converter = Price.Converter.class) - private Price totalPrice; - - protected Order() { - } - - private Order(Long userId, List orderItems) { - this.userId = userId; - this.orderItems = orderItems; - this.totalPrice = new Price(orderItems.stream().map(OrderItem::getTotalPrice).reduce(Math::addExact).get()); - } - - public static Order create(Long userId, List orderItems) { - if (userId == null || userId <= 0) { - throw new CoreException(ErrorType.BAD_REQUEST, "์‚ฌ์šฉ์ž ID๋Š” 1 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); - } - if (orderItems == null || orderItems.isEmpty()) { - throw new CoreException(ErrorType.BAD_REQUEST, "์ฃผ๋ฌธ ํ•ญ๋ชฉ์€ ์ตœ์†Œ 1๊ฐœ ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); - } - return new Order(userId, orderItems); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItem.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItem.java deleted file mode 100644 index e4a4eaa3f..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItem.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.loopers.domain.order; - -import com.loopers.domain.common.vo.Price; -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import jakarta.persistence.Convert; -import jakarta.persistence.Embeddable; -import lombok.Getter; -import org.apache.commons.lang3.StringUtils; - -@Embeddable -@Getter -public class OrderItem { - private Long productId; - private String productName; - private Integer quantity; - @Convert(converter = Price.Converter.class) - private Price price; - - public Integer getTotalPrice() { - return this.price.amount() * this.quantity; - } - - protected OrderItem() { - } - - private OrderItem(Long productId, String productName, Integer quantity, Price price) { - this.productId = productId; - this.productName = productName; - this.quantity = quantity; - this.price = price; - } - - public static OrderItem create(Long productId, String productName, Integer quantity, Price price) { - if (productId == null || productId <= 0) { - throw new CoreException(ErrorType.BAD_REQUEST, "์ƒํ’ˆ ID๋Š” 1 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); - } - if (StringUtils.isBlank(productName)) { - throw new CoreException(ErrorType.BAD_REQUEST, "์ƒํ’ˆ๋ช…์€ ํ•„์ˆ˜์ด๋ฉฐ ๊ณต๋ฐฑ์ผ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); - } - if (quantity == null || quantity <= 0) { - throw new CoreException(ErrorType.BAD_REQUEST, "์ฃผ๋ฌธ ์ˆ˜๋Ÿ‰์€ 1 ์ด์ƒ์˜ ์ž์—ฐ์ˆ˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค."); - } - if (price == null) { - throw new CoreException(ErrorType.BAD_REQUEST, "๊ฐ€๊ฒฉ์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."); - } - - return new OrderItem(productId, productName, quantity, price); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderRepository.java deleted file mode 100644 index 0118aa719..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderRepository.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.loopers.domain.order; - -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; - -import java.util.Optional; - -public interface OrderRepository{ - Optional findByIdAndUserId(Long id, Long userId); - - Order save(Order order); - - Page findByUserIdAndDeletedAtIsNull(Long userId, Pageable pageable); -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java deleted file mode 100644 index 979d19fbd..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.loopers.domain.order; - -import com.loopers.application.order.OrderItemRequest; -import com.loopers.domain.product.Product; -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.stereotype.Component; - -import java.util.List; -import java.util.Map; - -@RequiredArgsConstructor -@Component -public class OrderService { - private final OrderRepository orderRepository; - - public Order save(Order order) { - return orderRepository.save(order); - } - - public Order createOrder(List OrderItems, Map productMap, Long userId) { - List orderItems = OrderItems - .stream() - .map(item -> OrderItem.create( - item.productId(), - productMap.get(item.productId()).getName(), - item.quantity(), - productMap.get(item.productId()).getPrice() - )) - .toList(); - Order order = Order.create(userId, orderItems); - - return orderRepository.save(order); - } - - public Order getOrderByIdAndUserId(Long orderId, Long userId) { - return orderRepository.findByIdAndUserId(orderId, userId) - .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "์ฃผ๋ฌธ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); - } - - public Page getOrdersByUserId(Long userId, Pageable pageable) { - int page = pageable.getPageNumber(); - int size = pageable.getPageSize(); - Sort sort = Sort.by(Sort.Direction.DESC, "createdAt"); - return orderRepository.findByUserIdAndDeletedAtIsNull(userId, PageRequest.of(page, size, sort)); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java b/apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java deleted file mode 100644 index ebe3d964b..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.loopers.domain.point; - -import com.loopers.domain.BaseEntity; -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.Table; -import lombok.Getter; - -@Entity -@Table(name = "tb_point") -@Getter -public class Point extends BaseEntity { - @Column(name = "user_id", nullable = false, updatable = false, unique = true) - private Long userId; - private Long amount; - - protected Point() { - } - - private Point(Long userId, Long amount) { - this.userId = userId; - this.amount = amount; - } - - public static Point create(Long userId) { - return new Point(userId, 0L); - } - - public void charge(int otherAmount) { - if (otherAmount <= 0) { - throw new CoreException(ErrorType.BAD_REQUEST, "์ถฉ์ „ ํฌ์ธํŠธ๋Š” 0๋ณด๋‹ค ์ปค์•ผ ํ•ฉ๋‹ˆ๋‹ค."); - } - - this.amount += otherAmount; - } - - public void deduct(int otherAmount) { - if (otherAmount <= 0) { - throw new CoreException(ErrorType.BAD_REQUEST, "์ฐจ๊ฐํ•  ํฌ์ธํŠธ๋Š” 0๋ณด๋‹ค ์ปค์•ผ ํ•ฉ๋‹ˆ๋‹ค."); - } - if (this.amount < otherAmount) { - throw new CoreException(ErrorType.BAD_REQUEST, "ํฌ์ธํŠธ๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค."); - } - this.amount -= otherAmount; - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/point/PointRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/point/PointRepository.java deleted file mode 100644 index 07b90479c..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/point/PointRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.loopers.domain.point; - -import java.util.Optional; - -public interface PointRepository { - Optional findByUserId(Long userId); - - void save(Point point); -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java b/apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java deleted file mode 100644 index 2ea51a376..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.loopers.domain.point; - -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.Optional; - -@RequiredArgsConstructor -@Service -public class PointService { - private final PointRepository pointRepository; - - @Transactional - public void createPoint(Long userId) { - Point point = Point.create(userId); - pointRepository.save(point); - } - - @Transactional - public Long chargePoint(Long userId, int amount) { - Point point = pointRepository.findByUserId(userId) - .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "ํฌ์ธํŠธ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); - point.charge(amount); - pointRepository.save(point); - return point.getAmount(); - } - - @Transactional(readOnly = true) - public Optional getCurrentPoint(Long userId) { - return pointRepository.findByUserId(userId).map(Point::getAmount); - } - - @Transactional - public void checkAndDeductPoint(Long userId, Integer totalAmount) { - Point point = pointRepository.findByUserId(userId).orElseThrow( - () -> new CoreException(ErrorType.NOT_FOUND, "ํฌ์ธํŠธ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.") - ); - point.deduct(totalAmount); - pointRepository.save(point); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java deleted file mode 100644 index 250516420..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.loopers.domain.product; - -import com.loopers.domain.BaseEntity; -import com.loopers.domain.common.vo.Price; -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import jakarta.persistence.Column; -import jakarta.persistence.Convert; -import jakarta.persistence.Entity; -import jakarta.persistence.Table; -import lombok.Getter; -import org.apache.commons.lang3.StringUtils; - -@Entity -@Table(name = "tb_product") -@Getter -public class Product extends BaseEntity { - protected Product() { - } - - private String name; - @Column(name = "brand_id", nullable = false, updatable = false) - private Long brandId; - @Convert(converter = Price.Converter.class) - private Price price; - - public static Product create(String name, Long brandId, Price price) { - if (StringUtils.isBlank(name)) { - throw new CoreException(ErrorType.BAD_REQUEST, "์ƒํ’ˆ๋ช…์€ ํ•„์ˆ˜์ด๋ฉฐ ๊ณต๋ฐฑ์ผ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); - } - if (brandId == null || brandId <= 0) { - throw new CoreException(ErrorType.BAD_REQUEST, "๋ธŒ๋žœ๋“œ ID๋Š” 1 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); - } - if (price == null) { - throw new CoreException(ErrorType.BAD_REQUEST, "๊ฐ€๊ฒฉ์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."); - } - if (price.amount() < 0) { - throw new CoreException(ErrorType.BAD_REQUEST, "๊ฐ€๊ฒฉ์€ ์Œ์ˆ˜๊ฐ€ ๋  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); - } - Product product = new Product(); - product.name = name; - product.brandId = brandId; - product.price = price; - return product; - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductRepository.java deleted file mode 100644 index beb141147..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductRepository.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.loopers.domain.product; - -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; - -import java.util.Collection; -import java.util.List; -import java.util.Optional; - -public interface ProductRepository { - Optional findById(Long productId); - - Page findAll(Pageable pageable); - - List findAllByIdIn(Collection ids); - - boolean existsById(Long productId); -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java deleted file mode 100644 index 9a4bf8842..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.loopers.domain.product; - -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import lombok.RequiredArgsConstructor; -import org.apache.commons.lang3.StringUtils; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.stereotype.Component; - -import java.util.Collection; -import java.util.Map; -import java.util.stream.Collectors; - -@RequiredArgsConstructor -@Component -public class ProductService { - private final ProductRepository productRepository; - - public Product getProductById(Long productId) { - return productRepository.findById(productId) - .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "์ƒํ’ˆ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); - } - - public Map getProductMapByIds(Collection productIds) { - return productRepository.findAllByIdIn(productIds) - .stream() - .collect(Collectors.toMap(Product::getId, product -> product)); - } - - public Page getProducts(Pageable pageable) { - int page = pageable.getPageNumber(); - int size = pageable.getPageSize(); - String sortStr = pageable.getSort().toString().split(":")[0]; - Sort sort = Sort.by(Sort.Direction.DESC, "createdAt"); - - if (StringUtils.startsWith(sortStr, "price_asc")) { - sort = Sort.by(Sort.Direction.ASC, "price"); - } - return productRepository.findAll(PageRequest.of(page, size, sort)); - } - - public Integer calculateTotalAmount(Map items) { - return productRepository.findAllByIdIn(items.keySet()) - .stream() - .mapToInt(product -> product.getPrice().amount() * items.get(product.getId())) - .sum(); - } - - public boolean existsById(Long productId) { - return productRepository.existsById(productId); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/supply/Supply.java b/apps/commerce-api/src/main/java/com/loopers/domain/supply/Supply.java deleted file mode 100644 index 834bd4628..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/supply/Supply.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.loopers.domain.supply; - -import com.loopers.domain.BaseEntity; -import com.loopers.domain.supply.vo.Stock; -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import jakarta.persistence.Convert; -import jakarta.persistence.Entity; -import jakarta.persistence.Table; -import lombok.Getter; -import lombok.Setter; - -@Entity -@Table(name = "tb_supply") -@Getter -public class Supply extends BaseEntity { - private Long productId; - @Setter - @Convert(converter = Stock.Converter.class) - private Stock stock; - // think: ์ธ๋‹น ๊ตฌ๋งค์ œํ•œ? - - protected Supply() { - } - - public static Supply create(Long productId, Stock stock) { - if (productId == null || productId <= 0) { - throw new CoreException(ErrorType.BAD_REQUEST, "์ƒํ’ˆ ID๋Š” 1 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); - } - if (stock == null) { - throw new CoreException(ErrorType.BAD_REQUEST, "์žฌ๊ณ ๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."); - } - Supply supply = new Supply(); - supply.productId = productId; - supply.stock = stock; - return supply; - } - - // decreaseStock, increaseStock - public void decreaseStock(int quantity) { - this.stock = this.stock.decrease(quantity); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/supply/SupplyRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/supply/SupplyRepository.java deleted file mode 100644 index 7e7cb09ca..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/supply/SupplyRepository.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.loopers.domain.supply; - -import java.util.Collection; -import java.util.List; -import java.util.Optional; - -public interface SupplyRepository { - Optional findByProductId(Long productId); - - List findAllByProductIdIn(Collection productIds); - - Optional findByProductIdForUpdate(Long productId); - - Supply save(Supply supply); -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/supply/SupplyService.java b/apps/commerce-api/src/main/java/com/loopers/domain/supply/SupplyService.java deleted file mode 100644 index b8de2c1df..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/supply/SupplyService.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.loopers.domain.supply; - -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; - -import java.util.Collection; -import java.util.Map; -import java.util.stream.Collectors; - -@RequiredArgsConstructor -@Component -public class SupplyService { - private final SupplyRepository supplyRepository; - - public Supply getSupplyByProductId(Long productId) { - return supplyRepository.findByProductId(productId) - .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "ํ•ด๋‹น ์ƒํ’ˆ์˜ ์žฌ๊ณ  ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); - } - - public Map getSupplyMapByProductIds(Collection productIds) { - return supplyRepository.findAllByProductIdIn(productIds) - .stream() - .collect(Collectors.toMap(Supply::getProductId, supply -> supply)); - } - - @Transactional - public void checkAndDecreaseStock(Long productId, Integer quantity) { - Supply supply = supplyRepository.findByProductIdForUpdate(productId) - .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "ํ•ด๋‹น ์ƒํ’ˆ์˜ ์žฌ๊ณ  ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); - supply.decreaseStock(quantity); - supplyRepository.save(supply); - } - - public Supply saveSupply(Supply supply) { - return supplyRepository.save(supply); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/supply/vo/Stock.java b/apps/commerce-api/src/main/java/com/loopers/domain/supply/vo/Stock.java deleted file mode 100644 index b76e5f1a1..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/supply/vo/Stock.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.loopers.domain.supply.vo; - -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import jakarta.persistence.AttributeConverter; - -public record Stock(int quantity) { - public Stock { - if (quantity < 0) { - throw new CoreException(ErrorType.BAD_REQUEST, "์žฌ๊ณ ๋Š” ์Œ์ˆ˜๊ฐ€ ๋  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); - } - } - - public boolean isOutOfStock() { - return this.quantity <= 0; - } - - public Stock decrease(int orderQuantity) { - if (orderQuantity <= 0) { - throw new CoreException(ErrorType.BAD_REQUEST, "์ฃผ๋ฌธ ์ˆ˜๋Ÿ‰์€ 0๋ณด๋‹ค ์ปค์•ผ ํ•ฉ๋‹ˆ๋‹ค."); - } - if (orderQuantity > this.quantity) { - throw new CoreException(ErrorType.BAD_REQUEST, "์žฌ๊ณ ๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค."); - } - return new Stock(this.quantity - orderQuantity); - } - - public static class Converter implements AttributeConverter { - - @Override - public Integer convertToDatabaseColumn(Stock attribute) { - return attribute.quantity(); - } - - @Override - public Stock convertToEntityAttribute(Integer dbData) { - return new Stock(dbData); - } - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java deleted file mode 100644 index bd8bc6adf..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.loopers.domain.user; - -import com.loopers.domain.BaseEntity; -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.Table; -import lombok.Getter; -import org.apache.commons.lang3.StringUtils; - -@Entity -@Table(name = "tb_user") -@Getter -public class User extends BaseEntity { - protected User() { - } - - @Column(nullable = false, unique = true, length = 10) - private String userId; - private String email; - private String birthday; - private String gender; - - private User(String userId, String email, String birthday, String gender) { - this.userId = userId; - this.email = email; - this.birthday = birthday; - this.gender = gender; - } - - public static User create(String userId, String email, String birthday, String gender) { - // ID ๊ฐ€ ์˜๋ฌธ ๋ฐ ์ˆซ์ž 10์ž ์ด๋‚ด ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. - if (StringUtils.isBlank(userId) || !userId.matches("^(?=.*[a-zA-Z])(?=.*\\d)[a-zA-Z\\d]{1,10}$")) { - throw new CoreException(ErrorType.BAD_REQUEST, "ID๋Š” ์˜๋ฌธ ๋ฐ ์ˆซ์ž 10์ž ์ด๋‚ด์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค."); - } - // ์ด๋ฉ”์ผ์ด xx@yy.zz ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. - if (StringUtils.isBlank(email) || !email.matches("^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$")) { - throw new CoreException(ErrorType.BAD_REQUEST, "์ด๋ฉ”์ผ ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); - } - // ์ƒ๋…„์›”์ผ์ด YYYY-MM-DD ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. - if (StringUtils.isBlank(birthday) || !birthday.matches("^\\d{4}-\\d{2}-\\d{2}$")) { - throw new CoreException(ErrorType.BAD_REQUEST, "์ƒ๋…„์›”์ผ ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); - } - // ์„ฑ๋ณ„์ด ์—†์„ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. - if (StringUtils.isBlank(gender)) { - throw new CoreException(ErrorType.BAD_REQUEST, "์„ฑ๋ณ„์€ ํ•„์ˆ˜ ์ž…๋ ฅ ํ•ญ๋ชฉ์ž…๋‹ˆ๋‹ค."); - } - - return new User(userId, email, birthday, gender); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java deleted file mode 100644 index 90f701fbd..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.loopers.domain.user; - -import java.util.Optional; - -public interface UserRepository { - Optional findByUserId(String userId); - - Optional findByUserIdForUpdate(String userId); - - boolean existsUserByUserId(String userId); - - User save(User user); -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java deleted file mode 100644 index 8b1129b45..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.loopers.domain.user; - -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.Optional; - -@RequiredArgsConstructor -@Service -public class UserService { - private final UserRepository userRepository; - - @Transactional - public User registerUser(String userId, String email, String birthday, String gender) { - // ์ด๋ฏธ ๋“ฑ๋ก๋œ userId ์ธ ๊ฒฝ์šฐ, ์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚จ๋‹ค. - if (userRepository.existsUserByUserId(userId)) { - throw new CoreException(ErrorType.CONFLICT, "์ด๋ฏธ ์กด์žฌํ•˜๋Š” ์‚ฌ์šฉ์ž ID ์ž…๋‹ˆ๋‹ค."); - } - User user = User.create(userId, email, birthday, gender); - return userRepository.save(user); - } - - @Transactional(readOnly = true) - public Optional findByUserId(String userId) { - return userRepository.findByUserId(userId); - } - - // find by user id with lock for update - @Transactional - public Optional findByUserIdForUpdate(String userId) { - return userRepository.findByUserIdForUpdate(userId); - } - -} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandJpaRepository.java deleted file mode 100644 index aa99ac6ca..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandJpaRepository.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.loopers.infrastructure.brand; - -import com.loopers.domain.brand.Brand; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface BrandJpaRepository extends JpaRepository { - -} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandRepositoryImpl.java deleted file mode 100644 index 69b7cbb79..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandRepositoryImpl.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.loopers.infrastructure.brand; - -import com.loopers.domain.brand.Brand; -import com.loopers.domain.brand.BrandRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; - -import java.util.Collection; -import java.util.Optional; - -@RequiredArgsConstructor -@Component -public class BrandRepositoryImpl implements BrandRepository { - private final BrandJpaRepository brandJpaRepository; - - @Override - public Optional findById(Long id) { - return brandJpaRepository.findById(id); - } - - @Override - public Collection findAllByIdIn(Collection ids) { - return brandJpaRepository.findAllById(ids); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeProductJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeProductJpaRepository.java deleted file mode 100644 index 6c247ec44..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeProductJpaRepository.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.loopers.infrastructure.like; - -import com.loopers.domain.like.product.LikeProduct; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.repository.JpaRepository; - -import java.time.ZonedDateTime; -import java.util.Optional; - -public interface LikeProductJpaRepository extends JpaRepository { - Optional findByUserIdAndProductId(Long userId, Long productId); - - boolean existsByUserIdAndProductId(Long userId, Long productId); - - Page getLikeProductsByUserIdAndDeletedAtIsNull(Long userId, Pageable pageable); -} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeProductRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeProductRepositoryImpl.java deleted file mode 100644 index 8827a431f..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeProductRepositoryImpl.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.loopers.infrastructure.like; - -import com.loopers.domain.like.product.LikeProduct; -import com.loopers.domain.like.product.LikeProductRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Component; - -import java.util.Optional; - -@RequiredArgsConstructor -@Component -public class LikeProductRepositoryImpl implements LikeProductRepository { - private final LikeProductJpaRepository likeProductJpaRepository; - - @Override - public boolean existsByUserIdAndProductId(Long userId, Long productId) { - return likeProductJpaRepository.existsByUserIdAndProductId(userId, productId); - } - - @Override - public Optional findByUserIdAndProductId(Long userId, Long productId) { - return likeProductJpaRepository.findByUserIdAndProductId(userId, productId); - } - - @Override - public void save(LikeProduct likeProduct) { - likeProductJpaRepository.save(likeProduct); - } - - @Override - public Page getLikeProductsByUserIdAndDeletedAtIsNull(Long userId, Pageable pageable) { - return likeProductJpaRepository.getLikeProductsByUserIdAndDeletedAtIsNull(userId, pageable); - } - -} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/metrics/product/ProductMetricsJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/metrics/product/ProductMetricsJpaRepository.java deleted file mode 100644 index 42bde9788..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/metrics/product/ProductMetricsJpaRepository.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.loopers.infrastructure.metrics.product; - -import com.loopers.domain.metrics.product.ProductMetrics; -import org.springframework.data.jpa.repository.JpaRepository; - -import java.util.Optional; - -public interface ProductMetricsJpaRepository extends JpaRepository { - Optional findByProductId(Long productId); -} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/metrics/product/ProductMetricsRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/metrics/product/ProductMetricsRepositoryImpl.java deleted file mode 100644 index 3fb0ec2ce..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/metrics/product/ProductMetricsRepositoryImpl.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.loopers.infrastructure.metrics.product; - -import com.loopers.domain.metrics.product.ProductMetrics; -import com.loopers.domain.metrics.product.ProductMetricsRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Component; - -import java.util.Collection; -import java.util.Optional; - -@RequiredArgsConstructor -@Component -public class ProductMetricsRepositoryImpl implements ProductMetricsRepository { - private final ProductMetricsJpaRepository jpaRepository; - - @Override - public Optional findByProductId(Long productId) { - return jpaRepository.findByProductId(productId); - } - - @Override - public Collection findByProductIds(Collection productIds) { - return jpaRepository.findAllById(productIds); - } - - @Override - public Page findAll(Pageable pageable) { - return jpaRepository.findAll(pageable); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderJpaRepository.java deleted file mode 100644 index c3337045e..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderJpaRepository.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.loopers.infrastructure.order; - -import com.loopers.domain.order.Order; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.repository.JpaRepository; - -import java.util.Optional; - -public interface OrderJpaRepository extends JpaRepository { - - Optional findByIdAndUserIdAndDeletedAtIsNull(Long id, Long userId); - - Page findByUserIdAndDeletedAtIsNull(Long userId, Pageable pageable); - -} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderRepositoryImpl.java deleted file mode 100644 index 67a7462fe..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderRepositoryImpl.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.loopers.infrastructure.order; - -import com.loopers.domain.order.Order; -import com.loopers.domain.order.OrderRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Component; - -import java.util.Optional; - -@RequiredArgsConstructor -@Component -public class OrderRepositoryImpl implements OrderRepository { - private final OrderJpaRepository orderJpaRepository; - - @Override - public Optional findByIdAndUserId(Long id, Long userId) { - return orderJpaRepository.findByIdAndUserIdAndDeletedAtIsNull(id, userId); - } - - @Override - public Order save(Order order) { - return orderJpaRepository.save(order); - } - - @Override - public Page findByUserIdAndDeletedAtIsNull(Long userId, Pageable pageable) { - return orderJpaRepository.findByUserIdAndDeletedAtIsNull(userId, pageable); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointJpaRepository.java deleted file mode 100644 index 74320f6a2..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointJpaRepository.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.loopers.infrastructure.point; - -import com.loopers.domain.point.Point; -import org.springframework.data.jpa.repository.JpaRepository; - -import java.util.Optional; - -public interface PointJpaRepository extends JpaRepository { - - Optional findByUserId(Long userId); -} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointRepositoryImpl.java deleted file mode 100644 index 052de7762..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointRepositoryImpl.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.loopers.infrastructure.point; - -import com.loopers.domain.point.Point; -import com.loopers.domain.point.PointRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; - -import java.util.Optional; - -@RequiredArgsConstructor -@Component -public class PointRepositoryImpl implements PointRepository { - private final PointJpaRepository pointJpaRepository; - - @Override - public Optional findByUserId(Long userId) { - return pointJpaRepository.findByUserId(userId); - } - - @Override - public void save(Point point) { - pointJpaRepository.save(point); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.java deleted file mode 100644 index 0375b7ca7..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.loopers.infrastructure.product; - -import com.loopers.domain.product.Product; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface ProductJpaRepository extends JpaRepository { -} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java deleted file mode 100644 index b2b1115b9..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.loopers.infrastructure.product; - -import com.loopers.domain.product.Product; -import com.loopers.domain.product.ProductRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Component; - -import java.util.Collection; -import java.util.List; -import java.util.Optional; - -@RequiredArgsConstructor -@Component -public class ProductRepositoryImpl implements ProductRepository { - private final ProductJpaRepository productJpaRepository; - - @Override - public Optional findById(Long productId) { - return productJpaRepository.findById(productId); - } - - @Override - public Page findAll(Pageable pageable) { - return productJpaRepository.findAll(pageable); - } - - @Override - public List findAllByIdIn(Collection ids) { - return productJpaRepository.findAllById(ids); - } - - @Override - public boolean existsById(Long productId) { - return productJpaRepository.existsById(productId); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/supply/SupplyJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/supply/SupplyJpaRepository.java deleted file mode 100644 index ec66a450d..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/supply/SupplyJpaRepository.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.loopers.infrastructure.supply; - -import com.loopers.domain.supply.Supply; -import jakarta.persistence.LockModeType; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Lock; -import org.springframework.data.jpa.repository.Query; - -import java.util.Collection; -import java.util.List; -import java.util.Optional; - -public interface SupplyJpaRepository extends JpaRepository { - Optional findByProductId(Long productId); - - List findAllByProductIdIn(Collection productIds); - - @Lock(LockModeType.PESSIMISTIC_WRITE) - @Query("SELECT s FROM Supply s WHERE s.productId = :productId") - Optional findByProductIdForUpdate(Long productId); -} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/supply/SupplyRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/supply/SupplyRepositoryImpl.java deleted file mode 100644 index 92b13a07b..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/supply/SupplyRepositoryImpl.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.loopers.infrastructure.supply; - -import com.loopers.domain.supply.Supply; -import com.loopers.domain.supply.SupplyRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; - -import java.util.Collection; -import java.util.List; -import java.util.Optional; - -@RequiredArgsConstructor -@Component -public class SupplyRepositoryImpl implements SupplyRepository { - private final SupplyJpaRepository supplyJpaRepository; - - @Override - public Optional findByProductId(Long productId) { - return supplyJpaRepository.findByProductId(productId); - } - - @Override - public List findAllByProductIdIn(Collection productIds) { - return supplyJpaRepository.findAllByProductIdIn(productIds); - } - - @Override - public Optional findByProductIdForUpdate(Long productId) { - return supplyJpaRepository.findByProductIdForUpdate(productId); - } - - @Override - public Supply save(Supply supply) { - return supplyJpaRepository.save(supply); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserJpaRepository.java deleted file mode 100644 index 0f298e023..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserJpaRepository.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.loopers.infrastructure.user; - -import com.loopers.domain.user.User; -import jakarta.persistence.LockModeType; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Lock; -import org.springframework.data.jpa.repository.Query; - -import java.util.Optional; - -public interface UserJpaRepository extends JpaRepository { - Optional findByUserId(String userId); - - @Lock(LockModeType.PESSIMISTIC_WRITE) - @Query("SELECT u FROM User u WHERE u.userId = :userId") - Optional findByUserIdForUpdate(String userId); - - boolean existsUserByUserId(String userId); -} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java deleted file mode 100644 index 2018487e8..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.loopers.infrastructure.user; - -import com.loopers.domain.user.User; -import com.loopers.domain.user.UserRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; - -import java.util.Optional; - -@RequiredArgsConstructor -@Component -public class UserRepositoryImpl implements UserRepository { - private final UserJpaRepository userJpaRepository; - - @Override - public Optional findByUserId(String userId) { - return userJpaRepository.findByUserId(userId); - } - - @Override - public Optional findByUserIdForUpdate(String userId) { - return userJpaRepository.findByUserIdForUpdate(userId); - } - - @Override - public boolean existsUserByUserId(String userId) { - return userJpaRepository.existsUserByUserId(userId); - } - - @Override - public User save(User user) { - return userJpaRepository.save(user); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/product/LikeProductV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/product/LikeProductV1ApiSpec.java deleted file mode 100644 index 716f9b735..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/product/LikeProductV1ApiSpec.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.loopers.interfaces.api.like.product; - -import com.loopers.interfaces.api.ApiResponse; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.tags.Tag; -import org.springframework.data.domain.Pageable; -import org.springframework.web.bind.annotation.RequestHeader; - -@Tag(name = "Like Product V1 API", description = "์ƒํ’ˆ ์ข‹์•„์š” API ์ž…๋‹ˆ๋‹ค.") -public interface LikeProductV1ApiSpec { - // /api/v1/like/products/{productId} - POST - @Operation( - method = "POST", - summary = "์ƒํ’ˆ ์ข‹์•„์š” ์ถ”๊ฐ€", - description = "ํšŒ์›์ด ํŠน์ • ์ƒํ’ˆ์— ์ข‹์•„์š”๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค." - ) - ApiResponse likeProduct( - @RequestHeader(value = "X-USER-ID", required = false) String userId, - Long productId - ); - - // /api/v1/like/products/{productId} - DELETE - @Operation( - method = "DELETE", - summary = "์ƒํ’ˆ ์ข‹์•„์š” ์ทจ์†Œ", - description = "ํšŒ์›์ด ํŠน์ • ์ƒํ’ˆ์— ๋Œ€ํ•œ ์ข‹์•„์š”๋ฅผ ์ทจ์†Œํ•ฉ๋‹ˆ๋‹ค." - ) - ApiResponse unlikeProduct( - @RequestHeader(value = "X-USER-ID", required = false) String userId, - Long productId - ); - - // /api/v1/like/products - GET - @Operation( - method = "GET", - summary = "ํšŒ์›์ด ์ข‹์•„์š”ํ•œ ์ƒํ’ˆ ๋ชฉ๋ก ์กฐํšŒ", - description = "ํšŒ์›์ด ์ข‹์•„์š”ํ•œ ์ƒํ’ˆ๋“ค์˜ ๋ชฉ๋ก์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค." - ) - ApiResponse getLikedProducts( - @RequestHeader(value = "X-USER-ID", required = false) String userId, - @Schema( - name = "ํŽ˜์ด์ง€ ์ •๋ณด", - description = "ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ, ํŽ˜์ด์ง€ ํฌ๊ธฐ, ์ •๋ ฌ ์ •๋ณด๋ฅผ ํฌํ•จํ•œ ํŽ˜์ด์ง€ ์ •๋ณด" + - "\n- ๊ธฐ๋ณธ๊ฐ’: page=0, size=20" - ) - Pageable pageable - ); -} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/product/LikeProductV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/product/LikeProductV1Controller.java deleted file mode 100644 index f49e584d1..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/product/LikeProductV1Controller.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.loopers.interfaces.api.like.product; - -import com.loopers.application.like.product.LikeProductFacade; -import com.loopers.application.like.product.LikeProductInfo; -import com.loopers.interfaces.api.ApiResponse; -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import lombok.RequiredArgsConstructor; -import org.apache.commons.lang3.StringUtils; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.web.PageableDefault; -import org.springframework.web.bind.annotation.*; - -@RequiredArgsConstructor -@RestController -@RequestMapping("/api/v1/like/products") -public class LikeProductV1Controller implements LikeProductV1ApiSpec { - private final LikeProductFacade likeProductFacade; - - @RequestMapping(method = RequestMethod.POST, path = "/{productId}") - @Override - public ApiResponse likeProduct( - @RequestHeader(value = "X-USER-ID", required = false) String userId, - @PathVariable Long productId) { - if (StringUtils.isBlank(userId)) { - throw new CoreException(ErrorType.BAD_REQUEST); - } - likeProductFacade.likeProduct(userId, productId); - return ApiResponse.success(null); - } - - @RequestMapping(method = RequestMethod.DELETE, path = "/{productId}") - @Override - public ApiResponse unlikeProduct( - @RequestHeader(value = "X-USER-ID", required = false) String userId, - @PathVariable Long productId - ) { - if (StringUtils.isBlank(userId)) { - throw new CoreException(ErrorType.BAD_REQUEST); - } - likeProductFacade.unlikeProduct(userId, productId); - return ApiResponse.success(null); - } - - @RequestMapping(method = RequestMethod.GET) - @Override - public ApiResponse getLikedProducts( - @RequestHeader(value = "X-USER-ID", required = false) String userId, - @PageableDefault(size = 20) Pageable pageable - ) { - if (StringUtils.isBlank(userId)) { - throw new CoreException(ErrorType.BAD_REQUEST); - } - Page likedProducts = likeProductFacade.getLikedProducts(userId, pageable); - LikeProductV1Dto.ProductsResponse response = LikeProductV1Dto.ProductsResponse.from(likedProducts); - return ApiResponse.success(response); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/product/LikeProductV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/product/LikeProductV1Dto.java deleted file mode 100644 index 81c084b56..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/product/LikeProductV1Dto.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.loopers.interfaces.api.like.product; - -import com.loopers.application.like.product.LikeProductInfo; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Sort; - -import java.util.List; - -public class LikeProductV1Dto { - public record ProductResponse( - Long id, - String name, - String brand, - int price, - int likes, - int stock - ) { - public static LikeProductV1Dto.ProductResponse from(LikeProductInfo info) { - return new LikeProductV1Dto.ProductResponse( - info.id(), - info.name(), - info.brand(), - info.price(), - info.likes(), - info.stock() - ); - } - } - - public record ProductsResponse( - List content, - int totalPages, - long totalElements, - int number, - int size - - ) { - public static LikeProductV1Dto.ProductsResponse from(Page page) { - return new LikeProductV1Dto.ProductsResponse( - page.map(LikeProductV1Dto.ProductResponse::from).getContent(), - page.getTotalPages(), - page.getTotalElements(), - page.getNumber(), - page.getSize() - ); - } - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1ApiSpec.java deleted file mode 100644 index 37259802d..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1ApiSpec.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.loopers.interfaces.api.order; - -import com.loopers.application.order.OrderRequest; -import com.loopers.interfaces.api.ApiResponse; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.tags.Tag; -import org.springframework.data.domain.Pageable; -import org.springframework.data.web.PageableDefault; -import org.springframework.web.bind.annotation.RequestHeader; - -@Tag(name = "Order V1 API", description = "์ฃผ๋ฌธ API ์ž…๋‹ˆ๋‹ค.") -public interface OrderV1ApiSpec { - // /api/v1/orders - POST - @Operation( - method = "POST", - summary = "์ฃผ๋ฌธ ์ƒ์„ฑ", - description = "์ƒˆ๋กœ์šด ์ฃผ๋ฌธ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค." - ) - ApiResponse createOrder( - @RequestHeader(value = "X-USER-ID", required = false) String userId, - @Schema( - name = "์ฃผ๋ฌธ ์š”์ฒญ ์ •๋ณด", - description = "์ฃผ๋ฌธ ์ƒ์„ฑ์— ํ•„์š”ํ•œ ์ •๋ณด" - ) - OrderRequest request - ); - - // /api/v1/orders - GET - @Operation( - method = "GET", - summary = "์ฃผ๋ฌธ ๋ชฉ๋ก ์กฐํšŒ", - description = "ํšŒ์›์˜ ์ฃผ๋ฌธ ๋ชฉ๋ก์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค." - ) - ApiResponse getOrderList( - @RequestHeader(value = "X-USER-ID", required = false) String userId, - @PageableDefault(size = 20) Pageable pageable - ); - - // /api/v1/orders/{orderId} - GET - @Operation( - method = "GET", - summary = "์ฃผ๋ฌธ ์ƒ์„ธ ์กฐํšŒ", - description = "ํŠน์ • ์ฃผ๋ฌธ์˜ ์ƒ์„ธ ์ •๋ณด๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค." - ) - ApiResponse getOrderDetail( - @RequestHeader(value = "X-USER-ID", required = false) String userId, - @Schema( - name = "์ฃผ๋ฌธ ID", - description = "์กฐํšŒํ•  ์ฃผ๋ฌธ์˜ ID" - ) - Long orderId - ); -} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Controller.java deleted file mode 100644 index 0daeabd69..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Controller.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.loopers.interfaces.api.order; - -import com.loopers.application.order.OrderFacade; -import com.loopers.application.order.OrderInfo; -import com.loopers.application.order.OrderRequest; -import com.loopers.interfaces.api.ApiResponse; -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import lombok.RequiredArgsConstructor; -import org.apache.commons.lang3.StringUtils; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.web.PageableDefault; -import org.springframework.web.bind.annotation.*; - -@RequiredArgsConstructor -@RestController -@RequestMapping("/api/v1/orders") -public class OrderV1Controller implements OrderV1ApiSpec { - private final OrderFacade orderFacade; - - @RequestMapping(method = RequestMethod.POST) - @Override - public ApiResponse createOrder( - @RequestHeader(value = "X-USER-ID", required = false) String userId, - @RequestBody OrderRequest request - ) { - if (StringUtils.isBlank(userId)) { - throw new CoreException(ErrorType.BAD_REQUEST); - } - OrderInfo orderInfo = orderFacade.createOrder(userId, request); - OrderV1Dto.OrderResponse response = OrderV1Dto.OrderResponse.from(orderInfo); - return ApiResponse.success(response); - } - - @RequestMapping(method = RequestMethod.GET) - @Override - public ApiResponse getOrderList( - @RequestHeader(value = "X-USER-ID", required = false) String userId, - @PageableDefault(size = 20) Pageable pageable) { - if (StringUtils.isBlank(userId)) { - throw new CoreException(ErrorType.BAD_REQUEST); - } - Page orderInfos = orderFacade.getOrderList(userId, pageable); - OrderV1Dto.OrderPageResponse response = OrderV1Dto.OrderPageResponse.from(orderInfos); - return ApiResponse.success(response); - } - - @RequestMapping(method = RequestMethod.GET, path = "/{orderId}") - @Override - public ApiResponse getOrderDetail( - @RequestHeader(value = "X-USER-ID", required = false) String userId, - @PathVariable Long orderId) { - if (StringUtils.isBlank(userId)) { - throw new CoreException(ErrorType.BAD_REQUEST); - } - OrderInfo orderInfo = orderFacade.getOrderInfo(userId, orderId); - OrderV1Dto.OrderResponse response = OrderV1Dto.OrderResponse.from(orderInfo); - return ApiResponse.success(response); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Dto.java deleted file mode 100644 index 9b5312231..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Dto.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.loopers.interfaces.api.order; - -import com.loopers.application.order.OrderInfo; -import com.loopers.application.order.OrderItemInfo; -import org.springframework.data.domain.Page; - -import java.util.List; - -public class OrderV1Dto { - - public record OrderResponse( - Long orderId, - List items, - Integer totalPrice - ) { - public static OrderResponse from(OrderInfo info) { - return new OrderResponse( - info.orderId(), - OrderItem.fromList(info.items()), - info.totalPrice() - ); - } - } - - public record OrderItem( - Long productId, - String productName, - Integer quantity, - Integer totalPrice - ) { - public static OrderItem from(OrderItemInfo info) { - return new OrderItem( - info.productId(), - info.productName(), - info.quantity(), - info.totalPrice() - ); - } - - public static List fromList(List infos) { - return infos.stream() - .map(OrderItem::from) - .toList(); - } - } - - public record OrderPageResponse( - List content, - int totalPages, - long totalElements, - int number, - int size - ) { - public static OrderPageResponse from(Page page) { - return new OrderPageResponse( - page.map(OrderResponse::from).getContent(), - page.getTotalPages(), - page.getTotalElements(), - page.getNumber(), - page.getSize() - ); - } - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1ApiSpec.java deleted file mode 100644 index fb18c23d6..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1ApiSpec.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.loopers.interfaces.api.point; - -import com.loopers.interfaces.api.ApiResponse; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.tags.Tag; - -@Tag(name = "Point V1 API", description = "ใ…ใ…—์ธํŠธ API ์ž…๋‹ˆ๋‹ค.") -public interface PointV1ApiSpec { - - // /points - @Operation( - method = "GET", - summary = "ํฌ์ธํŠธ ์กฐํšŒ", - description = "ํšŒ์›์˜ ๋ณด์œ  ํฌ์ธํŠธ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค." - ) - // X-USER-ID ํ—ค๋”๊ฐ’ ์‚ฌ์šฉ - ApiResponse getUserPoints( - @Schema( - name = "ํšŒ์› ID", - description = "ํฌ์ธํŠธ๋ฅผ ์กฐํšŒํ•  ํšŒ์›์˜ ID" - ) - String userId - ); - - // /points post ํฌ์ธํŠธ ์ถฉ์ „ - @Operation( - method = "POST", - summary = "ํฌ์ธํŠธ ์ถฉ์ „", - description = "ํšŒ์›์˜ ํฌ์ธํŠธ๋ฅผ ์ถฉ์ „ํ•ฉ๋‹ˆ๋‹ค." - ) - ApiResponse chargeUserPoints( - @Schema( - name = "ํšŒ์› ID", - description = "ํฌ์ธํŠธ๋ฅผ ์ถฉ์ „ํ•  ํšŒ์›์˜ ID" - ) - String userId, - @Schema( - name = "์ถฉ์ „ํ•  ํฌ์ธํŠธ", - description = "์ถฉ์ „ํ•  ํฌ์ธํŠธ ๊ธˆ์•ก. ์–‘์ˆ˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค." - ) - PointV1Dto.PointChargeRequest request - ); -} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Controller.java deleted file mode 100644 index c656f69d3..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Controller.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.loopers.interfaces.api.point; - - -import com.loopers.application.point.PointFacade; -import com.loopers.interfaces.api.ApiResponse; -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import lombok.RequiredArgsConstructor; -import org.apache.commons.lang3.StringUtils; -import org.springframework.web.bind.annotation.*; - -@RequiredArgsConstructor -@RestController -@RequestMapping("/api/v1/points") -public class PointV1Controller implements PointV1ApiSpec { - private final PointFacade pointFacade; - - @RequestMapping(method = RequestMethod.GET) - @Override - public ApiResponse getUserPoints(@RequestHeader(value = "X-USER-ID", required = false) String userId) { - if (StringUtils.isBlank(userId)) { - throw new CoreException(ErrorType.BAD_REQUEST); - } - Long currentPoint = pointFacade.getCurrentPoint(userId); - PointV1Dto.PointResponse response = PointV1Dto.PointResponse.from(currentPoint); - return ApiResponse.success(response); - } - - @RequestMapping(method = RequestMethod.POST, path = "/charge") - @Override - public ApiResponse chargeUserPoints( - @RequestHeader(value = "X-USER-ID", required = false) String userId, - @RequestBody PointV1Dto.PointChargeRequest request) { - if (StringUtils.isBlank(userId)) { - throw new CoreException(ErrorType.BAD_REQUEST); - } - Long chargedPoint = pointFacade.chargePoint(userId, request.amount()); - PointV1Dto.PointResponse response = PointV1Dto.PointResponse.from(chargedPoint); - return ApiResponse.success(response); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Dto.java deleted file mode 100644 index a42ddec01..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Dto.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.loopers.interfaces.api.point; - -public class PointV1Dto { - - public record PointResponse( - Long currentPoint - ) { - public static PointV1Dto.PointResponse from(Long currentPoint) { - return new PointV1Dto.PointResponse( - currentPoint - ); - } - } - - public record PointChargeRequest( - int amount - ) { - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1ApiSpec.java deleted file mode 100644 index 5217803a1..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1ApiSpec.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.loopers.interfaces.api.product; - -import com.loopers.interfaces.api.ApiResponse; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.tags.Tag; -import org.springframework.data.domain.Pageable; - -@Tag(name = "Product V1 API", description = "์ƒํ’ˆ API ์ž…๋‹ˆ๋‹ค.") -public interface ProductV1ApiSpec { - // /api/v1/products - GET - @Operation( - method = "GET", - summary = "์ƒํ’ˆ ๋ชฉ๋ก ์กฐํšŒ", - description = "์ƒํ’ˆ ๋ชฉ๋ก์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค." - ) - ApiResponse getProductList( - @Schema( - name = "ํŽ˜์ด์ง€ ์ •๋ณด", - description = "ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ, ํŽ˜์ด์ง€ ํฌ๊ธฐ, ์ •๋ ฌ ์ •๋ณด๋ฅผ ํฌํ•จํ•œ ํŽ˜์ด์ง€ ์ •๋ณด" + - "\n- sort ์˜ต์…˜: latest (์ตœ์‹ ์ˆœ), price_asc (๊ฐ€๊ฒฉ ์˜ค๋ฆ„์ฐจ์ˆœ), like_desc (์ข‹์•„์š” ๋‚ด๋ฆผ์ฐจ์ˆœ)" + - "\n- ๊ธฐ๋ณธ๊ฐ’: page=0, size=20, sort=latest" - ) - Pageable pageable - ); - - // /api/v1/products/{productId} - GET - @Operation( - method = "GET", - summary = "์ƒํ’ˆ ์ƒ์„ธ ์กฐํšŒ", - description = "์ƒํ’ˆ ์ƒ์„ธ ์ •๋ณด๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค." - ) - ApiResponse getProductDetail( - @Schema( - name = "์ƒํ’ˆ ID", - description = "์กฐํšŒํ•  ์ƒํ’ˆ์˜ ID" - ) - Long productId - ); -} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java deleted file mode 100644 index 6597f8d5d..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.loopers.interfaces.api.product; - -import com.loopers.application.product.ProductFacade; -import com.loopers.application.product.ProductInfo; -import com.loopers.interfaces.api.ApiResponse; -import lombok.RequiredArgsConstructor; -import org.apache.commons.lang3.StringUtils; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.data.web.PageableDefault; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RestController; - -@RequiredArgsConstructor -@RestController -@RequestMapping("/api/v1/products") -public class ProductV1Controller implements ProductV1ApiSpec { - private final ProductFacade productFacade; - - @RequestMapping(method = RequestMethod.GET) - @Override - public ApiResponse getProductList(@PageableDefault(size = 20) Pageable pageable) { - Page products = productFacade.getProductList(pageable); - ProductV1Dto.ProductsPageResponse response = ProductV1Dto.ProductsPageResponse.from(products); - return ApiResponse.success(response); - } - - @RequestMapping(method = RequestMethod.GET, path = "/{productId}") - @Override - public ApiResponse getProductDetail(@PathVariable Long productId) { - ProductInfo info = productFacade.getProductDetail(productId); - ProductV1Dto.ProductResponse response = ProductV1Dto.ProductResponse.from(info); - return ApiResponse.success(response); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Dto.java deleted file mode 100644 index 29f957cf5..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Dto.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.loopers.interfaces.api.product; - -import com.loopers.application.product.ProductInfo; -import org.springframework.data.domain.Page; - -import java.util.List; - -public class ProductV1Dto { - public record ProductResponse( - Long id, - String name, - String brand, - int price, - int likes, - int stock - ) { - public static ProductResponse from(ProductInfo info) { - return new ProductResponse( - info.id(), - info.name(), - info.brand(), - info.price(), - info.likes(), - info.stock() - ); - } - } - - public record ProductsPageResponse( - List content, - int totalPages, - long totalElements, - int number, - int size - ) { - public static ProductsPageResponse from(Page page) { - return new ProductsPageResponse( - page.map(ProductResponse::from).getContent(), - page.getTotalPages(), - page.getTotalElements(), - page.getNumber(), - page.getSize() - ); - } - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1ApiSpec.java deleted file mode 100644 index bb1a413c8..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1ApiSpec.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.loopers.interfaces.api.user; - -import com.loopers.interfaces.api.ApiResponse; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.tags.Tag; - -@Tag(name = "User V1 API", description = "์‚ฌ์šฉ์ž API ์ž…๋‹ˆ๋‹ค.") -public interface UserV1ApiSpec { - - @Operation( - method = "POST", - summary = "ํšŒ์› ๊ฐ€์ž…", - description = "ํšŒ์›๊ฐ€์ž…์„ ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค." - ) - ApiResponse registerUser( - @Schema( - name = "ํšŒ์› ๊ฐ€์ž… ์š”์ฒญ", - description = "ํšŒ์› ๊ฐ€์ž…์— ํ•„์š”ํ•œ ์ •๋ณด๋ฅผ ์ž…๋ ฅํ•ฉ๋‹ˆ๋‹ค." - ) - UserV1Dto.UserRegisterRequest request - ); - - @Operation( - method = "GET", - summary = "๋‚ด ์ •๋ณด ์กฐํšŒ", - description = "ํšŒ์› ์ •๋ณด๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค." - ) - ApiResponse getUserInfo( - @Schema( - name = "ํšŒ์› ID", - description = "์กฐํšŒํ•  ํšŒ์›์˜ ID" - ) - String userId - ); - -} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java deleted file mode 100644 index e61ec5290..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.loopers.interfaces.api.user; - -import com.loopers.application.user.UserFacade; -import com.loopers.application.user.UserInfo; -import com.loopers.interfaces.api.ApiResponse; -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import lombok.RequiredArgsConstructor; -import org.apache.commons.lang3.StringUtils; -import org.springframework.web.bind.annotation.*; - -@RequiredArgsConstructor -@RestController -@RequestMapping("/api/v1/users") -public class UserV1Controller implements UserV1ApiSpec { - private final UserFacade userFacade; - - @RequestMapping(method = RequestMethod.POST) - @Override - public ApiResponse registerUser(@RequestBody UserV1Dto.UserRegisterRequest request) { - UserInfo info = userFacade.registerUser( - request.id(), - request.email(), - request.birthday(), - request.gender() - ); - UserV1Dto.UserResponse response = UserV1Dto.UserResponse.from(info); - return ApiResponse.success(response); - } - - @RequestMapping(method = RequestMethod.GET, path = "/me") - @Override - public ApiResponse getUserInfo(@RequestHeader(value = "X-USER-ID", required = false) String userId) { - if (StringUtils.isBlank(userId)) { - throw new CoreException(ErrorType.BAD_REQUEST); - } - UserInfo info = userFacade.getUserInfo(userId); - UserV1Dto.UserResponse response = UserV1Dto.UserResponse.from(info); - return ApiResponse.success(response); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java deleted file mode 100644 index a6500f737..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.loopers.interfaces.api.user; - -import com.loopers.application.user.UserInfo; - -public class UserV1Dto { - public record UserResponse( - String id, - String email, - String birthday, - String gender) { - public static UserResponse from(UserInfo info) { - return new UserResponse( - info.id(), - info.email(), - info.birthday(), - info.gender() - ); - } - } - - public record UserRegisterRequest( - String id, - String email, - String birthday, - String gender) { - } -} diff --git a/apps/commerce-api/src/main/resources/application.yml b/apps/commerce-api/src/main/resources/application.yml index a8b0b72e3..484c070d0 100644 --- a/apps/commerce-api/src/main/resources/application.yml +++ b/apps/commerce-api/src/main/resources/application.yml @@ -20,7 +20,7 @@ spring: config: import: - jpa.yml -# - redis.yml + - redis.yml - logging.yml - monitoring.yml diff --git a/apps/commerce-api/src/test/java/com/loopers/application/order/OrderFacadeIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/application/order/OrderFacadeIntegrationTest.java deleted file mode 100644 index 280d05573..000000000 --- a/apps/commerce-api/src/test/java/com/loopers/application/order/OrderFacadeIntegrationTest.java +++ /dev/null @@ -1,339 +0,0 @@ -package com.loopers.application.order; - -import com.loopers.domain.brand.Brand; -import com.loopers.domain.common.vo.Price; -import com.loopers.domain.metrics.product.ProductMetrics; -import com.loopers.domain.point.Point; -import com.loopers.domain.product.Product; -import com.loopers.domain.supply.Supply; -import com.loopers.domain.supply.vo.Stock; -import com.loopers.domain.user.User; -import com.loopers.infrastructure.brand.BrandJpaRepository; -import com.loopers.infrastructure.metrics.product.ProductMetricsJpaRepository; -import com.loopers.infrastructure.point.PointJpaRepository; -import com.loopers.infrastructure.product.ProductJpaRepository; -import com.loopers.infrastructure.supply.SupplyJpaRepository; -import com.loopers.infrastructure.user.UserJpaRepository; -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import com.loopers.utils.DatabaseCleanUp; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.transaction.annotation.Transactional; - -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; - -@SpringBootTest -@Transactional -@DisplayName("์ฃผ๋ฌธ Facade(OrderFacade) ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ") -public class OrderFacadeIntegrationTest { - - @Autowired - private OrderFacade orderFacade; - - @Autowired - private UserJpaRepository userJpaRepository; - - @Autowired - private PointJpaRepository pointJpaRepository; - - @Autowired - private BrandJpaRepository brandJpaRepository; - - @Autowired - private ProductJpaRepository productJpaRepository; - - @Autowired - private ProductMetricsJpaRepository productMetricsJpaRepository; - - @Autowired - private SupplyJpaRepository supplyJpaRepository; - - @Autowired - private DatabaseCleanUp databaseCleanUp; - - private String userId; - private Long userEntityId; - private Long brandId; - private Long productId1; - private Long productId2; - private Long productId3; - - @AfterEach - void tearDown() { - databaseCleanUp.truncateAllTables(); - } - - @BeforeEach - void setup() { - // User ๋“ฑ๋ก - User user = User.create("user123", "test@test.com", "1993-03-13", "male"); - User savedUser = userJpaRepository.save(user); - userId = savedUser.getUserId(); - userEntityId = savedUser.getId(); - - // Point ๋“ฑ๋ก ๋ฐ ์ถฉ์ „ - Point point = Point.create(userEntityId); - point.charge(100000); - pointJpaRepository.save(point); - - // Brand ๋“ฑ๋ก - Brand brand = Brand.create("Nike"); - Brand savedBrand = brandJpaRepository.save(brand); - brandId = savedBrand.getId(); - - // Product ๋“ฑ๋ก - Product product1 = Product.create("์ƒํ’ˆ1", brandId, new Price(10000)); - Product savedProduct1 = productJpaRepository.save(product1); - productId1 = savedProduct1.getId(); - ProductMetrics metrics1 = ProductMetrics.create(productId1, 0); - productMetricsJpaRepository.save(metrics1); - - Product product2 = Product.create("์ƒํ’ˆ2", brandId, new Price(20000)); - Product savedProduct2 = productJpaRepository.save(product2); - productId2 = savedProduct2.getId(); - ProductMetrics metrics2 = ProductMetrics.create(productId2, 0); - productMetricsJpaRepository.save(metrics2); - - Product product3 = Product.create("์ƒํ’ˆ3", brandId, new Price(15000)); - Product savedProduct3 = productJpaRepository.save(product3); - productId3 = savedProduct3.getId(); - ProductMetrics metrics3 = ProductMetrics.create(productId3, 0); - productMetricsJpaRepository.save(metrics3); - - // Supply ๋“ฑ๋ก (์žฌ๊ณ  ์„ค์ •) - Supply supply1 = Supply.create(productId1, new Stock(100)); - supplyJpaRepository.save(supply1); - - Supply supply2 = Supply.create(productId2, new Stock(50)); - supplyJpaRepository.save(supply2); - - Supply supply3 = Supply.create(productId3, new Stock(30)); - supplyJpaRepository.save(supply3); - } - - @DisplayName("์ฃผ๋ฌธ ์ƒ์„ฑ ์‹œ, ") - @Nested - class CreateOrder { - @DisplayName("์ •์ƒ์ ์ธ ์ฃผ๋ฌธ์„ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค. (Happy Path)") - @Test - void should_createOrder_when_validRequest() { - // arrange - OrderRequest request = new OrderRequest( - List.of( - new OrderItemRequest(productId1, 2), - new OrderItemRequest(productId2, 1) - ) - ); - - // act - OrderInfo orderInfo = orderFacade.createOrder(userId, request); - - // assert - assertThat(orderInfo).isNotNull(); - assertThat(orderInfo.orderId()).isNotNull(); - assertThat(orderInfo.items()).hasSize(2); - assertThat(orderInfo.totalPrice()).isEqualTo(40000); - } - - @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ƒํ’ˆ ID๊ฐ€ ํฌํ•จ๋œ ๊ฒฝ์šฐ, ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") - @Test - void should_throwException_when_productIdDoesNotExist() { - // arrange - Long nonExistentProductId = 99999L; - OrderRequest request = new OrderRequest( - List.of( - new OrderItemRequest(nonExistentProductId, 1) - ) - ); - - // act & assert - // Note: ํ˜„์žฌ ๊ตฌํ˜„์—์„œ๋Š” productMap.get()์ด null์„ ๋ฐ˜ํ™˜ํ•˜์—ฌ NullPointerException์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Œ - // ๋˜๋Š” SupplyService.checkAndDecreaseStock์—์„œ NOT_FOUND ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Œ - assertThrows(Exception.class, () -> orderFacade.createOrder(userId, request)); - } - - @DisplayName("๋‹จ์ผ ์ƒํ’ˆ ์žฌ๊ณ  ๋ถ€์กฑ ์‹œ, ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") - @Test - void should_throwException_when_singleProductStockInsufficient() { - // arrange - OrderRequest request = new OrderRequest( - List.of( - new OrderItemRequest(productId1, 99999) - ) - ); - - // act & assert - CoreException exception = assertThrows(CoreException.class, () -> orderFacade.createOrder(userId, request)); - assertThat(exception.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); - assertThat(exception.getMessage()).contains("์žฌ๊ณ ๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค"); - } - - @DisplayName("์—ฌ๋Ÿฌ ์ƒํ’ˆ ์ค‘ ์ผ๋ถ€๋งŒ ์žฌ๊ณ  ๋ถ€์กฑ ์‹œ, ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") - @Test - void should_throwException_when_partialStockInsufficient() { - // arrange - // productId1: ์žฌ๊ณ  100, productId2: ์žฌ๊ณ  50 - // productId1์€ ์ถฉ๋ถ„ํ•˜์ง€๋งŒ productId2๋Š” ๋ถ€์กฑ - OrderRequest request = new OrderRequest( - List.of( - new OrderItemRequest(productId1, 10), // ์žฌ๊ณ  ์ถฉ๋ถ„ - new OrderItemRequest(productId2, 99999) // ์žฌ๊ณ  ๋ถ€์กฑ - ) - ); - - // act & assert - CoreException exception = assertThrows(CoreException.class, () -> orderFacade.createOrder(userId, request)); - assertThat(exception.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); - assertThat(exception.getMessage()).contains("์žฌ๊ณ ๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค"); - // Note: ํ˜„์žฌ ๊ตฌํ˜„์—์„œ๋Š” ์ฒซ ๋ฒˆ์งธ ์žฌ๊ณ  ๋ถ€์กฑ ์ƒํ’ˆ์—์„œ๋งŒ ์˜ˆ์™ธ ๋ฐœ์ƒ - // ๊ฐœ์„  ํ›„์—๋Š” ๋ชจ๋“  ๋ถ€์กฑํ•œ ์ƒํ’ˆ์„ ํ•œ ๋ฒˆ์— ์•Œ๋ ค์ค„ ์ˆ˜ ์žˆ์Œ - } - - @DisplayName("์—ฌ๋Ÿฌ ์ƒํ’ˆ ๋ชจ๋‘ ์žฌ๊ณ  ๋ถ€์กฑ ์‹œ, ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") - @Test - void should_throwException_when_allProductsStockInsufficient() { - // arrange - OrderRequest request = new OrderRequest( - List.of( - new OrderItemRequest(productId1, 99999), - new OrderItemRequest(productId2, 99999) - ) - ); - - // act & assert - CoreException exception = assertThrows(CoreException.class, () -> orderFacade.createOrder(userId, request)); - assertThat(exception.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); - assertThat(exception.getMessage()).contains("์žฌ๊ณ ๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค"); - // Note: ํ˜„์žฌ ๊ตฌํ˜„์—์„œ๋Š” ์ฒซ ๋ฒˆ์งธ ์žฌ๊ณ  ๋ถ€์กฑ ์ƒํ’ˆ์—์„œ๋งŒ ์˜ˆ์™ธ ๋ฐœ์ƒ - } - - @DisplayName("Supply ์—”ํ‹ฐํ‹ฐ๊ฐ€ ์—†๋Š” ์ƒํ’ˆ ์ฃผ๋ฌธ ์‹œ, ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") - @Test - void should_throwException_when_supplyDoesNotExist() { - // arrange - // Supply๊ฐ€ ์—†๋Š” ์ƒํ’ˆ ์ƒ์„ฑ - Product productWithoutSupply = Product.create("์žฌ๊ณ ์—†๋Š”์ƒํ’ˆ", brandId, new Price(10000)); - Product savedProduct = productJpaRepository.save(productWithoutSupply); - ProductMetrics metrics = ProductMetrics.create(savedProduct.getId(), 0); - productMetricsJpaRepository.save(metrics); - // Supply๋Š” ์ƒ์„ฑํ•˜์ง€ ์•Š์Œ - - OrderRequest request = new OrderRequest( - List.of( - new OrderItemRequest(savedProduct.getId(), 1) - ) - ); - - // act & assert - CoreException exception = assertThrows(CoreException.class, () -> orderFacade.createOrder(userId, request)); - assertThat(exception.getErrorType()).isEqualTo(ErrorType.NOT_FOUND); - assertThat(exception.getMessage()).contains("์žฌ๊ณ  ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"); - } - - @DisplayName("ํฌ์ธํŠธ ๋ถ€์กฑ ์‹œ, ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") - @Test - void should_throwException_when_pointInsufficient() { - // arrange - // ํฌ์ธํŠธ๋ฅผ ๋ชจ๋‘ ์‚ฌ์šฉ - OrderRequest firstOrder = new OrderRequest( - List.of( - new OrderItemRequest(productId1, 10) - ) - ); - orderFacade.createOrder(userId, firstOrder); - - // ํฌ์ธํŠธ ๋ถ€์กฑํ•œ ์ฃผ๋ฌธ ์‹œ๋„ - OrderRequest request = new OrderRequest( - List.of( - new OrderItemRequest(productId2, 1) // 20000์› ํ•„์š” (๋ถ€์กฑ) - ) - ); - - // act & assert - CoreException exception = assertThrows(CoreException.class, () -> orderFacade.createOrder(userId, request)); - assertThat(exception.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); - // Note: ์žฌ๊ณ  ๋ถ€์กฑ ์˜ˆ์™ธ๊ฐ€ ๋จผ์ € ๋ฐœ์ƒํ•  ์ˆ˜๋„ ์žˆ์œผ๋ฏ€๋กœ, ํฌ์ธํŠธ ๋ถ€์กฑ ๋ฉ”์‹œ์ง€๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธ - // ๋˜๋Š” ์žฌ๊ณ  ๋ถ€์กฑ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Œ (99999๋Š” ์žฌ๊ณ  ๋ถ€์กฑ) - } - - @DisplayName("ํฌ์ธํŠธ๊ฐ€ ์ •ํ™•ํžˆ ์ผ์น˜ํ•  ๊ฒฝ์šฐ, ์ฃผ๋ฌธ์ด ์„ฑ๊ณตํ•œ๋‹ค. (Edge Case)") - @Test - void should_createOrder_when_pointExactlyMatches() { - // arrange - // ํฌ์ธํŠธ๋ฅผ ๊ฑฐ์˜ ๋ชจ๋‘ ์‚ฌ์šฉ - OrderRequest firstOrder = new OrderRequest( - List.of( - new OrderItemRequest(productId1, 9) // 90000์› ์‚ฌ์šฉ - ) - ); - orderFacade.createOrder(userId, firstOrder); - // ๋‚จ์€ ํฌ์ธํŠธ: 10000์› - - // ์ •ํ™•ํžˆ ์ผ์น˜ํ•˜๋Š” ์ฃผ๋ฌธ - OrderRequest request = new OrderRequest( - List.of( - new OrderItemRequest(productId1, 1) // ์ •ํ™•ํžˆ 10000์› - ) - ); - - // act - OrderInfo orderInfo = orderFacade.createOrder(userId, request); - - // assert - assertThat(orderInfo).isNotNull(); - assertThat(orderInfo.totalPrice()).isEqualTo(10000); - } - - @DisplayName("์ค‘๋ณต ์ƒํ’ˆ์ด ํฌํ•จ๋œ ๊ฒฝ์šฐ, IllegalStateException์ด ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") - @Test - void should_throwException_when_duplicateProducts() { - // arrange - // ๊ฐ™์€ ์ƒํ’ˆ์„ ์—ฌ๋Ÿฌ ๋ฒˆ ์ฃผ๋ฌธ ํ•ญ๋ชฉ์— ํฌํ•จ - // Note: Collectors.toMap()์€ ์ค‘๋ณต ํ‚ค๊ฐ€ ์žˆ์œผ๋ฉด IllegalStateException์„ ๋ฐœ์ƒ์‹œํ‚ด - OrderRequest request = new OrderRequest( - List.of( - new OrderItemRequest(productId1, 2), - new OrderItemRequest(productId1, 3) // ์ค‘๋ณต - ) - ); - - // act & assert - // Note: Collectors.toMap()์—์„œ ์ค‘๋ณต ํ‚ค๋กœ ์ธํ•ด IllegalStateException ๋ฐœ์ƒ - assertThrows(IllegalStateException.class, () -> orderFacade.createOrder(userId, request)); - } - - // Note: ํŠธ๋žœ์žญ์…˜ ๋กค๋ฐฑ ๊ฒ€์ฆ์€ E2E ํ…Œ์ŠคํŠธ์—์„œ ์ˆ˜ํ–‰ํ•˜๋Š” ๊ฒƒ์ด ๋” ์ ์ ˆํ•จ - // ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ๋Š” @Transactional๋กœ ์ธํ•ด ๋กค๋ฐฑ์ด ์ œ๋Œ€๋กœ ๊ฒ€์ฆ๋˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Œ - - @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์‚ฌ์šฉ์ž๋กœ ์ฃผ๋ฌธ ์‹œ๋„ ์‹œ, ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") - @Test - void should_throwException_when_userDoesNotExist() { - // arrange - String nonExistentUserId = "nonexist"; - OrderRequest request = new OrderRequest( - List.of( - new OrderItemRequest(productId1, 1) - ) - ); - - // act & assert - CoreException exception = assertThrows(CoreException.class, () -> orderFacade.createOrder(nonExistentUserId, request)); - assertThat(exception.getErrorType()).isEqualTo(ErrorType.NOT_FOUND); - assertThat(exception.getMessage()).contains("์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"); - } - - // Note: Point ์—”ํ‹ฐํ‹ฐ๊ฐ€ ์—†๋Š” ์‚ฌ์šฉ์ž ํ…Œ์ŠคํŠธ๋Š” E2E ํ…Œ์ŠคํŠธ์—์„œ ์ˆ˜ํ–‰ํ•˜๋Š” ๊ฒƒ์ด ๋” ์ ์ ˆํ•จ - // ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ๋Š” @Transactional๋กœ ์ธํ•ด ๋กค๋ฐฑ์ด ์ œ๋Œ€๋กœ ๊ฒ€์ฆ๋˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Œ - // E2E ํ…Œ์ŠคํŠธ์—์„œ ์‹ค์ œ HTTP ์š”์ฒญ์„ ํ†ตํ•ด ํŠธ๋žœ์žญ์…˜ ๋กค๋ฐฑ์„ ๊ฒ€์ฆํ•  ์ˆ˜ ์žˆ์Œ - } -} - diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandTest.java deleted file mode 100644 index 3b6dd2093..000000000 --- a/apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandTest.java +++ /dev/null @@ -1,127 +0,0 @@ -package com.loopers.domain.brand; - -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; - -@DisplayName("๋ธŒ๋žœ๋“œ(Brand) Entity ํ…Œ์ŠคํŠธ") -public class BrandTest { - - @DisplayName("๋ธŒ๋žœ๋“œ๋ฅผ ์ƒ์„ฑํ•  ๋•Œ, ") - @Nested - class Create { - @DisplayName("์ •์ƒ์ ์ธ ์ด๋ฆ„์œผ๋กœ ๋ธŒ๋žœ๋“œ๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค. (Happy Path)") - @Test - void should_createBrand_when_validName() { - // arrange - String brandName = "Nike"; - - // act - Brand brand = Brand.create(brandName); - - // assert - assertThat(brand.getName()).isEqualTo("Nike"); - } - - @DisplayName("๋นˆ ๋ฌธ์ž์—ด๋กœ ๋ธŒ๋žœ๋“œ๋ฅผ ์ƒ์„ฑํ•  ๊ฒฝ์šฐ, ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") - @Test - void should_throwException_when_emptyName() { - // arrange - String brandName = ""; - - // act & assert - CoreException exception = assertThrows(CoreException.class, () -> Brand.create(brandName)); - assertThat(exception.getMessage()).isEqualTo("๋ธŒ๋žœ๋“œ ์ด๋ฆ„์€ ํ•„์ˆ˜์ด๋ฉฐ 1์ž ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); - assertThat(exception.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); - } - - @DisplayName("null๋กœ ๋ธŒ๋žœ๋“œ๋ฅผ ์ƒ์„ฑํ•  ๊ฒฝ์šฐ, ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") - @Test - void should_throwException_when_nullName() { - // arrange - String brandName = null; - - // act & assert - CoreException exception = assertThrows(CoreException.class, () -> Brand.create(brandName)); - assertThat(exception.getMessage()).isEqualTo("๋ธŒ๋žœ๋“œ ์ด๋ฆ„์€ ํ•„์ˆ˜์ด๋ฉฐ 1์ž ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); - assertThat(exception.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); - } - - @DisplayName("๊ณต๋ฐฑ๋งŒ ์žˆ๋Š” ๋ฌธ์ž์—ด๋กœ ๋ธŒ๋žœ๋“œ๋ฅผ ์ƒ์„ฑํ•  ๊ฒฝ์šฐ, ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") - @Test - void should_throwException_when_blankName() { - // arrange - String brandName = " "; - - // act & assert - CoreException exception = assertThrows(CoreException.class, () -> Brand.create(brandName)); - assertThat(exception.getMessage()).isEqualTo("๋ธŒ๋žœ๋“œ ์ด๋ฆ„์€ ํ•„์ˆ˜์ด๋ฉฐ 1์ž ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); - assertThat(exception.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); - } - - @DisplayName("๊ธด ์ด๋ฆ„์œผ๋กœ๋„ ๋ธŒ๋žœ๋“œ๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค. (Edge Case)") - @Test - void should_createBrand_when_longName() { - // arrange - String brandName = "A".repeat(1000); - - // act - Brand brand = Brand.create(brandName); - - // assert - assertThat(brand.getName()).isEqualTo("A".repeat(1000)); - } - } - - @DisplayName("๋ธŒ๋žœ๋“œ ์กฐํšŒ๋ฅผ ํ•  ๋•Œ, ") - @Nested - class Retrieve { - @DisplayName("์ƒ์„ฑํ•œ ๋ธŒ๋žœ๋“œ์˜ ์ด๋ฆ„์„ ์กฐํšŒํ•  ์ˆ˜ ์žˆ๋‹ค. (Happy Path)") - @Test - void should_retrieveName_when_brandCreated() { - // arrange - Brand brand = Brand.create("Adidas"); - - // act - String name = brand.getName(); - - // assert - assertThat(name).isEqualTo("Adidas"); - } - } - - @DisplayName("๋ธŒ๋žœ๋“œ ๋™๋“ฑ์„ฑ์„ ํ™•์ธํ•  ๋•Œ, ") - @Nested - class Equality { - @DisplayName("๊ฐ™์€ ์ด๋ฆ„์„ ๊ฐ€์ง„ ๋ธŒ๋žœ๋“œ๋Š” ์„œ๋กœ ๋‹ค๋ฅธ ์ธ์Šคํ„ด์Šค์ด๋‹ค. (Edge Case)") - @Test - void should_beDifferentInstances_when_sameName() { - // arrange - String brandName = "Puma"; - Brand brand1 = Brand.create(brandName); - Brand brand2 = Brand.create(brandName); - - // act & assert - assertThat(brand1).isNotSameAs(brand2); - assertThat(brand1).isNotEqualTo(brand2); - } - - @DisplayName("๋‹ค๋ฅธ ์ด๋ฆ„์„ ๊ฐ€์ง„ ๋ธŒ๋žœ๋“œ๋Š” ์„œ๋กœ ๋‹ค๋ฅธ ์ธ์Šคํ„ด์Šค์ด๋‹ค. (Happy Path)") - @Test - void should_beDifferentInstances_when_differentNames() { - // arrange - Brand brand1 = Brand.create("Nike"); - Brand brand2 = Brand.create("Adidas"); - - // act & assert - assertThat(brand1).isNotSameAs(brand2); - assertThat(brand1).isNotEqualTo(brand2); - assertThat(brand1.getName()).isNotEqualTo(brand2.getName()); - } - } -} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/common/vo/PriceTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/common/vo/PriceTest.java deleted file mode 100644 index f4427c64b..000000000 --- a/apps/commerce-api/src/test/java/com/loopers/domain/common/vo/PriceTest.java +++ /dev/null @@ -1,62 +0,0 @@ -package com.loopers.domain.common.vo; - -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; - -@DisplayName("๊ฐ€๊ฒฉ(Price) Value Object ํ…Œ์ŠคํŠธ") -public class PriceTest { - - @DisplayName("๊ฐ€๊ฒฉ์„ ์ƒ์„ฑํ•  ๋•Œ, ") - @Nested - class Create { - @DisplayName("์ •์ƒ์ ์ธ ๊ฐ€๊ฒฉ์œผ๋กœ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค. (Happy Path)") - @Test - void should_createPrice_when_validAmount() { - // arrange - int amount = 10000; - - // act - Price price = new Price(amount); - - // assert - assertThat(price.amount()).isEqualTo(10000); - } - - @DisplayName("๊ฐ€๊ฒฉ์ด 0์ด์–ด๋„ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค. (Edge Case)") - @Test - void should_createPrice_when_amountIsZero() { - // arrange - int amount = 0; - - // act - Price price = new Price(amount); - - // assert - assertThat(price.amount()).isEqualTo(0); - } - - @DisplayName("์Œ์ˆ˜ ๊ฐ€๊ฒฉ์œผ๋กœ ์ƒ์„ฑํ•˜๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") - @ParameterizedTest - @ValueSource(ints = {-1, -10, -100, -1000, -10000}) - void should_throwException_when_amountIsNegative(int invalidAmount) { - // arrange: invalidAmount parameter - - // act - CoreException exception = assertThrows(CoreException.class, () -> { - new Price(invalidAmount); - }); - - // assert - assertThat(exception.getMessage()).isEqualTo("๊ฐ€๊ฒฉ์€ ์Œ์ˆ˜๊ฐ€ ๋  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); - assertThat(exception.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); - } - } -} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/like/product/LikeProductServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/like/product/LikeProductServiceIntegrationTest.java deleted file mode 100644 index d339870ce..000000000 --- a/apps/commerce-api/src/test/java/com/loopers/domain/like/product/LikeProductServiceIntegrationTest.java +++ /dev/null @@ -1,232 +0,0 @@ -package com.loopers.domain.like.product; - -import com.loopers.domain.product.ProductService; -import jakarta.transaction.Transactional; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; - -import java.util.List; -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; - -@SpringBootTest -@Transactional -@DisplayName("์ƒํ’ˆ ์ข‹์•„์š” ์„œ๋น„์Šค(LikeProductService) ํ…Œ์ŠคํŠธ") -public class LikeProductServiceIntegrationTest { - - @MockitoSpyBean - private LikeProductRepository spyLikeProductRepository; - - @MockitoSpyBean - private ProductService spyProductService; - - @Autowired - private LikeProductService likeProductService; - - @DisplayName("์ข‹์•„์š”๋ฅผ ๋“ฑ๋กํ•  ๋•Œ, ") - @Nested - class LikeProductTest { - @DisplayName("์ฒ˜์Œ ์ข‹์•„์š”๋ฅผ ๋“ฑ๋กํ•˜๋ฉด ์ƒˆ๋กœ์šด ์ข‹์•„์š”๊ฐ€ ์ƒ์„ฑ๋œ๋‹ค. (Happy Path)") - @Test - void should_createLikeProduct_when_firstLike() { - // arrange - Long userId = 1L; - Long productId = 100L; - when(spyLikeProductRepository.findByUserIdAndProductId(userId, productId)) - .thenReturn(Optional.empty()); - - // act - likeProductService.likeProduct(userId, productId); - - // assert - verify(spyLikeProductRepository).findByUserIdAndProductId(1L, 100L); - ArgumentCaptor captor = ArgumentCaptor.forClass(LikeProduct.class); - verify(spyLikeProductRepository, times(1)).save(captor.capture()); - LikeProduct savedLike = captor.getValue(); - assertThat(savedLike.getUserId()).isEqualTo(1L); - assertThat(savedLike.getProductId()).isEqualTo(100L); - } - - @DisplayName("์ด๋ฏธ ์‚ญ์ œ๋œ ์ข‹์•„์š”๋ฅผ ๋‹ค์‹œ ๋“ฑ๋กํ•˜๋ฉด ๋ณต์›๋œ๋‹ค. (Idempotency)") - @Test - void should_restoreLikeProduct_when_alreadyDeleted() { - // arrange - Long userId = 1L; - Long productId = 100L; - LikeProduct deletedLike = LikeProduct.create(userId, productId); - deletedLike.delete(); // ์‚ญ์ œ ์ƒํƒœ๋กœ ๋งŒ๋“ค๊ธฐ - when(spyLikeProductRepository.findByUserIdAndProductId(userId, productId)) - .thenReturn(Optional.of(deletedLike)); - - // act - likeProductService.likeProduct(userId, productId); - - // assert - verify(spyLikeProductRepository).findByUserIdAndProductId(1L, 100L); - verify(spyLikeProductRepository, never()).save(any()); - // restore๊ฐ€ ํ˜ธ์ถœ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ (deletedAt์ด null์ด ๋˜์–ด์•ผ ํ•จ) - assertThat(deletedLike.getDeletedAt()).isNull(); - } - - @DisplayName("๊ฐ™์€ ์‚ฌ์šฉ์ž๊ฐ€ ๊ฐ™์€ ์ƒํ’ˆ์— ์—ฌ๋Ÿฌ ๋ฒˆ ์ข‹์•„์š”๋ฅผ ๋“ฑ๋กํ•ด๋„ ํ•œ ๋ฒˆ๋งŒ ์ €์žฅ๋œ๋‹ค. (Idempotency)") - @Test - void should_notCreateDuplicate_when_likeMultipleTimes() { - // arrange - Long userId = 1L; - Long productId = 100L; - LikeProduct existingLike = LikeProduct.create(userId, productId); - when(spyLikeProductRepository.findByUserIdAndProductId(userId, productId)) - .thenReturn(Optional.of(existingLike)); - - // act - likeProductService.likeProduct(userId, productId); - likeProductService.likeProduct(userId, productId); - - // assert - verify(spyLikeProductRepository, times(2)).findByUserIdAndProductId(1L, 100L); - verify(spyLikeProductRepository, never()).save(any()); - } - } - - - @DisplayName("์ข‹์•„์š”๋ฅผ ์ทจ์†Œํ•  ๋•Œ, ") - @Nested - class UnlikeProduct { - @DisplayName("์กด์žฌํ•˜๋Š” ์ข‹์•„์š”๋ฅผ ์ทจ์†Œํ•˜๋ฉด ์‚ญ์ œ๋œ๋‹ค. (Happy Path)") - @Test - void should_deleteLikeProduct_when_likeExists() { - // arrange - Long userId = 1L; - Long productId = 100L; - LikeProduct likeProduct = LikeProduct.create(userId, productId); - when(spyLikeProductRepository.findByUserIdAndProductId(userId, productId)) - .thenReturn(Optional.of(likeProduct)); - - // act - likeProductService.unlikeProduct(userId, productId); - - // assert - verify(spyLikeProductRepository).findByUserIdAndProductId(1L, 100L); - assertThat(likeProduct.getDeletedAt()).isNotNull(); - } - - @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ข‹์•„์š”๋ฅผ ์ทจ์†Œํ•ด๋„ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š๋Š”๋‹ค. (Edge Case)") - @Test - void should_notThrowException_when_likeNotFound() { - // arrange - Long userId = 1L; - Long productId = 100L; - when(spyLikeProductRepository.findByUserIdAndProductId(userId, productId)) - .thenReturn(Optional.empty()); - - // act & assert - likeProductService.unlikeProduct(userId, productId); - verify(spyLikeProductRepository).findByUserIdAndProductId(1L, 100L); - // ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š์•„์•ผ ํ•จ - } - - @DisplayName("์ด๋ฏธ ์‚ญ์ œ๋œ ์ข‹์•„์š”๋ฅผ ๋‹ค์‹œ ์ทจ์†Œํ•ด๋„ ๋ฉฑ๋“ฑํ•˜๊ฒŒ ๋™์ž‘ํ•œ๋‹ค. (Idempotency)") - @Test - void should_beIdempotent_when_unlikeDeletedLike() { - // arrange - Long userId = 1L; - Long productId = 100L; - LikeProduct deletedLike = LikeProduct.create(userId, productId); - deletedLike.delete(); - when(spyLikeProductRepository.findByUserIdAndProductId(userId, productId)) - .thenReturn(Optional.of(deletedLike)); - - // act - likeProductService.unlikeProduct(userId, productId); - - // assert - verify(spyLikeProductRepository).findByUserIdAndProductId(1L, 100L); - // delete๋Š” ๋ฉฑ๋“ฑํ•˜๊ฒŒ ๋™์ž‘ํ•˜๋ฏ€๋กœ deletedAt์ด ๊ทธ๋Œ€๋กœ ์œ ์ง€๋˜์–ด์•ผ ํ•จ - assertThat(deletedLike.getDeletedAt()).isNotNull(); - } - } - - @DisplayName("์ข‹์•„์š” ํ† ๊ธ€์„ ํ•  ๋•Œ, ") - @Nested - class ToggleLike { - @DisplayName("์ข‹์•„์š”๊ฐ€ ์—†์œผ๋ฉด ๋“ฑ๋กํ•˜๊ณ , ์žˆ์œผ๋ฉด ์ทจ์†Œํ•œ๋‹ค. (Toggle)") - @Test - void should_toggleLike_when_likeAndUnlike() { - // arrange - Long userId = 1L; - Long productId = 100L; - when(spyLikeProductRepository.findByUserIdAndProductId(userId, productId)) - .thenReturn(Optional.empty()) - .thenReturn(Optional.of(LikeProduct.create(userId, productId))); - - // act - ์ฒซ ๋ฒˆ์งธ ํ˜ธ์ถœ: ์ข‹์•„์š” ๋“ฑ๋ก - likeProductService.likeProduct(userId, productId); - // ๋‘ ๋ฒˆ์งธ ํ˜ธ์ถœ: ์ข‹์•„์š” ์ทจ์†Œ - likeProductService.unlikeProduct(userId, productId); - - // assert - verify(spyLikeProductRepository, times(2)).findByUserIdAndProductId(1L, 100L); - verify(spyLikeProductRepository, times(1)).save(any(LikeProduct.class)); - } - } - - @DisplayName("์ข‹์•„์š”ํ•œ ์ƒํ’ˆ ๋ชฉ๋ก์„ ์กฐํšŒํ•  ๋•Œ, ") - @Nested - class GetLikedProducts { - @DisplayName("์ข‹์•„์š”ํ•œ ์ƒํ’ˆ์ด ์žˆ์œผ๋ฉด ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. (Happy Path)") - @Test - void should_returnLikedProducts_when_likesExist() { - // arrange - Long userId = 1L; - Pageable pageable = PageRequest.of(0, 20); - List likedProducts = List.of( - LikeProduct.create(userId, 100L), - LikeProduct.create(userId, 200L) - ); - Page productPage = new PageImpl<>(likedProducts, pageable, 2); - when(spyLikeProductRepository.getLikeProductsByUserIdAndDeletedAtIsNull(userId, pageable)) - .thenReturn(productPage); - - // act - Page result = likeProductService.getLikedProducts(userId, pageable); - - // assert - verify(spyLikeProductRepository).getLikeProductsByUserIdAndDeletedAtIsNull(1L, Pageable.ofSize(20)); - assertThat(result).isNotNull(); - assertThat(result.getContent()).hasSize(2); - assertThat(result.getTotalElements()).isEqualTo(2); - } - - @DisplayName("์ข‹์•„์š”ํ•œ ์ƒํ’ˆ์ด ์—†์œผ๋ฉด ๋นˆ ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. (Edge Case)") - @Test - void should_returnEmptyList_when_noLikes() { - // arrange - Long userId = 1L; - Pageable pageable = PageRequest.of(0, 20); - Page emptyPage = new PageImpl<>(List.of(), pageable, 0); - when(spyLikeProductRepository.getLikeProductsByUserIdAndDeletedAtIsNull(userId, pageable)) - .thenReturn(emptyPage); - - // act - Page result = likeProductService.getLikedProducts(userId, pageable); - - // assert - verify(spyLikeProductRepository).getLikeProductsByUserIdAndDeletedAtIsNull(1L, Pageable.ofSize(20)); - assertThat(result).isNotNull(); - assertThat(result.getContent()).isEmpty(); - assertThat(result.getTotalElements()).isEqualTo(0); - } - } -} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/like/product/LikeProductTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/like/product/LikeProductTest.java deleted file mode 100644 index 4b072ff62..000000000 --- a/apps/commerce-api/src/test/java/com/loopers/domain/like/product/LikeProductTest.java +++ /dev/null @@ -1,202 +0,0 @@ -package com.loopers.domain.like.product; - -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; - -@DisplayName("์ƒํ’ˆ ์ข‹์•„์š”(LikeProduct) Entity ํ…Œ์ŠคํŠธ") -public class LikeProductTest { - - @DisplayName("์ข‹์•„์š”๋ฅผ ์ƒ์„ฑํ•  ๋•Œ, ") - @Nested - class Create { - @DisplayName("์ •์ƒ์ ์ธ userId์™€ productId๋กœ ์ข‹์•„์š”๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค. (Happy Path)") - @Test - void should_createLikeProduct_when_validUserIdAndProductId() { - // arrange - Long userId = 1L; - Long productId = 100L; - - // act - LikeProduct likeProduct = LikeProduct.create(userId, productId); - - // assert - assertThat(likeProduct.getUserId()).isEqualTo(1L); - assertThat(likeProduct.getProductId()).isEqualTo(100L); - } - - @DisplayName("userId๊ฐ€ 0์ธ ๊ฒฝ์šฐ, ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") - @Test - void should_throwException_when_userIdIsZero() { - // arrange - Long userId = 0L; - Long productId = 100L; - - // act & assert - CoreException exception = assertThrows(CoreException.class, () -> LikeProduct.create(userId, productId)); - assertThat(exception.getMessage()).isEqualTo("์‚ฌ์šฉ์ž ID๋Š” 1 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); - assertThat(exception.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); - } - - @DisplayName("productId๊ฐ€ 0์ธ ๊ฒฝ์šฐ, ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") - @Test - void should_throwException_when_productIdIsZero() { - // arrange - Long userId = 1L; - Long productId = 0L; - - // act & assert - CoreException exception = assertThrows(CoreException.class, () -> LikeProduct.create(userId, productId)); - assertThat(exception.getMessage()).isEqualTo("์ƒํ’ˆ ID๋Š” 1 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); - assertThat(exception.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); - } - - @DisplayName("userId์™€ productId๊ฐ€ ๋ชจ๋‘ 0์ธ ๊ฒฝ์šฐ, ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") - @Test - void should_throwException_when_bothIdsAreZero() { - // arrange - Long userId = 0L; - Long productId = 0L; - - // act & assert - CoreException exception = assertThrows(CoreException.class, () -> LikeProduct.create(userId, productId)); - assertThat(exception.getMessage()).isEqualTo("์‚ฌ์šฉ์ž ID๋Š” 1 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); - assertThat(exception.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); - } - - @DisplayName("์Œ์ˆ˜ userId์ธ ๊ฒฝ์šฐ, ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") - @Test - void should_throwException_when_userIdIsNegative() { - // arrange - Long userId = -1L; - Long productId = 100L; - - // act & assert - CoreException exception = assertThrows(CoreException.class, () -> LikeProduct.create(userId, productId)); - assertThat(exception.getMessage()).isEqualTo("์‚ฌ์šฉ์ž ID๋Š” 1 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); - assertThat(exception.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); - } - - @DisplayName("์Œ์ˆ˜ productId์ธ ๊ฒฝ์šฐ, ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") - @Test - void should_throwException_when_productIdIsNegative() { - // arrange - Long userId = 1L; - Long productId = -1L; - - // act & assert - CoreException exception = assertThrows(CoreException.class, () -> LikeProduct.create(userId, productId)); - assertThat(exception.getMessage()).isEqualTo("์ƒํ’ˆ ID๋Š” 1 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); - assertThat(exception.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); - } - - @DisplayName("null userId์ธ ๊ฒฝ์šฐ, ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") - @Test - void should_throwException_when_userIdIsNull() { - // arrange - Long userId = null; - Long productId = 100L; - - // act & assert - CoreException exception = assertThrows(CoreException.class, () -> LikeProduct.create(userId, productId)); - assertThat(exception.getMessage()).isEqualTo("์‚ฌ์šฉ์ž ID๋Š” 1 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); - assertThat(exception.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); - } - - @DisplayName("null productId์ธ ๊ฒฝ์šฐ, ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") - @Test - void should_throwException_when_productIdIsNull() { - // arrange - Long userId = 1L; - Long productId = null; - - // act & assert - CoreException exception = assertThrows(CoreException.class, () -> LikeProduct.create(userId, productId)); - assertThat(exception.getMessage()).isEqualTo("์ƒํ’ˆ ID๋Š” 1 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); - assertThat(exception.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); - } - } - - @DisplayName("์ข‹์•„์š” ์กฐํšŒ๋ฅผ ํ•  ๋•Œ, ") - @Nested - class Retrieve { - @DisplayName("์ƒ์„ฑํ•œ ์ข‹์•„์š”์˜ userId๋ฅผ ์กฐํšŒํ•  ์ˆ˜ ์žˆ๋‹ค. (Happy Path)") - @Test - void should_retrieveUserId_when_likeProductCreated() { - // arrange - Long userId = 1L; - Long productId = 100L; - LikeProduct likeProduct = LikeProduct.create(userId, productId); - - // act - Long retrievedUserId = likeProduct.getUserId(); - - // assert - assertThat(retrievedUserId).isEqualTo(1L); - } - - @DisplayName("์ƒ์„ฑํ•œ ์ข‹์•„์š”์˜ productId๋ฅผ ์กฐํšŒํ•  ์ˆ˜ ์žˆ๋‹ค. (Happy Path)") - @Test - void should_retrieveProductId_when_likeProductCreated() { - // arrange - Long userId = 1L; - Long productId = 100L; - LikeProduct likeProduct = LikeProduct.create(userId, productId); - - // act - Long retrievedProductId = likeProduct.getProductId(); - - // assert - assertThat(retrievedProductId).isEqualTo(100L); - } - } - - @DisplayName("์ข‹์•„์š” ๋™๋“ฑ์„ฑ์„ ํ™•์ธํ•  ๋•Œ, ") - @Nested - class Equality { - @DisplayName("๊ฐ™์€ userId์™€ productId๋ฅผ ๊ฐ€์ง„ ์ข‹์•„์š”๋Š” ์„œ๋กœ ๋‹ค๋ฅธ ์ธ์Šคํ„ด์Šค์ด๋‹ค. (Edge Case)") - @Test - void should_beDifferentInstances_when_sameUserIdAndProductId() { - // arrange - Long userId = 1L; - Long productId = 100L; - LikeProduct likeProduct1 = LikeProduct.create(userId, productId); - LikeProduct likeProduct2 = LikeProduct.create(userId, productId); - - // act & assert - assertThat(likeProduct1).isNotSameAs(likeProduct2); - assertThat(likeProduct1).isNotEqualTo(likeProduct2); - } - - @DisplayName("๋‹ค๋ฅธ userId๋ฅผ ๊ฐ€์ง„ ์ข‹์•„์š”๋Š” ์„œ๋กœ ๋‹ค๋ฅธ ์ธ์Šคํ„ด์Šค์ด๋‹ค. (Happy Path)") - @Test - void should_beDifferentInstances_when_differentUserId() { - // arrange - LikeProduct likeProduct1 = LikeProduct.create(1L, 100L); - LikeProduct likeProduct2 = LikeProduct.create(2L, 100L); - - // act & assert - assertThat(likeProduct1).isNotSameAs(likeProduct2); - assertThat(likeProduct1).isNotEqualTo(likeProduct2); - assertThat(likeProduct1.getUserId()).isNotEqualTo(likeProduct2.getUserId()); - } - - @DisplayName("๋‹ค๋ฅธ productId๋ฅผ ๊ฐ€์ง„ ์ข‹์•„์š”๋Š” ์„œ๋กœ ๋‹ค๋ฅธ ์ธ์Šคํ„ด์Šค์ด๋‹ค. (Happy Path)") - @Test - void should_beDifferentInstances_when_differentProductId() { - // arrange - LikeProduct likeProduct1 = LikeProduct.create(1L, 100L); - LikeProduct likeProduct2 = LikeProduct.create(1L, 200L); - - // act & assert - assertThat(likeProduct1).isNotSameAs(likeProduct2); - assertThat(likeProduct1).isNotEqualTo(likeProduct2); - assertThat(likeProduct1.getProductId()).isNotEqualTo(likeProduct2.getProductId()); - } - } -} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderItemTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderItemTest.java deleted file mode 100644 index 35a5056a8..000000000 --- a/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderItemTest.java +++ /dev/null @@ -1,276 +0,0 @@ -package com.loopers.domain.order; - -import com.loopers.domain.common.vo.Price; -import com.loopers.support.error.CoreException; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; - -@DisplayName("์ฃผ๋ฌธ ํ•ญ๋ชฉ(OrderItem) Value Object ํ…Œ์ŠคํŠธ") -public class OrderItemTest { - - @DisplayName("์ฃผ๋ฌธ ํ•ญ๋ชฉ์„ ์ƒ์„ฑํ•  ๋•Œ, ") - @Nested - class Create { - @DisplayName("์ •์ƒ์ ์ธ ๊ฐ’์œผ๋กœ ์ฃผ๋ฌธ ํ•ญ๋ชฉ์„ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค. (Happy Path)") - @Test - void should_createOrderItem_when_validValues() { - // arrange - Long productId = 1L; - String productName = "์ƒํ’ˆ๋ช…"; - Integer quantity = 2; - Price price = new Price(10000); - - // act - OrderItem orderItem = OrderItem.create(productId, productName, quantity, price); - - // assert - assertThat(orderItem.getProductId()).isEqualTo(1L); - assertThat(orderItem.getProductName()).isEqualTo("์ƒํ’ˆ๋ช…"); - assertThat(orderItem.getQuantity()).isEqualTo(2); - assertThat(orderItem.getPrice().amount()).isEqualTo(10000); - } - - @DisplayName("์ˆ˜๋Ÿ‰์ด 0์ด๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") - @Test - void should_throwException_when_quantityIsZero() { - // arrange - Long productId = 1L; - String productName = "์ƒํ’ˆ๋ช…"; - Integer quantity = 0; - Price price = new Price(10000); - - // act - CoreException exception = assertThrows(CoreException.class, () -> { - OrderItem.create(productId, productName, quantity, price); - }); - - // assert - assertThat(exception.getMessage()).isEqualTo("์ฃผ๋ฌธ ์ˆ˜๋Ÿ‰์€ 1 ์ด์ƒ์˜ ์ž์—ฐ์ˆ˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค."); - } - - @DisplayName("์ˆ˜๋Ÿ‰์ด ์Œ์ˆ˜์ด๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") - @Test - void should_throwException_when_quantityIsNegative() { - // arrange - Long productId = 1L; - String productName = "์ƒํ’ˆ๋ช…"; - Integer quantity = -1; - Price price = new Price(10000); - - // act - CoreException exception = assertThrows(CoreException.class, () -> { - OrderItem.create(productId, productName, quantity, price); - }); - - // assert - assertThat(exception.getMessage()).isEqualTo("์ฃผ๋ฌธ ์ˆ˜๋Ÿ‰์€ 1 ์ด์ƒ์˜ ์ž์—ฐ์ˆ˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค."); - } - - @DisplayName("๊ฐ€๊ฒฉ์ด 0์ธ ์ฃผ๋ฌธ ํ•ญ๋ชฉ์„ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค. (Edge Case)") - @Test - void should_createOrderItem_when_priceIsZero() { - // arrange - Long productId = 1L; - String productName = "์ƒํ’ˆ๋ช…"; - Integer quantity = 1; - Price price = new Price(0); - - // act - OrderItem orderItem = OrderItem.create(productId, productName, quantity, price); - - // assert - assertThat(orderItem.getPrice().amount()).isEqualTo(0); - } - - @DisplayName("productName์ด null์ด๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") - @Test - void should_throwException_when_productNameIsNull() { - // arrange - Long productId = 1L; - String productName = null; - Integer quantity = 1; - Price price = new Price(10000); - - // act - CoreException exception = assertThrows(CoreException.class, () -> { - OrderItem.create(productId, productName, quantity, price); - }); - - // assert - assertThat(exception.getMessage()).isEqualTo("์ƒํ’ˆ๋ช…์€ ํ•„์ˆ˜์ด๋ฉฐ ๊ณต๋ฐฑ์ผ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); - } - - @DisplayName("productName์ด ๋นˆ ๋ฌธ์ž์—ด์ด๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") - @Test - void should_throwException_when_productNameIsEmpty() { - // arrange - Long productId = 1L; - String productName = ""; - Integer quantity = 1; - Price price = new Price(10000); - - // act - CoreException exception = assertThrows(CoreException.class, () -> { - OrderItem.create(productId, productName, quantity, price); - }); - - // assert - assertThat(exception.getMessage()).isEqualTo("์ƒํ’ˆ๋ช…์€ ํ•„์ˆ˜์ด๋ฉฐ ๊ณต๋ฐฑ์ผ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); - } - - @DisplayName("productName์ด ๊ณต๋ฐฑ๋งŒ ์žˆ์œผ๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") - @Test - void should_throwException_when_productNameIsBlank() { - // arrange - Long productId = 1L; - String productName = " "; - Integer quantity = 1; - Price price = new Price(10000); - - // act - CoreException exception = assertThrows(CoreException.class, () -> { - OrderItem.create(productId, productName, quantity, price); - }); - - // assert - assertThat(exception.getMessage()).isEqualTo("์ƒํ’ˆ๋ช…์€ ํ•„์ˆ˜์ด๋ฉฐ ๊ณต๋ฐฑ์ผ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); - } - - @DisplayName("productId๊ฐ€ null์ด๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") - @Test - void should_throwException_when_productIdIsNull() { - // arrange - Long productId = null; - String productName = "์ƒํ’ˆ๋ช…"; - Integer quantity = 1; - Price price = new Price(10000); - - // act - CoreException exception = assertThrows(CoreException.class, () -> { - OrderItem.create(productId, productName, quantity, price); - }); - - // assert - assertThat(exception.getMessage()).isEqualTo("์ƒํ’ˆ ID๋Š” 1 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); - } - - @DisplayName("productId๊ฐ€ 0์ด๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") - @Test - void should_throwException_when_productIdIsZero() { - // arrange - Long productId = 0L; - String productName = "์ƒํ’ˆ๋ช…"; - Integer quantity = 1; - Price price = new Price(10000); - - // act - CoreException exception = assertThrows(CoreException.class, () -> { - OrderItem.create(productId, productName, quantity, price); - }); - - // assert - assertThat(exception.getMessage()).isEqualTo("์ƒํ’ˆ ID๋Š” 1 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); - } - - @DisplayName("productId๊ฐ€ ์Œ์ˆ˜์ด๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") - @Test - void should_throwException_when_productIdIsNegative() { - // arrange - Long productId = -1L; - String productName = "์ƒํ’ˆ๋ช…"; - Integer quantity = 1; - Price price = new Price(10000); - - // act - CoreException exception = assertThrows(CoreException.class, () -> { - OrderItem.create(productId, productName, quantity, price); - }); - - // assert - assertThat(exception.getMessage()).isEqualTo("์ƒํ’ˆ ID๋Š” 1 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); - } - - @DisplayName("quantity๊ฐ€ null์ด๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") - @Test - void should_throwException_when_quantityIsNull() { - // arrange - Long productId = 1L; - String productName = "์ƒํ’ˆ๋ช…"; - Integer quantity = null; - Price price = new Price(10000); - - // act - CoreException exception = assertThrows(CoreException.class, () -> { - OrderItem.create(productId, productName, quantity, price); - }); - - // assert - assertThat(exception.getMessage()).isEqualTo("์ฃผ๋ฌธ ์ˆ˜๋Ÿ‰์€ 1 ์ด์ƒ์˜ ์ž์—ฐ์ˆ˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค."); - } - - @DisplayName("price๊ฐ€ null์ด๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") - @Test - void should_throwException_when_priceIsNull() { - // arrange - Long productId = 1L; - String productName = "์ƒํ’ˆ๋ช…"; - Integer quantity = 1; - Price price = null; - - // act - CoreException exception = assertThrows(CoreException.class, () -> { - OrderItem.create(productId, productName, quantity, price); - }); - - // assert - assertThat(exception.getMessage()).isEqualTo("๊ฐ€๊ฒฉ์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."); - } - } - - @DisplayName("์ฃผ๋ฌธ ํ•ญ๋ชฉ์˜ ์ด ๊ฐ€๊ฒฉ์„ ๊ณ„์‚ฐํ•  ๋•Œ, ") - @Nested - class GetTotalPrice { - @DisplayName("์ •์ƒ์ ์ธ ์ˆ˜๋Ÿ‰๊ณผ ๊ฐ€๊ฒฉ์œผ๋กœ ์ด ๊ฐ€๊ฒฉ์„ ๊ณ„์‚ฐํ•  ์ˆ˜ ์žˆ๋‹ค. (Happy Path)") - @Test - void should_calculateTotalPrice_when_validQuantityAndPrice() { - // arrange - OrderItem orderItem = OrderItem.create(1L, "์ƒํ’ˆ๋ช…", 3, new Price(10000)); - - // act - Integer totalPrice = orderItem.getTotalPrice(); - - // assert - assertThat(totalPrice).isEqualTo(30000); - } - - @DisplayName("์ˆ˜๋Ÿ‰์ด 1์ด๋ฉด ๊ฐ€๊ฒฉ๊ณผ ๋™์ผํ•˜๋‹ค. (Edge Case)") - @Test - void should_returnPrice_when_quantityIsOne() { - // arrange - OrderItem orderItem = OrderItem.create(1L, "์ƒํ’ˆ๋ช…", 1, new Price(10000)); - - // act - Integer totalPrice = orderItem.getTotalPrice(); - - // assert - assertThat(totalPrice).isEqualTo(10000); - } - - @DisplayName("๊ฐ€๊ฒฉ์ด 0์ด๋ฉด ์ด ๊ฐ€๊ฒฉ์ด 0์ด๋‹ค. (Edge Case)") - @Test - void should_returnZero_when_priceIsZero() { - // arrange - OrderItem orderItem = OrderItem.create(1L, "์ƒํ’ˆ๋ช…", 3, new Price(0)); - - // act - Integer totalPrice = orderItem.getTotalPrice(); - - // assert - assertThat(totalPrice).isEqualTo(0); - } - } -} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderServiceIntegrationTest.java deleted file mode 100644 index b3c481603..000000000 --- a/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderServiceIntegrationTest.java +++ /dev/null @@ -1,151 +0,0 @@ -package com.loopers.domain.order; - -import com.loopers.application.order.OrderItemRequest; -import com.loopers.domain.common.vo.Price; -import com.loopers.domain.product.Product; -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import jakarta.transaction.Transactional; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; - -import java.util.List; -import java.util.Map; -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -@SpringBootTest -@Transactional -@DisplayName("์ฃผ๋ฌธ ์„œ๋น„์Šค(OrderService) ํ…Œ์ŠคํŠธ") -public class OrderServiceIntegrationTest { - - @MockitoSpyBean - private OrderRepository spyOrderRepository; - - @Autowired - private OrderService orderService; - - @DisplayName("์ฃผ๋ฌธ์„ ์ €์žฅํ•  ๋•Œ, ") - @Nested - class SaveOrder { - @DisplayName("์ •์ƒ์ ์ธ ์ฃผ๋ฌธ์„ ์ €์žฅํ•˜๋ฉด ์ฃผ๋ฌธ์ด ์ €์žฅ๋œ๋‹ค. (Happy Path)") - @Test - void should_saveOrder_when_validOrder() { - // arrange - Long userId = 1L; - List orderItemRequests = List.of( - new OrderItemRequest(1L, 2), - new OrderItemRequest(2L, 1) - ); - Map productMap = Map.of( - 1L, Product.create("์ƒํ’ˆ1", 1L, new Price(10000)), - 2L, Product.create("์ƒํ’ˆ2", 1L, new Price(20000)) - ); - - // act - Order result = orderService.createOrder(orderItemRequests, productMap, userId); - - // assert - verify(spyOrderRepository).save(any(Order.class)); - assertThat(result).isNotNull(); - assertThat(result.getUserId()).isEqualTo(1L); - assertThat(result.getOrderItems()).hasSize(2); - } - - @DisplayName("๋‹จ์ผ ์ฃผ๋ฌธ ํ•ญ๋ชฉ์„ ๊ฐ€์ง„ ์ฃผ๋ฌธ์„ ์ €์žฅํ•  ์ˆ˜ ์žˆ๋‹ค. (Edge Case)") - @Test - void should_saveOrder_when_singleOrderItem() { - // arrange - Long userId = 1L; - List orderItemRequests = List.of( - new OrderItemRequest(1L, 1) - ); - Map productMap = Map.of( - 1L, Product.create("์ƒํ’ˆ1", 1L, new Price(15000)) - ); - - // act - Order result = orderService.createOrder(orderItemRequests, productMap, userId); - - // assert - verify(spyOrderRepository).save(any(Order.class)); - assertThat(result).isNotNull(); - assertThat(result.getOrderItems()).hasSize(1); - } - } - - @DisplayName("์ฃผ๋ฌธ ID์™€ ์‚ฌ์šฉ์ž ID๋กœ ์ฃผ๋ฌธ์„ ์กฐํšŒํ•  ๋•Œ, ") - @Nested - class GetOrderByIdAndUserId { - @DisplayName("์กด์žฌํ•˜๋Š” ์ฃผ๋ฌธ ID์™€ ์‚ฌ์šฉ์ž ID๋กœ ์กฐํšŒํ•˜๋ฉด ์ฃผ๋ฌธ์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. (Happy Path)") - @Test - void should_returnOrder_when_orderExists() { - // arrange - Long orderId = 1L; - Long userId = 1L; - List orderItems = List.of( - OrderItem.create(1L, "์ƒํ’ˆ1", 2, new Price(10000)) - ); - Order order = Order.create(userId, orderItems); - when(spyOrderRepository.findByIdAndUserId(orderId, userId)).thenReturn(Optional.of(order)); - - // act - Order result = orderService.getOrderByIdAndUserId(orderId, userId); - - // assert - verify(spyOrderRepository).findByIdAndUserId(1L, 1L); - assertThat(result).isNotNull(); - assertThat(result.getUserId()).isEqualTo(1L); - assertThat(result.getOrderItems()).hasSize(1); - } - - @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ฃผ๋ฌธ ID๋กœ ์กฐํšŒํ•˜๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") - @Test - void should_throwException_when_orderNotFound() { - // arrange - Long orderId = 999L; - Long userId = 1L; - when(spyOrderRepository.findByIdAndUserId(orderId, userId)).thenReturn(Optional.empty()); - - // act - CoreException exception = assertThrows(CoreException.class, () -> { - orderService.getOrderByIdAndUserId(orderId, userId); - }); - - // assert - verify(spyOrderRepository).findByIdAndUserId(999L, 1L); - assertThat(exception.getMessage()).isEqualTo("์ฃผ๋ฌธ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); - assertThat(exception.getErrorType()).isEqualTo(ErrorType.NOT_FOUND); - } - - @DisplayName("๋‹ค๋ฅธ ์‚ฌ์šฉ์ž์˜ ์ฃผ๋ฌธ ID๋กœ ์กฐํšŒํ•˜๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") - @Test - void should_throwException_when_orderBelongsToDifferentUser() { - // arrange - Long orderId = 1L; - Long userId = 1L; - Long differentUserId = 2L; - when(spyOrderRepository.findByIdAndUserId(orderId, differentUserId)).thenReturn(Optional.empty()); - - // act - CoreException exception = assertThrows(CoreException.class, () -> { - orderService.getOrderByIdAndUserId(orderId, differentUserId); - }); - - // assert - verify(spyOrderRepository).findByIdAndUserId(1L, 2L); - assertThat(exception.getMessage()).isEqualTo("์ฃผ๋ฌธ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); - assertThat(exception.getErrorType()).isEqualTo(ErrorType.NOT_FOUND); - } - } -} - diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderTest.java deleted file mode 100644 index b4521bb1b..000000000 --- a/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderTest.java +++ /dev/null @@ -1,176 +0,0 @@ -package com.loopers.domain.order; - -import com.loopers.domain.common.vo.Price; -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -import java.util.ArrayList; -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; - -@DisplayName("์ฃผ๋ฌธ(Order) Entity ํ…Œ์ŠคํŠธ") -public class OrderTest { - - @DisplayName("์ฃผ๋ฌธ์„ ์ƒ์„ฑํ•  ๋•Œ, ") - @Nested - class Create { - @DisplayName("์ •์ƒ์ ์ธ userId์™€ ์ฃผ๋ฌธ ํ•ญ๋ชฉ ๋ฆฌ์ŠคํŠธ๋กœ ์ฃผ๋ฌธ์„ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค. (Happy Path)") - @Test - void should_createOrder_when_validUserIdAndOrderItems() { - // arrange - Long userId = 1L; - List orderItems = List.of( - OrderItem.create(1L, "์ƒํ’ˆ1", 2, new Price(10000)), - OrderItem.create(2L, "์ƒํ’ˆ2", 1, new Price(20000)) - ); - - // act - Order order = Order.create(userId, orderItems); - - // assert - assertThat(order.getUserId()).isEqualTo(1L); - assertThat(order.getOrderItems()).hasSize(2); - assertThat(order.getOrderItems().get(0).getProductId()).isEqualTo(1L); - assertThat(order.getOrderItems().get(1).getProductId()).isEqualTo(2L); - } - - @DisplayName("๋‹จ์ผ ์ฃผ๋ฌธ ํ•ญ๋ชฉ์œผ๋กœ ์ฃผ๋ฌธ์„ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค. (Edge Case)") - @Test - void should_createOrder_when_singleOrderItem() { - // arrange - Long userId = 1L; - List orderItems = List.of( - OrderItem.create(1L, "์ƒํ’ˆ1", 1, new Price(10000)) - ); - - // act - Order order = Order.create(userId, orderItems); - - // assert - assertThat(order.getUserId()).isEqualTo(1L); - assertThat(order.getOrderItems()).hasSize(1); - } - - @DisplayName("๋นˆ ์ฃผ๋ฌธ ํ•ญ๋ชฉ ๋ฆฌ์ŠคํŠธ๋กœ ์ฃผ๋ฌธ์„ ์ƒ์„ฑํ•  ๊ฒฝ์šฐ, ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") - @Test - void should_throwException_when_emptyOrderItems() { - // arrange - Long userId = 1L; - List orderItems = new ArrayList<>(); - - // act & assert - CoreException exception = assertThrows(CoreException.class, () -> Order.create(userId, orderItems)); - assertThat(exception.getMessage()).isEqualTo("์ฃผ๋ฌธ ํ•ญ๋ชฉ์€ ์ตœ์†Œ 1๊ฐœ ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); - assertThat(exception.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); - } - - @DisplayName("null ์ฃผ๋ฌธ ํ•ญ๋ชฉ ๋ฆฌ์ŠคํŠธ๋กœ ์ฃผ๋ฌธ์„ ์ƒ์„ฑํ•  ๊ฒฝ์šฐ, ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") - @Test - void should_throwException_when_nullOrderItems() { - // arrange - Long userId = 1L; - List orderItems = null; - - // act & assert - CoreException exception = assertThrows(CoreException.class, () -> Order.create(userId, orderItems)); - assertThat(exception.getMessage()).isEqualTo("์ฃผ๋ฌธ ํ•ญ๋ชฉ์€ ์ตœ์†Œ 1๊ฐœ ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); - assertThat(exception.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); - } - - @DisplayName("null userId๋กœ ์ฃผ๋ฌธ์„ ์ƒ์„ฑํ•  ๊ฒฝ์šฐ, ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") - @Test - void should_throwException_when_nullUserId() { - // arrange - Long userId = null; - List orderItems = List.of( - OrderItem.create(1L, "์ƒํ’ˆ1", 1, new Price(10000)) - ); - - // act & assert - CoreException exception = assertThrows(CoreException.class, () -> Order.create(userId, orderItems)); - assertThat(exception.getMessage()).isEqualTo("์‚ฌ์šฉ์ž ID๋Š” 1 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); - assertThat(exception.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); - } - - @DisplayName("0 ์ดํ•˜์˜ userId๋กœ ์ฃผ๋ฌธ์„ ์ƒ์„ฑํ•  ๊ฒฝ์šฐ, ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") - @Test - void should_throwException_when_invalidUserId() { - // arrange - Long userId = 0L; - List orderItems = List.of( - OrderItem.create(1L, "์ƒํ’ˆ1", 1, new Price(10000)) - ); - - // act & assert - CoreException exception = assertThrows(CoreException.class, () -> Order.create(userId, orderItems)); - assertThat(exception.getMessage()).isEqualTo("์‚ฌ์šฉ์ž ID๋Š” 1 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); - assertThat(exception.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); - } - - @DisplayName("์—ฌ๋Ÿฌ ์ฃผ๋ฌธ ํ•ญ๋ชฉ์œผ๋กœ ์ฃผ๋ฌธ์„ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค. (Edge Case)") - @Test - void should_createOrder_when_multipleOrderItems() { - // arrange - Long userId = 1L; - List orderItems = List.of( - OrderItem.create(1L, "์ƒํ’ˆ1", 2, new Price(10000)), - OrderItem.create(2L, "์ƒํ’ˆ2", 1, new Price(20000)), - OrderItem.create(3L, "์ƒํ’ˆ3", 3, new Price(15000)) - ); - - // act - Order order = Order.create(userId, orderItems); - - // assert - assertThat(order.getUserId()).isEqualTo(1L); - assertThat(order.getOrderItems()).hasSize(3); - } - - } - - @DisplayName("์ฃผ๋ฌธ ์กฐํšŒ๋ฅผ ํ•  ๋•Œ, ") - @Nested - class Retrieve { - @DisplayName("์ƒ์„ฑํ•œ ์ฃผ๋ฌธ์˜ userId๋ฅผ ์กฐํšŒํ•  ์ˆ˜ ์žˆ๋‹ค. (Happy Path)") - @Test - void should_retrieveUserId_when_orderCreated() { - // arrange - Long userId = 1L; - List orderItems = List.of( - OrderItem.create(1L, "์ƒํ’ˆ1", 1, new Price(10000)) - ); - Order order = Order.create(userId, orderItems); - - // act - Long retrievedUserId = order.getUserId(); - - // assert - assertThat(retrievedUserId).isEqualTo(1L); - } - - @DisplayName("์ƒ์„ฑํ•œ ์ฃผ๋ฌธ์˜ ์ฃผ๋ฌธ ํ•ญ๋ชฉ ๋ฆฌ์ŠคํŠธ๋ฅผ ์กฐํšŒํ•  ์ˆ˜ ์žˆ๋‹ค. (Happy Path)") - @Test - void should_retrieveOrderItems_when_orderCreated() { - // arrange - Long userId = 1L; - List orderItems = List.of( - OrderItem.create(1L, "์ƒํ’ˆ1", 2, new Price(10000)), - OrderItem.create(2L, "์ƒํ’ˆ2", 1, new Price(20000)) - ); - Order order = Order.create(userId, orderItems); - - // act - List retrievedOrderItems = order.getOrderItems(); - - // assert - assertThat(retrievedOrderItems).hasSize(2); - assertThat(retrievedOrderItems.get(0).getProductId()).isEqualTo(1L); - assertThat(retrievedOrderItems.get(1).getProductId()).isEqualTo(2L); - } - } -} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/point/PointModelTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/point/PointModelTest.java deleted file mode 100644 index 8e702e629..000000000 --- a/apps/commerce-api/src/test/java/com/loopers/domain/point/PointModelTest.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.loopers.domain.point; - -import com.loopers.support.error.CoreException; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; - -public class PointModelTest { - @DisplayName("ํฌ์ธํŠธ ์ถฉ์ „์„ ํ•  ๋•Œ, ") - @Nested - class Create { - // 0 ์ดํ•˜์˜ ์ •์ˆ˜๋กœ ํฌ์ธํŠธ๋ฅผ ์ถฉ์ „ ์‹œ ์‹คํŒจํ•œ๋‹ค. - @DisplayName("0 ์ดํ•˜์˜ ์ •์ˆ˜๋กœ ํฌ์ธํŠธ๋ฅผ ์ถฉ์ „ ์‹œ ์‹คํŒจํ•œ๋‹ค.") - @ParameterizedTest - @ValueSource(ints = {0, -10, -100}) - void throwsException_whenPointIsZeroOrNegative(int invalidPoint) { - // arrange - Point point = Point.create(0L); - // act - CoreException result = assertThrows(CoreException.class, () -> point.charge(invalidPoint)); - - // assert - assertThat(result.getMessage()).isEqualTo("์ถฉ์ „ ํฌ์ธํŠธ๋Š” 0๋ณด๋‹ค ์ปค์•ผ ํ•ฉ๋‹ˆ๋‹ค."); - } - } -} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java deleted file mode 100644 index b22645d4f..000000000 --- a/apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java +++ /dev/null @@ -1,78 +0,0 @@ -package com.loopers.domain.point; - -import com.loopers.application.point.PointFacade; -import com.loopers.domain.user.User; -import com.loopers.domain.user.UserService; -import com.loopers.support.error.CoreException; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.transaction.annotation.Transactional; - -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; - -@SpringBootTest -@Transactional -public class PointServiceIntegrationTest { - @Autowired - private PointFacade pointFacade; - @Autowired - private PointService pointService; - @Autowired - private UserService userService; - - @BeforeEach - void setUp() { - String validId = "user123"; - String validEmail = "xx@yy.zz"; - String validBirthday = "1993-03-13"; - String validGender = "male"; - // ์œ ์ € ๋“ฑ๋ก - User registeredUser = userService.registerUser(validId, validEmail, validBirthday, validGender); - pointService.createPoint(registeredUser.getId()); - } - - @DisplayName("ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•  ๊ฒฝ์šฐ, ๋ณด์œ  ํฌ์ธํŠธ๊ฐ€ ๋ฐ˜ํ™˜๋œ๋‹ค.") - @Test - void returnsUserPoints_whenUserExists() { - // arrange: setUp() ๋ฉ”์„œ๋“œ์—์„œ ์ด๋ฏธ ์œ ์ € ๋“ฑ๋ก - String existingUserId = "user123"; - Long userId = userService.findByUserId(existingUserId).get().getId(); - - // act - Optional currentPoint = pointService.getCurrentPoint(userId); - - // assert - assertThat(currentPoint.orElse(null)).isEqualTo(0L); - } - - @DisplayName("ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ, null ์ด ๋ฐ˜ํ™˜๋œ๋‹ค.") - @Test - void returnsNullPoints_whenUserDoesNotExist() { - // arrange: setUp() ๋ฉ”์„œ๋“œ์—์„œ ๋“ฑ๋ก๋˜์ง€ ์•Š์€ ์œ ์ € ID ์‚ฌ์šฉ - Long nonExistingUserId = -1L; - - // act - Optional currentPoint = pointService.getCurrentPoint(nonExistingUserId); - - // assert - assertThat(currentPoint).isNotPresent(); - } - - //์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ์ € ID ๋กœ ์ถฉ์ „์„ ์‹œ๋„ํ•œ ๊ฒฝ์šฐ, ์‹คํŒจํ•œ๋‹ค. - @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ์ € ID ๋กœ ์ถฉ์ „์„ ์‹œ๋„ํ•œ ๊ฒฝ์šฐ, ์‹คํŒจํ•œ๋‹ค.") - @Test - void throwsExceptionWhenChargePointWithNonExistingUserId() { - // arrange - String nonExistingUserId = "nonexist"; - int chargeAmount = 1000; - - // act & assert - assertThrows(CoreException.class, () -> pointFacade.chargePoint(nonExistingUserId, chargeAmount)); - } -} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductServiceIntegrationTest.java deleted file mode 100644 index f7ccc360f..000000000 --- a/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductServiceIntegrationTest.java +++ /dev/null @@ -1,362 +0,0 @@ -package com.loopers.domain.product; - -import com.loopers.domain.brand.Brand; -import com.loopers.domain.common.vo.Price; -import com.loopers.domain.metrics.product.ProductMetrics; -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import com.loopers.utils.DatabaseCleanUp; -import jakarta.transaction.Transactional; -import org.junit.jupiter.api.*; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; - -import java.util.*; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -@SpringBootTest -@Transactional -@DisplayName("์ƒํ’ˆ ์„œ๋น„์Šค(ProductService) ํ…Œ์ŠคํŠธ") -public class ProductServiceIntegrationTest { - - @MockitoSpyBean - private ProductRepository spyProductRepository; - - @Autowired - private ProductService productService; - - @Autowired - private com.loopers.infrastructure.brand.BrandJpaRepository brandJpaRepository; - - @Autowired - private com.loopers.infrastructure.product.ProductJpaRepository productJpaRepository; - - @Autowired - private com.loopers.infrastructure.metrics.product.ProductMetricsJpaRepository productMetricsJpaRepository; - - @Autowired - private DatabaseCleanUp databaseCleanUp; - - private Long brandId; - private Long productId1; - private Long productId2; - private Long productId3; - - @BeforeEach - void setup() { - // Brand ๋“ฑ๋ก - Brand brand = Brand.create("Nike"); - Brand savedBrand = brandJpaRepository.save(brand); - brandId = savedBrand.getId(); - - // Product ๋“ฑ๋ก - Product product1 = Product.create("์ƒํ’ˆ1", brandId, new Price(10000)); - Product savedProduct1 = productJpaRepository.save(product1); - productId1 = savedProduct1.getId(); - // ProductMetrics ๋“ฑ๋ก - ProductMetrics metrics1 = ProductMetrics.create(productId1, 4); - productMetricsJpaRepository.save(metrics1); - - Product product2 = Product.create("์ƒํ’ˆ2", brandId, new Price(20000)); - Product savedProduct2 = productJpaRepository.save(product2); - productId2 = savedProduct2.getId(); - // ProductMetrics ๋“ฑ๋ก - ProductMetrics metrics2 = ProductMetrics.create(productId2, 0); - productMetricsJpaRepository.save(metrics2); - - Product product3 = Product.create("์ƒํ’ˆ3", brandId, new Price(15000)); - Product savedProduct3 = productJpaRepository.save(product3); - productId3 = savedProduct3.getId(); - // ProductMetrics ๋“ฑ๋ก - ProductMetrics metrics3 = ProductMetrics.create(productId3, 3); - productMetricsJpaRepository.save(metrics3); - } - - @AfterEach - void tearDown() { - databaseCleanUp.truncateAllTables(); - } - - @DisplayName("์ƒํ’ˆ ID๋กœ ์ƒํ’ˆ์„ ์กฐํšŒํ•  ๋•Œ, ") - @Nested - class GetProductById { - @DisplayName("์กด์žฌํ•˜๋Š” ์ƒํ’ˆ ID๋กœ ์กฐํšŒํ•˜๋ฉด ์ƒํ’ˆ์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. (Happy Path)") - @Test - void should_returnProduct_when_productExists() { - // arrange - Long productId = 1L; - Product product = createProduct(productId, "์ƒํ’ˆ๋ช…", 1L, 10000); - when(spyProductRepository.findById(productId)).thenReturn(Optional.of(product)); - - // act - Product result = productService.getProductById(productId); - - // assert - verify(spyProductRepository).findById(1L); - assertThat(result).isNotNull(); - assertThat(result.getId()).isEqualTo(1L); - assertThat(result.getName()).isEqualTo("์ƒํ’ˆ๋ช…"); - } - - @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ƒํ’ˆ ID๋กœ ์กฐํšŒํ•˜๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") - @Test - void should_throwException_when_productNotFound() { - // arrange - Long productId = 999L; - when(spyProductRepository.findById(productId)).thenReturn(Optional.empty()); - - // act - CoreException exception = assertThrows(CoreException.class, () -> { - productService.getProductById(productId); - }); - - // assert - verify(spyProductRepository).findById(999L); - assertThat(exception.getMessage()).isEqualTo("์ƒํ’ˆ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); - assertThat(exception.getErrorType()).isEqualTo(ErrorType.NOT_FOUND); - } - } - - @DisplayName("์—ฌ๋Ÿฌ ์ƒํ’ˆ ID๋กœ ์ƒํ’ˆ ๋งต์„ ์กฐํšŒํ•  ๋•Œ, ") - @Nested - class GetProductMapByIds { - @DisplayName("์กด์žฌํ•˜๋Š” ์ƒํ’ˆ ID๋“ค๋กœ ์กฐํšŒํ•˜๋ฉด ์ƒํ’ˆ ๋งต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. (Happy Path)") - @Test - void should_returnProductMap_when_productsExist() { - // arrange - List productIds = List.of(1L, 2L, 3L); - List products = List.of( - createProduct(1L, "์ƒํ’ˆ1", 1L, 10000), - createProduct(2L, "์ƒํ’ˆ2", 1L, 20000), - createProduct(3L, "์ƒํ’ˆ3", 2L, 15000) - ); - when(spyProductRepository.findAllByIdIn(productIds)).thenReturn(products); - - // act - Map result = productService.getProductMapByIds(productIds); - - // assert - verify(spyProductRepository).findAllByIdIn(any(Collection.class)); - assertThat(result).hasSize(3); - assertThat(result.get(1L).getName()).isEqualTo("์ƒํ’ˆ1"); - assertThat(result.get(2L).getName()).isEqualTo("์ƒํ’ˆ2"); - assertThat(result.get(3L).getName()).isEqualTo("์ƒํ’ˆ3"); - } - - @DisplayName("๋นˆ ID ๋ฆฌ์ŠคํŠธ๋กœ ์กฐํšŒํ•˜๋ฉด ๋นˆ ๋งต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. (Edge Case)") - @Test - void should_returnEmptyMap_when_emptyIdList() { - // arrange - List productIds = Collections.emptyList(); - when(spyProductRepository.findAllByIdIn(productIds)).thenReturn(Collections.emptyList()); - - // act - Map result = productService.getProductMapByIds(productIds); - - // assert - verify(spyProductRepository).findAllByIdIn(any(Collection.class)); - assertThat(result).isEmpty(); - } - - @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ƒํ’ˆ ID๋“ค๋กœ ์กฐํšŒํ•˜๋ฉด ๋นˆ ๋งต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. (Edge Case)") - @Test - void should_returnEmptyMap_when_productsNotFound() { - // arrange - List productIds = List.of(999L, 1000L); - when(spyProductRepository.findAllByIdIn(productIds)).thenReturn(Collections.emptyList()); - - // act - Map result = productService.getProductMapByIds(productIds); - - // assert - verify(spyProductRepository).findAllByIdIn(any(Collection.class)); - assertThat(result).isEmpty(); - } - } - - @DisplayName("์ƒํ’ˆ ๋ชฉ๋ก์„ ์กฐํšŒํ•  ๋•Œ, ") - @Nested - class GetProducts { - @DisplayName("๊ธฐ๋ณธ ํŽ˜์ด์ง€๋„ค์ด์…˜์œผ๋กœ ์กฐํšŒํ•˜๋ฉด ์ƒํ’ˆ ํŽ˜์ด์ง€๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค. (Happy Path)") - @Test - void should_returnProductPage_when_defaultPageable() { - // arrange - Pageable pageable = PageRequest.of(0, 20); - - // act - Page result = productService.getProducts(pageable); - - // assert - verify(spyProductRepository).findAll(any(Pageable.class)); - assertThat(result).isNotNull(); - assertThat(result.getContent()).hasSize(3); - assertThat(result.getTotalElements()).isEqualTo(3); - } - - @DisplayName("์ตœ์‹ ์ˆœ ์ •๋ ฌ๋กœ ์กฐํšŒํ•˜๋ฉด ์ƒํ’ˆ ํŽ˜์ด์ง€๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค. (Happy Path)") - @Test - void should_returnProductPage_when_sortedByLatest() { - // arrange - Sort sort = Sort.by("latest"); - Pageable pageable = PageRequest.of(0, 20, sort); - - // act - Page result = productService.getProducts(pageable); - - // assert - verify(spyProductRepository).findAll(any(Pageable.class)); - assertThat(result).isNotNull(); - assertThat(result.getContent()).hasSize(3); - // ์ตœ์‹ ์ˆœ ์ •๋ ฌ ๊ฒ€์ฆ createdAt ๊ธฐ์ค€ ๋‚ด๋ฆผ์ฐจ์ˆœ - assertThat(result.getContent().get(0).getCreatedAt()).isAfterOrEqualTo(result.getContent().get(1).getCreatedAt()); - assertThat(result.getContent().get(1).getCreatedAt()).isAfterOrEqualTo(result.getContent().get(2).getCreatedAt()); - } - - @DisplayName("๊ฐ€๊ฒฉ ์˜ค๋ฆ„์ฐจ์ˆœ ์ •๋ ฌ๋กœ ์กฐํšŒํ•˜๋ฉด ์ƒํ’ˆ ํŽ˜์ด์ง€๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค. (Happy Path)") - @Test - void should_returnProductPage_when_sortedByPriceAsc() { - // arrange - Sort sort = Sort.by("price_asc"); - Pageable pageable = PageRequest.of(0, 20, sort); - - // act - Page result = productService.getProducts(pageable); - - // assert - verify(spyProductRepository).findAll(any(Pageable.class)); - assertThat(result).isNotNull(); - assertThat(result.getContent()).hasSize(3); - // ๊ฐ€๊ฒฉ ์˜ค๋ฆ„์ฐจ์ˆœ ์ •๋ ฌ ๊ฒ€์ฆ - assertThat(result.getContent().get(0).getPrice().amount()).isLessThanOrEqualTo(result.getContent().get(1).getPrice().amount()); - assertThat(result.getContent().get(1).getPrice().amount()).isLessThanOrEqualTo(result.getContent().get(2).getPrice().amount()); - } - - @DisplayName("์ข‹์•„์š” ๋‚ด๋ฆผ์ฐจ์ˆœ ์ •๋ ฌ๋กœ ์กฐํšŒํ•˜๋ฉด ์ƒํ’ˆ ํŽ˜์ด์ง€๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค. (Happy Path)") - @Test - void should_returnProductPage_when_sortedByLikesDesc() { - // arrange - Sort sort = Sort.by("likes_desc"); - Pageable pageable = PageRequest.of(0, 20, sort); - - // act - Page result = productService.getProducts(pageable); - - // assert - verify(spyProductRepository).findAll(any(Pageable.class)); - assertThat(result).isNotNull(); - assertThat(result.getContent()).hasSize(3); - // ์ข‹์•„์š” ๋‚ด๋ฆผ์ฐจ์ˆœ ์ •๋ ฌ ๊ฒ€์ฆ - assertThat(result.getContent().get(0).getId()).isGreaterThanOrEqualTo(result.getContent().get(1).getId()); - assertThat(result.getContent().get(1).getId()).isGreaterThanOrEqualTo(result.getContent().get(2).getId()); - } - } - - @DisplayName("์ด ๊ฐ€๊ฒฉ์„ ๊ณ„์‚ฐํ•  ๋•Œ, ") - @Nested - class CalculateTotalAmount { - @DisplayName("์ •์ƒ์ ์ธ ์ƒํ’ˆ๊ณผ ์ˆ˜๋Ÿ‰์œผ๋กœ ์ด ๊ฐ€๊ฒฉ์„ ๊ณ„์‚ฐํ•  ์ˆ˜ ์žˆ๋‹ค. (Happy Path)") - @Test - void should_calculateTotalAmount_when_validProductsAndQuantities() { - // arrange - Map items = Map.of( - 1L, 2, - 2L, 3 - ); - List products = List.of( - createProduct(1L, "์ƒํ’ˆ1", 1L, 10000), - createProduct(2L, "์ƒํ’ˆ2", 1L, 20000) - ); - when(spyProductRepository.findAllByIdIn(items.keySet())).thenReturn(products); - - // act - Integer result = productService.calculateTotalAmount(items); - - // assert - verify(spyProductRepository).findAllByIdIn(any(Collection.class)); - assertThat(result).isEqualTo(80000); - } - - @DisplayName("๋‹จ์ผ ์ƒํ’ˆ์œผ๋กœ ์ด ๊ฐ€๊ฒฉ์„ ๊ณ„์‚ฐํ•  ์ˆ˜ ์žˆ๋‹ค. (Edge Case)") - @Test - void should_calculateTotalAmount_when_singleProduct() { - // arrange - Map items = Map.of(1L, 5); - List products = List.of(createProduct(1L, "์ƒํ’ˆ1", 1L, 10000)); - when(spyProductRepository.findAllByIdIn(items.keySet())).thenReturn(products); - - // act - Integer result = productService.calculateTotalAmount(items); - - // assert - verify(spyProductRepository).findAllByIdIn(any(Collection.class)); - assertThat(result).isEqualTo(50000); - } - - @DisplayName("์ˆ˜๋Ÿ‰์ด 1์ธ ์ƒํ’ˆ๋“ค๋กœ ์ด ๊ฐ€๊ฒฉ์„ ๊ณ„์‚ฐํ•  ์ˆ˜ ์žˆ๋‹ค. (Edge Case)") - @Test - void should_calculateTotalAmount_when_quantityIsOne() { - // arrange - Map items = Map.of( - 1L, 1, - 2L, 1 - ); - List products = List.of( - createProduct(1L, "์ƒํ’ˆ1", 1L, 10000), - createProduct(2L, "์ƒํ’ˆ2", 1L, 20000) - ); - when(spyProductRepository.findAllByIdIn(items.keySet())).thenReturn(products); - - // act - Integer result = productService.calculateTotalAmount(items); - - // assert - verify(spyProductRepository).findAllByIdIn(any(Collection.class)); - assertThat(result).isEqualTo(30000); - } - - @DisplayName("๊ฐ€๊ฒฉ์ด 0์ธ ์ƒํ’ˆ์ด ํฌํ•จ๋˜์–ด๋„ ์ด ๊ฐ€๊ฒฉ์„ ๊ณ„์‚ฐํ•  ์ˆ˜ ์žˆ๋‹ค. (Edge Case)") - @Test - void should_calculateTotalAmount_when_priceIsZero() { - // arrange - Map items = Map.of( - 1L, 2, - 2L, 1 - ); - List products = List.of( - createProduct(1L, "์ƒํ’ˆ1", 1L, 0), - createProduct(2L, "์ƒํ’ˆ2", 1L, 20000) - ); - when(spyProductRepository.findAllByIdIn(items.keySet())).thenReturn(products); - - // act - Integer result = productService.calculateTotalAmount(items); - - // assert - verify(spyProductRepository).findAllByIdIn(any(Collection.class)); - assertThat(result).isEqualTo(20000); - } - } - - private Product createProduct(Long id, String name, Long brandId, int priceAmount) { - Product product = Product.create(name, brandId, new Price(priceAmount)); - // ํ…Œ์ŠคํŠธ์šฉ์œผ๋กœ id ์„ค์ • (๋ฆฌํ”Œ๋ ‰์…˜ ์‚ฌ์šฉ) - try { - java.lang.reflect.Field idField = Product.class.getSuperclass().getDeclaredField("id"); - idField.setAccessible(true); - idField.set(product, id); - } catch (Exception e) { - throw new RuntimeException("Failed to set Product id", e); - } - return product; - } -} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/supply/SupplyTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/supply/SupplyTest.java deleted file mode 100644 index 6a27fdd5a..000000000 --- a/apps/commerce-api/src/test/java/com/loopers/domain/supply/SupplyTest.java +++ /dev/null @@ -1,130 +0,0 @@ -package com.loopers.domain.supply; - -import com.loopers.domain.supply.vo.Stock; -import com.loopers.support.error.CoreException; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; - -@DisplayName("์žฌ๊ณ  ๊ณต๊ธ‰(Supply) Entity ํ…Œ์ŠคํŠธ") -public class SupplyTest { - - @DisplayName("์žฌ๊ณ  ์ฐจ๊ฐ์„ ํ•  ๋•Œ, ") - @Nested - class DecreaseStock { - @DisplayName("์ •์ƒ์ ์ธ ์ˆ˜๋Ÿ‰์œผ๋กœ ์ฐจ๊ฐํ•˜๋ฉด ์žฌ๊ณ ๊ฐ€ ๊ฐ์†Œํ•œ๋‹ค. (Happy Path)") - @Test - void should_decreaseStock_when_validQuantity() { - // arrange - Supply supply = createSupply(10); - int orderQuantity = 3; - - // act - supply.decreaseStock(orderQuantity); - - // assert - assertThat(supply.getStock().quantity()).isEqualTo(7); - } - - @DisplayName("์žฌ๊ณ ๊ฐ€ ์ฐจ๊ฐ ์ˆ˜๋Ÿ‰๊ณผ ์ •ํ™•ํžˆ ๊ฐ™์œผ๋ฉด ์žฌ๊ณ ๊ฐ€ 0์ด ๋œ๋‹ค. (Edge Case)") - @Test - void should_setStockToZero_when_stockEqualsOrderQuantity() { - // arrange - Supply supply = createSupply(5); - int orderQuantity = 5; - - // act - supply.decreaseStock(orderQuantity); - - // assert - assertThat(supply.getStock().quantity()).isEqualTo(0); - } - - @DisplayName("์žฌ๊ณ ๊ฐ€ 1๊ฐœ์ผ ๋•Œ 1๊ฐœ ์ฐจ๊ฐํ•˜๋ฉด ์žฌ๊ณ ๊ฐ€ 0์ด ๋œ๋‹ค. (Edge Case)") - @Test - void should_setStockToZero_when_stockIsOneAndDecreaseOne() { - // arrange - Supply supply = createSupply(1); - int orderQuantity = 1; - - // act - supply.decreaseStock(orderQuantity); - - // assert - assertThat(supply.getStock().quantity()).isEqualTo(0); - } - - @DisplayName("0 ์ดํ•˜์˜ ์ˆ˜๋Ÿ‰์œผ๋กœ ์ฐจ๊ฐํ•˜๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") - @ParameterizedTest - @ValueSource(ints = {0, -1, -10}) - void should_throwException_when_orderQuantityIsZeroOrNegative(int invalidQuantity) { - // arrange - Supply supply = createSupply(10); - - // act - CoreException exception = assertThrows(CoreException.class, () -> { - supply.decreaseStock(invalidQuantity); - }); - - // assert - assertThat(exception.getMessage()).isEqualTo("์ฃผ๋ฌธ ์ˆ˜๋Ÿ‰์€ 0๋ณด๋‹ค ์ปค์•ผ ํ•ฉ๋‹ˆ๋‹ค."); - } - - @DisplayName("์žฌ๊ณ ๋ณด๋‹ค ๋งŽ์€ ์ˆ˜๋Ÿ‰์œผ๋กœ ์ฐจ๊ฐํ•˜๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") - @Test - void should_throwException_when_orderQuantityExceedsStock() { - // arrange - Supply supply = createSupply(5); - int orderQuantity = 10; - - // act - CoreException exception = assertThrows(CoreException.class, () -> { - supply.decreaseStock(orderQuantity); - }); - - // assert - assertThat(exception.getMessage()).isEqualTo("์žฌ๊ณ ๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค."); - } - - @DisplayName("์žฌ๊ณ ๊ฐ€ 0์ผ ๋•Œ ์ฐจ๊ฐํ•˜๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") - @Test - void should_throwException_when_stockIsZero() { - // arrange - Supply supply = createSupply(0); - int orderQuantity = 1; - - // act - CoreException exception = assertThrows(CoreException.class, () -> { - supply.decreaseStock(orderQuantity); - }); - - // assert - assertThat(exception.getMessage()).isEqualTo("์žฌ๊ณ ๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค."); - } - - @DisplayName("์—ฌ๋Ÿฌ ๋ฒˆ ์ฐจ๊ฐํ•˜๋ฉด ์žฌ๊ณ ๊ฐ€ ๋ˆ„์  ๊ฐ์†Œํ•œ๋‹ค. (Edge Case)") - @Test - void should_accumulateDecrease_when_decreaseMultipleTimes() { - // arrange - Supply supply = createSupply(10); - - // act - supply.decreaseStock(2); - supply.decreaseStock(3); - supply.decreaseStock(1); - - // assert - assertThat(supply.getStock().quantity()).isEqualTo(4); - } - } - - private Supply createSupply(int stockQuantity) { - // ํ…Œ์ŠคํŠธ์šฉ์œผ๋กœ ๋”๋ฏธ productId ์‚ฌ์šฉ - return Supply.create(1L, new Stock(stockQuantity)); - } -} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/supply/vo/StockTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/supply/vo/StockTest.java deleted file mode 100644 index 81bf89a91..000000000 --- a/apps/commerce-api/src/test/java/com/loopers/domain/supply/vo/StockTest.java +++ /dev/null @@ -1,183 +0,0 @@ -package com.loopers.domain.supply.vo; - -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; - -@DisplayName("์žฌ๊ณ (Stock) Value Object ํ…Œ์ŠคํŠธ") -public class StockTest { - - @DisplayName("์žฌ๊ณ ๋ฅผ ์ƒ์„ฑํ•  ๋•Œ, ") - @Nested - class Create { - @DisplayName("์ •์ƒ์ ์ธ ์žฌ๊ณ  ์ˆ˜๋Ÿ‰์œผ๋กœ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค. (Happy Path)") - @Test - void should_createStock_when_validQuantity() { - // arrange - int quantity = 10; - - // act - Stock stock = new Stock(quantity); - - // assert - assertThat(stock.quantity()).isEqualTo(10); - } - - @DisplayName("์žฌ๊ณ ๊ฐ€ 0์ด์–ด๋„ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค. (Edge Case)") - @Test - void should_createStock_when_quantityIsZero() { - // arrange - int quantity = 0; - - // act - Stock stock = new Stock(quantity); - - // assert - assertThat(stock.quantity()).isEqualTo(0); - } - - @DisplayName("์Œ์ˆ˜ ์žฌ๊ณ ๋กœ ์ƒ์„ฑํ•  ๊ฒฝ์šฐ, ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") - @Test - void should_throwException_when_quantityIsNegative() { - // arrange - int quantity = -1; - - // act & assert - CoreException exception = assertThrows(CoreException.class, () -> new Stock(quantity)); - assertThat(exception.getMessage()).isEqualTo("์žฌ๊ณ ๋Š” ์Œ์ˆ˜๊ฐ€ ๋  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); - assertThat(exception.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); - } - } - - @DisplayName("์žฌ๊ณ  ์ฐจ๊ฐ์„ ํ•  ๋•Œ, ") - @Nested - class Decrease { - @DisplayName("์ •์ƒ์ ์ธ ์ˆ˜๋Ÿ‰์œผ๋กœ ์ฐจ๊ฐํ•˜๋ฉด ์žฌ๊ณ ๊ฐ€ ๊ฐ์†Œํ•œ๋‹ค. (Happy Path)") - @Test - void should_decreaseStock_when_validQuantity() { - // arrange - Stock stock = new Stock(10); - int orderQuantity = 3; - - // act - Stock result = stock.decrease(orderQuantity); - - // assert - assertThat(result.quantity()).isEqualTo(7); - } - - @DisplayName("์žฌ๊ณ ๊ฐ€ ์ฐจ๊ฐ ์ˆ˜๋Ÿ‰๊ณผ ์ •ํ™•ํžˆ ๊ฐ™์œผ๋ฉด ์žฌ๊ณ ๊ฐ€ 0์ด ๋œ๋‹ค. (Edge Case)") - @Test - void should_setStockToZero_when_stockEqualsOrderQuantity() { - // arrange - Stock stock = new Stock(5); - int orderQuantity = 5; - - // act - Stock result = stock.decrease(orderQuantity); - - // assert - assertThat(result.quantity()).isEqualTo(0); - } - - @DisplayName("์žฌ๊ณ ๊ฐ€ 1๊ฐœ์ผ ๋•Œ 1๊ฐœ ์ฐจ๊ฐํ•˜๋ฉด ์žฌ๊ณ ๊ฐ€ 0์ด ๋œ๋‹ค. (Edge Case)") - @Test - void should_setStockToZero_when_stockIsOneAndDecreaseOne() { - // arrange - Stock stock = new Stock(1); - int orderQuantity = 1; - - // act - Stock result = stock.decrease(orderQuantity); - - // assert - assertThat(result.quantity()).isEqualTo(0); - } - - @DisplayName("0 ์ดํ•˜์˜ ์ˆ˜๋Ÿ‰์œผ๋กœ ์ฐจ๊ฐํ•˜๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") - @ParameterizedTest - @ValueSource(ints = {0, -1, -10}) - void should_throwException_when_orderQuantityIsZeroOrNegative(int invalidQuantity) { - // arrange - Stock stock = new Stock(10); - - // act - CoreException exception = assertThrows(CoreException.class, () -> { - stock.decrease(invalidQuantity); - }); - - // assert - assertThat(exception.getMessage()).isEqualTo("์ฃผ๋ฌธ ์ˆ˜๋Ÿ‰์€ 0๋ณด๋‹ค ์ปค์•ผ ํ•ฉ๋‹ˆ๋‹ค."); - } - - @DisplayName("์žฌ๊ณ ๋ณด๋‹ค ๋งŽ์€ ์ˆ˜๋Ÿ‰์œผ๋กœ ์ฐจ๊ฐํ•˜๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") - @Test - void should_throwException_when_orderQuantityExceedsStock() { - // arrange - Stock stock = new Stock(5); - int orderQuantity = 10; - - // act - CoreException exception = assertThrows(CoreException.class, () -> { - stock.decrease(orderQuantity); - }); - - // assert - assertThat(exception.getMessage()).isEqualTo("์žฌ๊ณ ๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค."); - } - - @DisplayName("์žฌ๊ณ ๊ฐ€ 0์ผ ๋•Œ ์ฐจ๊ฐํ•˜๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. (Exception)") - @Test - void should_throwException_when_stockIsZero() { - // arrange - Stock stock = new Stock(0); - int orderQuantity = 1; - - // act - CoreException exception = assertThrows(CoreException.class, () -> { - stock.decrease(orderQuantity); - }); - - // assert - assertThat(exception.getMessage()).isEqualTo("์žฌ๊ณ ๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค."); - } - } - - @DisplayName("์žฌ๊ณ  ๋ถ€์กฑ ํ™•์ธ์„ ํ•  ๋•Œ, ") - @Nested - class IsOutOfStock { - @DisplayName("์žฌ๊ณ ๊ฐ€ 0์ด๋ฉด true๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค. (Edge Case)") - @Test - void should_returnTrue_when_stockIsZero() { - // arrange - Stock stock = new Stock(0); - - // act - boolean result = stock.isOutOfStock(); - - // assert - assertThat(result).isTrue(); - } - - - @DisplayName("์žฌ๊ณ ๊ฐ€ 1 ์ด์ƒ์ด๋ฉด false๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค. (Happy Path)") - @Test - void should_returnFalse_when_stockIsPositive() { - // arrange - Stock stock = new Stock(10); - - // act - boolean result = stock.isOutOfStock(); - - // assert - assertThat(result).isFalse(); - } - } -} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserModelTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserModelTest.java deleted file mode 100644 index 03b394446..000000000 --- a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserModelTest.java +++ /dev/null @@ -1,139 +0,0 @@ -package com.loopers.domain.user; - -import com.loopers.support.error.CoreException; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; - -public class UserModelTest { - @DisplayName("ํšŒ์› ๊ฐ€์ž…์„ ํ•  ๋•Œ, ") - @Nested - class Create { - private final String validId = "user123"; - private final String validEmail = "xx@yy.zz"; - private final String validBirthday = "1993-03-13"; - private final String validGender = "male"; - - - @DisplayName("ID ๊ฐ€ ์˜๋ฌธ ๋ฐ ์ˆซ์ž 10์ž ์ด๋‚ด ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. - null ์ธ ๊ฒฝ์šฐ") - @Test - void throwsException_whenIdIsInvalidFormat_Null() { - // arrange - String invalidId = null; - - // act - CoreException result = assertThrows(CoreException.class, () -> { - User.create(invalidId, validEmail, validBirthday, validGender); - }); - - // assert - assertThat(result.getMessage()).isEqualTo("ID๋Š” ์˜๋ฌธ ๋ฐ ์ˆซ์ž 10์ž ์ด๋‚ด์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค."); - } - - @DisplayName("ID ๊ฐ€ ์˜๋ฌธ ๋ฐ ์ˆซ์ž 10์ž ์ด๋‚ด ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. - null์•„๋‹Œ ์—ฌ๋Ÿฌ ์ž˜๋ชป๋œ ํ˜•์‹๋“ค") - @ParameterizedTest - @ValueSource(strings = { - "", // ๋นˆ ๋ฌธ์ž์—ด - "user!@#", // ์˜๋ฌธ ๋ฐ ์ˆซ์ž๊ฐ€ ์•„๋‹Œ ๊ฒฝ์šฐ - "user1234567" // ์˜๋ฌธ ๋ฐ ์ˆซ์ž 10์ž ์ดˆ๊ณผ์ธ ๊ฒฝ์šฐ - }) - void throwsException_whenIdIsInvalidFormat(String invalidId) { - // arrange: invalidId parameter - - // act - CoreException result = assertThrows(CoreException.class, () -> { - User.create(invalidId, validEmail, validBirthday, validGender); - }); - - // assert - assertThat(result.getMessage()).isEqualTo("ID๋Š” ์˜๋ฌธ ๋ฐ ์ˆซ์ž 10์ž ์ด๋‚ด์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค."); - } - - // extra case - // 0์ž ์ดํ•˜์ธ ๊ฒฝ์šฐ - // ์ˆซ์ž๋งŒ ์žˆ๋Š” ๊ฒฝ์šฐ - - // ์ด๋ฉ”์ผ์ด xx@yy.zz ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. - - @DisplayName("์ด๋ฉ”์ผ์ด xx@yy.zz ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. - null ์ธ ๊ฒฝ์šฐ") - @Test - void throwsException_whenEmailIsInvalidFormat_Null() { - // arrange - String invalidEmail = null; - - // act - CoreException result = assertThrows(CoreException.class, () -> { - User.create(validId, invalidEmail, validBirthday, validGender); - }); - - // assert - assertThat(result.getMessage()).isEqualTo("์ด๋ฉ”์ผ ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); - } - - - @DisplayName("์ด๋ฉ”์ผ์ด xx@yy.zz ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. - null ์ด ์•„๋‹Œ ์—ฌ๋Ÿฌ ์ž˜๋ชป๋œ ํ˜•์‹๋“ค") - @ParameterizedTest - @ValueSource(strings = { - "", // ๋นˆ ๋ฌธ์ž์—ด - "userexample.com", // @๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ - "user@.com", // ๋„๋ฉ”์ธ ๋ถ€๋ถ„์ด ์—†๋Š” ๊ฒฝ์šฐ - "user@example", // ์ตœ์ƒ์œ„ ๋„๋ฉ”์ธ์ด ์—†๋Š” ๊ฒฝ์šฐ - "@." // @.๋งŒ ์žˆ๋Š” ๊ฒฝ์šฐ - }) - void throwsException_whenEmailIsInvalidFormat(String invalidEmail) { - // arrange: invalidEmail parameter - // act - CoreException result = assertThrows(CoreException.class, () -> { - User.create(validId, invalidEmail, validBirthday, validGender); - }); - - // assert - assertThat(result.getMessage()).isEqualTo("์ด๋ฉ”์ผ ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); - } - - // extra case - // ๊ณต๋ฐฑ์ด ํฌํ•จ๋œ ๊ฒฝ์šฐ - - // ์ƒ๋…„์›”์ผ์ด yyyy-MM-dd ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. - - @DisplayName("์ƒ๋…„์›”์ผ์ด yyyy-MM-dd ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. - null ์ธ ๊ฒฝ์šฐ") - @Test - void throwsException_whenBirthdayIsInvalidFormat_Null() { - // arrange - String invalidBirthday = null; - - // act - CoreException result = assertThrows(CoreException.class, () -> { - User.create(validId, validEmail, invalidBirthday, validGender); - }); - - // assert - assertThat(result.getMessage()).isEqualTo("์ƒ๋…„์›”์ผ ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); - } - - @DisplayName("์ƒ๋…„์›”์ผ์ด yyyy-MM-dd ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. - null์ด ์•„๋‹Œ ์—ฌ๋Ÿฌ ์ž˜๋ชป๋œ ํ˜•์‹๋“ค") - @ParameterizedTest - @ValueSource(strings = { - "13-03-1993", // ์ž˜๋ชป๋œ ํ˜•์‹ - "1993/03/13", // ์ž˜๋ชป๋œ ํ˜•์‹ - "19930313", // ์ž˜๋ชป๋œ ํ˜•์‹ - "930313", // ์ž˜๋ชป๋œ ํ˜•์‹ - "" // ๋นˆ ๋ฌธ์ž์—ด - }) - void throwsException_whenBirthdayIsInvalidFormat(String invalidBirthday) { - // arrange: invalidBirthday parameter - // act - CoreException result = assertThrows(CoreException.class, () -> { - User.create(validId, validEmail, invalidBirthday, validGender); - }); - - // assert - assertThat(result.getMessage()).isEqualTo("์ƒ๋…„์›”์ผ ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); - } - } -} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java deleted file mode 100644 index 0bd755cdf..000000000 --- a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java +++ /dev/null @@ -1,108 +0,0 @@ -package com.loopers.domain.user; - -import com.loopers.support.error.CoreException; -import jakarta.transaction.Transactional; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; - -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.verify; - -@SpringBootTest -@Transactional -public class UserServiceIntegrationTest { - @Autowired - private UserService userService; - - @MockitoSpyBean - private UserRepository spyUserRepository; - - @DisplayName("ํšŒ์› ๊ฐ€์ž…์‹œ User ์ €์žฅ์ด ์ˆ˜ํ–‰๋œ๋‹ค.") - @Test - void saveUserWhenRegister() { - // arrange - String validId = "user123"; - String validEmail = "xx@yy.zz"; - String validBirthday = "1993-03-13"; - String validGender = "male"; - - // act - // ์œ ์ € ๋“ฑ๋ก - userService.registerUser(validId, validEmail, validBirthday, validGender); - // ์ €์žฅ๋œ ์œ ์ € ์กฐํšŒ - Optional foundUser = userService.findByUserId(validId); - - // assert - verify(spyUserRepository).save(any(User.class)); - verify(spyUserRepository).findByUserId("user123"); - assertThat(foundUser).isPresent(); - assertThat(foundUser.get().getUserId()).isEqualTo("user123"); - } - - @DisplayName("์ด๋ฏธ ๊ฐ€์ž…๋œ ID ๋กœ ํšŒ์›๊ฐ€์ž… ์‹œ๋„ ์‹œ, ์‹คํŒจํ•œ๋‹ค.") - @Test - void throwsExceptionWhenRegisterWithExistingUserId() { - // arrange - String validId = "user123"; - String validEmail = "xx@yy.zz"; - String validBirthday = "1993-03-13"; - String validGender = "male"; - - // act - // ๊ธฐ์กด ์œ ์ € ๋“ฑ๋ก - userService.registerUser(validId, validEmail, validBirthday, validGender); - // ๋™์ผ ID ๋กœ ์œ ์ € ๋“ฑ๋ก ์‹œ๋„ - CoreException result = assertThrows(CoreException.class, () -> { - userService.registerUser(validId, "zz@cc.xx", "1992-06-07", "female"); - }); - - // assert - assertThat(result.getMessage()).isEqualTo("์ด๋ฏธ ์กด์žฌํ•˜๋Š” ์‚ฌ์šฉ์ž ID ์ž…๋‹ˆ๋‹ค."); - } - - @DisplayName("ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•  ๊ฒฝ์šฐ, ํšŒ์› ์ •๋ณด๊ฐ€ ๋ฐ˜ํ™˜๋œ๋‹ค.") - @Test - void returnsUserInfo_whenUserExists() { - // arrange - String validId = "user123"; - String validEmail = "xx@yy.zz"; - String validBirthday = "1993-03-13"; - String validGender = "male"; - // ๊ธฐ์กด ์œ ์ € ๋“ฑ๋ก - userService.registerUser(validId, validEmail, validBirthday, validGender); - - // act - Optional foundUser = userService.findByUserId(validId); - - // assert - assertThat(foundUser).isPresent(); - assertThat(foundUser.get().getUserId()).isEqualTo("user123"); - } - - @DisplayName("ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ, null ์ด ๋ฐ˜ํ™˜๋œ๋‹ค.") - @Test - void returnsNull_whenUserDoesNotExist() { - // arrange - String validId = "user123"; - String validEmail = "xx@yy.zz"; - String validBirthday = "1993-03-13"; - String validGender = "male"; - // ๊ธฐ์กด ์œ ์ € ๋“ฑ๋ก - userService.registerUser(validId, validEmail, validBirthday, validGender); - String nonExistId = "nonexist"; - - - // act - Optional foundUser = userService.findByUserId(nonExistId); - - // assert - assertThat(foundUser).isNotPresent(); - } -} diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/LikeProductV1ApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/LikeProductV1ApiE2ETest.java deleted file mode 100644 index de6c6d615..000000000 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/LikeProductV1ApiE2ETest.java +++ /dev/null @@ -1,485 +0,0 @@ -package com.loopers.interfaces.api; - -import com.loopers.domain.brand.Brand; -import com.loopers.domain.common.vo.Price; -import com.loopers.domain.metrics.product.ProductMetrics; -import com.loopers.domain.product.Product; -import com.loopers.domain.supply.Supply; -import com.loopers.domain.supply.SupplyService; -import com.loopers.domain.supply.vo.Stock; -import com.loopers.infrastructure.brand.BrandJpaRepository; -import com.loopers.infrastructure.metrics.product.ProductMetricsJpaRepository; -import com.loopers.infrastructure.product.ProductJpaRepository; -import com.loopers.interfaces.api.like.product.LikeProductV1Dto; -import com.loopers.interfaces.api.user.UserV1Dto; -import com.loopers.utils.DatabaseCleanUp; -import org.junit.jupiter.api.*; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.web.client.TestRestTemplate; -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.ResponseEntity; -import org.springframework.transaction.annotation.Transactional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertAll; -import static org.junit.jupiter.api.Assertions.assertTrue; - -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -public class LikeProductV1ApiE2ETest { - - private final String ENDPOINT_USER = "/api/v1/users"; - private final String ENDPOINT_LIKE_PRODUCTS = "/api/v1/like/products"; - - private final TestRestTemplate testRestTemplate; - private final DatabaseCleanUp databaseCleanUp; - private final BrandJpaRepository brandJpaRepository; - private final ProductJpaRepository productJpaRepository; - private final ProductMetricsJpaRepository productMetricsJpaRepository; - private final SupplyService supplyService; - - @Autowired - public LikeProductV1ApiE2ETest( - TestRestTemplate testRestTemplate, - DatabaseCleanUp databaseCleanUp, - BrandJpaRepository brandJpaRepository, - ProductJpaRepository productJpaRepository, - ProductMetricsJpaRepository productMetricsJpaRepository, - SupplyService supplyService - ) { - this.testRestTemplate = testRestTemplate; - this.databaseCleanUp = databaseCleanUp; - this.brandJpaRepository = brandJpaRepository; - this.productJpaRepository = productJpaRepository; - this.productMetricsJpaRepository = productMetricsJpaRepository; - this.supplyService = supplyService; - } - - @AfterEach - void tearDown() { - databaseCleanUp.truncateAllTables(); - } - - private final String validUserId = "user123"; - private final String validEmail = "xx@yy.zz"; - private final String validBirthday = "1993-03-13"; - private final String validGender = "male"; - - private Long brandId; - private Long productId1; - private Long productId2; - - @BeforeEach - @Transactional - void setupUserAndProducts() { - // User ๋“ฑ๋ก - UserV1Dto.UserRegisterRequest request = new UserV1Dto.UserRegisterRequest( - validUserId, - validEmail, - validBirthday, - validGender - ); - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - testRestTemplate.exchange(ENDPOINT_USER, HttpMethod.POST, new HttpEntity<>(request), responseType); - - // Brand ๋“ฑ๋ก - Brand brand = Brand.create("Nike"); - Brand savedBrand = brandJpaRepository.save(brand); - brandId = savedBrand.getId(); - - // Product ๋“ฑ๋ก - Product product1 = createProduct("์ƒํ’ˆ1", brandId, 10000); - Product savedProduct1 = productJpaRepository.save(product1); - productId1 = savedProduct1.getId(); - // ProductMetrics ๋“ฑ๋ก - ProductMetrics metrics1 = ProductMetrics.create(productId1, 0); - productMetricsJpaRepository.save(metrics1); - // Supply ๋“ฑ๋ก - Supply supply1 = Supply.create(productId1, new Stock(100)); - supplyService.saveSupply(supply1); - - Product product2 = createProduct("์ƒํ’ˆ2", brandId, 20000); - Product savedProduct2 = productJpaRepository.save(product2); - productId2 = savedProduct2.getId(); - // ProductMetrics ๋“ฑ๋ก - ProductMetrics metrics2 = ProductMetrics.create(productId2, 0); - productMetricsJpaRepository.save(metrics2); - // Supply ๋“ฑ๋ก - Supply supply2 = Supply.create(productId2, new Stock(200)); - supplyService.saveSupply(supply2); - } - - - private Product createProduct(String name, Long brandId, int priceAmount) { - return Product.create(name, brandId, new Price(priceAmount)); - } - - private HttpHeaders createHeaders() { - HttpHeaders headers = new HttpHeaders(); - headers.add("X-USER-ID", validUserId); - return headers; - } - - @DisplayName("POST /api/v1/like/products/{productId}") - @Nested - class PostLikeProduct { - @DisplayName("๋กœ๊ทธ์ธํ•œ ์œ ์ €๊ฐ€ ์ƒํ’ˆ ์ข‹์•„์š” ๋“ฑ๋ก์— ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, `200 OK` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnOk_whenLikeProductSuccess() { - // arrange - String url = ENDPOINT_LIKE_PRODUCTS + "/" + productId1; - HttpHeaders headers = createHeaders(); - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - ResponseEntity> response = testRestTemplate.exchange( - url, HttpMethod.POST, new HttpEntity<>(null, headers), responseType); - - // assert - assertTrue(response.getStatusCode().is2xxSuccessful()); - } - - @DisplayName("๊ฐ™์€ ์ƒํ’ˆ์— ์—ฌ๋Ÿฌ ๋ฒˆ ์ข‹์•„์š”๋ฅผ ๋“ฑ๋กํ•ด๋„ ๋ฉฑ๋“ฑํ•˜๊ฒŒ ๋™์ž‘ํ•œ๋‹ค.") - @Test - void beIdempotent_whenLikeProductMultipleTimes() { - // arrange - String url = ENDPOINT_LIKE_PRODUCTS + "/" + productId1; - HttpHeaders headers = createHeaders(); - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - ResponseEntity> response1 = testRestTemplate.exchange( - url, HttpMethod.POST, new HttpEntity<>(null, headers), responseType); - ResponseEntity> response2 = testRestTemplate.exchange( - url, HttpMethod.POST, new HttpEntity<>(null, headers), responseType); - - // assert - assertTrue(response1.getStatusCode().is2xxSuccessful()); - assertTrue(response2.getStatusCode().is2xxSuccessful()); - } - - @DisplayName("๋น„๋กœ๊ทธ์ธ ์œ ์ €๊ฐ€ ์ข‹์•„์š” ๋“ฑ๋ก์„ ์‹œ๋„ํ•  ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnBadRequest_whenXUserIdHeaderIsMissing() { - // arrange - String url = ENDPOINT_LIKE_PRODUCTS + "/" + productId1; - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - ResponseEntity> response = testRestTemplate.exchange( - url, HttpMethod.POST, new HttpEntity<>(null, null), responseType); - - // assert - assertThat(response.getStatusCode().value()).isEqualTo(400); - } - - @DisplayName("X-USER-ID ํ—ค๋”๊ฐ€ ๋นˆ ๋ฌธ์ž์—ด์ผ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnBadRequest_whenXUserIdHeaderIsEmpty() { - // arrange - String url = ENDPOINT_LIKE_PRODUCTS + "/" + productId1; - HttpHeaders headers = new HttpHeaders(); - headers.add("X-USER-ID", ""); - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - ResponseEntity> response = testRestTemplate.exchange( - url, HttpMethod.POST, new HttpEntity<>(null, headers), responseType); - - // assert - assertThat(response.getStatusCode().value()).isEqualTo(400); - } - - @DisplayName("X-USER-ID ํ—ค๋”๊ฐ€ ๊ณต๋ฐฑ๋งŒ ์žˆ์„ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnBadRequest_whenXUserIdHeaderIsBlank() { - // arrange - String url = ENDPOINT_LIKE_PRODUCTS + "/" + productId1; - HttpHeaders headers = new HttpHeaders(); - headers.add("X-USER-ID", " "); - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - ResponseEntity> response = testRestTemplate.exchange( - url, HttpMethod.POST, new HttpEntity<>(null, headers), responseType); - - // assert - assertThat(response.getStatusCode().value()).isEqualTo(400); - } - - @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ์ € ID๋กœ ์ข‹์•„์š” ๋“ฑ๋ก์„ ์‹œ๋„ํ•  ๊ฒฝ์šฐ, `404 Not Found` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnNotFound_whenUserIdDoesNotExist() { - // arrange - String url = ENDPOINT_LIKE_PRODUCTS + "/" + productId1; - HttpHeaders headers = new HttpHeaders(); - headers.add("X-USER-ID", "nonexist"); - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - ResponseEntity> response = testRestTemplate.exchange( - url, HttpMethod.POST, new HttpEntity<>(null, headers), responseType); - - // assert - // Note: ํ˜„์žฌ ์‹คํŒจํ•  ์ˆ˜ ์žˆ์ง€๋งŒ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๋Š” ์ž‘์„ฑํ•จ - assertThat(response.getStatusCode().value() == 400 || response.getStatusCode().value() == 404).isTrue(); - } - - @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ƒํ’ˆ ID๋กœ ์ข‹์•„์š” ๋“ฑ๋ก์„ ์‹œ๋„ํ•  ๊ฒฝ์šฐ, `404 Not Found` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnNotFound_whenProductIdDoesNotExist() { - // arrange - Long nonExistentProductId = 99999L; - String url = ENDPOINT_LIKE_PRODUCTS + "/" + nonExistentProductId; - HttpHeaders headers = createHeaders(); - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - ResponseEntity> response = testRestTemplate.exchange( - url, HttpMethod.POST, new HttpEntity<>(null, headers), responseType); - - // assert - // Note: ํ˜„์žฌ ์‹คํŒจํ•  ์ˆ˜ ์žˆ์ง€๋งŒ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๋Š” ์ž‘์„ฑํ•จ - assertThat(response.getStatusCode().value() == 400 || response.getStatusCode().value() == 404).isTrue(); - } - } - - @DisplayName("DELETE /api/v1/like/products/{productId}") - @Nested - class DeleteLikeProduct { - @DisplayName("๋กœ๊ทธ์ธํ•œ ์œ ์ €๊ฐ€ ์ƒํ’ˆ ์ข‹์•„์š” ์ทจ์†Œ์— ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, `200 OK` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnOk_whenUnlikeProductSuccess() { - // arrange - String url = ENDPOINT_LIKE_PRODUCTS + "/" + productId1; - HttpHeaders headers = createHeaders(); - // ๋จผ์ € ์ข‹์•„์š” ๋“ฑ๋ก - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - testRestTemplate.exchange(url, HttpMethod.POST, new HttpEntity<>(null, headers), responseType); - - // act - ResponseEntity> response = testRestTemplate.exchange( - url, HttpMethod.DELETE, new HttpEntity<>(null, headers), responseType); - - // assert - assertTrue(response.getStatusCode().is2xxSuccessful()); - } - - @DisplayName("์ข‹์•„์š”ํ•˜์ง€ ์•Š์€ ์ƒํ’ˆ์„ ์ทจ์†Œํ•ด๋„ ๋ฉฑ๋“ฑํ•˜๊ฒŒ ๋™์ž‘ํ•œ๋‹ค.") - @Test - void beIdempotent_whenUnlikeProductNotLiked() { - // arrange - String url = ENDPOINT_LIKE_PRODUCTS + "/" + productId1; - HttpHeaders headers = createHeaders(); - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - ResponseEntity> response = testRestTemplate.exchange( - url, HttpMethod.DELETE, new HttpEntity<>(null, headers), responseType); - - // assert - assertTrue(response.getStatusCode().is2xxSuccessful()); - } - - @DisplayName("๋น„๋กœ๊ทธ์ธ ์œ ์ €๊ฐ€ ์ข‹์•„์š” ์ทจ์†Œ๋ฅผ ์‹œ๋„ํ•  ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnBadRequest_whenXUserIdHeaderIsMissing() { - // arrange - String url = ENDPOINT_LIKE_PRODUCTS + "/" + productId1; - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - ResponseEntity> response = testRestTemplate.exchange( - url, HttpMethod.DELETE, new HttpEntity<>(null, null), responseType); - - // assert - assertThat(response.getStatusCode().value()).isEqualTo(400); - } - - @DisplayName("X-USER-ID ํ—ค๋”๊ฐ€ ๋นˆ ๋ฌธ์ž์—ด์ผ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnBadRequest_whenXUserIdHeaderIsEmpty() { - // arrange - String url = ENDPOINT_LIKE_PRODUCTS + "/" + productId1; - HttpHeaders headers = new HttpHeaders(); - headers.add("X-USER-ID", ""); - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - ResponseEntity> response = testRestTemplate.exchange( - url, HttpMethod.DELETE, new HttpEntity<>(null, headers), responseType); - - // assert - assertThat(response.getStatusCode().value()).isEqualTo(400); - } - - @DisplayName("X-USER-ID ํ—ค๋”๊ฐ€ ๊ณต๋ฐฑ๋งŒ ์žˆ์„ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnBadRequest_whenXUserIdHeaderIsBlank() { - // arrange - String url = ENDPOINT_LIKE_PRODUCTS + "/" + productId1; - HttpHeaders headers = new HttpHeaders(); - headers.add("X-USER-ID", " "); - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - ResponseEntity> response = testRestTemplate.exchange( - url, HttpMethod.DELETE, new HttpEntity<>(null, headers), responseType); - - // assert - assertThat(response.getStatusCode().value()).isEqualTo(400); - } - - @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ์ € ID๋กœ ์ข‹์•„์š” ์ทจ์†Œ๋ฅผ ์‹œ๋„ํ•  ๊ฒฝ์šฐ, `404 Not Found` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnNotFound_whenUserIdDoesNotExist() { - // arrange - String url = ENDPOINT_LIKE_PRODUCTS + "/" + productId1; - HttpHeaders headers = new HttpHeaders(); - headers.add("X-USER-ID", "nonexist"); - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - ResponseEntity> response = testRestTemplate.exchange( - url, HttpMethod.DELETE, new HttpEntity<>(null, headers), responseType); - - // assert - // Note: ํ˜„์žฌ ์‹คํŒจํ•  ์ˆ˜ ์žˆ์ง€๋งŒ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๋Š” ์ž‘์„ฑํ•จ - assertThat(response.getStatusCode().value() == 400 || response.getStatusCode().value() == 404).isTrue(); - } - } - - @DisplayName("GET /api/v1/like/products") - @Nested - class GetLikedProducts { - @DisplayName("๋กœ๊ทธ์ธํ•œ ์œ ์ €๊ฐ€ ์ข‹์•„์š”ํ•œ ์ƒํ’ˆ ๋ชฉ๋ก ์กฐํšŒ์— ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ 200 OK ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค") - @Test - void returnLikedProducts_whenGetLikedProductsSuccess() { - // arrange - HttpHeaders headers = createHeaders(); - // ์ข‹์•„์š” ๋“ฑ๋ก - ParameterizedTypeReference> likeResponseType = new ParameterizedTypeReference<>() { - }; - testRestTemplate.exchange(ENDPOINT_LIKE_PRODUCTS + "/" + productId1, HttpMethod.POST, new HttpEntity<>(null, headers), likeResponseType); - testRestTemplate.exchange(ENDPOINT_LIKE_PRODUCTS + "/" + productId2, HttpMethod.POST, new HttpEntity<>(null, headers), likeResponseType); - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - ResponseEntity> response = testRestTemplate.exchange( - ENDPOINT_LIKE_PRODUCTS, HttpMethod.GET, new HttpEntity<>(null, headers), responseType); - - // assert - assertTrue(response.getStatusCode().is2xxSuccessful()); - } - - @DisplayName("ํŽ˜์ด์ง€๋„ค์ด์…˜ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ์กฐํšŒํ•  ๊ฒฝ์šฐ, ํ•ด๋‹น ํŽ˜์ด์ง€์˜ ์ข‹์•„์š”ํ•œ ์ƒํ’ˆ ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnLikedProducts_whenWithPagination() { - // arrange - String url = ENDPOINT_LIKE_PRODUCTS + "?page=0&size=10"; - HttpHeaders headers = createHeaders(); - // ์ข‹์•„์š” ๋“ฑ๋ก - ParameterizedTypeReference> likeResponseType = new ParameterizedTypeReference<>() { - }; - testRestTemplate.exchange(ENDPOINT_LIKE_PRODUCTS + "/" + productId1, HttpMethod.POST, new HttpEntity<>(null, headers), likeResponseType); - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - ResponseEntity> response = testRestTemplate.exchange( - url, HttpMethod.GET, new HttpEntity<>(null, headers), responseType); - - // assert - assertAll( - () -> assertTrue(response.getStatusCode().is2xxSuccessful()), - () -> assertThat(response.getBody()).isNotNull(), - () -> assertThat(response.getBody().data()).isNotNull() - ); - } - - @DisplayName("๋น„๋กœ๊ทธ์ธ ์œ ์ €๊ฐ€ ์ข‹์•„์š”ํ•œ ์ƒํ’ˆ ๋ชฉ๋ก ์กฐํšŒ๋ฅผ ์‹œ๋„ํ•  ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnBadRequest_whenXUserIdHeaderIsMissing() { - // arrange - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - ResponseEntity> response = testRestTemplate.exchange( - ENDPOINT_LIKE_PRODUCTS, HttpMethod.GET, new HttpEntity<>(null, null), responseType); - - // assert - assertThat(response.getStatusCode().value()).isEqualTo(400); - } - - @DisplayName("X-USER-ID ํ—ค๋”๊ฐ€ ๋นˆ ๋ฌธ์ž์—ด์ผ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnBadRequest_whenXUserIdHeaderIsEmpty() { - // arrange - HttpHeaders headers = new HttpHeaders(); - headers.add("X-USER-ID", ""); - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - ResponseEntity> response = testRestTemplate.exchange( - ENDPOINT_LIKE_PRODUCTS, HttpMethod.GET, new HttpEntity<>(null, headers), responseType); - - // assert - assertThat(response.getStatusCode().value()).isEqualTo(400); - } - - @DisplayName("X-USER-ID ํ—ค๋”๊ฐ€ ๊ณต๋ฐฑ๋งŒ ์žˆ์„ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnBadRequest_whenXUserIdHeaderIsBlank() { - // arrange - HttpHeaders headers = new HttpHeaders(); - headers.add("X-USER-ID", " "); - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - ResponseEntity> response = testRestTemplate.exchange( - ENDPOINT_LIKE_PRODUCTS, HttpMethod.GET, new HttpEntity<>(null, headers), responseType); - - // assert - assertThat(response.getStatusCode().value()).isEqualTo(400); - } - - @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ์ € ID๋กœ ์ข‹์•„์š”ํ•œ ์ƒํ’ˆ ๋ชฉ๋ก ์กฐํšŒ๋ฅผ ์‹œ๋„ํ•  ๊ฒฝ์šฐ, `404 Not Found` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnNotFound_whenUserIdDoesNotExist() { - // arrange - HttpHeaders headers = new HttpHeaders(); - headers.add("X-USER-ID", "nonexist"); - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - ResponseEntity> response = testRestTemplate.exchange( - ENDPOINT_LIKE_PRODUCTS, HttpMethod.GET, new HttpEntity<>(null, headers), responseType); - - // assert - // Note: ํ˜„์žฌ ์‹คํŒจํ•  ์ˆ˜ ์žˆ์ง€๋งŒ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๋Š” ์ž‘์„ฑํ•จ - assertThat(response.getStatusCode().value() == 400 || response.getStatusCode().value() == 404).isTrue(); - } - } -} diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/OrderV1ApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/OrderV1ApiE2ETest.java deleted file mode 100644 index 48e79e710..000000000 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/OrderV1ApiE2ETest.java +++ /dev/null @@ -1,830 +0,0 @@ -package com.loopers.interfaces.api; - -import com.loopers.application.order.OrderItemRequest; -import com.loopers.application.order.OrderRequest; -import com.loopers.domain.brand.Brand; -import com.loopers.domain.common.vo.Price; -import com.loopers.domain.metrics.product.ProductMetrics; -import com.loopers.domain.product.Product; -import com.loopers.domain.supply.Supply; -import com.loopers.domain.supply.vo.Stock; -import com.loopers.infrastructure.brand.BrandJpaRepository; -import com.loopers.infrastructure.metrics.product.ProductMetricsJpaRepository; -import com.loopers.infrastructure.product.ProductJpaRepository; -import com.loopers.infrastructure.supply.SupplyJpaRepository; -import com.loopers.interfaces.api.order.OrderV1Dto; -import com.loopers.interfaces.api.point.PointV1Dto; -import com.loopers.interfaces.api.user.UserV1Dto; -import com.loopers.utils.DatabaseCleanUp; -import org.junit.jupiter.api.*; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.web.client.TestRestTemplate; -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.ResponseEntity; - -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertAll; -import static org.junit.jupiter.api.Assertions.assertTrue; - -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -public class OrderV1ApiE2ETest { - - private final String ENDPOINT_USER = "/api/v1/users"; - private final String ENDPOINT_POINT = "/api/v1/points"; - private final String ENDPOINT_ORDERS = "/api/v1/orders"; - - private final TestRestTemplate testRestTemplate; - private final DatabaseCleanUp databaseCleanUp; - private final BrandJpaRepository brandJpaRepository; - private final ProductJpaRepository productJpaRepository; - private final SupplyJpaRepository supplyJpaRepository; - private final ProductMetricsJpaRepository productMetricsJpaRepository; - - @Autowired - public OrderV1ApiE2ETest( - TestRestTemplate testRestTemplate, - DatabaseCleanUp databaseCleanUp, - BrandJpaRepository brandJpaRepository, - ProductJpaRepository productJpaRepository, - SupplyJpaRepository supplyJpaRepository, - ProductMetricsJpaRepository productMetricsJpaRepository - ) { - this.testRestTemplate = testRestTemplate; - this.databaseCleanUp = databaseCleanUp; - this.brandJpaRepository = brandJpaRepository; - this.productJpaRepository = productJpaRepository; - this.supplyJpaRepository = supplyJpaRepository; - this.productMetricsJpaRepository = productMetricsJpaRepository; - } - - @AfterEach - void tearDown() { - databaseCleanUp.truncateAllTables(); - } - - private final String validUserId = "user123"; - private final String validEmail = "xx@yy.zz"; - private final String validBirthday = "1993-03-13"; - private final String validGender = "male"; - - private Long brandId; - private Long productId1; - private Long productId2; - private Long productId3; - - @BeforeEach - void setupUserAndProducts() { - // User ๋“ฑ๋ก - UserV1Dto.UserRegisterRequest userRequest = new UserV1Dto.UserRegisterRequest( - validUserId, - validEmail, - validBirthday, - validGender - ); - ParameterizedTypeReference> userResponseType = new ParameterizedTypeReference<>() { - }; - testRestTemplate.exchange(ENDPOINT_USER, HttpMethod.POST, new HttpEntity<>(userRequest), userResponseType); - - // ํฌ์ธํŠธ ์ถฉ์ „ - HttpHeaders headers = createHeaders(); - PointV1Dto.PointChargeRequest pointRequest = new PointV1Dto.PointChargeRequest(100000); - ParameterizedTypeReference> pointResponseType = new ParameterizedTypeReference<>() { - }; - testRestTemplate.exchange(ENDPOINT_POINT + "/charge", HttpMethod.POST, new HttpEntity<>(pointRequest, headers), pointResponseType); - - // Brand ๋“ฑ๋ก - Brand brand = Brand.create("Nike"); - Brand savedBrand = brandJpaRepository.save(brand); - brandId = savedBrand.getId(); - - // Product ๋“ฑ๋ก - Product product1 = createProduct("์ƒํ’ˆ1", brandId, 10000); - Product savedProduct1 = productJpaRepository.save(product1); - productId1 = savedProduct1.getId(); - // ProductMetrics ๋“ฑ๋ก - ProductMetrics metrics1 = ProductMetrics.create(productId1, 0); - productMetricsJpaRepository.save(metrics1); - - Product product2 = createProduct("์ƒํ’ˆ2", brandId, 20000); - Product savedProduct2 = productJpaRepository.save(product2); - productId2 = savedProduct2.getId(); - // ProductMetrics ๋“ฑ๋ก - ProductMetrics metrics2 = ProductMetrics.create(productId2, 0); - productMetricsJpaRepository.save(metrics2); - - Product product3 = createProduct("์ƒํ’ˆ3", brandId, 15000); - Product savedProduct3 = productJpaRepository.save(product3); - productId3 = savedProduct3.getId(); - // ProductMetrics ๋“ฑ๋ก - ProductMetrics metrics3 = ProductMetrics.create(productId3, 0); - productMetricsJpaRepository.save(metrics3); - - // Supply ๋“ฑ๋ก (์žฌ๊ณ  ์„ค์ •) - Supply supply1 = createSupply(productId1, 100); - supplyJpaRepository.save(supply1); - - Supply supply2 = createSupply(productId2, 50); - supplyJpaRepository.save(supply2); - - Supply supply3 = createSupply(productId3, 30); - supplyJpaRepository.save(supply3); - } - - private Product createProduct(String name, Long brandId, int priceAmount) { - return Product.create(name, brandId, new Price(priceAmount)); - } - - private Supply createSupply(Long productId, int stockQuantity) { - return Supply.create(productId, new Stock(stockQuantity)); - } - - private HttpHeaders createHeaders() { - HttpHeaders headers = new HttpHeaders(); - headers.add("X-USER-ID", validUserId); - return headers; - } - - @DisplayName("POST /api/v1/orders") - @Nested - class PostOrder { - @DisplayName("๋กœ๊ทธ์ธํ•œ ์œ ์ €๊ฐ€ ์ฃผ๋ฌธ ์ƒ์„ฑ์— ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ์ƒ์„ฑ๋œ ์ฃผ๋ฌธ ์ •๋ณด๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnOrderInfo_whenCreateOrderSuccess() { - // arrange - OrderRequest request = new OrderRequest( - List.of( - new OrderItemRequest(productId1, 2), - new OrderItemRequest(productId2, 1) - ) - ); - HttpHeaders headers = createHeaders(); - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - ResponseEntity> response = testRestTemplate.exchange( - ENDPOINT_ORDERS, HttpMethod.POST, new HttpEntity<>(request, headers), responseType); - - // assert - assertAll( - () -> assertTrue(response.getStatusCode().is2xxSuccessful()), - () -> assertThat(response.getBody()).isNotNull(), - () -> assertThat(response.getBody().data()).isNotNull(), - () -> assertThat(response.getBody().data().orderId()).isNotNull(), - () -> assertThat(response.getBody().data().items()).hasSize(2), - () -> assertThat(response.getBody().data().totalPrice()).isEqualTo(40000) - ); - } - - @DisplayName("์žฌ๊ณ ๊ฐ€ ๋ถ€์กฑํ•œ ์ƒํ’ˆ์„ ์ฃผ๋ฌธํ•  ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnBadRequest_whenStockInsufficient() { - // arrange - OrderRequest request = new OrderRequest( - List.of( - new OrderItemRequest(productId1, 99999) - ) - ); - HttpHeaders headers = createHeaders(); - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - ResponseEntity> response = testRestTemplate.exchange( - ENDPOINT_ORDERS, HttpMethod.POST, new HttpEntity<>(request, headers), responseType); - - // assert - assertThat(response.getStatusCode().value() == 400).isTrue(); - } - - @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ƒํ’ˆ ID๋กœ ์ฃผ๋ฌธํ•  ๊ฒฝ์šฐ, `404 Not Found` ๋˜๋Š” `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnNotFoundOrBadRequest_whenProductIdDoesNotExist() { - // arrange - Long nonExistentProductId = 99999L; - OrderRequest request = new OrderRequest( - List.of( - new OrderItemRequest(nonExistentProductId, 1) - ) - ); - HttpHeaders headers = createHeaders(); - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - ResponseEntity> response = testRestTemplate.exchange( - ENDPOINT_ORDERS, HttpMethod.POST, new HttpEntity<>(request, headers), responseType); - - // assert - // Note: OrderFacade์—์„œ getProductMapByIds๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด ์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ƒํ’ˆ์€ ๋งต์— ํฌํ•จ๋˜์ง€ ์•Š์Œ - // ์ดํ›„ OrderItem.create์—์„œ productMap.get()์ด null์„ ๋ฐ˜ํ™˜ํ•˜๋ฉด NullPointerException์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Œ - // ๋˜๋Š” SupplyService.checkAndDecreaseStock์—์„œ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Œ - assertThat(response.getStatusCode().value() == 400 || response.getStatusCode().value() == 404 || response.getStatusCode().value() == 500).isTrue(); - } - - @DisplayName("ํฌ์ธํŠธ๊ฐ€ ๋ถ€์กฑํ•œ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnBadRequest_whenPointInsufficient() { - // arrange - // ํฌ์ธํŠธ๋ฅผ ๋ชจ๋‘ ์‚ฌ์šฉ - HttpHeaders headers = createHeaders(); - OrderRequest firstOrder = new OrderRequest( - List.of( - new OrderItemRequest(productId1, 10) - ) - ); - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - testRestTemplate.exchange(ENDPOINT_ORDERS, HttpMethod.POST, new HttpEntity<>(firstOrder, headers), responseType); - - // ํฌ์ธํŠธ ๋ถ€์กฑํ•œ ์ฃผ๋ฌธ ์‹œ๋„ - OrderRequest request = new OrderRequest( - List.of( - new OrderItemRequest(productId2, 99999) - ) - ); - - // act - ResponseEntity> response = testRestTemplate.exchange( - ENDPOINT_ORDERS, HttpMethod.POST, new HttpEntity<>(request, headers), responseType); - - // assert - assertThat(response.getStatusCode().value() == 400).isTrue(); - } - - @DisplayName("๋น„๋กœ๊ทธ์ธ ์œ ์ €๊ฐ€ ์ฃผ๋ฌธ ์ƒ์„ฑ์„ ์‹œ๋„ํ•  ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnBadRequest_whenXUserIdHeaderIsMissing() { - // arrange - OrderRequest request = new OrderRequest( - List.of( - new OrderItemRequest(productId1, 1) - ) - ); - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - ResponseEntity> response = testRestTemplate.exchange( - ENDPOINT_ORDERS, HttpMethod.POST, new HttpEntity<>(request, null), responseType); - - // assert - assertThat(response.getStatusCode().value()).isEqualTo(400); - } - - @DisplayName("X-USER-ID ํ—ค๋”๊ฐ€ ๋นˆ ๋ฌธ์ž์—ด์ผ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnBadRequest_whenXUserIdHeaderIsEmpty() { - // arrange - OrderRequest request = new OrderRequest( - List.of( - new OrderItemRequest(productId1, 1) - ) - ); - HttpHeaders headers = new HttpHeaders(); - headers.add("X-USER-ID", ""); - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - ResponseEntity> response = testRestTemplate.exchange( - ENDPOINT_ORDERS, HttpMethod.POST, new HttpEntity<>(request, headers), responseType); - - // assert - assertThat(response.getStatusCode().value()).isEqualTo(400); - } - - @DisplayName("X-USER-ID ํ—ค๋”๊ฐ€ ๊ณต๋ฐฑ๋งŒ ์žˆ์„ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnBadRequest_whenXUserIdHeaderIsBlank() { - // arrange - OrderRequest request = new OrderRequest( - List.of( - new OrderItemRequest(productId1, 1) - ) - ); - HttpHeaders headers = new HttpHeaders(); - headers.add("X-USER-ID", " "); - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - ResponseEntity> response = testRestTemplate.exchange( - ENDPOINT_ORDERS, HttpMethod.POST, new HttpEntity<>(request, headers), responseType); - - // assert - assertThat(response.getStatusCode().value()).isEqualTo(400); - } - - @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ์ € ID๋กœ ์ฃผ๋ฌธ ์ƒ์„ฑ์„ ์‹œ๋„ํ•  ๊ฒฝ์šฐ, `404 Not Found` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnNotFound_whenUserIdDoesNotExist() { - // arrange - OrderRequest request = new OrderRequest( - List.of( - new OrderItemRequest(productId1, 1) - ) - ); - HttpHeaders headers = new HttpHeaders(); - headers.add("X-USER-ID", "nonexist"); - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - ResponseEntity> response = testRestTemplate.exchange( - ENDPOINT_ORDERS, HttpMethod.POST, new HttpEntity<>(request, headers), responseType); - - // assert - assertThat(response.getStatusCode().value() == 404).isTrue(); - } - - @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ƒํ’ˆ ID๋กœ ์ฃผ๋ฌธ ์ƒ์„ฑ์„ ์‹œ๋„ํ•  ๊ฒฝ์šฐ, `404 Not Found` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnNotFound_whenProductIdDoesNotExist() { - // arrange - Long nonExistentProductId = 99999L; - OrderRequest request = new OrderRequest( - List.of( - new OrderItemRequest(nonExistentProductId, 1) - ) - ); - HttpHeaders headers = createHeaders(); - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - ResponseEntity> response = testRestTemplate.exchange( - ENDPOINT_ORDERS, HttpMethod.POST, new HttpEntity<>(request, headers), responseType); - - // assert - assertThat(response.getStatusCode().value() == 404).isTrue(); - } - - @DisplayName("์—ฌ๋Ÿฌ ์ƒํ’ˆ ์ค‘ ์ผ๋ถ€๋งŒ ์žฌ๊ณ  ๋ถ€์กฑ ์‹œ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnBadRequest_whenPartialStockInsufficient() { - // arrange - // productId1: ์žฌ๊ณ  100, productId2: ์žฌ๊ณ  50 - // productId1์€ ์ถฉ๋ถ„ํ•˜์ง€๋งŒ productId2๋Š” ๋ถ€์กฑ - OrderRequest request = new OrderRequest( - List.of( - new OrderItemRequest(productId1, 10), // ์žฌ๊ณ  ์ถฉ๋ถ„ - new OrderItemRequest(productId2, 99999) // ์žฌ๊ณ  ๋ถ€์กฑ - ) - ); - HttpHeaders headers = createHeaders(); - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - ResponseEntity> response = testRestTemplate.exchange( - ENDPOINT_ORDERS, HttpMethod.POST, new HttpEntity<>(request, headers), responseType); - - // assert - assertThat(response.getStatusCode().value()).isEqualTo(400); - // Note: ํ˜„์žฌ ๊ตฌํ˜„์—์„œ๋Š” ์ฒซ ๋ฒˆ์งธ ์žฌ๊ณ  ๋ถ€์กฑ ์ƒํ’ˆ์—์„œ๋งŒ ์˜ˆ์™ธ ๋ฐœ์ƒ - // ๊ฐœ์„  ํ›„์—๋Š” ๋ชจ๋“  ๋ถ€์กฑํ•œ ์ƒํ’ˆ์„ ํ•œ ๋ฒˆ์— ์•Œ๋ ค์ค„ ์ˆ˜ ์žˆ์Œ - } - - @DisplayName("์—ฌ๋Ÿฌ ์ƒํ’ˆ ๋ชจ๋‘ ์žฌ๊ณ  ๋ถ€์กฑ ์‹œ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnBadRequest_whenAllProductsStockInsufficient() { - // arrange - OrderRequest request = new OrderRequest( - List.of( - new OrderItemRequest(productId1, 99999), - new OrderItemRequest(productId2, 99999) - ) - ); - HttpHeaders headers = createHeaders(); - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - ResponseEntity> response = testRestTemplate.exchange( - ENDPOINT_ORDERS, HttpMethod.POST, new HttpEntity<>(request, headers), responseType); - - // assert - assertThat(response.getStatusCode().value()).isEqualTo(400); - // Note: ํ˜„์žฌ ๊ตฌํ˜„์—์„œ๋Š” ์ฒซ ๋ฒˆ์งธ ์žฌ๊ณ  ๋ถ€์กฑ ์ƒํ’ˆ์—์„œ๋งŒ ์˜ˆ์™ธ ๋ฐœ์ƒ - } - - @DisplayName("ํฌ์ธํŠธ๊ฐ€ ์ •ํ™•ํžˆ ์ผ์น˜ํ•  ๊ฒฝ์šฐ, ์ฃผ๋ฌธ์ด ์„ฑ๊ณตํ•œ๋‹ค.") - @Test - void returnOrderInfo_whenPointExactlyMatches() { - // arrange - // ํฌ์ธํŠธ๋ฅผ ๊ฑฐ์˜ ๋ชจ๋‘ ์‚ฌ์šฉ - HttpHeaders headers = createHeaders(); - OrderRequest firstOrder = new OrderRequest( - List.of( - new OrderItemRequest(productId1, 9) // 90000์› ์‚ฌ์šฉ - ) - ); - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - testRestTemplate.exchange(ENDPOINT_ORDERS, HttpMethod.POST, new HttpEntity<>(firstOrder, headers), responseType); - // ๋‚จ์€ ํฌ์ธํŠธ: 10000์› - - // ์ •ํ™•ํžˆ ์ผ์น˜ํ•˜๋Š” ์ฃผ๋ฌธ - OrderRequest request = new OrderRequest( - List.of( - new OrderItemRequest(productId1, 1) // ์ •ํ™•ํžˆ 10000์› - ) - ); - - // act - ResponseEntity> response = testRestTemplate.exchange( - ENDPOINT_ORDERS, HttpMethod.POST, new HttpEntity<>(request, headers), responseType); - - // assert - assertAll( - () -> assertTrue(response.getStatusCode().is2xxSuccessful()), - () -> assertThat(response.getBody()).isNotNull(), - () -> assertThat(response.getBody().data()).isNotNull(), - () -> assertThat(response.getBody().data().totalPrice()).isEqualTo(10000) - ); - } - - @DisplayName("์ค‘๋ณต ์ƒํ’ˆ์ด ํฌํ•จ๋œ ๊ฒฝ์šฐ, `500 Internal Server Error` ๋˜๋Š” `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnError_whenDuplicateProducts() { - // arrange - // ๊ฐ™์€ ์ƒํ’ˆ์„ ์—ฌ๋Ÿฌ ๋ฒˆ ์ฃผ๋ฌธ ํ•ญ๋ชฉ์— ํฌํ•จ - // Note: Collectors.toMap()์€ ์ค‘๋ณต ํ‚ค๊ฐ€ ์žˆ์œผ๋ฉด IllegalStateException์„ ๋ฐœ์ƒ์‹œํ‚ด - OrderRequest request = new OrderRequest( - List.of( - new OrderItemRequest(productId1, 2), - new OrderItemRequest(productId1, 3) // ์ค‘๋ณต - ) - ); - HttpHeaders headers = createHeaders(); - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - ResponseEntity> response = testRestTemplate.exchange( - ENDPOINT_ORDERS, HttpMethod.POST, new HttpEntity<>(request, headers), responseType); - - // assert - // Note: Collectors.toMap()์—์„œ ์ค‘๋ณต ํ‚ค๋กœ ์ธํ•ด IllegalStateException ๋ฐœ์ƒ - // ์ด๋Š” 500 Internal Server Error๋กœ ๋ณ€ํ™˜๋˜๊ฑฐ๋‚˜, 400 Bad Request๋กœ ์ฒ˜๋ฆฌ๋  ์ˆ˜ ์žˆ์Œ - assertThat(response.getStatusCode().value() == 400 || response.getStatusCode().value() == 500).isTrue(); - } - - @DisplayName("Point ์—”ํ‹ฐํ‹ฐ๊ฐ€ ์—†๋Š” ์‚ฌ์šฉ์ž๋กœ ์ฃผ๋ฌธ ์‹œ๋„ ์‹œ, `404 Not Found` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•˜๊ณ  ์žฌ๊ณ ๊ฐ€ ๋กค๋ฐฑ๋œ๋‹ค.") - @Test - void returnNotFoundAndRollbackStock_whenPointDoesNotExist() { - // arrange - // Point๊ฐ€ ์—†๋Š” ์‚ฌ์šฉ์ž ์ƒ์„ฑ - String userWithoutPointId = "userWithoutPoint"; - UserV1Dto.UserRegisterRequest userRequest = new UserV1Dto.UserRegisterRequest( - userWithoutPointId, - "test2@test.com", - "1993-03-13", - "male" - ); - ParameterizedTypeReference> userResponseType = new ParameterizedTypeReference<>() { - }; - testRestTemplate.exchange(ENDPOINT_USER, HttpMethod.POST, new HttpEntity<>(userRequest), userResponseType); - // Point๋Š” ์ƒ์„ฑํ•˜์ง€ ์•Š์Œ - - // ์ดˆ๊ธฐ ์žฌ๊ณ  ํ™•์ธ - Supply initialSupply = supplyJpaRepository.findByProductId(productId1).orElseThrow(); - int initialStock = initialSupply.getStock().quantity(); - - OrderRequest request = new OrderRequest( - List.of( - new OrderItemRequest(productId1, 1) - ) - ); - HttpHeaders headers = new HttpHeaders(); - headers.add("X-USER-ID", userWithoutPointId); - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - ResponseEntity> response = testRestTemplate.exchange( - ENDPOINT_ORDERS, HttpMethod.POST, new HttpEntity<>(request, headers), responseType); - - // assert - assertThat(response.getStatusCode().value()).isEqualTo(404); - // ์žฌ๊ณ ๊ฐ€ ๋กค๋ฐฑ๋˜์–ด ์›๋ž˜๋Œ€๋กœ ๋ณต๊ตฌ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ - Supply afterRollbackSupply = supplyJpaRepository.findByProductId(productId1).orElseThrow(); - int afterStock = afterRollbackSupply.getStock().quantity(); - // ์žฌ๊ณ ๊ฐ€ ์›๋ž˜๋Œ€๋กœ ๋ณต๊ตฌ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ (์ดˆ๊ธฐ ์žฌ๊ณ ์™€ ๋™์ผํ•ด์•ผ ํ•จ) - assertThat(afterStock).isEqualTo(initialStock); - } - - @DisplayName("์žฌ๊ณ  ์ฐจ๊ฐ ํ›„ ํฌ์ธํŠธ ๋ถ€์กฑ ์‹œ, ๋กค๋ฐฑ๋˜์–ด ์žฌ๊ณ ๊ฐ€ ๋ณต๊ตฌ๋œ๋‹ค.") - @Test - void should_rollbackStock_whenPointInsufficientAfterStockDecrease() { - // arrange - // ์ดˆ๊ธฐ ์žฌ๊ณ  ํ™•์ธ - Supply initialSupply = supplyJpaRepository.findByProductId(productId1).orElseThrow(); - int initialStock = initialSupply.getStock().quantity(); - - // ํฌ์ธํŠธ๋ฅผ ๊ฑฐ์˜ ๋ชจ๋‘ ์‚ฌ์šฉ - HttpHeaders headers = createHeaders(); - OrderRequest firstOrder = new OrderRequest( - List.of( - new OrderItemRequest(productId1, 9) // 90000์› ์‚ฌ์šฉ - ) - ); - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - testRestTemplate.exchange(ENDPOINT_ORDERS, HttpMethod.POST, new HttpEntity<>(firstOrder, headers), responseType); - // ๋‚จ์€ ํฌ์ธํŠธ: 10000์› - - // ํฌ์ธํŠธ ๋ถ€์กฑํ•œ ์ฃผ๋ฌธ ์‹œ๋„ (์žฌ๊ณ ๋Š” ์ถฉ๋ถ„) - OrderRequest request = new OrderRequest( - List.of( - new OrderItemRequest(productId1, 2) // 20000์› ํ•„์š” (๋ถ€์กฑ) - ) - ); - - // act - ResponseEntity> response = testRestTemplate.exchange( - ENDPOINT_ORDERS, HttpMethod.POST, new HttpEntity<>(request, headers), responseType); - - // assert - assertThat(response.getStatusCode().value()).isEqualTo(400); - // ์žฌ๊ณ ๊ฐ€ ๋กค๋ฐฑ๋˜์–ด ์›๋ž˜๋Œ€๋กœ ๋ณต๊ตฌ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ - Supply afterRollbackSupply = supplyJpaRepository.findByProductId(productId1).orElseThrow(); - int afterStock = afterRollbackSupply.getStock().quantity(); - // ์ฒซ ์ฃผ๋ฌธ์—์„œ 9๊ฐœ ์ฐจ๊ฐ๋˜์—ˆ์œผ๋ฏ€๋กœ, ์ดˆ๊ธฐ ์žฌ๊ณ  - 9 = ํ˜„์žฌ ์žฌ๊ณ ์—ฌ์•ผ ํ•จ - assertThat(afterStock).isEqualTo(initialStock - 9); - } - - @DisplayName("๋ถ€๋ถ„ ์žฌ๊ณ  ๋ถ€์กฑ ์‹œ, ๋กค๋ฐฑ๋˜์–ด ์žฌ๊ณ ๊ฐ€ ๋ณต๊ตฌ๋œ๋‹ค.") - @Test - void should_rollbackStock_whenPartialStockInsufficient() { - // arrange - // ์ดˆ๊ธฐ ์žฌ๊ณ  ํ™•์ธ - Supply initialSupply1 = supplyJpaRepository.findByProductId(productId1).orElseThrow(); - int initialStock1 = initialSupply1.getStock().quantity(); - Supply initialSupply2 = supplyJpaRepository.findByProductId(productId2).orElseThrow(); - int initialStock2 = initialSupply2.getStock().quantity(); - - // productId1์€ ์ถฉ๋ถ„ํ•˜์ง€๋งŒ productId2๋Š” ๋ถ€์กฑํ•œ ์ฃผ๋ฌธ - OrderRequest request = new OrderRequest( - List.of( - new OrderItemRequest(productId1, 10), // ์žฌ๊ณ  ์ถฉ๋ถ„ - new OrderItemRequest(productId2, 99999) // ์žฌ๊ณ  ๋ถ€์กฑ - ) - ); - HttpHeaders headers = createHeaders(); - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - ResponseEntity> response = testRestTemplate.exchange( - ENDPOINT_ORDERS, HttpMethod.POST, new HttpEntity<>(request, headers), responseType); - - // assert - assertThat(response.getStatusCode().value()).isEqualTo(400); - // ๋ชจ๋“  ์žฌ๊ณ ๊ฐ€ ๋กค๋ฐฑ๋˜์–ด ์›๋ž˜๋Œ€๋กœ ๋ณต๊ตฌ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ - Supply afterRollbackSupply1 = supplyJpaRepository.findByProductId(productId1).orElseThrow(); - int afterStock1 = afterRollbackSupply1.getStock().quantity(); - Supply afterRollbackSupply2 = supplyJpaRepository.findByProductId(productId2).orElseThrow(); - int afterStock2 = afterRollbackSupply2.getStock().quantity(); - - assertThat(afterStock1).isEqualTo(initialStock1); - assertThat(afterStock2).isEqualTo(initialStock2); - } - } - - @DisplayName("GET /api/v1/orders") - @Nested - class GetOrderList { - @DisplayName("๋กœ๊ทธ์ธํ•œ ์œ ์ €๊ฐ€ ์ฃผ๋ฌธ ๋ชฉ๋ก ์กฐํšŒ์— ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ์ฃผ๋ฌธ ๋ชฉ๋ก์„ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnOrderList_whenGetOrderListSuccess() { - // arrange - HttpHeaders headers = createHeaders(); - // ์ฃผ๋ฌธ ์ƒ์„ฑ - OrderRequest orderRequest = new OrderRequest( - List.of( - new OrderItemRequest(productId1, 1) - ) - ); - ParameterizedTypeReference> orderResponseType = new ParameterizedTypeReference<>() { - }; - testRestTemplate.exchange(ENDPOINT_ORDERS, HttpMethod.POST, new HttpEntity<>(orderRequest, headers), orderResponseType); - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - ResponseEntity> response = testRestTemplate.exchange( - ENDPOINT_ORDERS, HttpMethod.GET, new HttpEntity<>(null, headers), responseType); - - // assert - // Note: ํ˜„์žฌ todo ์ƒํƒœ์ด์ง€๋งŒ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๋Š” ์ž‘์„ฑํ•จ - assertThat(response.getStatusCode().is2xxSuccessful()).isTrue(); - } - - @DisplayName("๋น„๋กœ๊ทธ์ธ ์œ ์ €๊ฐ€ ์ฃผ๋ฌธ ๋ชฉ๋ก ์กฐํšŒ๋ฅผ ์‹œ๋„ํ•  ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnBadRequest_whenXUserIdHeaderIsMissing() { - // arrange - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - ResponseEntity> response = testRestTemplate.exchange( - ENDPOINT_ORDERS, HttpMethod.GET, new HttpEntity<>(null, null), responseType); - - // assert - assertThat(response.getStatusCode().value()).isEqualTo(400); - } - - @DisplayName("X-USER-ID ํ—ค๋”๊ฐ€ ๋นˆ ๋ฌธ์ž์—ด์ผ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnBadRequest_whenXUserIdHeaderIsEmpty() { - // arrange - HttpHeaders headers = new HttpHeaders(); - headers.add("X-USER-ID", ""); - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - ResponseEntity> response = testRestTemplate.exchange( - ENDPOINT_ORDERS, HttpMethod.GET, new HttpEntity<>(null, headers), responseType); - - // assert - assertThat(response.getStatusCode().value()).isEqualTo(400); - } - - @DisplayName("X-USER-ID ํ—ค๋”๊ฐ€ ๊ณต๋ฐฑ๋งŒ ์žˆ์„ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnBadRequest_whenXUserIdHeaderIsBlank() { - // arrange - HttpHeaders headers = new HttpHeaders(); - headers.add("X-USER-ID", " "); - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - ResponseEntity> response = testRestTemplate.exchange( - ENDPOINT_ORDERS, HttpMethod.GET, new HttpEntity<>(null, headers), responseType); - - // assert - assertThat(response.getStatusCode().value()).isEqualTo(400); - } - - @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ์ € ID๋กœ ์ฃผ๋ฌธ ๋ชฉ๋ก ์กฐํšŒ๋ฅผ ์‹œ๋„ํ•  ๊ฒฝ์šฐ, `404 Not Found` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnNotFound_whenUserIdDoesNotExist() { - // arrange - HttpHeaders headers = new HttpHeaders(); - headers.add("X-USER-ID", "nonexist"); - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - ResponseEntity> response = testRestTemplate.exchange( - ENDPOINT_ORDERS, HttpMethod.GET, new HttpEntity<>(null, headers), responseType); - - // assert - assertThat(response.getStatusCode().value() == 404).isTrue(); - } - } - - @DisplayName("GET /api/v1/orders/{orderId}") - @Nested - class GetOrderDetail { - private Long orderId; - - @BeforeEach - void setupOrder() { - // ์ฃผ๋ฌธ ์ƒ์„ฑ - HttpHeaders headers = createHeaders(); - OrderRequest orderRequest = new OrderRequest( - List.of( - new OrderItemRequest(productId1, 1) - ) - ); - ParameterizedTypeReference> orderResponseType = new ParameterizedTypeReference<>() { - }; - ResponseEntity> orderResponse = testRestTemplate.exchange( - ENDPOINT_ORDERS, HttpMethod.POST, new HttpEntity<>(orderRequest, headers), orderResponseType); - if (orderResponse.getStatusCode().is2xxSuccessful() && orderResponse.getBody() != null) { - orderId = orderResponse.getBody().data().orderId(); - } else { - orderId = 1L; // fallback - } - } - - @DisplayName("๋กœ๊ทธ์ธํ•œ ์œ ์ €๊ฐ€ ์กด์žฌํ•˜๋Š” ์ฃผ๋ฌธ ID๋กœ ์กฐํšŒํ•  ๊ฒฝ์šฐ, ์ฃผ๋ฌธ ์ƒ์„ธ ์ •๋ณด๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnOrderDetail_whenOrderExists() { - // arrange - String url = ENDPOINT_ORDERS + "/" + orderId; - HttpHeaders headers = createHeaders(); - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - ResponseEntity> response = testRestTemplate.exchange( - url, HttpMethod.GET, new HttpEntity<>(null, headers), responseType); - - // assert - // Note: ํ˜„์žฌ ์‹คํŒจํ•  ์ˆ˜ ์žˆ์ง€๋งŒ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๋Š” ์ž‘์„ฑํ•จ - assertThat(response.getStatusCode().is2xxSuccessful() || response.getStatusCode().value() == 404).isTrue(); - } - - @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ฃผ๋ฌธ ID๋กœ ์กฐํšŒํ•  ๊ฒฝ์šฐ, `404 Not Found` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnNotFound_whenOrderDoesNotExist() { - // arrange - Long nonExistentOrderId = 99999L; - String url = ENDPOINT_ORDERS + "/" + nonExistentOrderId; - HttpHeaders headers = createHeaders(); - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - ResponseEntity> response = testRestTemplate.exchange( - url, HttpMethod.GET, new HttpEntity<>(null, headers), responseType); - - // assert - assertThat(response.getStatusCode().value()).isEqualTo(404); - } - - @DisplayName("๋น„๋กœ๊ทธ์ธ ์œ ์ €๊ฐ€ ์ฃผ๋ฌธ ์ƒ์„ธ ์กฐํšŒ๋ฅผ ์‹œ๋„ํ•  ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnBadRequest_whenXUserIdHeaderIsMissing() { - // arrange - String url = ENDPOINT_ORDERS + "/" + orderId; - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - ResponseEntity> response = testRestTemplate.exchange( - url, HttpMethod.GET, new HttpEntity<>(null, null), responseType); - - // assert - assertThat(response.getStatusCode().value()).isEqualTo(400); - } - - @DisplayName("X-USER-ID ํ—ค๋”๊ฐ€ ๋นˆ ๋ฌธ์ž์—ด์ผ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnBadRequest_whenXUserIdHeaderIsEmpty() { - // arrange - String url = ENDPOINT_ORDERS + "/" + orderId; - HttpHeaders headers = new HttpHeaders(); - headers.add("X-USER-ID", ""); - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - ResponseEntity> response = testRestTemplate.exchange( - url, HttpMethod.GET, new HttpEntity<>(null, headers), responseType); - - // assert - assertThat(response.getStatusCode().value()).isEqualTo(400); - } - - @DisplayName("X-USER-ID ํ—ค๋”๊ฐ€ ๊ณต๋ฐฑ๋งŒ ์žˆ์„ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnBadRequest_whenXUserIdHeaderIsBlank() { - // arrange - String url = ENDPOINT_ORDERS + "/" + orderId; - HttpHeaders headers = new HttpHeaders(); - headers.add("X-USER-ID", " "); - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - ResponseEntity> response = testRestTemplate.exchange( - url, HttpMethod.GET, new HttpEntity<>(null, headers), responseType); - - // assert - assertThat(response.getStatusCode().value()).isEqualTo(400); - } - - @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ์ € ID๋กœ ์ฃผ๋ฌธ ์ƒ์„ธ ์กฐํšŒ๋ฅผ ์‹œ๋„ํ•  ๊ฒฝ์šฐ, `404 Not Found` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnNotFound_whenUserIdDoesNotExist() { - // arrange - String url = ENDPOINT_ORDERS + "/" + orderId; - HttpHeaders headers = new HttpHeaders(); - headers.add("X-USER-ID", "nonexist"); - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - ResponseEntity> response = testRestTemplate.exchange( - url, HttpMethod.GET, new HttpEntity<>(null, headers), responseType); - - // assert - // Note: ํ˜„์žฌ ์‹คํŒจํ•  ์ˆ˜ ์žˆ์ง€๋งŒ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๋Š” ์ž‘์„ฑํ•จ - assertThat(response.getStatusCode().value() == 400 || response.getStatusCode().value() == 404).isTrue(); - } - } -} diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/PointV1ApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/PointV1ApiE2ETest.java deleted file mode 100644 index fdca89097..000000000 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/PointV1ApiE2ETest.java +++ /dev/null @@ -1,246 +0,0 @@ -package com.loopers.interfaces.api; - -import com.loopers.interfaces.api.point.PointV1Dto; -import com.loopers.interfaces.api.user.UserV1Dto; -import com.loopers.utils.DatabaseCleanUp; -import org.junit.jupiter.api.*; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.web.client.TestRestTemplate; -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.ResponseEntity; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertAll; -import static org.junit.jupiter.api.Assertions.assertTrue; - -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -public class PointV1ApiE2ETest { - private final String ENDPOINT_USER = "/api/v1/users"; - private final String ENDPOINT_POINT = "/api/v1/points"; - - private final TestRestTemplate testRestTemplate; - private final DatabaseCleanUp databaseCleanUp; - - @Autowired - public PointV1ApiE2ETest( - TestRestTemplate testRestTemplate, - DatabaseCleanUp databaseCleanUp - ) { - this.testRestTemplate = testRestTemplate; - this.databaseCleanUp = databaseCleanUp; - } - - @AfterEach - void tearDown() { - databaseCleanUp.truncateAllTables(); - } - - private final String validUserId = "user123"; - private final String validEmail = "xx@yy.zz"; - private final String validBirthday = "1993-03-13"; - private final String validGender = "male"; - - @BeforeEach - void setupUser() { - UserV1Dto.UserRegisterRequest request = new UserV1Dto.UserRegisterRequest( - validUserId, - validEmail, - validBirthday, - validGender - ); - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - testRestTemplate.exchange(ENDPOINT_USER, HttpMethod.POST, new HttpEntity<>(request), responseType); - } - - @DisplayName("GET /api/v1/points") - @Nested - class GetPoints { - @DisplayName("๋กœ๊ทธ์ธํ•œ ์œ ์ €๊ฐ€ ํฌ์ธํŠธ ์กฐํšŒ์— ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ๋ณด์œ  ํฌ์ธํŠธ๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnUserPoints_whenGetUserPointsSuccess() { - // arrange - HttpHeaders headers = new HttpHeaders(); - headers.add("X-USER-ID", validUserId); - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - HttpEntity requestEntity = new HttpEntity<>(null, headers); - ResponseEntity> response = testRestTemplate.exchange(ENDPOINT_POINT, HttpMethod.GET, requestEntity, responseType); - - // assert - assertAll( - () -> assertTrue(response.getStatusCode().is2xxSuccessful()), - () -> assertThat(response.getBody().data().currentPoint()).isEqualTo(0L) - ); - } - - @DisplayName("๋น„๋กœ๊ทธ์ธ ์œ ์ €๊ฐ€ ํฌ์ธํŠธ ์กฐํšŒ๋ฅผ ์‹œ๋„ํ•  ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnBadRequest_whenXUserIdHeaderIsMissing() { - // arrange - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - HttpEntity requestEntity = new HttpEntity<>(null, null); - ResponseEntity> response = testRestTemplate.exchange(ENDPOINT_POINT, HttpMethod.GET, requestEntity, responseType); - - // assert - assertThat(response.getStatusCode().value()).isEqualTo(400); - } - - @DisplayName("X-USER-ID ํ—ค๋”๊ฐ€ ๋นˆ ๋ฌธ์ž์—ด์ผ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnBadRequest_whenXUserIdHeaderIsEmpty() { - // arrange - HttpHeaders headers = new HttpHeaders(); - headers.add("X-USER-ID", ""); - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - HttpEntity requestEntity = new HttpEntity<>(null, headers); - ResponseEntity> response = testRestTemplate.exchange(ENDPOINT_POINT, HttpMethod.GET, requestEntity, responseType); - - // assert - assertThat(response.getStatusCode().value()).isEqualTo(400); - } - - @DisplayName("X-USER-ID ํ—ค๋”๊ฐ€ ๊ณต๋ฐฑ๋งŒ ์žˆ์„ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnBadRequest_whenXUserIdHeaderIsBlank() { - // arrange - HttpHeaders headers = new HttpHeaders(); - headers.add("X-USER-ID", " "); - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - HttpEntity requestEntity = new HttpEntity<>(null, headers); - ResponseEntity> response = testRestTemplate.exchange(ENDPOINT_POINT, HttpMethod.GET, requestEntity, responseType); - - // assert - assertThat(response.getStatusCode().value()).isEqualTo(400); - } - - @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ์ € ID๋กœ ์กฐํšŒํ•  ๊ฒฝ์šฐ, `404 Not Found` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnNotFound_whenUserIdDoesNotExist() { - // arrange - String invalidUserId = "nonexist"; - HttpHeaders headers = new HttpHeaders(); - headers.add("X-USER-ID", invalidUserId); - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - HttpEntity requestEntity = new HttpEntity<>(null, headers); - ResponseEntity> response = testRestTemplate.exchange(ENDPOINT_POINT, HttpMethod.GET, requestEntity, responseType); - - // assert - assertThat(response.getStatusCode().value()).isEqualTo(404); - } - } - - @DisplayName("POST /api/v1/points/charge") - @Nested - class ChargePoints { - @DisplayName("๋กœ๊ทธ์ธํ•œ ์œ ์ €๊ฐ€ ํฌ์ธํŠธ ์ถฉ์ „์— ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ์ถฉ์ „๋œ ๋ณด์œ  ์ด๋Ÿ‰์„ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnChargedPoints_whenChargeUserPointsSuccess() { - // arrange - HttpHeaders headers = new HttpHeaders(); - headers.add("X-USER-ID", validUserId); - PointV1Dto.PointChargeRequest request = new PointV1Dto.PointChargeRequest(1000); - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - HttpEntity requestEntity = new HttpEntity<>(request, headers); - ResponseEntity> response = testRestTemplate.exchange(ENDPOINT_POINT + "/charge", HttpMethod.POST, requestEntity, responseType); - - // assert - assertAll( - () -> assertTrue(response.getStatusCode().is2xxSuccessful()), - () -> assertThat(response.getBody().data().currentPoint()).isEqualTo(1000L) - ); - } - - @DisplayName("๋น„๋กœ๊ทธ์ธ ์œ ์ €๊ฐ€ ํฌ์ธํŠธ ์ถฉ์ „์„ ์‹œ๋„ํ•  ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnBadRequest_whenXUserIdHeaderIsMissing() { - // arrange - PointV1Dto.PointChargeRequest request = new PointV1Dto.PointChargeRequest(1000); - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - HttpEntity requestEntity = new HttpEntity<>(request, null); - ResponseEntity> response = testRestTemplate.exchange(ENDPOINT_POINT + "/charge", HttpMethod.POST, requestEntity, responseType); - - // assert - assertThat(response.getStatusCode().value()).isEqualTo(400); - } - - @DisplayName("X-USER-ID ํ—ค๋”๊ฐ€ ๋นˆ ๋ฌธ์ž์—ด์ผ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnBadRequest_whenXUserIdHeaderIsEmpty() { - // arrange - HttpHeaders headers = new HttpHeaders(); - headers.add("X-USER-ID", ""); - PointV1Dto.PointChargeRequest request = new PointV1Dto.PointChargeRequest(1000); - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - HttpEntity requestEntity = new HttpEntity<>(request, headers); - ResponseEntity> response = testRestTemplate.exchange(ENDPOINT_POINT + "/charge", HttpMethod.POST, requestEntity, responseType); - - // assert - assertThat(response.getStatusCode().value()).isEqualTo(400); - } - - @DisplayName("X-USER-ID ํ—ค๋”๊ฐ€ ๊ณต๋ฐฑ๋งŒ ์žˆ์„ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnBadRequest_whenXUserIdHeaderIsBlank() { - // arrange - HttpHeaders headers = new HttpHeaders(); - headers.add("X-USER-ID", " "); - PointV1Dto.PointChargeRequest request = new PointV1Dto.PointChargeRequest(1000); - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - HttpEntity requestEntity = new HttpEntity<>(request, headers); - ResponseEntity> response = testRestTemplate.exchange(ENDPOINT_POINT + "/charge", HttpMethod.POST, requestEntity, responseType); - - // assert - assertThat(response.getStatusCode().value()).isEqualTo(400); - } - - @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ์ €๋กœ ์š”์ฒญํ•  ๊ฒฝ์šฐ, `404 Not Found` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnNotFound_whenChargePointsForNonExistentUser() { - // arrange - String invalidUserId = "nonexist"; - HttpHeaders headers = new HttpHeaders(); - headers.add("X-USER-ID", invalidUserId); - PointV1Dto.PointChargeRequest request = new PointV1Dto.PointChargeRequest(1000); - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - HttpEntity requestEntity = new HttpEntity<>(request, headers); - ResponseEntity> response = testRestTemplate.exchange(ENDPOINT_POINT + "/charge", HttpMethod.POST, requestEntity, responseType); - - // assert - assertThat(response.getStatusCode().value()).isEqualTo(404); - } - } -} diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/ProductV1ApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/ProductV1ApiE2ETest.java deleted file mode 100644 index 55c80c579..000000000 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/ProductV1ApiE2ETest.java +++ /dev/null @@ -1,265 +0,0 @@ -package com.loopers.interfaces.api; - -import com.loopers.domain.brand.Brand; -import com.loopers.domain.common.vo.Price; -import com.loopers.domain.metrics.product.ProductMetrics; -import com.loopers.domain.product.Product; -import com.loopers.domain.supply.Supply; -import com.loopers.domain.supply.SupplyService; -import com.loopers.domain.supply.vo.Stock; -import com.loopers.infrastructure.brand.BrandJpaRepository; -import com.loopers.infrastructure.metrics.product.ProductMetricsJpaRepository; -import com.loopers.infrastructure.product.ProductJpaRepository; -import com.loopers.interfaces.api.product.ProductV1Dto; -import com.loopers.utils.DatabaseCleanUp; -import org.junit.jupiter.api.*; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.web.client.TestRestTemplate; -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.ResponseEntity; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertAll; -import static org.junit.jupiter.api.Assertions.assertTrue; - -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -public class ProductV1ApiE2ETest { - - private final String ENDPOINT_PRODUCTS = "/api/v1/products"; - - private final TestRestTemplate testRestTemplate; - private final DatabaseCleanUp databaseCleanUp; - private final BrandJpaRepository brandJpaRepository; - private final ProductJpaRepository productJpaRepository; - private final ProductMetricsJpaRepository productMetricsJpaRepository; -private final SupplyService supplyService; - - @Autowired - public ProductV1ApiE2ETest( - TestRestTemplate testRestTemplate, - DatabaseCleanUp databaseCleanUp, - BrandJpaRepository brandJpaRepository, - ProductJpaRepository productJpaRepository, - ProductMetricsJpaRepository productMetricsJpaRepository, - SupplyService supplyService - ) { - this.testRestTemplate = testRestTemplate; - this.databaseCleanUp = databaseCleanUp; - this.brandJpaRepository = brandJpaRepository; - this.productJpaRepository = productJpaRepository; - this.productMetricsJpaRepository = productMetricsJpaRepository; - this.supplyService = supplyService; - } - - @AfterEach - void tearDown() { - databaseCleanUp.truncateAllTables(); - } - - private Long brandId; - private Long productId1; - private Long productId2; - - @BeforeEach - void setupProducts() { - // Brand ๋“ฑ๋ก - Brand brand = Brand.create("Nike"); - Brand savedBrand = brandJpaRepository.save(brand); - brandId = savedBrand.getId(); - - // Product ๋“ฑ๋ก - Product product1 = createProduct("์ƒํ’ˆ1", brandId, 10000); - Product savedProduct1 = productJpaRepository.save(product1); - productId1 = savedProduct1.getId(); - // ProductMetrics ๋“ฑ๋ก - ProductMetrics metrics1 = ProductMetrics.create(productId1, 0); - productMetricsJpaRepository.save(metrics1); - // Supply ๋“ฑ๋ก - Supply supply1 = Supply.create(productId1, new Stock(10)); - supplyService.saveSupply(supply1); - - Product product2 = createProduct("์ƒํ’ˆ2", brandId, 20000); - Product savedProduct2 = productJpaRepository.save(product2); - productId2 = savedProduct2.getId(); - // ProductMetrics ๋“ฑ๋ก - ProductMetrics metrics2 = ProductMetrics.create(productId2, 0); - productMetricsJpaRepository.save(metrics2); - // Supply ๋“ฑ๋ก - Supply supply2 = Supply.create(productId2, new Stock(20)); - supplyService.saveSupply(supply2); - } - - private Product createProduct(String name, Long brandId, int priceAmount) { - return Product.create(name, brandId, new Price(priceAmount)); - } - - @DisplayName("GET /api/v1/products") - @Nested - class GetProductList { - @DisplayName("๋น„๋กœ๊ทธ์ธ ์œ ์ €๊ฐ€ ์ƒํ’ˆ ๋ชฉ๋ก ์กฐํšŒ์— ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ์ƒํ’ˆ ๋ชฉ๋ก์„ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnProductList_whenGetProductListSuccess() { - // arrange - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - ResponseEntity> response = testRestTemplate.exchange( - ENDPOINT_PRODUCTS, HttpMethod.GET, new HttpEntity<>(null), responseType); - - // assert - assertAll( - () -> assertTrue(response.getStatusCode().is2xxSuccessful()), - () -> assertThat(response.getBody()).isNotNull(), - () -> assertThat(response.getBody().data()).isNotNull(), - () -> assertThat(response.getBody().data().content()).isNotNull(), - () -> assertThat(response.getBody().data().size()).isGreaterThanOrEqualTo(2) - ); - } - - @DisplayName("๋กœ๊ทธ์ธํ•œ ์œ ์ €๊ฐ€ ์ƒํ’ˆ ๋ชฉ๋ก ์กฐํšŒ์— ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ์ƒํ’ˆ ๋ชฉ๋ก์„ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnProductList_whenLoggedInUser() { - // arrange - HttpHeaders headers = new HttpHeaders(); - headers.add("X-USER-ID", "user123"); - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - ResponseEntity> response = testRestTemplate.exchange( - ENDPOINT_PRODUCTS, HttpMethod.GET, new HttpEntity<>(null, headers), responseType); - - // assert - assertAll( - () -> assertTrue(response.getStatusCode().is2xxSuccessful()), - () -> assertThat(response.getBody()).isNotNull(), - () -> assertThat(response.getBody().data()).isNotNull(), - () -> assertThat(response.getBody().data().content()).isNotNull(), - () -> assertThat(response.getBody().data().size()).isGreaterThanOrEqualTo(2) - ); - } - - @DisplayName("ํŽ˜์ด์ง€๋„ค์ด์…˜ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ์กฐํšŒํ•  ๊ฒฝ์šฐ, ํ•ด๋‹น ํŽ˜์ด์ง€์˜ ์ƒํ’ˆ ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnProductList_whenWithPagination() { - // arrange - String url = ENDPOINT_PRODUCTS + "?page=0&size=10"; - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - ResponseEntity> response = testRestTemplate.exchange( - url, HttpMethod.GET, new HttpEntity<>(null), responseType); - - // assert - assertAll( - () -> assertTrue(response.getStatusCode().is2xxSuccessful()), - () -> assertThat(response.getBody()).isNotNull(), - () -> assertThat(response.getBody().data()).isNotNull() - ); - } - - @DisplayName("๊ฐ€๊ฒฉ ์˜ค๋ฆ„์ฐจ์ˆœ ์ •๋ ฌ๋กœ ์กฐํšŒํ•  ๊ฒฝ์šฐ, ๊ฐ€๊ฒฉ์ด ๋‚ฎ์€ ์ˆœ์œผ๋กœ ์ƒํ’ˆ ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnProductList_whenSortedByPriceAsc() { - // arrange - String url = ENDPOINT_PRODUCTS + "?sort=price_asc"; - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - ResponseEntity> response = testRestTemplate.exchange( - url, HttpMethod.GET, new HttpEntity<>(null), responseType); - - // assert - assertAll( - () -> assertTrue(response.getStatusCode().is2xxSuccessful()), - () -> assertThat(response.getBody()).isNotNull(), - () -> assertThat(response.getBody().data()).isNotNull(), - // ๊ฐ€๊ฒฉ ์˜ค๋ฆ„์ฐจ์ˆœ ์ •๋ ฌ ๊ฒ€์ฆ - () -> { - var products = response.getBody().data().content(); - for (int i = 0; i < products.size() - 1; i++) { - assertThat(products.get(i).price()).isLessThanOrEqualTo(products.get(i + 1).price()); - } - } - ); - } - - @DisplayName("์ข‹์•„์š” ๋‚ด๋ฆผ์ฐจ์ˆœ ์ •๋ ฌ๋กœ ์กฐํšŒํ•  ๊ฒฝ์šฐ, ์ข‹์•„์š”๊ฐ€ ๋งŽ์€ ์ˆœ์œผ๋กœ ์ƒํ’ˆ ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnProductList_whenSortedByLikesDesc() { - // arrange - String url = ENDPOINT_PRODUCTS + "?sort=like_desc"; - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - ResponseEntity> response = testRestTemplate.exchange( - url, HttpMethod.GET, new HttpEntity<>(null), responseType); - - // assert - assertAll( - () -> assertTrue(response.getStatusCode().is2xxSuccessful()), - () -> assertThat(response.getBody()).isNotNull(), - () -> assertThat(response.getBody().data()).isNotNull(), - // ์ข‹์•„์š” ๋‚ด๋ฆผ์ฐจ์ˆœ ์ •๋ ฌ ๊ฒ€์ฆ - () -> { - var products = response.getBody().data().content(); - for (int i = 0; i < products.size() - 1; i++) { - assertThat(products.get(i).likes()).isGreaterThanOrEqualTo(products.get(i + 1).likes()); - } - } - ); - } - } - - @DisplayName("GET /api/v1/products/{productId}") - @Nested - class GetProductDetail { - @DisplayName("์กด์žฌํ•˜๋Š” ์ƒํ’ˆ ID๋กœ ์กฐํšŒํ•  ๊ฒฝ์šฐ, ์ƒํ’ˆ ์ƒ์„ธ ์ •๋ณด๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnProductDetail_whenProductExists() { - // arrange - String url = ENDPOINT_PRODUCTS + "/" + productId1; - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - ResponseEntity> response = testRestTemplate.exchange( - url, HttpMethod.GET, new HttpEntity<>(null), responseType); - - // assert - assertAll( - () -> assertTrue(response.getStatusCode().is2xxSuccessful()), - () -> assertThat(response.getBody()).isNotNull(), - () -> assertThat(response.getBody().data()).isNotNull(), - () -> assertThat(response.getBody().data().id()).isEqualTo(productId1), - () -> assertThat(response.getBody().data().name()).isEqualTo("์ƒํ’ˆ1"), - () -> assertThat(response.getBody().data().price()).isEqualTo(10000) - ); - } - - @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ƒํ’ˆ ID๋กœ ์กฐํšŒํ•  ๊ฒฝ์šฐ, `404 Not Found` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnNotFound_whenProductIdDoesNotExist() { - // arrange - Long nonExistentProductId = 99999L; - String url = ENDPOINT_PRODUCTS + "/" + nonExistentProductId; - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - ResponseEntity> response = testRestTemplate.exchange( - url, HttpMethod.GET, new HttpEntity<>(null), responseType); - - // assert - assertThat(response.getStatusCode().value()).isEqualTo(404); - } - } -} diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java deleted file mode 100644 index dc4df056b..000000000 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java +++ /dev/null @@ -1,202 +0,0 @@ -package com.loopers.interfaces.api; - -import com.loopers.interfaces.api.user.UserV1Dto; -import com.loopers.utils.DatabaseCleanUp; -import org.junit.jupiter.api.*; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.web.client.TestRestTemplate; -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.ResponseEntity; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertAll; -import static org.junit.jupiter.api.Assertions.assertTrue; - -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -public class UserV1ApiE2ETest { - - private final String ENDPOINT_USER = "/api/v1/users"; - - private final TestRestTemplate testRestTemplate; - private final DatabaseCleanUp databaseCleanUp; - - @Autowired - public UserV1ApiE2ETest( - TestRestTemplate testRestTemplate, - DatabaseCleanUp databaseCleanUp - ) { - this.testRestTemplate = testRestTemplate; - this.databaseCleanUp = databaseCleanUp; - } - - @AfterEach - void tearDown() { - databaseCleanUp.truncateAllTables(); - } - - @DisplayName("POST /api/v1/users") - @Nested - class Post { - @DisplayName("ํšŒ์› ๊ฐ€์ž…์ด ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ์ƒ์„ฑ๋œ ์œ ์ € ์ •๋ณด๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnUserInfo_whenRegisterSuccess() { - // arrange - UserV1Dto.UserRegisterRequest request = new UserV1Dto.UserRegisterRequest( - "user123", - "xx@yy.zz", - "1993-03-13", - "male" - ); - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - ResponseEntity> response = testRestTemplate.exchange(ENDPOINT_USER, HttpMethod.POST, new HttpEntity<>(request), responseType); - - // assert - assertAll( - () -> assertTrue(response.getStatusCode().is2xxSuccessful()), - () -> assertThat(response.getBody().data().id()).isEqualTo(request.id()), - () -> assertThat(response.getBody().data().email()).isEqualTo(request.email()), - () -> assertThat(response.getBody().data().birthday()).isEqualTo(request.birthday()), - () -> assertThat(response.getBody().data().gender()).isEqualTo(request.gender()) - ); - } - - @DisplayName("ํšŒ์› ๊ฐ€์ž… ์‹œ์— ์„ฑ๋ณ„์ด ์—†์„ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnBadRequest_whenGenderIsMissing() { - // arrange - UserV1Dto.UserRegisterRequest request = new UserV1Dto.UserRegisterRequest( - "user123", - "xx@yy.zz", - "1993-03-13", - null - ); - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - ResponseEntity> response = testRestTemplate.exchange(ENDPOINT_USER, HttpMethod.POST, new HttpEntity<>(request), responseType); - - // assert - assertThat(response.getStatusCode().value()).isEqualTo(400); - } - } - - @DisplayName("GET /api/v1/users/me") - @Nested - class Get { - private final String validUserId = "user123"; - private final String validEmail = "xx@yy.zz"; - private final String validBirthday = "1993-03-13"; - private final String validGender = "male"; - - @BeforeEach - void setupUser() { - UserV1Dto.UserRegisterRequest request = new UserV1Dto.UserRegisterRequest( - validUserId, - validEmail, - validBirthday, - validGender - ); - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - testRestTemplate.exchange(ENDPOINT_USER, HttpMethod.POST, new HttpEntity<>(request), responseType); - } - - @DisplayName("๋กœ๊ทธ์ธํ•œ ์œ ์ €๊ฐ€ ๋‚ด ์ •๋ณด ์กฐํšŒ์— ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ํ•ด๋‹นํ•˜๋Š” ์œ ์ € ์ •๋ณด๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnUserInfo_whenGetUserInfoSuccess() { - // arrange - HttpHeaders headers = new HttpHeaders(); - headers.add("X-USER-ID", validUserId); - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - HttpEntity requestEntity = new HttpEntity<>(null, headers); - ResponseEntity> response = testRestTemplate.exchange(ENDPOINT_USER + "/me", HttpMethod.GET, requestEntity, responseType); - - // assert - assertAll( - () -> assertTrue(response.getStatusCode().is2xxSuccessful()), - () -> assertThat(response.getBody().data().id()).isEqualTo("user123"), - () -> assertThat(response.getBody().data().email()).isEqualTo("xx@yy.zz"), - () -> assertThat(response.getBody().data().birthday()).isEqualTo("1993-03-13"), - () -> assertThat(response.getBody().data().gender()).isEqualTo("male") - ); - } - - @DisplayName("๋น„๋กœ๊ทธ์ธ ์œ ์ €๊ฐ€ ๋‚ด ์ •๋ณด ์กฐํšŒ๋ฅผ ์‹œ๋„ํ•  ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnBadRequest_whenXUserIdHeaderIsMissing() { - // arrange - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - HttpEntity requestEntity = new HttpEntity<>(null, null); - ResponseEntity> response = testRestTemplate.exchange(ENDPOINT_USER + "/me", HttpMethod.GET, requestEntity, responseType); - - // assert - assertThat(response.getStatusCode().value()).isEqualTo(400); - } - - @DisplayName("X-USER-ID ํ—ค๋”๊ฐ€ ๋นˆ ๋ฌธ์ž์—ด์ผ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnBadRequest_whenXUserIdHeaderIsEmpty() { - // arrange - HttpHeaders headers = new HttpHeaders(); - headers.add("X-USER-ID", ""); - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - HttpEntity requestEntity = new HttpEntity<>(null, headers); - ResponseEntity> response = testRestTemplate.exchange(ENDPOINT_USER + "/me", HttpMethod.GET, requestEntity, responseType); - - // assert - assertThat(response.getStatusCode().value()).isEqualTo(400); - } - - @DisplayName("X-USER-ID ํ—ค๋”๊ฐ€ ๊ณต๋ฐฑ๋งŒ ์žˆ์„ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnBadRequest_whenXUserIdHeaderIsBlank() { - // arrange - HttpHeaders headers = new HttpHeaders(); - headers.add("X-USER-ID", " "); - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - HttpEntity requestEntity = new HttpEntity<>(null, headers); - ResponseEntity> response = testRestTemplate.exchange(ENDPOINT_USER + "/me", HttpMethod.GET, requestEntity, responseType); - - // assert - assertThat(response.getStatusCode().value()).isEqualTo(400); - } - - @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ID๋กœ ์กฐํšŒํ•  ๊ฒฝ์šฐ, `404 Not Found` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void returnNotFound_whenUserIdDoesNotExist() { - // arrange - String invalidUserId = "nonexist"; - HttpHeaders headers = new HttpHeaders(); - headers.add("X-USER-ID", invalidUserId); - - // act - ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { - }; - HttpEntity requestEntity = new HttpEntity<>(null, headers); - ResponseEntity> response = testRestTemplate.exchange(ENDPOINT_USER + "/me", HttpMethod.GET, requestEntity, responseType); - - // assert - assertThat(response.getStatusCode().value()).isEqualTo(404); - } - } -} diff --git a/docker/infra-compose.yml b/docker/infra-compose.yml index d2607d47a..18e5fcf5f 100644 --- a/docker/infra-compose.yml +++ b/docker/infra-compose.yml @@ -14,97 +14,97 @@ services: volumes: - mysql-8-data:/var/lib/mysql -# redis-master: -# image: redis:7.0 -# container_name: redis-master -# ports: -# - "6379:6379" -# volumes: -# - redis_master_data:/data -# command: -# [ -# "redis-server", # redis ์„œ๋ฒ„ ์‹คํ–‰ ๋ช…๋ น์–ด -# "--appendonly", "yes", # AOF (AppendOnlyFile) ์˜์†์„ฑ ๊ธฐ๋Šฅ ์ผœ๊ธฐ -# "--save", "", -# "--latency-monitor-threshold", "100", # ํŠน์ • command ๊ฐ€ ์ง€์ • ์‹œ๊ฐ„(ms) ์ด์ƒ ๊ฑธ๋ฆฌ๋ฉด monitor ๊ธฐ๋ก -# ] -# healthcheck: -# test: ["CMD", "redis-cli", "-p", "6379", "PING"] -# interval: 5s -# timeout: 2s -# retries: 10 -# -# redis-readonly: -# image: redis:7.0 -# container_name: redis-readonly -# depends_on: -# redis-master: -# condition: service_healthy -# ports: -# - "6380:6379" -# volumes: -# - redis_readonly_data:/data -# command: -# [ -# "redis-server", -# "--appendonly", "yes", -# "--appendfsync", "everysec", -# "--replicaof", "redis-master", "6379", # replica ๋ชจ๋“œ๋กœ ์‹คํ–‰ + ์„œ๋น„์Šค ๋ช…, ์„œ๋น„์Šค ํฌํŠธ -# "--replica-read-only", "yes", # ์ฝ๊ธฐ ์ „์šฉ์œผ๋กœ ์„ค์ • -# "--latency-monitor-threshold", "100", -# ] -# healthcheck: -# test: ["CMD", "redis-cli", "-p", "6379", "PING"] -# interval: 5s -# timeout: 2s -# retries: 10 -# -# kafka: -# image: bitnamilegacy/kafka:3.5.1 -# container_name: kafka -# ports: -# - "9092:9092" # ์นดํ”„์นด ๋ธŒ๋กœ์ปค PORT -# - "19092:19092" # ํ˜ธ์ŠคํŠธ ๋ฆฌ์Šค๋„ˆ ์–˜ ๋–„๋ฌธ์ธ๊ฐ€ -# environment: -# - KAFKA_CFG_NODE_ID=1 # ๋ธŒ๋กœ์ปค ๊ณ ์œ  ID -# - KAFKA_CFG_PROCESS_ROLES=broker,controller # KRaft ๋ชจ๋“œ์—ฌ์„œ, broker / controller ์—ญํ•  ๋ชจ๋‘ ๋ถ€์—ฌ -# - KAFKA_CFG_LISTENERS=PLAINTEXT://:9092,PLAINTEXT_HOST://:19092,CONTROLLER://:9093 -# # ๋ธŒ๋กœ์ปค ํด๋ผ์ด์–ธํŠธ (PLAINTEXT), ๋ธŒ๋กœ์ปค ํ˜ธ์ŠคํŠธ (PLAINTEXT) ๋‚ด๋ถ€ ์ปจํŠธ๋กค๋Ÿฌ (CONTROLLER) -# - KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://kafka:9092,PLAINTEXT_HOST://localhost:19092 -# # ์™ธ๋ถ€ ํด๋ผ์ด์–ธํŠธ ์ ‘์† ํ˜ธ์ŠคํŠธ (localhost:9092), ๋ธŒ๋กœ์ปค ์ ‘์† ํ˜ธ์ŠคํŠธ (localhost:19092) -# - KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT,CONTROLLER:PLAINTEXT -# # ๊ฐ ๋ฆฌ์Šค๋„ˆ๋ณ„ ๋ณด์•ˆ ํ”„๋กœํ† ์ฝœ ์„ค์ • -# - KAFKA_CFG_INTER_BROKER_LISTENER_NAME=PLAINTEXT -# - KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER # ์ปจํŠธ๋กค๋Ÿฌ ๋‹ด๋‹น ๋ฆฌ์Šค๋„ˆ ์ง€์ • -# - KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=1@kafka:9093 # ์ปจํŠธ๋กค๋Ÿฌ ํ›„๋ณด ๋…ธ๋“œ ์ •์˜ (๋‹จ์ผ ๋ธŒ๋กœ์ปค๋ผ ์ž๊ธฐ ์ž์‹ ๋งŒ ์žˆ์Œ) -# - KAFKA_CFG_OFFSETS_TOPIC_REPLICATION_FACTOR=1 # consumer offset ๋ณต์ œ ๊ฐฏ์ˆ˜ (ํ˜„์žฌ๋Š” 1 - ๋กœ์ปฌ์šฉ์ด๋ผ์„œ) -# - KAFKA_CFG_TRANSACTION_STATE_LOG_REPLICATION_FACTOR=1 # transaction log ํ† ํ”ฝ ๋ณต์ œ ๊ฐฏ์ˆ˜ (ํ˜„์žฌ๋Š” 1 - ๋กœ์ปฌ์šฉ์ด๋ผ์„œ) -# - KAFKA_CFG_TRANSACTION_STATE_LOG_MIN_ISR=1 # In-Sync-Replica ์ตœ์†Œ ์ˆ˜ (ํ˜„์žฌ๋Š” 1 - ๋กœ์ปฌ์šฉ์ด๋ผ์„œ) -# volumes: -# - kafka-data:/bitnami/kafka -# healthcheck: -# test: ["CMD", "bash", "-c", "kafka-topics.sh --bootstrap-server localhost:9092 --list || exit 1"] -# interval: 10s -# timeout: 5s -# retries: 10 -# -# kafka-ui: -# image: provectuslabs/kafka-ui:latest -# container_name: kafka-ui -# depends_on: -# kafka: -# condition: service_healthy -# ports: -# - "9099:8080" -# environment: -# KAFKA_CLUSTERS_0_NAME: local # kafka-ui ์—์„œ ๋ณด์ด๋Š” ํด๋Ÿฌ์Šคํ„ฐ๋ช… -# KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:9092 # kafka-ui ๊ฐ€ ์—ฐ๊ฒทํ•  ๋ธŒ๋กœ์ปค ์ฃผ์†Œ + redis-master: + image: redis:7.0 + container_name: redis-master + ports: + - "6379:6379" + volumes: + - redis_master_data:/data + command: + [ + "redis-server", # redis ์„œ๋ฒ„ ์‹คํ–‰ ๋ช…๋ น์–ด + "--appendonly", "yes", # AOF (AppendOnlyFile) ์˜์†์„ฑ ๊ธฐ๋Šฅ ์ผœ๊ธฐ + "--save", "", + "--latency-monitor-threshold", "100", # ํŠน์ • command ๊ฐ€ ์ง€์ • ์‹œ๊ฐ„(ms) ์ด์ƒ ๊ฑธ๋ฆฌ๋ฉด monitor ๊ธฐ๋ก + ] + healthcheck: + test: ["CMD", "redis-cli", "-p", "6379", "PING"] + interval: 5s + timeout: 2s + retries: 10 + + redis-readonly: + image: redis:7.0 + container_name: redis-readonly + depends_on: + redis-master: + condition: service_healthy + ports: + - "6380:6379" + volumes: + - redis_readonly_data:/data + command: + [ + "redis-server", + "--appendonly", "yes", + "--appendfsync", "everysec", + "--replicaof", "redis-master", "6379", # replica ๋ชจ๋“œ๋กœ ์‹คํ–‰ + ์„œ๋น„์Šค ๋ช…, ์„œ๋น„์Šค ํฌํŠธ + "--replica-read-only", "yes", # ์ฝ๊ธฐ ์ „์šฉ์œผ๋กœ ์„ค์ • + "--latency-monitor-threshold", "100", + ] + healthcheck: + test: ["CMD", "redis-cli", "-p", "6379", "PING"] + interval: 5s + timeout: 2s + retries: 10 + + kafka: + image: bitnamilegacy/kafka:3.5.1 + container_name: kafka + ports: + - "9092:9092" # ์นดํ”„์นด ๋ธŒ๋กœ์ปค PORT + - "19092:19092" # ํ˜ธ์ŠคํŠธ ๋ฆฌ์Šค๋„ˆ ์–˜ ๋–„๋ฌธ์ธ๊ฐ€ + environment: + - KAFKA_CFG_NODE_ID=1 # ๋ธŒ๋กœ์ปค ๊ณ ์œ  ID + - KAFKA_CFG_PROCESS_ROLES=broker,controller # KRaft ๋ชจ๋“œ์—ฌ์„œ, broker / controller ์—ญํ•  ๋ชจ๋‘ ๋ถ€์—ฌ + - KAFKA_CFG_LISTENERS=PLAINTEXT://:9092,PLAINTEXT_HOST://:19092,CONTROLLER://:9093 + # ๋ธŒ๋กœ์ปค ํด๋ผ์ด์–ธํŠธ (PLAINTEXT), ๋ธŒ๋กœ์ปค ํ˜ธ์ŠคํŠธ (PLAINTEXT) ๋‚ด๋ถ€ ์ปจํŠธ๋กค๋Ÿฌ (CONTROLLER) + - KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://kafka:9092,PLAINTEXT_HOST://localhost:19092 + # ์™ธ๋ถ€ ํด๋ผ์ด์–ธํŠธ ์ ‘์† ํ˜ธ์ŠคํŠธ (localhost:9092), ๋ธŒ๋กœ์ปค ์ ‘์† ํ˜ธ์ŠคํŠธ (localhost:19092) + - KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT,CONTROLLER:PLAINTEXT + # ๊ฐ ๋ฆฌ์Šค๋„ˆ๋ณ„ ๋ณด์•ˆ ํ”„๋กœํ† ์ฝœ ์„ค์ • + - KAFKA_CFG_INTER_BROKER_LISTENER_NAME=PLAINTEXT + - KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER # ์ปจํŠธ๋กค๋Ÿฌ ๋‹ด๋‹น ๋ฆฌ์Šค๋„ˆ ์ง€์ • + - KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=1@kafka:9093 # ์ปจํŠธ๋กค๋Ÿฌ ํ›„๋ณด ๋…ธ๋“œ ์ •์˜ (๋‹จ์ผ ๋ธŒ๋กœ์ปค๋ผ ์ž๊ธฐ ์ž์‹ ๋งŒ ์žˆ์Œ) + - KAFKA_CFG_OFFSETS_TOPIC_REPLICATION_FACTOR=1 # consumer offset ๋ณต์ œ ๊ฐฏ์ˆ˜ (ํ˜„์žฌ๋Š” 1 - ๋กœ์ปฌ์šฉ์ด๋ผ์„œ) + - KAFKA_CFG_TRANSACTION_STATE_LOG_REPLICATION_FACTOR=1 # transaction log ํ† ํ”ฝ ๋ณต์ œ ๊ฐฏ์ˆ˜ (ํ˜„์žฌ๋Š” 1 - ๋กœ์ปฌ์šฉ์ด๋ผ์„œ) + - KAFKA_CFG_TRANSACTION_STATE_LOG_MIN_ISR=1 # In-Sync-Replica ์ตœ์†Œ ์ˆ˜ (ํ˜„์žฌ๋Š” 1 - ๋กœ์ปฌ์šฉ์ด๋ผ์„œ) + volumes: + - kafka-data:/bitnami/kafka + healthcheck: + test: ["CMD", "bash", "-c", "kafka-topics.sh --bootstrap-server localhost:9092 --list || exit 1"] + interval: 10s + timeout: 5s + retries: 10 + + kafka-ui: + image: provectuslabs/kafka-ui:latest + container_name: kafka-ui + depends_on: + kafka: + condition: service_healthy + ports: + - "9099:8080" + environment: + KAFKA_CLUSTERS_0_NAME: local # kafka-ui ์—์„œ ๋ณด์ด๋Š” ํด๋Ÿฌ์Šคํ„ฐ๋ช… + KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:9092 # kafka-ui ๊ฐ€ ์—ฐ๊ฒทํ•  ๋ธŒ๋กœ์ปค ์ฃผ์†Œ volumes: mysql-8-data: -# redis_master_data: + redis_master_data: redis_readonly_data: -# kafka-data: + kafka-data: networks: default: diff --git a/settings.gradle.kts b/settings.gradle.kts index 83ff00abc..c99fb6360 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -2,10 +2,10 @@ rootProject.name = "loopers-java-spring-template" include( ":apps:commerce-api", -// ":apps:commerce-streamer", + ":apps:commerce-streamer", ":modules:jpa", -// ":modules:redis", -// ":modules:kafka", + ":modules:redis", + ":modules:kafka", ":supports:jackson", ":supports:logging", ":supports:monitoring", From da195a3964c6d383b3d475cce20a4263dae9fac4 Mon Sep 17 00:00:00 2001 From: hubtwork Date: Wed, 31 Dec 2025 16:09:54 +0900 Subject: [PATCH 66/85] =?UTF-8?q?commerce-batch=20=EB=AA=A8=EB=93=88?= =?UTF-8?q?=EC=9D=84=20=EC=B6=94=EA=B0=80=ED=95=98=EB=A9=B0,=20=EB=8D=B0?= =?UTF-8?q?=EB=AA=A8=20Batch=20Job=20=EB=B0=8F=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EB=A5=BC=20=EC=B6=94=EA=B0=80=ED=95=A9=EB=8B=88?= =?UTF-8?q?=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/commerce-batch/build.gradle.kts | 21 +++++ .../com/loopers/CommerceBatchApplication.java | 24 ++++++ .../loopers/batch/job/demo/DemoJobConfig.java | 48 ++++++++++++ .../batch/job/demo/step/DemoTasklet.java | 32 ++++++++ .../loopers/batch/listener/ChunkListener.java | 21 +++++ .../loopers/batch/listener/JobListener.java | 53 +++++++++++++ .../batch/listener/StepMonitorListener.java | 44 +++++++++++ .../src/main/resources/application.yml | 54 +++++++++++++ .../loopers/CommerceBatchApplicationTest.java | 10 +++ .../com/loopers/job/demo/DemoJobE2ETest.java | 76 +++++++++++++++++++ settings.gradle.kts | 1 + 11 files changed, 384 insertions(+) create mode 100644 apps/commerce-batch/build.gradle.kts create mode 100644 apps/commerce-batch/src/main/java/com/loopers/CommerceBatchApplication.java create mode 100644 apps/commerce-batch/src/main/java/com/loopers/batch/job/demo/DemoJobConfig.java create mode 100644 apps/commerce-batch/src/main/java/com/loopers/batch/job/demo/step/DemoTasklet.java create mode 100644 apps/commerce-batch/src/main/java/com/loopers/batch/listener/ChunkListener.java create mode 100644 apps/commerce-batch/src/main/java/com/loopers/batch/listener/JobListener.java create mode 100644 apps/commerce-batch/src/main/java/com/loopers/batch/listener/StepMonitorListener.java create mode 100644 apps/commerce-batch/src/main/resources/application.yml create mode 100644 apps/commerce-batch/src/test/java/com/loopers/CommerceBatchApplicationTest.java create mode 100644 apps/commerce-batch/src/test/java/com/loopers/job/demo/DemoJobE2ETest.java diff --git a/apps/commerce-batch/build.gradle.kts b/apps/commerce-batch/build.gradle.kts new file mode 100644 index 000000000..b22b6477c --- /dev/null +++ b/apps/commerce-batch/build.gradle.kts @@ -0,0 +1,21 @@ +dependencies { + // add-ons + implementation(project(":modules:jpa")) + implementation(project(":modules:redis")) + implementation(project(":supports:jackson")) + implementation(project(":supports:logging")) + implementation(project(":supports:monitoring")) + + // batch + implementation("org.springframework.boot:spring-boot-starter-batch") + testImplementation("org.springframework.batch:spring-batch-test") + + // querydsl + annotationProcessor("com.querydsl:querydsl-apt::jakarta") + annotationProcessor("jakarta.persistence:jakarta.persistence-api") + annotationProcessor("jakarta.annotation:jakarta.annotation-api") + + // test-fixtures + testImplementation(testFixtures(project(":modules:jpa"))) + testImplementation(testFixtures(project(":modules:redis"))) +} diff --git a/apps/commerce-batch/src/main/java/com/loopers/CommerceBatchApplication.java b/apps/commerce-batch/src/main/java/com/loopers/CommerceBatchApplication.java new file mode 100644 index 000000000..e5005c373 --- /dev/null +++ b/apps/commerce-batch/src/main/java/com/loopers/CommerceBatchApplication.java @@ -0,0 +1,24 @@ +package com.loopers; + +import jakarta.annotation.PostConstruct; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.ConfigurationPropertiesScan; + +import java.util.TimeZone; + +@ConfigurationPropertiesScan +@SpringBootApplication +public class CommerceBatchApplication { + + @PostConstruct + public void started() { + // set timezone + TimeZone.setDefault(TimeZone.getTimeZone("Asia/Seoul")); + } + + public static void main(String[] args) { + int exitCode = SpringApplication.exit(SpringApplication.run(CommerceBatchApplication.class, args)); + System.exit(exitCode); + } +} diff --git a/apps/commerce-batch/src/main/java/com/loopers/batch/job/demo/DemoJobConfig.java b/apps/commerce-batch/src/main/java/com/loopers/batch/job/demo/DemoJobConfig.java new file mode 100644 index 000000000..7c486483f --- /dev/null +++ b/apps/commerce-batch/src/main/java/com/loopers/batch/job/demo/DemoJobConfig.java @@ -0,0 +1,48 @@ +package com.loopers.batch.job.demo; + +import com.loopers.batch.job.demo.step.DemoTasklet; +import com.loopers.batch.listener.JobListener; +import com.loopers.batch.listener.StepMonitorListener; +import lombok.RequiredArgsConstructor; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.configuration.annotation.JobScope; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.batch.core.launch.support.RunIdIncrementer; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.batch.support.transaction.ResourcelessTransactionManager; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@ConditionalOnProperty(name = "spring.batch.job.name", havingValue = DemoJobConfig.JOB_NAME) +@RequiredArgsConstructor +@Configuration +public class DemoJobConfig { + public static final String JOB_NAME = "demoJob"; + private static final String STEP_DEMO_SIMPLE_TASK_NAME = "demoSimpleTask"; + + private final JobRepository jobRepository; + private final JobListener jobListener; + private final StepMonitorListener stepMonitorListener; + private final DemoTasklet demoTasklet; + + @Bean(JOB_NAME) + public Job demoJob() { + return new JobBuilder(JOB_NAME, jobRepository) + .incrementer(new RunIdIncrementer()) + .start(categorySyncStep()) + .listener(jobListener) + .build(); + } + + @JobScope + @Bean(STEP_DEMO_SIMPLE_TASK_NAME) + public Step categorySyncStep() { + return new StepBuilder(STEP_DEMO_SIMPLE_TASK_NAME, jobRepository) + .tasklet(demoTasklet, new ResourcelessTransactionManager()) + .listener(stepMonitorListener) + .build(); + } +} diff --git a/apps/commerce-batch/src/main/java/com/loopers/batch/job/demo/step/DemoTasklet.java b/apps/commerce-batch/src/main/java/com/loopers/batch/job/demo/step/DemoTasklet.java new file mode 100644 index 000000000..800fe5a03 --- /dev/null +++ b/apps/commerce-batch/src/main/java/com/loopers/batch/job/demo/step/DemoTasklet.java @@ -0,0 +1,32 @@ +package com.loopers.batch.job.demo.step; + +import com.loopers.batch.job.demo.DemoJobConfig; +import lombok.RequiredArgsConstructor; +import org.springframework.batch.core.StepContribution; +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.batch.core.scope.context.ChunkContext; +import org.springframework.batch.core.step.tasklet.Tasklet; +import org.springframework.batch.repeat.RepeatStatus; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; + +@StepScope +@ConditionalOnProperty(name = "spring.batch.job.name", havingValue = DemoJobConfig.JOB_NAME) +@RequiredArgsConstructor +@Component +public class DemoTasklet implements Tasklet { + @Value("#{jobParameters['requestDate']}") + private String requestDate; + + @Override + public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { + if (requestDate == null) { + throw new RuntimeException("requestDate is null"); + } + System.out.println("Demo Tasklet ์‹คํ–‰ (์‹คํ–‰ ์ผ์ž : " + requestDate + ")"); + Thread.sleep(1000); + System.out.println("Demo Tasklet ์ž‘์—… ์™„๋ฃŒ"); + return RepeatStatus.FINISHED; + } +} diff --git a/apps/commerce-batch/src/main/java/com/loopers/batch/listener/ChunkListener.java b/apps/commerce-batch/src/main/java/com/loopers/batch/listener/ChunkListener.java new file mode 100644 index 000000000..10b09b8fc --- /dev/null +++ b/apps/commerce-batch/src/main/java/com/loopers/batch/listener/ChunkListener.java @@ -0,0 +1,21 @@ +package com.loopers.batch.listener; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.batch.core.annotation.AfterChunk; +import org.springframework.batch.core.scope.context.ChunkContext; +import org.springframework.stereotype.Component; + +@Slf4j +@RequiredArgsConstructor +@Component +public class ChunkListener { + + @AfterChunk + void afterChunk(ChunkContext chunkContext) { + log.info( + "์ฒญํฌ ์ข…๋ฃŒ: readCount: ${chunkContext.stepContext.stepExecution.readCount}, " + + "writeCount: ${chunkContext.stepContext.stepExecution.writeCount}" + ); + } +} diff --git a/apps/commerce-batch/src/main/java/com/loopers/batch/listener/JobListener.java b/apps/commerce-batch/src/main/java/com/loopers/batch/listener/JobListener.java new file mode 100644 index 000000000..cb5c8bebd --- /dev/null +++ b/apps/commerce-batch/src/main/java/com/loopers/batch/listener/JobListener.java @@ -0,0 +1,53 @@ +package com.loopers.batch.listener; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.annotation.AfterJob; +import org.springframework.batch.core.annotation.BeforeJob; +import org.springframework.stereotype.Component; + +import java.time.Duration; +import java.time.Instant; +import java.time.ZoneId; + +@Slf4j +@RequiredArgsConstructor +@Component +public class JobListener { + + @BeforeJob + void beforeJob(JobExecution jobExecution) { + log.info("Job '${jobExecution.jobInstance.jobName}' ์‹œ์ž‘"); + jobExecution.getExecutionContext().putLong("startTime", System.currentTimeMillis()); + } + + @AfterJob + void afterJob(JobExecution jobExecution) { + var startTime = jobExecution.getExecutionContext().getLong("startTime"); + var endTime = System.currentTimeMillis(); + + var startDateTime = Instant.ofEpochMilli(startTime) + .atZone(ZoneId.systemDefault()) + .toLocalDateTime(); + var endDateTime = Instant.ofEpochMilli(endTime) + .atZone(ZoneId.systemDefault()) + .toLocalDateTime(); + + var totalTime = endTime - startTime; + var duration = Duration.ofMillis(totalTime); + var hours = duration.toHours(); + var minutes = duration.toMinutes() % 60; + var seconds = duration.getSeconds() % 60; + + var message = String.format( + """ + *Start Time:* %s + *End Time:* %s + *Total Time:* %d์‹œ๊ฐ„ %d๋ถ„ %d์ดˆ + """, startDateTime, endDateTime, hours, minutes, seconds + ).trim(); + + log.info(message); + } +} diff --git a/apps/commerce-batch/src/main/java/com/loopers/batch/listener/StepMonitorListener.java b/apps/commerce-batch/src/main/java/com/loopers/batch/listener/StepMonitorListener.java new file mode 100644 index 000000000..4f22f40b0 --- /dev/null +++ b/apps/commerce-batch/src/main/java/com/loopers/batch/listener/StepMonitorListener.java @@ -0,0 +1,44 @@ +package com.loopers.batch.listener; + +import jakarta.annotation.Nonnull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.batch.core.ExitStatus; +import org.springframework.batch.core.StepExecution; +import org.springframework.batch.core.StepExecutionListener; +import org.springframework.stereotype.Component; +import java.util.Objects; +import java.util.stream.Collectors; + +@Slf4j +@RequiredArgsConstructor +@Component +public class StepMonitorListener implements StepExecutionListener { + + @Override + public void beforeStep(@Nonnull StepExecution stepExecution) { + log.info("Step '{}' ์‹œ์ž‘", stepExecution.getStepName()); + } + + @Override + public ExitStatus afterStep(@Nonnull StepExecution stepExecution) { + if (!stepExecution.getFailureExceptions().isEmpty()) { + var jobName = stepExecution.getJobExecution().getJobInstance().getJobName(); + var exceptions = stepExecution.getFailureExceptions().stream() + .map(Throwable::getMessage) + .filter(Objects::nonNull) + .collect(Collectors.joining("\n")); + log.info( + """ + [์—๋Ÿฌ ๋ฐœ์ƒ] + jobName: {} + exceptions: + {} + """.trim(), jobName, exceptions + ); + // error ๋ฐœ์ƒ ์‹œ slack ๋“ฑ ๋‹ค๋ฅธ ์ฑ„๋„๋กœ ๋ชจ๋‹ˆํ„ฐ ์ „์†ก + return ExitStatus.FAILED; + } + return ExitStatus.COMPLETED; + } +} diff --git a/apps/commerce-batch/src/main/resources/application.yml b/apps/commerce-batch/src/main/resources/application.yml new file mode 100644 index 000000000..9aa0d760a --- /dev/null +++ b/apps/commerce-batch/src/main/resources/application.yml @@ -0,0 +1,54 @@ +spring: + main: + web-application-type: none + application: + name: commerce-batch + profiles: + active: local + config: + import: + - jpa.yml + - redis.yml + - logging.yml + - monitoring.yml + batch: + job: + name: ${job.name:NONE} + jdbc: + initialize-schema: never + +management: + health: + defaults: + enabled: false + +--- +spring: + config: + activate: + on-profile: local, test + batch: + jdbc: + initialize-schema: always + +--- +spring: + config: + activate: + on-profile: dev + +--- +spring: + config: + activate: + on-profile: qa + +--- +spring: + config: + activate: + on-profile: prd + +springdoc: + api-docs: + enabled: false \ No newline at end of file diff --git a/apps/commerce-batch/src/test/java/com/loopers/CommerceBatchApplicationTest.java b/apps/commerce-batch/src/test/java/com/loopers/CommerceBatchApplicationTest.java new file mode 100644 index 000000000..c5e3bc7a3 --- /dev/null +++ b/apps/commerce-batch/src/test/java/com/loopers/CommerceBatchApplicationTest.java @@ -0,0 +1,10 @@ +package com.loopers; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +public class CommerceBatchApplicationTest { + @Test + void contextLoads() {} +} diff --git a/apps/commerce-batch/src/test/java/com/loopers/job/demo/DemoJobE2ETest.java b/apps/commerce-batch/src/test/java/com/loopers/job/demo/DemoJobE2ETest.java new file mode 100644 index 000000000..dafe59a18 --- /dev/null +++ b/apps/commerce-batch/src/test/java/com/loopers/job/demo/DemoJobE2ETest.java @@ -0,0 +1,76 @@ +package com.loopers.job.demo; + +import com.loopers.batch.job.demo.DemoJobConfig; +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.batch.core.ExitStatus; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobParametersBuilder; +import org.springframework.batch.test.JobLauncherTestUtils; +import org.springframework.batch.test.context.SpringBatchTest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.TestPropertySource; + +import java.time.LocalDate; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +@SpringBootTest +@SpringBatchTest +@TestPropertySource(properties = "spring.batch.job.name=" + DemoJobConfig.JOB_NAME) +class DemoJobE2ETest { + + // IDE ์ •์  ๋ถ„์„ ์ƒ [SpringBatchTest] ์˜ ์ฃผ์ž…๋ณด๋‹ค [SpringBootTest] ์˜ ์ฃผ์ž…์ด ์šฐ์„ ๋˜์–ด, ํ•ด๋‹น ์ปดํฌ๋„ŒํŠธ๋Š” ์—†์œผ๋ฏ€๋กœ ์˜ค๋ฅ˜์ฒ˜๋Ÿผ ๋ณด์ผ ์ˆ˜ ์žˆ์Œ. + // [SpringBatchTest] ์ž์ฒด๊ฐ€ Scope ๊ธฐ๋ฐ˜์œผ๋กœ ์ฃผ์ž…ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ •์ƒ ๋™์ž‘ํ•จ. + @Autowired + private JobLauncherTestUtils jobLauncherTestUtils; + + @Autowired + @Qualifier(DemoJobConfig.JOB_NAME) + private Job job; + + @BeforeEach + void beforeEach() { + + } + + @DisplayName("jobParameter ์ค‘ requestDate ์ธ์ž๊ฐ€ ์ฃผ์–ด์ง€์ง€ ์•Š์•˜์„ ๋•Œ, demoJob ๋ฐฐ์น˜๋Š” ์‹คํŒจํ•œ๋‹ค.") + @Test + void shouldNotSaveCategories_whenApiError() throws Exception { + // arrange + jobLauncherTestUtils.setJob(job); + + // act + var jobExecution = jobLauncherTestUtils.launchJob(); + + // assert + assertAll( + () -> assertThat(jobExecution).isNotNull(), + () -> assertThat(jobExecution.getExitStatus().getExitCode()).isEqualTo(ExitStatus.FAILED.getExitCode()) + ); + } + + @DisplayName("demoJob ๋ฐฐ์น˜๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ์‹คํ–‰๋œ๋‹ค.") + @Test + void success() throws Exception { + // arrange + jobLauncherTestUtils.setJob(job); + + // act + var jobParameters = new JobParametersBuilder() + .addLocalDate("requestDate", LocalDate.now()) + .toJobParameters(); + var jobExecution = jobLauncherTestUtils.launchJob(jobParameters); + + // assert + assertAll( + () -> assertThat(jobExecution).isNotNull(), + () -> assertThat(jobExecution.getExitStatus().getExitCode()).isEqualTo(ExitStatus.COMPLETED.getExitCode()) + ); + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index c99fb6360..a2c303835 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -3,6 +3,7 @@ rootProject.name = "loopers-java-spring-template" include( ":apps:commerce-api", ":apps:commerce-streamer", + ":apps:commerce-batch", ":modules:jpa", ":modules:redis", ":modules:kafka", From 6c87e71091e34a42019f9aafa136a87e468f6824 Mon Sep 17 00:00:00 2001 From: hubtwork Date: Wed, 31 Dec 2025 16:16:04 +0900 Subject: [PATCH 67/85] =?UTF-8?q?commerce-batch=20=EB=AA=A8=EB=93=88?= =?UTF-8?q?=EC=97=90=20=EB=8C=80=ED=95=B4=20README=20=EC=97=90=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=ED=95=A9=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 04950f29d..f86e4dd8a 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ docker-compose -f ./docker/monitoring-compose.yml up Root โ”œโ”€โ”€ apps ( spring-applications ) โ”‚ โ”œโ”€โ”€ ๐Ÿ“ฆ commerce-api +โ”‚ โ”œโ”€โ”€ ๐Ÿ“ฆ commerce-batch โ”‚ โ””โ”€โ”€ ๐Ÿ“ฆ commerce-streamer โ”œโ”€โ”€ modules ( reusable-configurations ) โ”‚ โ”œโ”€โ”€ ๐Ÿ“ฆ jpa From 7f3a2d779b4d9feb02faa7078969b879ce31a9ce Mon Sep 17 00:00:00 2001 From: hyujikoh Date: Wed, 31 Dec 2025 23:11:41 +0900 Subject: [PATCH 68/85] =?UTF-8?q?feat(metrics):=20=EC=83=81=ED=92=88=20?= =?UTF-8?q?=EB=A9=94=ED=8A=B8=EB=A6=AD=20=EC=97=94=ED=8B=B0=ED=8B=B0=20?= =?UTF-8?q?=EB=B0=8F=20=EB=A6=AC=ED=8F=AC=EC=A7=80=ED=86=A0=EB=A6=AC=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 - ์ƒํ’ˆ๋ณ„ ์ผ๊ฐ„ ๋ฉ”ํŠธ๋ฆญ์„ ์ €์žฅํ•˜๋Š” ProductMetricsEntity ์ถ”๊ฐ€ - ๋ณตํ•ฉํ‚ค๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฉ”ํŠธ๋ฆญ์„ ๊ตฌ๋ถ„ํ•˜๋Š” ProductMetricsId ์ถ”๊ฐ€ - ์ƒํ’ˆ ID์™€ ๋‚ ์งœ๋กœ ๋ฉ”ํŠธ๋ฆญ์„ ์กฐํšŒํ•˜๋Š” ๋ฉ”์„œ๋“œ ๊ตฌํ˜„ - ๋ฉ”ํŠธ๋ฆญ ์ €์žฅ ๋ฐ ์กฐํšŒ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜๋Š” ProductMetricsRepository ์ถ”๊ฐ€ - ๋ฉ”ํŠธ๋ฆญ ๊ด€๋ จ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ์ฒ˜๋ฆฌํ•˜๋Š” ProductMetricsService ์ถ”๊ฐ€ - ๋ฉ”ํŠธ๋ฆญ ์—”ํ‹ฐํ‹ฐ์— ๋Œ€ํ•œ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ์ถ”๊ฐ€ --- .../domain/metrics/ProductMetricsEntity.java | 81 ------- .../metrics/ProductMetricsRepository.java | 25 --- .../domain/metrics/ProductMetricsService.java | 48 ++-- .../metrics/ProductMetricsJpaRepository.java | 49 ++++- .../metrics/ProductMetricsRepositoryImpl.java | 29 ++- ...MetricsEventProcessingIntegrationTest.java | 29 ++- .../domain/metrics/ProductMetricsEntity.java | 122 +++++++++++ .../domain/metrics/ProductMetricsId.java | 57 +++++ .../metrics/ProductMetricsRepository.java | 68 ++++++ .../metrics/ProductMetricsEntityUnitTest.java | 207 ++++++++++++++++++ 10 files changed, 585 insertions(+), 130 deletions(-) delete mode 100644 apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsEntity.java delete mode 100644 apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsRepository.java create mode 100644 modules/jpa/src/main/java/com/loopers/domain/metrics/ProductMetricsEntity.java create mode 100644 modules/jpa/src/main/java/com/loopers/domain/metrics/ProductMetricsId.java create mode 100644 modules/jpa/src/main/java/com/loopers/domain/metrics/ProductMetricsRepository.java create mode 100644 modules/jpa/src/test/java/com/loopers/domain/metrics/ProductMetricsEntityUnitTest.java diff --git a/apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsEntity.java b/apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsEntity.java deleted file mode 100644 index a07933520..000000000 --- a/apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsEntity.java +++ /dev/null @@ -1,81 +0,0 @@ -package com.loopers.domain.metrics; - -import java.time.ZonedDateTime; -import java.util.Objects; - -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.Id; -import jakarta.persistence.Table; - -/** - * - * @author hyunjikoh - * @since 2025. 12. 16. - */ - -@Entity -@Getter -@Table(name = "product_metrics") -@AllArgsConstructor -@NoArgsConstructor -public class ProductMetricsEntity { - @Id - @Column(name = "product_id", nullable = false) - private Long id; - - @Column(name = "view_count", nullable = false) - private long viewCount = 0L; - - @Column(name = "like_count", nullable = false) - private long likeCount = 0L; - - @Column(name = "sales_count", nullable = false) - private long salesCount = 0L; - - @Column(name = "order_count", nullable = false) - private long orderCount = 0L; - - @Column(name = "last_event_at") - private ZonedDateTime lastEventAt; - - - private ProductMetricsEntity(final Long productId) { - Objects.requireNonNull(productId); - this.id = productId; - } - - public static ProductMetricsEntity create(final Long productId) { - return new ProductMetricsEntity(productId); - } - - - public void incrementView(ZonedDateTime eventTime) { - this.viewCount += 1; - this.lastEventAt = eventTime; - } - - public void applyLikeDelta(final int delta, ZonedDateTime eventTime) { - final long next = this.likeCount + delta; - - // ์ข‹์•„์š” ์ˆ˜๋Š” 0 ๋ฏธ๋งŒ์œผ๋กœ ๋‚ด๋ ค๊ฐ€์ง€ ์•Š๋„๋ก ๋ณด์žฅ - this.likeCount = Math.max(0, next); - - this.lastEventAt = eventTime; - } - - - public void addSales(final int quantity, ZonedDateTime eventTime) { - if (quantity <= 0) { - return; - } - this.salesCount += quantity; - this.orderCount += 1; // ์ฃผ๋ฌธ ๊ฑด์ˆ˜๋„ ํ•จ๊ป˜ ์ฆ๊ฐ€ - this.lastEventAt = eventTime; - } - -} diff --git a/apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsRepository.java b/apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsRepository.java deleted file mode 100644 index c9ec7e09d..000000000 --- a/apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsRepository.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.loopers.domain.metrics; - -import java.util.Optional; - -/** - * ์ƒํ’ˆ ๋ฉ”ํŠธ๋ฆญ Repository ์ธํ„ฐํŽ˜์ด์Šค - *

- * Domain ๊ณ„์ธต์˜ ์ˆœ์ˆ˜ํ•œ Repository ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. - * Infrastructure ๊ณ„์ธต์—์„œ JPA๋กœ ๊ตฌํ˜„๋ฉ๋‹ˆ๋‹ค. - * - * @author hyunjikoh - * @since 2025. 12. 16. - */ -public interface ProductMetricsRepository { - - /** - * ๋ฉ”ํŠธ๋ฆญ ์ €์žฅ - */ - ProductMetricsEntity save(ProductMetricsEntity metrics); - - /** - * ์ƒํ’ˆ ID๋กœ ๋ฉ”ํŠธ๋ฆญ ์กฐํšŒ - */ - Optional findById(Long productId); -} diff --git a/apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsService.java b/apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsService.java index befdcf1d1..f9510d857 100644 --- a/apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsService.java +++ b/apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsService.java @@ -1,5 +1,6 @@ package com.loopers.domain.metrics; +import java.time.LocalDate; import java.time.ZonedDateTime; import java.util.Optional; @@ -14,6 +15,8 @@ *

* ์ƒํ’ˆ ๋ฉ”ํŠธ๋ฆญ ๊ด€๋ จ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ๋‹ด๋‹นํ•˜๋Š” Domain ๊ณ„์ธต ์„œ๋น„์Šค์ž…๋‹ˆ๋‹ค. * ํŠธ๋žœ์žญ์…˜ ๊ฒฝ๊ณ„๋ฅผ ๊ด€๋ฆฌํ•˜๊ณ  Repository๋ฅผ ํ†ตํ•ด ๋ฐ์ดํ„ฐ๋ฅผ ์กฐ์ž‘ํ•ฉ๋‹ˆ๋‹ค. + *

+ * ์ผ๊ฐ„ ์ง‘๊ณ„๋ฅผ ์œ„ํ•ด ์ด๋ฒคํŠธ ์‹œ๊ฐ„์—์„œ ๋‚ ์งœ๋ฅผ ์ถ”์ถœํ•˜์—ฌ ๋ฉ”ํŠธ๋ฆญ์„ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค. * * @author hyunjikoh * @since 2025. 12. 26. @@ -21,42 +24,52 @@ @Service @RequiredArgsConstructor @Slf4j +@Transactional(readOnly = true) public class ProductMetricsService { private final ProductMetricsRepository productMetricsRepository; /** * ์กฐํšŒ์ˆ˜ ์ฆ๊ฐ€ + * + * @param productId ์ƒํ’ˆ ID + * @param eventTime ์ด๋ฒคํŠธ ๋ฐœ์ƒ ์‹œ๊ฐ„ */ @Transactional public void incrementView(Long productId, ZonedDateTime eventTime) { - ProductMetricsEntity metrics = getOrCreateMetrics(productId); + LocalDate metricDate = eventTime.toLocalDate(); + ProductMetricsEntity metrics = getOrCreateMetrics(productId, metricDate); metrics.incrementView(eventTime); productMetricsRepository.save(metrics); - log.debug("์กฐํšŒ์ˆ˜ ์ฆ๊ฐ€ ์™„๋ฃŒ: productId={}", productId); + log.debug("์กฐํšŒ์ˆ˜ ์ฆ๊ฐ€ ์™„๋ฃŒ: productId={}, date={}", productId, metricDate); } /** * ์ข‹์•„์š” ์ˆ˜ ๋ณ€๊ฒฝ - * + * + * @param productId ์ƒํ’ˆ ID + * @param delta ๋ณ€๊ฒฝ๋Ÿ‰ (์–‘์ˆ˜: ์ฆ๊ฐ€, ์Œ์ˆ˜: ๊ฐ์†Œ) + * @param eventTime ์ด๋ฒคํŠธ ๋ฐœ์ƒ ์‹œ๊ฐ„ * @return true: ๋ณ€๊ฒฝ๋จ, false: ๋ณ€๊ฒฝ ์•ˆ ๋จ (์ƒˆ ์ƒํ’ˆ์— ๋Œ€ํ•œ ์ข‹์•„์š” ๊ฐ์†Œ) */ @Transactional public boolean applyLikeDelta(Long productId, int delta, ZonedDateTime eventTime) { - Optional existing = productMetricsRepository.findById(productId); + LocalDate metricDate = eventTime.toLocalDate(); + Optional existing = productMetricsRepository + .findByProductIdAndMetricDate(productId, metricDate); if (existing.isPresent()) { ProductMetricsEntity metrics = existing.get(); metrics.applyLikeDelta(delta, eventTime); productMetricsRepository.save(metrics); - log.debug("์ข‹์•„์š” ์ˆ˜ ๋ณ€๊ฒฝ ์™„๋ฃŒ: productId={}, delta={}", productId, delta); + log.debug("์ข‹์•„์š” ์ˆ˜ ๋ณ€๊ฒฝ ์™„๋ฃŒ: productId={}, delta={}, date={}", productId, delta, metricDate); return true; } else if (delta > 0) { // ์ƒˆ๋กœ์šด ์ƒํ’ˆ์— ๋Œ€ํ•œ ์ข‹์•„์š” ์ถ”๊ฐ€๋งŒ ํ—ˆ์šฉ - ProductMetricsEntity newMetrics = ProductMetricsEntity.create(productId); + ProductMetricsEntity newMetrics = ProductMetricsEntity.create(productId, metricDate); newMetrics.applyLikeDelta(delta, eventTime); productMetricsRepository.save(newMetrics); - log.debug("์ƒˆ ์ƒํ’ˆ ์ข‹์•„์š” ์ถ”๊ฐ€ ์™„๋ฃŒ: productId={}, delta={}", productId, delta); + log.debug("์ƒˆ ์ƒํ’ˆ ์ข‹์•„์š” ์ถ”๊ฐ€ ์™„๋ฃŒ: productId={}, delta={}, date={}", productId, delta, metricDate); return true; } else { log.debug("์ƒˆ๋กœ์šด ์ƒํ’ˆ์— ๋Œ€ํ•œ ์ข‹์•„์š” ๊ฐ์†Œ ๋ฌด์‹œ: productId={}, delta={}", productId, delta); @@ -66,7 +79,10 @@ public boolean applyLikeDelta(Long productId, int delta, ZonedDateTime eventTime /** * ํŒ๋งค๋Ÿ‰ ์ฆ๊ฐ€ - * + * + * @param productId ์ƒํ’ˆ ID + * @param quantity ํŒ๋งค ์ˆ˜๋Ÿ‰ + * @param eventTime ์ด๋ฒคํŠธ ๋ฐœ์ƒ ์‹œ๊ฐ„ * @return true: ์ฆ๊ฐ€๋จ, false: ์ฆ๊ฐ€ ์•ˆ ๋จ (์ž˜๋ชป๋œ ์ˆ˜๋Ÿ‰) */ @Transactional @@ -76,18 +92,24 @@ public boolean addSales(Long productId, int quantity, ZonedDateTime eventTime) { return false; } - ProductMetricsEntity metrics = getOrCreateMetrics(productId); + LocalDate metricDate = eventTime.toLocalDate(); + ProductMetricsEntity metrics = getOrCreateMetrics(productId, metricDate); metrics.addSales(quantity, eventTime); productMetricsRepository.save(metrics); - log.debug("ํŒ๋งค๋Ÿ‰ ์ฆ๊ฐ€ ์™„๋ฃŒ: productId={}, quantity={}", productId, quantity); + log.debug("ํŒ๋งค๋Ÿ‰ ์ฆ๊ฐ€ ์™„๋ฃŒ: productId={}, quantity={}, date={}", productId, quantity, metricDate); return true; } /** * ์ƒํ’ˆ ๋ฉ”ํŠธ๋ฆญ ์กฐํšŒ ๋˜๋Š” ์ƒ์„ฑ + * + * @param productId ์ƒํ’ˆ ID + * @param metricDate ๋ฉ”ํŠธ๋ฆญ ๋‚ ์งœ + * @return ๋ฉ”ํŠธ๋ฆญ ์—”ํ‹ฐํ‹ฐ */ - private ProductMetricsEntity getOrCreateMetrics(Long productId) { - return productMetricsRepository.findById(productId) - .orElseGet(() -> ProductMetricsEntity.create(productId)); + private ProductMetricsEntity getOrCreateMetrics(Long productId, LocalDate metricDate) { + return productMetricsRepository + .findByProductIdAndMetricDate(productId, metricDate) + .orElseGet(() -> ProductMetricsEntity.create(productId, metricDate)); } } diff --git a/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.java b/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.java index 6615061fa..d0aa2dbd4 100644 --- a/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.java +++ b/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.java @@ -1,13 +1,60 @@ package com.loopers.infrastructure.metrics; +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; + import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import com.loopers.domain.metrics.ProductMetricsEntity; +import com.loopers.domain.metrics.ProductMetricsId; /** + * ์ƒํ’ˆ ๋ฉ”ํŠธ๋ฆญ JPA Repository * * @author hyunjikoh * @since 2025. 12. 16. */ -public interface ProductMetricsJpaRepository extends JpaRepository { +public interface ProductMetricsJpaRepository extends JpaRepository { + + /** + * ์ƒํ’ˆ ID์™€ ๋‚ ์งœ๋กœ ๋ฉ”ํŠธ๋ฆญ ์กฐํšŒ + */ + @Query("SELECT m FROM ProductMetricsEntity m WHERE m.id.productId = :productId AND m.id.metricDate = :metricDate") + Optional findByProductIdAndMetricDate( + @Param("productId") Long productId, + @Param("metricDate") LocalDate metricDate); + + /** + * ๊ธฐ๊ฐ„๋ณ„ ๋ฉ”ํŠธ๋ฆญ ์กฐํšŒ + */ + @Query("SELECT m FROM ProductMetricsEntity m WHERE m.id.metricDate BETWEEN :startDate AND :endDate") + List findByMetricDateBetween( + @Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate); + + /** + * ํŠน์ • ๋‚ ์งœ์˜ ์ „์ฒด ๋ฉ”ํŠธ๋ฆญ ์กฐํšŒ + */ + @Query("SELECT m FROM ProductMetricsEntity m WHERE m.id.metricDate = :metricDate") + List findByMetricDate(@Param("metricDate") LocalDate metricDate); + + /** + * ๊ธฐ๊ฐ„๋ณ„ ์ƒํ’ˆ ์ง‘๊ณ„ (GROUP BY) + */ + @Query(""" + SELECT m.id.productId, + SUM(m.viewCount), + SUM(m.likeCount), + SUM(m.salesCount), + SUM(m.orderCount) + FROM ProductMetricsEntity m + WHERE m.id.metricDate BETWEEN :startDate AND :endDate + GROUP BY m.id.productId + """) + List aggregateByDateRange( + @Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate); } diff --git a/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java b/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java index 633220ac4..486987a5c 100644 --- a/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java +++ b/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java @@ -1,15 +1,19 @@ package com.loopers.infrastructure.metrics; +import java.time.LocalDate; +import java.util.List; import java.util.Optional; import org.springframework.stereotype.Component; import com.loopers.domain.metrics.ProductMetricsEntity; +import com.loopers.domain.metrics.ProductMetricsId; import com.loopers.domain.metrics.ProductMetricsRepository; import lombok.RequiredArgsConstructor; /** + * ์ƒํ’ˆ ๋ฉ”ํŠธ๋ฆญ Repository ๊ตฌํ˜„์ฒด * * @author hyunjikoh * @since 2025. 12. 16. @@ -17,6 +21,7 @@ @Component @RequiredArgsConstructor public class ProductMetricsRepositoryImpl implements ProductMetricsRepository { + private final ProductMetricsJpaRepository productMetricsJpaRepository; @Override @@ -25,7 +30,27 @@ public ProductMetricsEntity save(ProductMetricsEntity metrics) { } @Override - public Optional findById(Long productId) { - return productMetricsJpaRepository.findById(productId); + public Optional findById(ProductMetricsId id) { + return productMetricsJpaRepository.findById(id); + } + + @Override + public Optional findByProductIdAndMetricDate(Long productId, LocalDate metricDate) { + return productMetricsJpaRepository.findByProductIdAndMetricDate(productId, metricDate); + } + + @Override + public List findByMetricDateBetween(LocalDate startDate, LocalDate endDate) { + return productMetricsJpaRepository.findByMetricDateBetween(startDate, endDate); + } + + @Override + public List findByMetricDate(LocalDate metricDate) { + return productMetricsJpaRepository.findByMetricDate(metricDate); + } + + @Override + public List aggregateByDateRange(LocalDate startDate, LocalDate endDate) { + return productMetricsJpaRepository.aggregateByDateRange(startDate, endDate); } } diff --git a/apps/commerce-streamer/src/test/java/com/loopers/integration/MetricsEventProcessingIntegrationTest.java b/apps/commerce-streamer/src/test/java/com/loopers/integration/MetricsEventProcessingIntegrationTest.java index 679ce3c79..e61823e4b 100644 --- a/apps/commerce-streamer/src/test/java/com/loopers/integration/MetricsEventProcessingIntegrationTest.java +++ b/apps/commerce-streamer/src/test/java/com/loopers/integration/MetricsEventProcessingIntegrationTest.java @@ -2,7 +2,9 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; + import java.time.Duration; +import java.time.LocalDate; import java.util.Optional; import org.junit.jupiter.api.BeforeEach; @@ -61,6 +63,7 @@ void setUp() { void shouldIncrementViewCountOnProductViewEvent() throws Exception { // Given Long productId = 1L; + LocalDate today = LocalDate.now(); String eventId = "product-view-test-" + System.currentTimeMillis(); ProductViewPayloadV1 payload = new ProductViewPayloadV1(productId, 100L); @@ -80,7 +83,8 @@ void shouldIncrementViewCountOnProductViewEvent() throws Exception { // Then await().atMost(Duration.ofSeconds(10)) .untilAsserted(() -> { - Optional metrics = productMetricsRepository.findById(productId); + Optional metrics = productMetricsRepository + .findByProductIdAndMetricDate(productId, today); assertThat(metrics).isPresent(); assertThat(metrics.get().getViewCount()).isEqualTo(1L); assertThat(metrics.get().getLastEventAt()).isNotNull(); @@ -95,6 +99,7 @@ void shouldIncrementViewCountOnProductViewEvent() throws Exception { void shouldProcessDuplicateEventOnlyOnce() throws Exception { // Given Long productId = 2L; + LocalDate today = LocalDate.now(); String eventId = "duplicate-test-" + System.currentTimeMillis(); ProductViewPayloadV1 payload = new ProductViewPayloadV1(productId, 200L); @@ -115,7 +120,8 @@ void shouldProcessDuplicateEventOnlyOnce() throws Exception { // Then - ์กฐํšŒ์ˆ˜๋Š” 1๋งŒ ์ฆ๊ฐ€ํ•ด์•ผ ํ•จ await().atMost(Duration.ofSeconds(10)) .untilAsserted(() -> { - Optional metrics = productMetricsRepository.findById(productId); + Optional metrics = productMetricsRepository + .findByProductIdAndMetricDate(productId, today); assertThat(metrics).isPresent(); assertThat(metrics.get().getViewCount()).isEqualTo(1L); }); @@ -126,6 +132,7 @@ void shouldProcessDuplicateEventOnlyOnce() throws Exception { void shouldIncrementSalesCountOnPaymentSuccessEvent() throws Exception { // Given Long productId = 3L; + LocalDate today = LocalDate.now(); String eventId = "payment-success-test-" + System.currentTimeMillis(); // ์ƒˆ๋กœ์šด PaymentSuccessPayloadV1 ๊ตฌ์กฐ (์ƒํ’ˆ๋ณ„ ๊ฐœ๋ณ„ ์ด๋ฒคํŠธ) @@ -154,7 +161,8 @@ void shouldIncrementSalesCountOnPaymentSuccessEvent() throws Exception { // Then await().atMost(Duration.ofSeconds(10)) .untilAsserted(() -> { - Optional metrics = productMetricsRepository.findById(productId); + Optional metrics = productMetricsRepository + .findByProductIdAndMetricDate(productId, today); assertThat(metrics).isPresent(); assertThat(metrics.get().getSalesCount()).isEqualTo(2L); @@ -167,6 +175,7 @@ void shouldIncrementSalesCountOnPaymentSuccessEvent() throws Exception { void shouldIgnoreOldEvents() throws Exception { // Given Long productId = 5L; + LocalDate today = LocalDate.now(); long currentTime = System.currentTimeMillis(); // ๋จผ์ € ์ตœ์‹  ์ด๋ฒคํŠธ๋ฅผ ์ฒ˜๋ฆฌ @@ -187,7 +196,8 @@ void shouldIgnoreOldEvents() throws Exception { // ์ตœ์‹  ์ด๋ฒคํŠธ๊ฐ€ ์ฒ˜๋ฆฌ๋  ๋•Œ๊นŒ์ง€ ๋Œ€๊ธฐ await().atMost(Duration.ofSeconds(10)) .untilAsserted(() -> { - Optional metrics = productMetricsRepository.findById(productId); + Optional metrics = productMetricsRepository + .findByProductIdAndMetricDate(productId, today); assertThat(metrics).isPresent(); assertThat(metrics.get().getViewCount()).isEqualTo(1L); }); @@ -207,11 +217,12 @@ void shouldIgnoreOldEvents() throws Exception { kafkaTemplate.send("catalog-events", oldEnvelope); -// Then - ์กฐํšŒ์ˆ˜๋Š” ์—ฌ์ „ํžˆ 1์ด์–ด์•ผ ํ•จ (๊ณผ๊ฑฐ ์ด๋ฒคํŠธ ๋ฌด์‹œ) + // Then - ์กฐํšŒ์ˆ˜๋Š” ์—ฌ์ „ํžˆ 1์ด์–ด์•ผ ํ•จ (๊ณผ๊ฑฐ ์ด๋ฒคํŠธ ๋ฌด์‹œ) await().atMost(Duration.ofSeconds(2)) .until(() -> { - Optional finalMetrics = productMetricsRepository.findById(productId); - return finalMetrics.isPresent() && finalMetrics.get().getViewCount()==1L; + Optional finalMetrics = productMetricsRepository + .findByProductIdAndMetricDate(productId, today); + return finalMetrics.isPresent() && finalMetrics.get().getViewCount() == 1L; }); // ๊ณผ๊ฑฐ ์ด๋ฒคํŠธ๋„ ๋ฉฑ๋“ฑ์„ฑ ํ…Œ์ด๋ธ”์—๋Š” ๊ธฐ๋ก๋˜์–ด์•ผ ํ•จ @@ -223,6 +234,7 @@ void shouldIgnoreOldEvents() throws Exception { void shouldInitializeNewMetricFields() throws Exception { // Given Long productId = 6L; + LocalDate today = LocalDate.now(); String eventId = "new-metrics-test-" + System.currentTimeMillis(); ProductViewPayloadV1 payload = new ProductViewPayloadV1(productId, 100L); @@ -242,7 +254,8 @@ void shouldInitializeNewMetricFields() throws Exception { // Then - ์ƒˆ๋กœ์šด ๋ฉ”ํŠธ๋ฆญ ํ•„๋“œ๋“ค์ด 0์œผ๋กœ ์ดˆ๊ธฐํ™”๋˜์–ด์•ผ ํ•จ await().atMost(Duration.ofSeconds(10)) .untilAsserted(() -> { - Optional metrics = productMetricsRepository.findById(productId); + Optional metrics = productMetricsRepository + .findByProductIdAndMetricDate(productId, today); assertThat(metrics).isPresent(); ProductMetricsEntity entity = metrics.get(); diff --git a/modules/jpa/src/main/java/com/loopers/domain/metrics/ProductMetricsEntity.java b/modules/jpa/src/main/java/com/loopers/domain/metrics/ProductMetricsEntity.java new file mode 100644 index 000000000..cba44c95a --- /dev/null +++ b/modules/jpa/src/main/java/com/loopers/domain/metrics/ProductMetricsEntity.java @@ -0,0 +1,122 @@ +package com.loopers.domain.metrics; + +import java.time.LocalDate; +import java.time.ZonedDateTime; +import java.util.Objects; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import jakarta.persistence.Column; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; + +/** + * ์ƒํ’ˆ ๋ฉ”ํŠธ๋ฆญ ์—”ํ‹ฐํ‹ฐ (์ผ๊ฐ„ ์ง‘๊ณ„) + *

+ * ์ƒํ’ˆ๋ณ„ ์ผ๊ฐ„ ๋ฉ”ํŠธ๋ฆญ์„ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. + * ๋ณตํ•ฉํ‚ค(product_id + metric_date)๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ผ๊ฐ„ ์ง‘๊ณ„๋ฅผ ๊ตฌ๋ถ„ํ•ฉ๋‹ˆ๋‹ค. + * + * @author hyunjikoh + * @since 2025. 12. 16. + */ +@Entity +@Getter +@Table(name = "product_metrics") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class ProductMetricsEntity { + + @EmbeddedId + private ProductMetricsId id; + + @Column(name = "view_count", nullable = false) + private long viewCount = 0L; + + @Column(name = "like_count", nullable = false) + private long likeCount = 0L; + + @Column(name = "sales_count", nullable = false) + private long salesCount = 0L; + + @Column(name = "order_count", nullable = false) + private long orderCount = 0L; + + @Column(name = "last_event_at") + private ZonedDateTime lastEventAt; + + private ProductMetricsEntity(Long productId, LocalDate metricDate) { + Objects.requireNonNull(productId, "์ƒํ’ˆ ID๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."); + Objects.requireNonNull(metricDate, "๋ฉ”ํŠธ๋ฆญ ๋‚ ์งœ๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."); + this.id = ProductMetricsId.of(productId, metricDate); + } + + /** + * ์ƒํ’ˆ ๋ฉ”ํŠธ๋ฆญ ์—”ํ‹ฐํ‹ฐ ์ƒ์„ฑ + * + * @param productId ์ƒํ’ˆ ID + * @param metricDate ๋ฉ”ํŠธ๋ฆญ ๋‚ ์งœ + * @return ProductMetricsEntity ์ธ์Šคํ„ด์Šค + */ + public static ProductMetricsEntity create(Long productId, LocalDate metricDate) { + return new ProductMetricsEntity(productId, metricDate); + } + + // === ํŽธ์˜ ๋ฉ”์„œ๋“œ === + + /** + * ์ƒํ’ˆ ID ์กฐํšŒ + */ + public Long getProductId() { + return id.getProductId(); + } + + /** + * ๋ฉ”ํŠธ๋ฆญ ๋‚ ์งœ ์กฐํšŒ + */ + public LocalDate getMetricDate() { + return id.getMetricDate(); + } + + // === ๋น„์ฆˆ๋‹ˆ์Šค ๋ฉ”์„œ๋“œ === + + /** + * ์กฐํšŒ์ˆ˜ ์ฆ๊ฐ€ + * + * @param eventTime ์ด๋ฒคํŠธ ๋ฐœ์ƒ ์‹œ๊ฐ„ + */ + public void incrementView(ZonedDateTime eventTime) { + this.viewCount += 1; + this.lastEventAt = eventTime; + } + + /** + * ์ข‹์•„์š” ์ˆ˜ ๋ณ€๊ฒฝ + *

+ * ์ข‹์•„์š” ์ˆ˜๋Š” 0 ๋ฏธ๋งŒ์œผ๋กœ ๋‚ด๋ ค๊ฐ€์ง€ ์•Š๋„๋ก ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค. + * + * @param delta ๋ณ€๊ฒฝ๋Ÿ‰ (์–‘์ˆ˜: ์ฆ๊ฐ€, ์Œ์ˆ˜: ๊ฐ์†Œ) + * @param eventTime ์ด๋ฒคํŠธ ๋ฐœ์ƒ ์‹œ๊ฐ„ + */ + public void applyLikeDelta(int delta, ZonedDateTime eventTime) { + long next = this.likeCount + delta; + this.likeCount = Math.max(0, next); + this.lastEventAt = eventTime; + } + + /** + * ํŒ๋งค๋Ÿ‰ ์ฆ๊ฐ€ + * + * @param quantity ํŒ๋งค ์ˆ˜๋Ÿ‰ + * @param eventTime ์ด๋ฒคํŠธ ๋ฐœ์ƒ ์‹œ๊ฐ„ + */ + public void addSales(int quantity, ZonedDateTime eventTime) { + if (quantity <= 0) { + return; + } + this.salesCount += quantity; + this.orderCount += 1; + this.lastEventAt = eventTime; + } +} diff --git a/modules/jpa/src/main/java/com/loopers/domain/metrics/ProductMetricsId.java b/modules/jpa/src/main/java/com/loopers/domain/metrics/ProductMetricsId.java new file mode 100644 index 000000000..0fbc00ff9 --- /dev/null +++ b/modules/jpa/src/main/java/com/loopers/domain/metrics/ProductMetricsId.java @@ -0,0 +1,57 @@ +package com.loopers.domain.metrics; + +import java.io.Serial; +import java.io.Serializable; +import java.time.LocalDate; +import java.util.Objects; + +import lombok.AccessLevel; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; + +/** + * ์ƒํ’ˆ ๋ฉ”ํŠธ๋ฆญ ๋ณตํ•ฉ PK + *

+ * product_id + metric_date ์กฐํ•ฉ์œผ๋กœ ์ผ๊ฐ„ ์ง‘๊ณ„๋ฅผ ๊ตฌ๋ถ„ํ•ฉ๋‹ˆ๋‹ค. + * Hibernate 6.x ๊ถŒ์žฅ ๋ฐฉ์‹์ธ @Embeddable + @EmbeddedId ํŒจํ„ด์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. + * + * @author hyunjikoh + * @since 2025. 12. 31. + */ +@Embeddable +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@EqualsAndHashCode +public class ProductMetricsId implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Column(name = "product_id", nullable = false) + private Long productId; + + @Column(name = "metric_date", nullable = false) + private LocalDate metricDate; + + private ProductMetricsId(Long productId, LocalDate metricDate) { + Objects.requireNonNull(productId, "์ƒํ’ˆ ID๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."); + Objects.requireNonNull(metricDate, "๋ฉ”ํŠธ๋ฆญ ๋‚ ์งœ๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."); + this.productId = productId; + this.metricDate = metricDate; + } + + /** + * ๋ณตํ•ฉํ‚ค ์ƒ์„ฑ ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ + * + * @param productId ์ƒํ’ˆ ID + * @param metricDate ๋ฉ”ํŠธ๋ฆญ ๋‚ ์งœ + * @return ProductMetricsId ์ธ์Šคํ„ด์Šค + */ + public static ProductMetricsId of(Long productId, LocalDate metricDate) { + return new ProductMetricsId(productId, metricDate); + } +} diff --git a/modules/jpa/src/main/java/com/loopers/domain/metrics/ProductMetricsRepository.java b/modules/jpa/src/main/java/com/loopers/domain/metrics/ProductMetricsRepository.java new file mode 100644 index 000000000..34d68bc54 --- /dev/null +++ b/modules/jpa/src/main/java/com/loopers/domain/metrics/ProductMetricsRepository.java @@ -0,0 +1,68 @@ +package com.loopers.domain.metrics; + +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; + +/** + * ์ƒํ’ˆ ๋ฉ”ํŠธ๋ฆญ Repository ์ธํ„ฐํŽ˜์ด์Šค + *

+ * Domain ๊ณ„์ธต์˜ ์ˆœ์ˆ˜ํ•œ Repository ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + * Infrastructure ๊ณ„์ธต์—์„œ JPA๋กœ ๊ตฌํ˜„๋ฉ๋‹ˆ๋‹ค. + * + * @author hyunjikoh + * @since 2025. 12. 16. + */ +public interface ProductMetricsRepository { + + /** + * ๋ฉ”ํŠธ๋ฆญ ์ €์žฅ + * + * @param metrics ์ €์žฅํ•  ๋ฉ”ํŠธ๋ฆญ ์—”ํ‹ฐํ‹ฐ + * @return ์ €์žฅ๋œ ๋ฉ”ํŠธ๋ฆญ ์—”ํ‹ฐํ‹ฐ + */ + ProductMetricsEntity save(ProductMetricsEntity metrics); + + /** + * ๋ณตํ•ฉํ‚ค๋กœ ๋ฉ”ํŠธ๋ฆญ ์กฐํšŒ + * + * @param id ๋ณตํ•ฉํ‚ค (productId + metricDate) + * @return ๋ฉ”ํŠธ๋ฆญ ์—”ํ‹ฐํ‹ฐ + */ + Optional findById(ProductMetricsId id); + + /** + * ์ƒํ’ˆ ID์™€ ๋‚ ์งœ๋กœ ๋ฉ”ํŠธ๋ฆญ ์กฐํšŒ + * + * @param productId ์ƒํ’ˆ ID + * @param metricDate ๋ฉ”ํŠธ๋ฆญ ๋‚ ์งœ + * @return ๋ฉ”ํŠธ๋ฆญ ์—”ํ‹ฐํ‹ฐ + */ + Optional findByProductIdAndMetricDate(Long productId, LocalDate metricDate); + + /** + * ๊ธฐ๊ฐ„๋ณ„ ๋ฉ”ํŠธ๋ฆญ ์กฐํšŒ (๋ฐฐ์น˜์šฉ) + * + * @param startDate ์‹œ์ž‘ ๋‚ ์งœ + * @param endDate ์ข…๋ฃŒ ๋‚ ์งœ + * @return ๋ฉ”ํŠธ๋ฆญ ์—”ํ‹ฐํ‹ฐ ๋ชฉ๋ก + */ + List findByMetricDateBetween(LocalDate startDate, LocalDate endDate); + + /** + * ํŠน์ • ๋‚ ์งœ์˜ ์ „์ฒด ๋ฉ”ํŠธ๋ฆญ ์กฐํšŒ + * + * @param metricDate ๋ฉ”ํŠธ๋ฆญ ๋‚ ์งœ + * @return ๋ฉ”ํŠธ๋ฆญ ์—”ํ‹ฐํ‹ฐ ๋ชฉ๋ก + */ + List findByMetricDate(LocalDate metricDate); + + /** + * ๊ธฐ๊ฐ„๋ณ„ ์ƒํ’ˆ ์ง‘๊ณ„ (๋ฐฐ์น˜์šฉ - GROUP BY) + * + * @param startDate ์‹œ์ž‘ ๋‚ ์งœ + * @param endDate ์ข…๋ฃŒ ๋‚ ์งœ + * @return ์ง‘๊ณ„ ๊ฒฐ๊ณผ (productId, viewCount, likeCount, salesCount, orderCount) + */ + List aggregateByDateRange(LocalDate startDate, LocalDate endDate); +} diff --git a/modules/jpa/src/test/java/com/loopers/domain/metrics/ProductMetricsEntityUnitTest.java b/modules/jpa/src/test/java/com/loopers/domain/metrics/ProductMetricsEntityUnitTest.java new file mode 100644 index 000000000..eaca5818a --- /dev/null +++ b/modules/jpa/src/test/java/com/loopers/domain/metrics/ProductMetricsEntityUnitTest.java @@ -0,0 +1,207 @@ +package com.loopers.domain.metrics; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.time.LocalDate; +import java.time.ZonedDateTime; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +/** + * ProductMetricsEntity ๋‹จ์œ„ ํ…Œ์ŠคํŠธ + * + * @author hyunjikoh + * @since 2025. 12. 31. + */ +@DisplayName("ProductMetricsEntity ๋‹จ์œ„ ํ…Œ์ŠคํŠธ") +class ProductMetricsEntityUnitTest { + + @Nested + @DisplayName("์—”ํ‹ฐํ‹ฐ ์ƒ์„ฑ") + class ์—”ํ‹ฐํ‹ฐ_์ƒ์„ฑ { + + @Test + @DisplayName("์œ ํšจํ•œ ์ •๋ณด๋กœ ๋ฉ”ํŠธ๋ฆญ ์—”ํ‹ฐํ‹ฐ๋ฅผ ์ƒ์„ฑํ•˜๋ฉด ์„ฑ๊ณตํ•œ๋‹ค") + void should_create_metrics_entity_successfully_with_valid_information() { + // given + Long productId = 1L; + LocalDate metricDate = LocalDate.of(2024, 12, 31); + + // when + ProductMetricsEntity entity = ProductMetricsEntity.create(productId, metricDate); + + // then + assertThat(entity).isNotNull(); + assertThat(entity.getProductId()).isEqualTo(productId); + assertThat(entity.getMetricDate()).isEqualTo(metricDate); + assertThat(entity.getViewCount()).isZero(); + assertThat(entity.getLikeCount()).isZero(); + assertThat(entity.getSalesCount()).isZero(); + assertThat(entity.getOrderCount()).isZero(); + } + + @Test + @DisplayName("์ƒํ’ˆ ID๊ฐ€ null์ด๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค") + void should_throw_exception_when_product_id_is_null() { + // given + LocalDate metricDate = LocalDate.of(2024, 12, 31); + + // when & then + assertThatThrownBy(() -> ProductMetricsEntity.create(null, metricDate)) + .isInstanceOf(NullPointerException.class) + .hasMessage("์ƒํ’ˆ ID๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."); + } + + @Test + @DisplayName("๋ฉ”ํŠธ๋ฆญ ๋‚ ์งœ๊ฐ€ null์ด๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค") + void should_throw_exception_when_metric_date_is_null() { + // given + Long productId = 1L; + + // when & then + assertThatThrownBy(() -> ProductMetricsEntity.create(productId, null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("๋ฉ”ํŠธ๋ฆญ ๋‚ ์งœ๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."); + } + } + + @Nested + @DisplayName("์กฐํšŒ์ˆ˜ ์ฆ๊ฐ€") + class ์กฐํšŒ์ˆ˜_์ฆ๊ฐ€ { + + @Test + @DisplayName("์กฐํšŒ์ˆ˜๊ฐ€ 1 ์ฆ๊ฐ€ํ•œ๋‹ค") + void should_increment_view_count_by_one() { + // given + ProductMetricsEntity entity = ProductMetricsEntity.create(1L, LocalDate.now()); + ZonedDateTime eventTime = ZonedDateTime.now(); + + // when + entity.incrementView(eventTime); + + // then + assertThat(entity.getViewCount()).isEqualTo(1L); + assertThat(entity.getLastEventAt()).isEqualTo(eventTime); + } + + @Test + @DisplayName("์—ฌ๋Ÿฌ ๋ฒˆ ํ˜ธ์ถœํ•˜๋ฉด ์กฐํšŒ์ˆ˜๊ฐ€ ๋ˆ„์ ๋œ๋‹ค") + void should_accumulate_view_count_on_multiple_calls() { + // given + ProductMetricsEntity entity = ProductMetricsEntity.create(1L, LocalDate.now()); + ZonedDateTime eventTime = ZonedDateTime.now(); + + // when + entity.incrementView(eventTime); + entity.incrementView(eventTime); + entity.incrementView(eventTime); + + // then + assertThat(entity.getViewCount()).isEqualTo(3L); + } + } + + @Nested + @DisplayName("์ข‹์•„์š” ์ˆ˜ ๋ณ€๊ฒฝ") + class ์ข‹์•„์š”_์ˆ˜_๋ณ€๊ฒฝ { + + @Test + @DisplayName("์ข‹์•„์š” ์ˆ˜๊ฐ€ ์ฆ๊ฐ€ํ•œ๋‹ค") + void should_increase_like_count() { + // given + ProductMetricsEntity entity = ProductMetricsEntity.create(1L, LocalDate.now()); + ZonedDateTime eventTime = ZonedDateTime.now(); + + // when + entity.applyLikeDelta(5, eventTime); + + // then + assertThat(entity.getLikeCount()).isEqualTo(5L); + } + + @Test + @DisplayName("์ข‹์•„์š” ์ˆ˜๊ฐ€ ๊ฐ์†Œํ•œ๋‹ค") + void should_decrease_like_count() { + // given + ProductMetricsEntity entity = ProductMetricsEntity.create(1L, LocalDate.now()); + ZonedDateTime eventTime = ZonedDateTime.now(); + entity.applyLikeDelta(10, eventTime); + + // when + entity.applyLikeDelta(-3, eventTime); + + // then + assertThat(entity.getLikeCount()).isEqualTo(7L); + } + + @Test + @DisplayName("์ข‹์•„์š” ์ˆ˜๋Š” 0 ๋ฏธ๋งŒ์œผ๋กœ ๋‚ด๋ ค๊ฐ€์ง€ ์•Š๋Š”๋‹ค") + void should_not_go_below_zero() { + // given + ProductMetricsEntity entity = ProductMetricsEntity.create(1L, LocalDate.now()); + ZonedDateTime eventTime = ZonedDateTime.now(); + entity.applyLikeDelta(5, eventTime); + + // when + entity.applyLikeDelta(-10, eventTime); + + // then + assertThat(entity.getLikeCount()).isZero(); + } + } + + @Nested + @DisplayName("ํŒ๋งค๋Ÿ‰ ์ฆ๊ฐ€") + class ํŒ๋งค๋Ÿ‰_์ฆ๊ฐ€ { + + @Test + @DisplayName("ํŒ๋งค๋Ÿ‰๊ณผ ์ฃผ๋ฌธ ๊ฑด์ˆ˜๊ฐ€ ์ฆ๊ฐ€ํ•œ๋‹ค") + void should_increase_sales_and_order_count() { + // given + ProductMetricsEntity entity = ProductMetricsEntity.create(1L, LocalDate.now()); + ZonedDateTime eventTime = ZonedDateTime.now(); + + // when + entity.addSales(5, eventTime); + + // then + assertThat(entity.getSalesCount()).isEqualTo(5L); + assertThat(entity.getOrderCount()).isEqualTo(1L); + } + + @Test + @DisplayName("0 ์ดํ•˜์˜ ์ˆ˜๋Ÿ‰์€ ๋ฌด์‹œ๋œ๋‹ค") + void should_ignore_zero_or_negative_quantity() { + // given + ProductMetricsEntity entity = ProductMetricsEntity.create(1L, LocalDate.now()); + ZonedDateTime eventTime = ZonedDateTime.now(); + + // when + entity.addSales(0, eventTime); + entity.addSales(-5, eventTime); + + // then + assertThat(entity.getSalesCount()).isZero(); + assertThat(entity.getOrderCount()).isZero(); + } + + @Test + @DisplayName("์—ฌ๋Ÿฌ ๋ฒˆ ํ˜ธ์ถœํ•˜๋ฉด ํŒ๋งค๋Ÿ‰๊ณผ ์ฃผ๋ฌธ ๊ฑด์ˆ˜๊ฐ€ ๋ˆ„์ ๋œ๋‹ค") + void should_accumulate_sales_and_order_count() { + // given + ProductMetricsEntity entity = ProductMetricsEntity.create(1L, LocalDate.now()); + ZonedDateTime eventTime = ZonedDateTime.now(); + + // when + entity.addSales(3, eventTime); + entity.addSales(2, eventTime); + + // then + assertThat(entity.getSalesCount()).isEqualTo(5L); + assertThat(entity.getOrderCount()).isEqualTo(2L); + } + } +} From 9121d96006b6e0af569a7c7ad7ac99fc7952a098 Mon Sep 17 00:00:00 2001 From: hyujikoh Date: Fri, 2 Jan 2026 16:03:36 +0900 Subject: [PATCH 69/85] =?UTF-8?q?feat(metrics):=20=EC=83=81=ED=92=88=20?= =?UTF-8?q?=EB=A9=94=ED=8A=B8=EB=A6=AD=20=EC=97=94=ED=8B=B0=ED=8B=B0=20?= =?UTF-8?q?=EB=B0=8F=20=EB=A6=AC=ED=8F=AC=EC=A7=80=ED=86=A0=EB=A6=AC=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 - ์ƒํ’ˆ๋ณ„ ์ผ๊ฐ„ ๋ฉ”ํŠธ๋ฆญ์„ ์ €์žฅํ•˜๋Š” ProductMetricsEntity ์ถ”๊ฐ€ - ๋ณตํ•ฉํ‚ค๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฉ”ํŠธ๋ฆญ์„ ๊ตฌ๋ถ„ํ•˜๋Š” ProductMetricsId ์ถ”๊ฐ€ - ์ƒํ’ˆ ID์™€ ๋‚ ์งœ๋กœ ๋ฉ”ํŠธ๋ฆญ์„ ์กฐํšŒํ•˜๋Š” ๋ฉ”์„œ๋“œ ๊ตฌํ˜„ - ๋ฉ”ํŠธ๋ฆญ ์ €์žฅ ๋ฐ ์กฐํšŒ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜๋Š” ProductMetricsRepository ์ถ”๊ฐ€ - ๋ฉ”ํŠธ๋ฆญ ๊ด€๋ จ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ์ฒ˜๋ฆฌํ•˜๋Š” ProductMetricsService ์ถ”๊ฐ€ - ๋ฉ”ํŠธ๋ฆญ ์—”ํ‹ฐํ‹ฐ์— ๋Œ€ํ•œ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ์ถ”๊ฐ€ --- .../application/product/ProductFacade.java | 120 ++++++++++++++++++ .../api/product/ProductV1ApiSpec.java | 21 +++ .../api/product/ProductV1Controller.java | 17 ++- .../main/java/com/loopers/support/Uris.java | 1 + 4 files changed, 158 insertions(+), 1 deletion(-) diff --git a/apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java index 1d912a891..f6247d037 100644 --- a/apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java @@ -13,6 +13,8 @@ import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; +import com.loopers.application.ranking.MonthlyRankingService; +import com.loopers.application.ranking.WeeklyRankingService; import com.loopers.cache.CacheStrategy; import com.loopers.cache.RankingRedisService; import com.loopers.cache.dto.CachePayloads.RankingItem; @@ -21,6 +23,9 @@ import com.loopers.domain.like.LikeService; import com.loopers.domain.product.*; import com.loopers.domain.product.dto.ProductSearchFilter; +import com.loopers.domain.ranking.MonthlyRankEntity; +import com.loopers.domain.ranking.RankingPeriod; +import com.loopers.domain.ranking.WeeklyRankEntity; import com.loopers.domain.tracking.UserBehaviorTracker; import com.loopers.domain.user.UserService; @@ -46,6 +51,8 @@ public class ProductFacade { private final BrandService brandService; private final UserBehaviorTracker behaviorTracker; private final RankingRedisService rankingRedisService; + private final WeeklyRankingService weeklyRankingService; + private final MonthlyRankingService monthlyRankingService; /** * ๋„๋ฉ”์ธ ์„œ๋น„์Šค์—์„œ MV ์—”ํ‹ฐํ‹ฐ๋ฅผ ์กฐํšŒํ•˜๊ณ , Facade์—์„œ DTO๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. @@ -181,6 +188,119 @@ public Page getRankingProducts(Pageable pageable, LocalDate date) { return new PageImpl<>(sortedProducts, pageable, totalCount); } + /** + * ๊ธฐ๊ฐ„๋ณ„ ๋žญํ‚น ์ƒํ’ˆ ๋ชฉ๋ก ์กฐํšŒ + * + * @param period ๋žญํ‚น ๊ธฐ๊ฐ„ (DAILY, WEEKLY, MONTHLY) + * @param pageable ํŽ˜์ด์ง• ์ •๋ณด + * @param date ์กฐํšŒ ๋‚ ์งœ (DAILY์šฉ, null์ด๋ฉด ์˜ค๋Š˜) + * @param yearWeek ์กฐํšŒ ์ฃผ์ฐจ (WEEKLY์šฉ, ์˜ˆ: "2024-W52") + * @param yearMonth ์กฐํšŒ ์›” (MONTHLY์šฉ, ์˜ˆ: "2024-12") + * @return ๋žญํ‚น ์ƒํ’ˆ ๋ชฉ๋ก + */ + @Transactional(readOnly = true) + public Page getRankingProductsByPeriod( + RankingPeriod period, + Pageable pageable, + LocalDate date, + String yearWeek, + String yearMonth) { + + return switch (period) { + case DAILY -> getRankingProducts(pageable, date); + case WEEKLY -> getWeeklyRankingProducts(pageable, yearWeek); + case MONTHLY -> getMonthlyRankingProducts(pageable, yearMonth); + }; + } + + /** + * ์ฃผ๊ฐ„ ๋žญํ‚น ์ƒํ’ˆ ๋ชฉ๋ก ์กฐํšŒ + * + * @param pageable ํŽ˜์ด์ง• ์ •๋ณด + * @param yearWeek ์กฐํšŒ ์ฃผ์ฐจ (์˜ˆ: "2024-W52") + * @return ์ฃผ๊ฐ„ ๋žญํ‚น ์ƒํ’ˆ ๋ชฉ๋ก + */ + @Transactional(readOnly = true) + public Page getWeeklyRankingProducts(Pageable pageable, String yearWeek) { + if (yearWeek == null || yearWeek.trim().isEmpty()) { + log.warn("์ฃผ๊ฐ„ ๋žญํ‚น ์กฐํšŒ ์‹œ yearWeek ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค"); + return Page.empty(pageable); + } + + // 1. ์ฃผ๊ฐ„ ๋žญํ‚น ์กฐํšŒ + Page weeklyRankings = weeklyRankingService.getWeeklyRanking(yearWeek, pageable); + + if (weeklyRankings.isEmpty()) { + log.debug("์ฃผ๊ฐ„ ๋žญํ‚น ๋ฐ์ดํ„ฐ ์—†์Œ: yearWeek={}", yearWeek); + return Page.empty(pageable); + } + + // 2. ์ƒํ’ˆ ID ๋ชฉ๋ก ์ถ”์ถœ + List productIds = weeklyRankings.getContent().stream() + .map(WeeklyRankEntity::getProductId) + .collect(Collectors.toList()); + + // 3. ์ƒํ’ˆ ์ •๋ณด ์กฐํšŒ (MV ์‚ฌ์šฉ) + List products = mvService.getByIds(productIds); + + // 4. ๋žญํ‚น ์ˆœ์„œ๋Œ€๋กœ ์ •๋ ฌ + List sortedProducts = productIds.stream() + .map(productId -> products.stream() + .filter(p -> p.getProductId().equals(productId)) + .findFirst() + .map(ProductInfo::from) + .orElse(null)) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + // 5. Page ๊ฐ์ฒด ์ƒ์„ฑ + return new PageImpl<>(sortedProducts, pageable, weeklyRankings.getTotalElements()); + } + + /** + * ์›”๊ฐ„ ๋žญํ‚น ์ƒํ’ˆ ๋ชฉ๋ก ์กฐํšŒ + * + * @param pageable ํŽ˜์ด์ง• ์ •๋ณด + * @param yearMonth ์กฐํšŒ ์›” (์˜ˆ: "2024-12") + * @return ์›”๊ฐ„ ๋žญํ‚น ์ƒํ’ˆ ๋ชฉ๋ก + */ + @Transactional(readOnly = true) + public Page getMonthlyRankingProducts(Pageable pageable, String yearMonth) { + if (yearMonth == null || yearMonth.trim().isEmpty()) { + log.warn("์›”๊ฐ„ ๋žญํ‚น ์กฐํšŒ ์‹œ yearMonth ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค"); + return Page.empty(pageable); + } + + // 1. ์›”๊ฐ„ ๋žญํ‚น ์กฐํšŒ + Page monthlyRankings = monthlyRankingService.getMonthlyRanking(yearMonth, pageable); + + if (monthlyRankings.isEmpty()) { + log.debug("์›”๊ฐ„ ๋žญํ‚น ๋ฐ์ดํ„ฐ ์—†์Œ: yearMonth={}", yearMonth); + return Page.empty(pageable); + } + + // 2. ์ƒํ’ˆ ID ๋ชฉ๋ก ์ถ”์ถœ + List productIds = monthlyRankings.getContent().stream() + .map(MonthlyRankEntity::getProductId) + .collect(Collectors.toList()); + + // 3. ์ƒํ’ˆ ์ •๋ณด ์กฐํšŒ (MV ์‚ฌ์šฉ) + List products = mvService.getByIds(productIds); + + // 4. ๋žญํ‚น ์ˆœ์„œ๋Œ€๋กœ ์ •๋ ฌ + List sortedProducts = productIds.stream() + .map(productId -> products.stream() + .filter(p -> p.getProductId().equals(productId)) + .findFirst() + .map(ProductInfo::from) + .orElse(null)) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + // 5. Page ๊ฐ์ฒด ์ƒ์„ฑ + return new PageImpl<>(sortedProducts, pageable, monthlyRankings.getTotalElements()); + } + /** * ์ƒํ’ˆ์„ ์‚ญ์ œํ•ฉ๋‹ˆ๋‹ค. *

diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1ApiSpec.java index daed7d359..da2e801e5 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1ApiSpec.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1ApiSpec.java @@ -13,6 +13,7 @@ import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestParam; +import com.loopers.domain.ranking.RankingPeriod; import com.loopers.interfaces.api.ApiResponse; import com.loopers.interfaces.api.common.PageResponse; @@ -48,6 +49,26 @@ ApiResponse> getRankingProducts( @RequestParam(required = false) LocalDate date ); + @Operation( + summary = "๊ธฐ๊ฐ„๋ณ„ ๋žญํ‚น ์ƒํ’ˆ ๋ชฉ๋ก ์กฐํšŒ", + description = "๊ธฐ๊ฐ„๋ณ„(์ผ๊ฐ„/์ฃผ๊ฐ„/์›”๊ฐ„) ๋žญํ‚น ์ƒํ’ˆ ๋ชฉ๋ก์„ ํŽ˜์ด์ง•ํ•˜์—ฌ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค." + ) + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "์กฐํšŒ ์„ฑ๊ณต"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "์ž˜๋ชป๋œ ์š”์ฒญ") + }) + ApiResponse> getRankingProductsByPeriod( + @Parameter(description = "๋žญํ‚น ๊ธฐ๊ฐ„", example = "DAILY", required = true) + @RequestParam RankingPeriod period, + @PageableDefault(size = 20) Pageable pageable, + @Parameter(description = "์กฐํšŒ ๋‚ ์งœ (DAILY์šฉ, yyyy-MM-dd ํ˜•์‹)", example = "2025-12-23") + @RequestParam(required = false) LocalDate date, + @Parameter(description = "์กฐํšŒ ์ฃผ์ฐจ (WEEKLY์šฉ, yyyy-Www ํ˜•์‹)", example = "2024-W52") + @RequestParam(required = false) String yearWeek, + @Parameter(description = "์กฐํšŒ ์›” (MONTHLY์šฉ, yyyy-MM ํ˜•์‹)", example = "2024-12") + @RequestParam(required = false) String yearMonth + ); + @Operation( summary = "์ƒํ’ˆ ์ƒ์„ธ ์กฐํšŒ", description = "์ƒํ’ˆ ID๋กœ ์ƒํ’ˆ ์ƒ์„ธ ์ •๋ณด๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. ๋กœ๊ทธ์ธํ•œ ์‚ฌ์šฉ์ž์˜ ๊ฒฝ์šฐ ์ข‹์•„์š” ์—ฌ๋ถ€๋„ ํ•จ๊ป˜ ์กฐํšŒ๋ฉ๋‹ˆ๋‹ค." diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java index 85bac4c0c..7abe836fb 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java @@ -10,8 +10,8 @@ import com.loopers.application.product.ProductDetailInfo; import com.loopers.application.product.ProductFacade; import com.loopers.application.product.ProductInfo; -import com.loopers.cache.dto.CachePayloads.RankingItem; import com.loopers.domain.product.dto.ProductSearchFilter; +import com.loopers.domain.ranking.RankingPeriod; import com.loopers.interfaces.api.ApiResponse; import com.loopers.interfaces.api.common.PageResponse; import com.loopers.support.Uris; @@ -48,6 +48,21 @@ public ApiResponse> getRankingPr return ApiResponse.success(PageResponse.from(responsePage)); } + @GetMapping(Uris.Ranking.GET_RANKING_BY_PERIOD) + @Override + public ApiResponse> getRankingProductsByPeriod( + @RequestParam RankingPeriod period, + @PageableDefault(size = 20) Pageable pageable, + @RequestParam(required = false) LocalDate date, + @RequestParam(required = false) String yearWeek, + @RequestParam(required = false) String yearMonth + ) { + Page products = productFacade.getRankingProductsByPeriod( + period, pageable, date, yearWeek, yearMonth); + Page responsePage = products.map(ProductV1Dtos.ProductListResponse::from); + return ApiResponse.success(PageResponse.from(responsePage)); + } + @GetMapping(Uris.Product.GET_DETAIL) @Override public ApiResponse getProductDetail( diff --git a/apps/commerce-api/src/main/java/com/loopers/support/Uris.java b/apps/commerce-api/src/main/java/com/loopers/support/Uris.java index 598e71128..9409a5158 100644 --- a/apps/commerce-api/src/main/java/com/loopers/support/Uris.java +++ b/apps/commerce-api/src/main/java/com/loopers/support/Uris.java @@ -88,6 +88,7 @@ private Ranking() { public static final String BASE = API_V1 + "/rankings"; public static final String GET_RANKING = BASE; + public static final String GET_RANKING_BY_PERIOD = BASE + "/period"; } /** From 1034b9837d5e100822b3f84e700e0d228066a63c Mon Sep 17 00:00:00 2001 From: hyujikoh Date: Fri, 2 Jan 2026 16:04:43 +0900 Subject: [PATCH 70/85] =?UTF-8?q?feat(ranking):=20=EC=A3=BC=EA=B0=84=20?= =?UTF-8?q?=EB=B0=8F=20=EC=9B=94=EA=B0=84=20=EB=9E=AD=ED=82=B9=20=EC=A7=91?= =?UTF-8?q?=EA=B3=84=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ์ฃผ๊ฐ„ ๋ฐ ์›”๊ฐ„ ๋žญํ‚น ์ง‘๊ณ„ Job ์„ค์ • - ๋ฉ”ํŠธ๋ฆญ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ๊ธฐ ๋ฐ ์ ์ˆ˜ ๊ณ„์‚ฐ๊ธฐ ๊ตฌํ˜„ - ๋žญํ‚น ์ง‘๊ณ„ ๋ฐ ์ €์žฅ ๋กœ์ง ์ถ”๊ฐ€ - ๋‚ ์งœ ๋ฒ”์œ„ ํŒŒ์‹ฑ ์œ ํ‹ธ๋ฆฌํ‹ฐ ๊ตฌํ˜„ - ๊ด€๋ จ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ถ”๊ฐ€ --- .../job/ranking/MonthlyRankingJobConfig.java | 78 +++++++ .../job/ranking/WeeklyRankingJobConfig.java | 78 +++++++ .../job/ranking/dto/RankingAggregation.java | 83 +++++++ .../ranking/processor/RankingProcessor.java | 24 ++ .../ranking/reader/MonthlyMetricsReader.java | 73 +++++++ .../ranking/reader/WeeklyMetricsReader.java | 73 +++++++ .../job/ranking/support/DateRangeParser.java | 73 +++++++ .../ranking/support/RankingAggregator.java | 60 +++++ .../job/ranking/support/ScoreCalculator.java | 42 ++++ .../job/ranking/writer/WeeklyRankWriter.java | 74 +++++++ .../metrics/ProductMetricsJpaRepository.java | 39 ++++ .../metrics/ProductMetricsRepositoryImpl.java | 53 +++++ .../ranking/MonthlyRankJpaRepository.java | 39 ++++ .../ranking/MonthlyRankRepositoryImpl.java | 46 ++++ .../ranking/WeeklyRankJpaRepository.java | 39 ++++ .../ranking/WeeklyRankRepositoryImpl.java | 46 ++++ .../batch/job/ranking/ManualBatchJobTest.java | 73 +++++++ .../job/ranking/RankingBatchE2ETest.java | 206 ++++++++++++++++++ .../dto/RankingAggregationUnitTest.java | 172 +++++++++++++++ .../support/DateRangeParserUnitTest.java | 161 ++++++++++++++ .../support/RankingAggregatorUnitTest.java | 134 ++++++++++++ .../support/ScoreCalculatorUnitTest.java | 102 +++++++++ .../writer/MonthlyRankWriterUnitTest.java | 123 +++++++++++ .../writer/WeeklyRankWriterUnitTest.java | 125 +++++++++++ 24 files changed, 2016 insertions(+) create mode 100644 apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/MonthlyRankingJobConfig.java create mode 100644 apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/WeeklyRankingJobConfig.java create mode 100644 apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/dto/RankingAggregation.java create mode 100644 apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/processor/RankingProcessor.java create mode 100644 apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/reader/MonthlyMetricsReader.java create mode 100644 apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/reader/WeeklyMetricsReader.java create mode 100644 apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/support/DateRangeParser.java create mode 100644 apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/support/RankingAggregator.java create mode 100644 apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/support/ScoreCalculator.java create mode 100644 apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/writer/WeeklyRankWriter.java create mode 100644 apps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.java create mode 100644 apps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java create mode 100644 apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/MonthlyRankJpaRepository.java create mode 100644 apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/MonthlyRankRepositoryImpl.java create mode 100644 apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/WeeklyRankJpaRepository.java create mode 100644 apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/WeeklyRankRepositoryImpl.java create mode 100644 apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/ManualBatchJobTest.java create mode 100644 apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/RankingBatchE2ETest.java create mode 100644 apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/dto/RankingAggregationUnitTest.java create mode 100644 apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/support/DateRangeParserUnitTest.java create mode 100644 apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/support/RankingAggregatorUnitTest.java create mode 100644 apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/support/ScoreCalculatorUnitTest.java create mode 100644 apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/writer/MonthlyRankWriterUnitTest.java create mode 100644 apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/writer/WeeklyRankWriterUnitTest.java diff --git a/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/MonthlyRankingJobConfig.java b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/MonthlyRankingJobConfig.java new file mode 100644 index 000000000..2c699ca57 --- /dev/null +++ b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/MonthlyRankingJobConfig.java @@ -0,0 +1,78 @@ +package com.loopers.batch.job.ranking; + +import com.loopers.batch.job.ranking.dto.RankingAggregation; +import com.loopers.batch.job.ranking.processor.RankingProcessor; +import com.loopers.batch.job.ranking.reader.MonthlyMetricsReader; +import com.loopers.batch.job.ranking.writer.MonthlyRankWriter; +import com.loopers.batch.listener.JobListener; +import com.loopers.batch.listener.StepMonitorListener; +import lombok.RequiredArgsConstructor; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.configuration.annotation.JobScope; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.batch.core.launch.support.RunIdIncrementer; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.PlatformTransactionManager; + +/** + * ์›”๊ฐ„ ๋žญํ‚น ๋ฐฐ์น˜ Job ์„ค์ • + * + * ์‹คํ–‰ ๋ฐฉ๋ฒ•: + * java -jar commerce-batch.jar --spring.batch.job.name=monthlyRankingJob --yearMonth=2024-12 + */ +@Configuration +@ConditionalOnProperty(name = "spring.batch.job.name", havingValue = MonthlyRankingJobConfig.JOB_NAME) +@RequiredArgsConstructor +public class MonthlyRankingJobConfig { + + public static final String JOB_NAME = "monthlyRankingJob"; + private static final String STEP_NAME = "monthlyAggregationStep"; + private static final int CHUNK_SIZE = 100; // TOP 100์ด๋ฏ€๋กœ ํ•œ ๋ฒˆ์— ์ฒ˜๋ฆฌ + + private final JobRepository jobRepository; + private final PlatformTransactionManager transactionManager; + private final JobListener jobListener; + private final StepMonitorListener stepMonitorListener; + private final MonthlyMetricsReader monthlyMetricsReader; + private final RankingProcessor rankingProcessor; + private final MonthlyRankWriter monthlyRankWriter; + + /** + * ์›”๊ฐ„ ๋žญํ‚น ์ง‘๊ณ„ Job + * + * @return ์›”๊ฐ„ ๋žญํ‚น Job + */ + @Bean(JOB_NAME) + public Job monthlyRankingJob() { + return new JobBuilder(JOB_NAME, jobRepository) + .incrementer(new RunIdIncrementer()) + .start(monthlyAggregationStep()) + .listener(jobListener) + .build(); + } + + /** + * ์›”๊ฐ„ ๋žญํ‚น ์ง‘๊ณ„ Step + * - Reader: ์›”๊ฐ„ ๋ฉ”ํŠธ๋ฆญ ๋ฐ์ดํ„ฐ ์ง‘๊ณ„ ๋ฐ TOP 100 ์„ ๋ณ„ + * - Processor: ์ถ”๊ฐ€ ๊ฐ€๊ณต (ํ˜„์žฌ๋Š” pass-through) + * - Writer: mv_product_rank_monthly ํ…Œ์ด๋ธ”์— ์ €์žฅ + * + * @return ์›”๊ฐ„ ์ง‘๊ณ„ Step + */ + @Bean(STEP_NAME) + @JobScope + public Step monthlyAggregationStep() { + return new StepBuilder(STEP_NAME, jobRepository) + .chunk(CHUNK_SIZE, transactionManager) + .reader(monthlyMetricsReader) + .processor(rankingProcessor) + .writer(monthlyRankWriter) + .listener(stepMonitorListener) + .build(); + } +} \ No newline at end of file diff --git a/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/WeeklyRankingJobConfig.java b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/WeeklyRankingJobConfig.java new file mode 100644 index 000000000..06c2354bf --- /dev/null +++ b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/WeeklyRankingJobConfig.java @@ -0,0 +1,78 @@ +package com.loopers.batch.job.ranking; + +import com.loopers.batch.job.ranking.dto.RankingAggregation; +import com.loopers.batch.job.ranking.processor.RankingProcessor; +import com.loopers.batch.job.ranking.reader.WeeklyMetricsReader; +import com.loopers.batch.job.ranking.writer.WeeklyRankWriter; +import com.loopers.batch.listener.JobListener; +import com.loopers.batch.listener.StepMonitorListener; +import lombok.RequiredArgsConstructor; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.configuration.annotation.JobScope; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.batch.core.launch.support.RunIdIncrementer; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.PlatformTransactionManager; + +/** + * ์ฃผ๊ฐ„ ๋žญํ‚น ๋ฐฐ์น˜ Job ์„ค์ • + * + * ์‹คํ–‰ ๋ฐฉ๋ฒ•: + * java -jar commerce-batch.jar --spring.batch.job.name=weeklyRankingJob --yearWeek=2024-W52 + */ +@Configuration +@ConditionalOnProperty(name = "spring.batch.job.name", havingValue = WeeklyRankingJobConfig.JOB_NAME) +@RequiredArgsConstructor +public class WeeklyRankingJobConfig { + + public static final String JOB_NAME = "weeklyRankingJob"; + private static final String STEP_NAME = "weeklyAggregationStep"; + private static final int CHUNK_SIZE = 100; // TOP 100์ด๋ฏ€๋กœ ํ•œ ๋ฒˆ์— ์ฒ˜๋ฆฌ + + private final JobRepository jobRepository; + private final PlatformTransactionManager transactionManager; + private final JobListener jobListener; + private final StepMonitorListener stepMonitorListener; + private final WeeklyMetricsReader weeklyMetricsReader; + private final RankingProcessor rankingProcessor; + private final WeeklyRankWriter weeklyRankWriter; + + /** + * ์ฃผ๊ฐ„ ๋žญํ‚น ์ง‘๊ณ„ Job + * + * @return ์ฃผ๊ฐ„ ๋žญํ‚น Job + */ + @Bean(JOB_NAME) + public Job weeklyRankingJob() { + return new JobBuilder(JOB_NAME, jobRepository) + .incrementer(new RunIdIncrementer()) + .start(weeklyAggregationStep()) + .listener(jobListener) + .build(); + } + + /** + * ์ฃผ๊ฐ„ ๋žญํ‚น ์ง‘๊ณ„ Step + * - Reader: 7์ผ์น˜ ๋ฉ”ํŠธ๋ฆญ ๋ฐ์ดํ„ฐ ์ง‘๊ณ„ ๋ฐ TOP 100 ์„ ๋ณ„ + * - Processor: ์ถ”๊ฐ€ ๊ฐ€๊ณต (ํ˜„์žฌ๋Š” pass-through) + * - Writer: mv_product_rank_weekly ํ…Œ์ด๋ธ”์— ์ €์žฅ + * + * @return ์ฃผ๊ฐ„ ์ง‘๊ณ„ Step + */ + @Bean(STEP_NAME) + @JobScope + public Step weeklyAggregationStep() { + return new StepBuilder(STEP_NAME, jobRepository) + .chunk(CHUNK_SIZE, transactionManager) + .reader(weeklyMetricsReader) + .processor(rankingProcessor) + .writer(weeklyRankWriter) + .listener(stepMonitorListener) + .build(); + } +} \ No newline at end of file diff --git a/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/dto/RankingAggregation.java b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/dto/RankingAggregation.java new file mode 100644 index 000000000..eee7f526d --- /dev/null +++ b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/dto/RankingAggregation.java @@ -0,0 +1,83 @@ +package com.loopers.batch.job.ranking.dto; + +import com.loopers.batch.job.ranking.support.ScoreCalculator; +import lombok.Getter; + +/** + * ๋žญํ‚น ์ง‘๊ณ„ ๊ฒฐ๊ณผ DTO + * - DB ์ง‘๊ณ„ ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ๋ฅผ ๋‹ด๋Š” ๋ถˆ๋ณ€ ๊ฐ์ฒด + * - ์ ์ˆ˜ ๊ณ„์‚ฐ ๋ฐ ์ˆœ์œ„ ๋ถ€์—ฌ ๊ธฐ๋Šฅ ํฌํ•จ + */ +@Getter +public class RankingAggregation { + + private final Long productId; + private final long viewCount; + private final long likeCount; + private final long salesCount; + private final long orderCount; + private final long totalScore; + private int rankPosition; // ๊ฐ€๋ณ€ ํ•„๋“œ (์ˆœ์œ„ ๋ถ€์—ฌ์šฉ) + + private RankingAggregation(Long productId, long viewCount, long likeCount, + long salesCount, long orderCount, long totalScore) { + this.productId = productId; + this.viewCount = viewCount; + this.likeCount = likeCount; + this.salesCount = salesCount; + this.orderCount = orderCount; + this.totalScore = totalScore; + this.rankPosition = 0; // ์ดˆ๊ธฐ๊ฐ’ + } + + /** + * DB ์ง‘๊ณ„ ๊ฒฐ๊ณผ๋กœ๋ถ€ํ„ฐ RankingAggregation์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param row DB ์ง‘๊ณ„ ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ (Object[] ํ˜•ํƒœ) + * @param calculator ์ ์ˆ˜ ๊ณ„์‚ฐ๊ธฐ + * @return ์ƒ์„ฑ๋œ RankingAggregation ๊ฐ์ฒด + * @throws IllegalArgumentException row๊ฐ€ null์ด๊ฑฐ๋‚˜ ํ˜•์‹์ด ์ž˜๋ชป๋œ ๊ฒฝ์šฐ + */ + public static RankingAggregation from(Object[] row, ScoreCalculator calculator) { + if (row == null || row.length < 5) { + throw new IllegalArgumentException("์ง‘๊ณ„ ๊ฒฐ๊ณผ ๋ฐฐ์—ด์ด null์ด๊ฑฐ๋‚˜ ๊ธธ์ด๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค."); + } + + try { + Long productId = (Long) row[0]; + long viewCount = ((Number) row[1]).longValue(); + long likeCount = ((Number) row[2]).longValue(); + long salesCount = ((Number) row[3]).longValue(); + long orderCount = ((Number) row[4]).longValue(); + + long totalScore = calculator.calculate(viewCount, likeCount, salesCount, orderCount); + + return new RankingAggregation(productId, viewCount, likeCount, salesCount, orderCount, totalScore); + } catch (ClassCastException | NullPointerException e) { + throw new IllegalArgumentException("์ง‘๊ณ„ ๊ฒฐ๊ณผ ๋ฐ์ดํ„ฐ ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค.", e); + } + } + + /** + * ์ˆœ์œ„๋ฅผ ๋ถ€์—ฌํ•ฉ๋‹ˆ๋‹ค. + * + * @param rank ๋ถ€์—ฌํ•  ์ˆœ์œ„ (1~100) + * @throws IllegalArgumentException ์ˆœ์œ„๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์€ ๊ฒฝ์šฐ + */ + public void assignRank(int rank) { + if (rank < 1 || rank > 100) { + throw new IllegalArgumentException( + String.format("์ˆœ์œ„๋Š” 1~100 ๋ฒ”์œ„์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค. (์ž…๋ ฅ๊ฐ’: %d)", rank)); + } + this.rankPosition = rank; + } + + /** + * ๋””๋ฒ„๊น…์šฉ ๋ฌธ์ž์—ด ํ‘œํ˜„ + */ + @Override + public String toString() { + return String.format("RankingAggregation{productId=%d, score=%d, rank=%d}", + productId, totalScore, rankPosition); + } +} \ No newline at end of file diff --git a/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/processor/RankingProcessor.java b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/processor/RankingProcessor.java new file mode 100644 index 000000000..da29368de --- /dev/null +++ b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/processor/RankingProcessor.java @@ -0,0 +1,24 @@ +package com.loopers.batch.job.ranking.processor; + +import com.loopers.batch.job.ranking.dto.RankingAggregation; +import org.springframework.batch.item.ItemProcessor; +import org.springframework.stereotype.Component; + +/** + * ๋žญํ‚น ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ๊ธฐ + * - Reader์—์„œ ์ด๋ฏธ ์ ์ˆ˜ ๊ณ„์‚ฐ ๋ฐ ์ˆœ์œ„ ๋ถ€์—ฌ๊ฐ€ ์™„๋ฃŒ๋จ + * - ์ถ”๊ฐ€ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์ด ํ•„์š”ํ•  ๋•Œ ํ™•์žฅ ํฌ์ธํŠธ๋กœ ํ™œ์šฉ + */ +@Component +public class RankingProcessor implements ItemProcessor { + + @Override + public RankingAggregation process(RankingAggregation item) throws Exception { + // Reader์—์„œ ์ด๋ฏธ ์ˆœ์œ„ ๋ถ€์—ฌ๋จ + // ์ถ”๊ฐ€ ๊ฐ€๊ณต์ด ํ•„์š”ํ•˜๋ฉด ์—ฌ๊ธฐ์„œ ์ฒ˜๋ฆฌ + // ์˜ˆ: ํŠน์ • ์กฐ๊ฑด ํ•„ํ„ฐ๋ง, ๋ฐ์ดํ„ฐ ๋ณด์ • ๋“ฑ + + // ํ˜„์žฌ๋Š” ๋‹จ์ˆœ ํ†ต๊ณผ (pass-through) + return item; + } +} \ No newline at end of file diff --git a/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/reader/MonthlyMetricsReader.java b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/reader/MonthlyMetricsReader.java new file mode 100644 index 000000000..865193589 --- /dev/null +++ b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/reader/MonthlyMetricsReader.java @@ -0,0 +1,73 @@ +package com.loopers.batch.job.ranking.reader; + +import com.loopers.batch.job.ranking.dto.RankingAggregation; +import com.loopers.batch.job.ranking.support.DateRangeParser; +import com.loopers.batch.job.ranking.support.RankingAggregator; +import com.loopers.domain.metrics.ProductMetricsRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.batch.item.ItemReader; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.time.LocalDate; +import java.util.Iterator; +import java.util.List; + +/** + * ์›”๊ฐ„ ๋ฉ”ํŠธ๋ฆญ Reader + * - ์ง€์ •๋œ ์›”์˜ ์ „์ฒด ์ผ๊ฐ„ ๋ฐ์ดํ„ฐ๋ฅผ ์ง‘๊ณ„ + * - TOP 100 ๋žญํ‚น ์ƒ์„ฑ ๋ฐ ์ˆœ์œ„ ๋ถ€์—ฌ + */ +@Slf4j +@StepScope +@Component +@RequiredArgsConstructor +public class MonthlyMetricsReader implements ItemReader { + + private final ProductMetricsRepository productMetricsRepository; + private final DateRangeParser dateRangeParser; + private final RankingAggregator rankingAggregator; + + private Iterator iterator; + + @Value("#{jobParameters['yearMonth']}") + private String yearMonth; // e.g., "2024-12" + + @Override + public RankingAggregation read() throws Exception { + if (iterator == null) { + initializeIterator(); + } + + return iterator.hasNext() ? iterator.next() : null; + } + + private void initializeIterator() { + log.info("์›”๊ฐ„ ๋žญํ‚น ์ง‘๊ณ„ ์‹œ์ž‘: yearMonth={}", yearMonth); + + try { + // 1. ์›” โ†’ ๋‚ ์งœ ๋ฒ”์œ„ ๋ณ€ํ™˜ + LocalDate[] dateRange = dateRangeParser.parseYearMonth(yearMonth); + LocalDate startDate = dateRange[0]; + LocalDate endDate = dateRange[1]; + + log.info("์ง‘๊ณ„ ๊ธฐ๊ฐ„: {} ~ {}", startDate, endDate); + + // 2. DB์—์„œ ์ง‘๊ณ„ ์ฟผ๋ฆฌ ์‹คํ–‰ + List aggregationResults = productMetricsRepository.aggregateByDateRange(startDate, endDate); + log.info("์ง‘๊ณ„ ๋Œ€์ƒ ์ƒํ’ˆ ์ˆ˜: {}", aggregationResults.size()); + + // 3. ๋žญํ‚น ์ฒ˜๋ฆฌ (์ •๋ ฌ + TOP 100 + ์ˆœ์œ„ ๋ถ€์—ฌ) + List rankings = rankingAggregator.processRankings(aggregationResults); + log.info("์ƒ์„ฑ๋œ ๋žญํ‚น ์ˆ˜: {}", rankings.size()); + + iterator = rankings.iterator(); + + } catch (Exception e) { + log.error("์›”๊ฐ„ ๋žญํ‚น ์ง‘๊ณ„ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: yearMonth={}", yearMonth, e); + throw new RuntimeException("์›”๊ฐ„ ๋žญํ‚น ์ง‘๊ณ„ ์‹คํŒจ", e); + } + } +} \ No newline at end of file diff --git a/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/reader/WeeklyMetricsReader.java b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/reader/WeeklyMetricsReader.java new file mode 100644 index 000000000..3530e3f4f --- /dev/null +++ b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/reader/WeeklyMetricsReader.java @@ -0,0 +1,73 @@ +package com.loopers.batch.job.ranking.reader; + +import com.loopers.batch.job.ranking.dto.RankingAggregation; +import com.loopers.batch.job.ranking.support.DateRangeParser; +import com.loopers.batch.job.ranking.support.RankingAggregator; +import com.loopers.domain.metrics.ProductMetricsRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.batch.item.ItemReader; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.time.LocalDate; +import java.util.Iterator; +import java.util.List; + +/** + * ์ฃผ๊ฐ„ ๋ฉ”ํŠธ๋ฆญ Reader + * - ์ง€์ •๋œ ์ฃผ์ฐจ์˜ 7์ผ๊ฐ„ ๋ฐ์ดํ„ฐ๋ฅผ ์ง‘๊ณ„ + * - TOP 100 ๋žญํ‚น ์ƒ์„ฑ ๋ฐ ์ˆœ์œ„ ๋ถ€์—ฌ + */ +@Slf4j +@StepScope +@Component +@RequiredArgsConstructor +public class WeeklyMetricsReader implements ItemReader { + + private final ProductMetricsRepository productMetricsRepository; + private final DateRangeParser dateRangeParser; + private final RankingAggregator rankingAggregator; + + private Iterator iterator; + + @Value("#{jobParameters['yearWeek']}") + private String yearWeek; // e.g., "2024-W52" + + @Override + public RankingAggregation read() throws Exception { + if (iterator == null) { + initializeIterator(); + } + + return iterator.hasNext() ? iterator.next() : null; + } + + private void initializeIterator() { + log.info("์ฃผ๊ฐ„ ๋žญํ‚น ์ง‘๊ณ„ ์‹œ์ž‘: yearWeek={}", yearWeek); + + try { + // 1. ์ฃผ์ฐจ โ†’ ๋‚ ์งœ ๋ฒ”์œ„ ๋ณ€ํ™˜ + LocalDate[] dateRange = dateRangeParser.parseYearWeek(yearWeek); + LocalDate startDate = dateRange[0]; + LocalDate endDate = dateRange[1]; + + log.info("์ง‘๊ณ„ ๊ธฐ๊ฐ„: {} ~ {}", startDate, endDate); + + // 2. DB์—์„œ ์ง‘๊ณ„ ์ฟผ๋ฆฌ ์‹คํ–‰ + List aggregationResults = productMetricsRepository.aggregateByDateRange(startDate, endDate); + log.info("์ง‘๊ณ„ ๋Œ€์ƒ ์ƒํ’ˆ ์ˆ˜: {}", aggregationResults.size()); + + // 3. ๋žญํ‚น ์ฒ˜๋ฆฌ (์ •๋ ฌ + TOP 100 + ์ˆœ์œ„ ๋ถ€์—ฌ) + List rankings = rankingAggregator.processRankings(aggregationResults); + log.info("์ƒ์„ฑ๋œ ๋žญํ‚น ์ˆ˜: {}", rankings.size()); + + iterator = rankings.iterator(); + + } catch (Exception e) { + log.error("์ฃผ๊ฐ„ ๋žญํ‚น ์ง‘๊ณ„ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: yearWeek={}", yearWeek, e); + throw new RuntimeException("์ฃผ๊ฐ„ ๋žญํ‚น ์ง‘๊ณ„ ์‹คํŒจ", e); + } + } +} \ No newline at end of file diff --git a/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/support/DateRangeParser.java b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/support/DateRangeParser.java new file mode 100644 index 000000000..e713b42c2 --- /dev/null +++ b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/support/DateRangeParser.java @@ -0,0 +1,73 @@ +package com.loopers.batch.job.ranking.support; + +import org.springframework.stereotype.Component; + +import java.time.LocalDate; +import java.time.YearMonth; +import java.time.temporal.WeekFields; + +/** + * ๋‚ ์งœ ๋ฒ”์œ„ ํŒŒ์‹ฑ ์œ ํ‹ธ๋ฆฌํ‹ฐ + * - yearWeek (e.g., "2024-W52") โ†’ ์ฃผ๊ฐ„ ๋‚ ์งœ ๋ฒ”์œ„ + * - yearMonth (e.g., "2024-12") โ†’ ์›”๊ฐ„ ๋‚ ์งœ ๋ฒ”์œ„ + */ +@Component +public class DateRangeParser { + + /** + * yearWeek ๋ฌธ์ž์—ด์„ ์ฃผ๊ฐ„ ๋‚ ์งœ ๋ฒ”์œ„๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param yearWeek "2024-W52" ํ˜•์‹์˜ ๋ฌธ์ž์—ด + * @return [startDate, endDate] ๋ฐฐ์—ด + * @throws IllegalArgumentException ์ž˜๋ชป๋œ ํ˜•์‹์ธ ๊ฒฝ์šฐ + */ + public LocalDate[] parseYearWeek(String yearWeek) { + if (yearWeek == null || !yearWeek.matches("\\d{4}-W\\d{1,2}")) { + throw new IllegalArgumentException( + String.format("์ž˜๋ชป๋œ yearWeek ํ˜•์‹์ž…๋‹ˆ๋‹ค. ์˜ˆ์ƒ: '2024-W52', ์‹ค์ œ: '%s'", yearWeek)); + } + + try { + String[] parts = yearWeek.split("-W"); + int year = Integer.parseInt(parts[0]); + int week = Integer.parseInt(parts[1]); + + // ISO ์ฃผ์ฐจ ์‹œ์Šคํ…œ ์‚ฌ์šฉ (์›”์š”์ผ ์‹œ์ž‘) + WeekFields weekFields = WeekFields.ISO; + LocalDate startOfWeek = LocalDate.of(year, 1, 1) + .with(weekFields.weekOfYear(), week) + .with(weekFields.dayOfWeek(), 1); + LocalDate endOfWeek = startOfWeek.plusDays(6); + + return new LocalDate[]{startOfWeek, endOfWeek}; + } catch (Exception e) { + throw new IllegalArgumentException( + String.format("yearWeek ํŒŒ์‹ฑ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: %s", yearWeek), e); + } + } + + /** + * yearMonth ๋ฌธ์ž์—ด์„ ์›”๊ฐ„ ๋‚ ์งœ ๋ฒ”์œ„๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param yearMonth "2024-12" ํ˜•์‹์˜ ๋ฌธ์ž์—ด + * @return [startDate, endDate] ๋ฐฐ์—ด + * @throws IllegalArgumentException ์ž˜๋ชป๋œ ํ˜•์‹์ธ ๊ฒฝ์šฐ + */ + public LocalDate[] parseYearMonth(String yearMonth) { + if (yearMonth == null || !yearMonth.matches("\\d{4}-\\d{2}")) { + throw new IllegalArgumentException( + String.format("์ž˜๋ชป๋œ yearMonth ํ˜•์‹์ž…๋‹ˆ๋‹ค. ์˜ˆ์ƒ: '2024-12', ์‹ค์ œ: '%s'", yearMonth)); + } + + try { + YearMonth ym = YearMonth.parse(yearMonth); + LocalDate startOfMonth = ym.atDay(1); + LocalDate endOfMonth = ym.atEndOfMonth(); + + return new LocalDate[]{startOfMonth, endOfMonth}; + } catch (Exception e) { + throw new IllegalArgumentException( + String.format("yearMonth ํŒŒ์‹ฑ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: %s", yearMonth), e); + } + } +} \ No newline at end of file diff --git a/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/support/RankingAggregator.java b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/support/RankingAggregator.java new file mode 100644 index 000000000..cf3b2ace7 --- /dev/null +++ b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/support/RankingAggregator.java @@ -0,0 +1,60 @@ +package com.loopers.batch.job.ranking.support; + +import com.loopers.batch.job.ranking.dto.RankingAggregation; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.Comparator; +import java.util.List; + +/** + * ๋žญํ‚น ์ง‘๊ณ„ ์ฒ˜๋ฆฌ๊ธฐ + * - ์ง‘๊ณ„ ๊ฒฐ๊ณผ๋ฅผ ์ ์ˆ˜ ๊ธฐ์ค€์œผ๋กœ ์ •๋ ฌ + * - TOP 100 ํ•„ํ„ฐ๋ง + * - ์ˆœ์œ„ ๋ถ€์—ฌ + */ +@Component +@RequiredArgsConstructor +public class RankingAggregator { + + private static final int TOP_RANK_LIMIT = 100; + + private final ScoreCalculator scoreCalculator; + + /** + * DB ์ง‘๊ณ„ ๊ฒฐ๊ณผ๋ฅผ ๋žญํ‚น์œผ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param aggregationResults DB ์ง‘๊ณ„ ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ ๋ชฉ๋ก + * @return TOP 100 ๋žญํ‚น ๋ชฉ๋ก (์ˆœ์œ„ ๋ถ€์—ฌ ์™„๋ฃŒ) + */ + public List processRankings(List aggregationResults) { + if (aggregationResults == null || aggregationResults.isEmpty()) { + return List.of(); + } + + // 1. DTO ๋ณ€ํ™˜ + ์ ์ˆ˜ ๊ณ„์‚ฐ + List aggregations = aggregationResults.stream() + .map(row -> RankingAggregation.from(row, scoreCalculator)) + .toList(); + + // 2. ์ ์ˆ˜ ๊ธฐ์ค€ ๋‚ด๋ฆผ์ฐจ์ˆœ ์ •๋ ฌ + TOP 100 ํ•„ํ„ฐ๋ง + List topRankings = aggregations.stream() + .sorted(Comparator.comparingLong(RankingAggregation::getTotalScore).reversed()) + .limit(TOP_RANK_LIMIT) + .toList(); + + // 3. ์ˆœ์œ„ ๋ถ€์—ฌ (1์œ„๋ถ€ํ„ฐ ์‹œ์ž‘) + for (int i = 0; i < topRankings.size(); i++) { + topRankings.get(i).assignRank(i + 1); + } + + return topRankings; + } + + /** + * TOP ๋žญํ‚น ์ œํ•œ ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + public int getTopRankLimit() { + return TOP_RANK_LIMIT; + } +} \ No newline at end of file diff --git a/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/support/ScoreCalculator.java b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/support/ScoreCalculator.java new file mode 100644 index 000000000..3d79dfcd0 --- /dev/null +++ b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/support/ScoreCalculator.java @@ -0,0 +1,42 @@ +package com.loopers.batch.job.ranking.support; + +import org.springframework.stereotype.Component; + +/** + * ๋žญํ‚น ์ ์ˆ˜ ๊ณ„์‚ฐ๊ธฐ + * - Redis ZSET๊ณผ ๋™์ผํ•œ ๊ฐ€์ค‘์น˜ ์ ์šฉ + * - ์ ์ˆ˜ = viewCount*1 + likeCount*3 + salesCount*5 + orderCount*2 + */ +@Component +public class ScoreCalculator { + + // ๊ฐ€์ค‘์น˜ ์ƒ์ˆ˜ (Redis ZSET๊ณผ ๋™์ผํ•˜๊ฒŒ ์œ ์ง€) + private static final int VIEW_WEIGHT = 1; + private static final int LIKE_WEIGHT = 3; + private static final int SALES_WEIGHT = 5; + private static final int ORDER_WEIGHT = 2; + + /** + * ๋ฉ”ํŠธ๋ฆญ ๋ฐ์ดํ„ฐ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๋žญํ‚น ์ ์ˆ˜๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + * + * @param viewCount ์กฐํšŒ์ˆ˜ + * @param likeCount ์ข‹์•„์š”์ˆ˜ + * @param salesCount ํŒ๋งค์ˆ˜๋Ÿ‰ + * @param orderCount ์ฃผ๋ฌธ์ˆ˜ + * @return ๊ณ„์‚ฐ๋œ ์ด ์ ์ˆ˜ + */ + public long calculate(long viewCount, long likeCount, long salesCount, long orderCount) { + return viewCount * VIEW_WEIGHT + + likeCount * LIKE_WEIGHT + + salesCount * SALES_WEIGHT + + orderCount * ORDER_WEIGHT; + } + + /** + * ๊ฐ€์ค‘์น˜ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. (ํ…Œ์ŠคํŠธ ๋ฐ ๋””๋ฒ„๊น…์šฉ) + */ + public String getWeightInfo() { + return String.format("VIEW=%d, LIKE=%d, SALES=%d, ORDER=%d", + VIEW_WEIGHT, LIKE_WEIGHT, SALES_WEIGHT, ORDER_WEIGHT); + } +} \ No newline at end of file diff --git a/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/writer/WeeklyRankWriter.java b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/writer/WeeklyRankWriter.java new file mode 100644 index 000000000..cb831467a --- /dev/null +++ b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/writer/WeeklyRankWriter.java @@ -0,0 +1,74 @@ +package com.loopers.batch.job.ranking.writer; + +import com.loopers.batch.job.ranking.dto.RankingAggregation; +import com.loopers.domain.ranking.WeeklyRankEntity; +import com.loopers.domain.ranking.WeeklyRankRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.batch.item.Chunk; +import org.springframework.batch.item.ItemWriter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * ์ฃผ๊ฐ„ ๋žญํ‚น Writer + * - RankingAggregation์„ WeeklyRankEntity๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ์ €์žฅ + * - ๋ฉฑ๋“ฑ์„ฑ ๋ณด์žฅ์„ ์œ„ํ•ด ๊ธฐ์กด ๋ฐ์ดํ„ฐ ์‚ญ์ œ ํ›„ ์ €์žฅ + */ +@Slf4j +@StepScope +@Component +@RequiredArgsConstructor +public class WeeklyRankWriter implements ItemWriter { + + private final WeeklyRankRepository weeklyRankRepository; + + @Value("#{jobParameters['yearWeek']}") + private String yearWeek; + + @Override + public void write(Chunk chunk) throws Exception { + List items = chunk.getItems(); + + if (items.isEmpty()) { + log.info("์ €์žฅํ•  ์ฃผ๊ฐ„ ๋žญํ‚น ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค: yearWeek={}", yearWeek); + return; + } + + log.info("์ฃผ๊ฐ„ ๋žญํ‚น ์ €์žฅ ์‹œ์ž‘: yearWeek={}, ์ €์žฅํ•  ๋ฐ์ดํ„ฐ ์ˆ˜={}", yearWeek, items.size()); + + try { + // 1. ๊ธฐ์กด ๋ฐ์ดํ„ฐ ์‚ญ์ œ (๋ฉฑ๋“ฑ์„ฑ ๋ณด์žฅ) + long deletedCount = weeklyRankRepository.deleteByYearWeek(yearWeek); + log.info("๊ธฐ์กด ์ฃผ๊ฐ„ ๋žญํ‚น ๋ฐ์ดํ„ฐ ์‚ญ์ œ: yearWeek={}, ์‚ญ์ œ๋œ ์ˆ˜={}", yearWeek, deletedCount); + + // 2. ์ƒˆ๋กœ์šด ๋ฐ์ดํ„ฐ ์ €์žฅ + List entities = items.stream() + .map(this::convertToEntity) + .toList(); + + List savedEntities = weeklyRankRepository.saveAll(entities); + log.info("์ฃผ๊ฐ„ ๋žญํ‚น ์ €์žฅ ์™„๋ฃŒ: yearWeek={}, ์ €์žฅ๋œ ์ˆ˜={}", yearWeek, savedEntities.size()); + + } catch (Exception e) { + log.error("์ฃผ๊ฐ„ ๋žญํ‚น ์ €์žฅ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: yearWeek={}", yearWeek, e); + throw new RuntimeException("์ฃผ๊ฐ„ ๋žญํ‚น ์ €์žฅ ์‹คํŒจ", e); + } + } + + private WeeklyRankEntity convertToEntity(RankingAggregation aggregation) { + return WeeklyRankEntity.create( + aggregation.getProductId(), + yearWeek, + aggregation.getViewCount(), + aggregation.getLikeCount(), + aggregation.getSalesCount(), + aggregation.getOrderCount(), + aggregation.getTotalScore(), + aggregation.getRankPosition() + ); + } +} \ No newline at end of file diff --git a/apps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.java b/apps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.java new file mode 100644 index 000000000..1a769a02b --- /dev/null +++ b/apps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.java @@ -0,0 +1,39 @@ +package com.loopers.infrastructure.metrics; + +import com.loopers.domain.metrics.ProductMetricsEntity; +import com.loopers.domain.metrics.ProductMetricsId; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.time.LocalDate; +import java.util.List; + +/** + * ProductMetrics JPA Repository + * - ๋ฐฐ์น˜ Job์—์„œ ์‚ฌ์šฉํ•˜๋Š” ์ง‘๊ณ„ ์ฟผ๋ฆฌ ํฌํ•จ + */ +public interface ProductMetricsJpaRepository extends JpaRepository { + + /** + * ๊ธฐ๊ฐ„๋ณ„ ์ƒํ’ˆ ์ง‘๊ณ„ (GROUP BY product_id) + * - ๋ฐฐ์น˜ Job์—์„œ ์‚ฌ์šฉํ•˜๋Š” ํ•ต์‹ฌ ์ฟผ๋ฆฌ + * + * @param startDate ์‹œ์ž‘ ๋‚ ์งœ (ํฌํ•จ) + * @param endDate ์ข…๋ฃŒ ๋‚ ์งœ (ํฌํ•จ) + * @return ์ง‘๊ณ„ ๊ฒฐ๊ณผ [productId, viewCount, likeCount, salesCount, orderCount] + */ + @Query(""" + SELECT m.id.productId, + SUM(m.viewCount), + SUM(m.likeCount), + SUM(m.salesCount), + SUM(m.orderCount) + FROM ProductMetricsEntity m + WHERE m.id.metricDate BETWEEN :startDate AND :endDate + GROUP BY m.id.productId + """) + List aggregateByDateRange( + @Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate); +} \ No newline at end of file diff --git a/apps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java b/apps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java new file mode 100644 index 000000000..9aa28b943 --- /dev/null +++ b/apps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java @@ -0,0 +1,53 @@ +package com.loopers.infrastructure.metrics; + +import com.loopers.domain.metrics.ProductMetricsEntity; +import com.loopers.domain.metrics.ProductMetricsId; +import com.loopers.domain.metrics.ProductMetricsRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; + +/** + * ProductMetrics Repository ๊ตฌํ˜„์ฒด + * - JPA Repository๋ฅผ ๋ž˜ํ•‘ํ•˜์—ฌ ๋„๋ฉ”์ธ ์ธํ„ฐํŽ˜์ด์Šค ๊ตฌํ˜„ + */ +@Repository +@RequiredArgsConstructor +public class ProductMetricsRepositoryImpl implements ProductMetricsRepository { + + private final ProductMetricsJpaRepository jpaRepository; + + @Override + public ProductMetricsEntity save(ProductMetricsEntity entity) { + return jpaRepository.save(entity); + } + + @Override + public Optional findById(ProductMetricsId id) { + return jpaRepository.findById(id); + } + + @Override + public Optional findByProductIdAndMetricDate(Long productId, LocalDate metricDate) { + ProductMetricsId id = ProductMetricsId.of(productId, metricDate); + return jpaRepository.findById(id); + } + + @Override + public List findByMetricDateBetween(LocalDate startDate, LocalDate endDate) { + return jpaRepository.findAll().stream() + .filter(entity -> { + LocalDate metricDate = entity.getMetricDate(); + return !metricDate.isBefore(startDate) && !metricDate.isAfter(endDate); + }) + .toList(); + } + + @Override + public List aggregateByDateRange(LocalDate startDate, LocalDate endDate) { + return jpaRepository.aggregateByDateRange(startDate, endDate); + } +} \ No newline at end of file diff --git a/apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/MonthlyRankJpaRepository.java b/apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/MonthlyRankJpaRepository.java new file mode 100644 index 000000000..c7cd7dc32 --- /dev/null +++ b/apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/MonthlyRankJpaRepository.java @@ -0,0 +1,39 @@ +package com.loopers.infrastructure.ranking; + +import com.loopers.domain.ranking.MonthlyRankEntity; +import com.loopers.domain.ranking.MonthlyRankId; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +/** + * ์›”๊ฐ„ ๋žญํ‚น JPA Repository + */ +public interface MonthlyRankJpaRepository extends JpaRepository { + + /** + * ํŠน์ • ์›”์˜ ๋žญํ‚น์„ ์ˆœ์œ„ ์ˆœ์œผ๋กœ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + */ + @Query("SELECT m FROM MonthlyRankEntity m WHERE m.id.yearMonth = :yearMonth ORDER BY m.rankPosition ASC") + List findByIdYearMonthOrderByRankPosition(@Param("yearMonth") String yearMonth); + + /** + * ํŠน์ • ์›”์˜ ๋žญํ‚น์„ ์ˆœ์œ„ ์ˆœ์œผ๋กœ ํŽ˜์ด์ง€๋„ค์ด์…˜ํ•˜์—ฌ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + */ + @Query("SELECT m FROM MonthlyRankEntity m WHERE m.id.yearMonth = :yearMonth ORDER BY m.rankPosition ASC") + List findByIdYearMonthOrderByRankPosition(@Param("yearMonth") String yearMonth, Pageable pageable); + + /** + * ํŠน์ • ์›”์˜ ๋ชจ๋“  ๋žญํ‚น์„ ์‚ญ์ œํ•ฉ๋‹ˆ๋‹ค. + * + * @param yearMonth ์‚ญ์ œํ•  ์›” + * @return ์‚ญ์ œ๋œ ๋ ˆ์ฝ”๋“œ ์ˆ˜ + */ + @Modifying + @Query("DELETE FROM MonthlyRankEntity m WHERE m.id.yearMonth = :yearMonth") + long deleteByIdYearMonth(@Param("yearMonth") String yearMonth); +} \ No newline at end of file diff --git a/apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/MonthlyRankRepositoryImpl.java b/apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/MonthlyRankRepositoryImpl.java new file mode 100644 index 000000000..3e3420421 --- /dev/null +++ b/apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/MonthlyRankRepositoryImpl.java @@ -0,0 +1,46 @@ +package com.loopers.infrastructure.ranking; + +import com.loopers.domain.ranking.MonthlyRankEntity; +import com.loopers.domain.ranking.MonthlyRankRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * ์›”๊ฐ„ ๋žญํ‚น Repository ๊ตฌํ˜„์ฒด + */ +@Repository +@RequiredArgsConstructor +public class MonthlyRankRepositoryImpl implements MonthlyRankRepository { + + private final MonthlyRankJpaRepository jpaRepository; + + @Override + public MonthlyRankEntity save(MonthlyRankEntity entity) { + return jpaRepository.save(entity); + } + + @Override + public List saveAll(List entities) { + return jpaRepository.saveAll(entities); + } + + @Override + public List findByYearMonth(String yearMonth) { + return jpaRepository.findByIdYearMonthOrderByRankPosition(yearMonth); + } + + @Override + public List findByYearMonthWithPagination(String yearMonth, int page, int size) { + Pageable pageable = PageRequest.of(page, size); + return jpaRepository.findByIdYearMonthOrderByRankPosition(yearMonth, pageable); + } + + @Override + public long deleteByYearMonth(String yearMonth) { + return jpaRepository.deleteByIdYearMonth(yearMonth); + } +} \ No newline at end of file diff --git a/apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/WeeklyRankJpaRepository.java b/apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/WeeklyRankJpaRepository.java new file mode 100644 index 000000000..a7d8c875d --- /dev/null +++ b/apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/WeeklyRankJpaRepository.java @@ -0,0 +1,39 @@ +package com.loopers.infrastructure.ranking; + +import com.loopers.domain.ranking.WeeklyRankEntity; +import com.loopers.domain.ranking.WeeklyRankId; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +/** + * ์ฃผ๊ฐ„ ๋žญํ‚น JPA Repository + */ +public interface WeeklyRankJpaRepository extends JpaRepository { + + /** + * ํŠน์ • ์ฃผ์ฐจ์˜ ๋žญํ‚น์„ ์ˆœ์œ„ ์ˆœ์œผ๋กœ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + */ + @Query("SELECT w FROM WeeklyRankEntity w WHERE w.id.yearWeek = :yearWeek ORDER BY w.rankPosition ASC") + List findByIdYearWeekOrderByRankPosition(@Param("yearWeek") String yearWeek); + + /** + * ํŠน์ • ์ฃผ์ฐจ์˜ ๋žญํ‚น์„ ์ˆœ์œ„ ์ˆœ์œผ๋กœ ํŽ˜์ด์ง€๋„ค์ด์…˜ํ•˜์—ฌ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + */ + @Query("SELECT w FROM WeeklyRankEntity w WHERE w.id.yearWeek = :yearWeek ORDER BY w.rankPosition ASC") + List findByIdYearWeekOrderByRankPosition(@Param("yearWeek") String yearWeek, Pageable pageable); + + /** + * ํŠน์ • ์ฃผ์ฐจ์˜ ๋ชจ๋“  ๋žญํ‚น์„ ์‚ญ์ œํ•ฉ๋‹ˆ๋‹ค. + * + * @param yearWeek ์‚ญ์ œํ•  ์ฃผ์ฐจ + * @return ์‚ญ์ œ๋œ ๋ ˆ์ฝ”๋“œ ์ˆ˜ + */ + @Modifying + @Query("DELETE FROM WeeklyRankEntity w WHERE w.id.yearWeek = :yearWeek") + long deleteByIdYearWeek(@Param("yearWeek") String yearWeek); +} \ No newline at end of file diff --git a/apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/WeeklyRankRepositoryImpl.java b/apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/WeeklyRankRepositoryImpl.java new file mode 100644 index 000000000..795292473 --- /dev/null +++ b/apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/WeeklyRankRepositoryImpl.java @@ -0,0 +1,46 @@ +package com.loopers.infrastructure.ranking; + +import com.loopers.domain.ranking.WeeklyRankEntity; +import com.loopers.domain.ranking.WeeklyRankRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * ์ฃผ๊ฐ„ ๋žญํ‚น Repository ๊ตฌํ˜„์ฒด + */ +@Repository +@RequiredArgsConstructor +public class WeeklyRankRepositoryImpl implements WeeklyRankRepository { + + private final WeeklyRankJpaRepository jpaRepository; + + @Override + public WeeklyRankEntity save(WeeklyRankEntity entity) { + return jpaRepository.save(entity); + } + + @Override + public List saveAll(List entities) { + return jpaRepository.saveAll(entities); + } + + @Override + public List findByYearWeek(String yearWeek) { + return jpaRepository.findByIdYearWeekOrderByRankPosition(yearWeek); + } + + @Override + public List findByYearWeekWithPagination(String yearWeek, int page, int size) { + Pageable pageable = PageRequest.of(page, size); + return jpaRepository.findByIdYearWeekOrderByRankPosition(yearWeek, pageable); + } + + @Override + public long deleteByYearWeek(String yearWeek) { + return jpaRepository.deleteByIdYearWeek(yearWeek); + } +} \ No newline at end of file diff --git a/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/ManualBatchJobTest.java b/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/ManualBatchJobTest.java new file mode 100644 index 000000000..92c04bd0a --- /dev/null +++ b/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/ManualBatchJobTest.java @@ -0,0 +1,73 @@ +package com.loopers.batch.job.ranking; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.JobParametersBuilder; +import org.springframework.batch.core.launch.JobLauncher; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.TestPropertySource; + +/** + * ์ˆ˜๋™ ๋ฐฐ์น˜ Job ์‹คํ–‰ ํ…Œ์ŠคํŠธ + * - ์‹ค์ œ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์„ ๋•Œ ์ˆ˜๋™์œผ๋กœ ์‹คํ–‰ํ•˜์—ฌ ๊ฒ€์ฆ + * - @Disabled๋กœ ๊ธฐ๋ณธ์ ์œผ๋กœ ๋น„ํ™œ์„ฑํ™” + */ +@SpringBootTest +@TestPropertySource(properties = { + "spring.batch.job.enabled=false" +}) +@DisplayName("์ˆ˜๋™ ๋ฐฐ์น˜ Job ์‹คํ–‰ ํ…Œ์ŠคํŠธ") +class ManualBatchJobTest { + + @Autowired + private JobLauncher jobLauncher; + + @Autowired + @Qualifier("weeklyRankingJob") + private Job weeklyRankingJob; + + @Autowired + @Qualifier("monthlyRankingJob") + private Job monthlyRankingJob; + + @Test + @Disabled("์ˆ˜๋™ ์‹คํ–‰์šฉ - ์‹ค์ œ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์„ ๋•Œ๋งŒ ํ™œ์„ฑํ™”") + @DisplayName("์ฃผ๊ฐ„ ๋žญํ‚น Job ์ˆ˜๋™ ์‹คํ–‰") + void manual_weekly_ranking_job_execution() throws Exception { + // given + JobParameters jobParameters = new JobParametersBuilder() + .addString("yearWeek", "2024-W52") + .addLong("timestamp", System.currentTimeMillis()) + .toJobParameters(); + + // when + jobLauncher.run(weeklyRankingJob, jobParameters); + + // then + System.out.println("์ฃผ๊ฐ„ ๋žญํ‚น Job ์‹คํ–‰ ์™„๋ฃŒ - 2024-W52"); + System.out.println("MV ํ…Œ์ด๋ธ”(mv_product_rank_weekly)์„ ํ™•์ธํ•˜์„ธ์š”."); + } + + @Test + @Disabled("์ˆ˜๋™ ์‹คํ–‰์šฉ - ์‹ค์ œ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์„ ๋•Œ๋งŒ ํ™œ์„ฑํ™”") + @DisplayName("์›”๊ฐ„ ๋žญํ‚น Job ์ˆ˜๋™ ์‹คํ–‰") + void manual_monthly_ranking_job_execution() throws Exception { + // given + JobParameters jobParameters = new JobParametersBuilder() + .addString("yearMonth", "2024-12") + .addLong("timestamp", System.currentTimeMillis()) + .toJobParameters(); + + // when + jobLauncher.run(monthlyRankingJob, jobParameters); + + // then + System.out.println("์›”๊ฐ„ ๋žญํ‚น Job ์‹คํ–‰ ์™„๋ฃŒ - 2024-12"); + System.out.println("MV ํ…Œ์ด๋ธ”(mv_product_rank_monthly)์„ ํ™•์ธํ•˜์„ธ์š”."); + } +} \ No newline at end of file diff --git a/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/RankingBatchE2ETest.java b/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/RankingBatchE2ETest.java new file mode 100644 index 000000000..2f78153a1 --- /dev/null +++ b/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/RankingBatchE2ETest.java @@ -0,0 +1,206 @@ +package com.loopers.batch.job.ranking; + +import com.loopers.domain.ranking.MonthlyRankEntity; +import com.loopers.domain.ranking.MonthlyRankRepository; +import com.loopers.domain.ranking.WeeklyRankEntity; +import com.loopers.domain.ranking.WeeklyRankRepository; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.batch.core.BatchStatus; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.JobParametersBuilder; +import org.springframework.batch.test.JobLauncherTestUtils; +import org.springframework.batch.test.context.SpringBatchTest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.TestPropertySource; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * ๋žญํ‚น ๋ฐฐ์น˜ Job E2E ํ…Œ์ŠคํŠธ + * - ์‹ค์ œ Job ์‹คํ–‰๋ถ€ํ„ฐ MV ํ…Œ์ด๋ธ” ์ €์žฅ๊นŒ์ง€ ์ „์ฒด ํ”Œ๋กœ์šฐ ๊ฒ€์ฆ + */ +@SpringBatchTest +@SpringBootTest +@TestPropertySource(properties = { + "spring.batch.job.enabled=false" // ์ž๋™ ์‹คํ–‰ ๋ฐฉ์ง€ +}) +@DisplayName("๋žญํ‚น ๋ฐฐ์น˜ Job E2E ํ…Œ์ŠคํŠธ") +class RankingBatchE2ETest { + + @Autowired + private JobLauncherTestUtils jobLauncherTestUtils; + + @Autowired + private WeeklyRankRepository weeklyRankRepository; + + @Autowired + private MonthlyRankRepository monthlyRankRepository; + + @AfterEach + void tearDown() { + // ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ ์ •๋ฆฌ + weeklyRankRepository.deleteByYearWeek("2024-W52"); + monthlyRankRepository.deleteByYearMonth("2024-12"); + } + + @Nested + @DisplayName("์ฃผ๊ฐ„ ๋žญํ‚น ๋ฐฐ์น˜ Job") + class ์ฃผ๊ฐ„_๋žญํ‚น_๋ฐฐ์น˜_Job { + + @Test + @DisplayName("์ฃผ๊ฐ„ ๋žญํ‚น Job์ด ์„ฑ๊ณต์ ์œผ๋กœ ์‹คํ–‰๋˜๊ณ  MV ํ…Œ์ด๋ธ”์— ๋ฐ์ดํ„ฐ๊ฐ€ ์ €์žฅ๋œ๋‹ค") + void should_execute_weekly_ranking_job_successfully() throws Exception { + // given + String yearWeek = "2024-W52"; + JobParameters jobParameters = new JobParametersBuilder() + .addString("yearWeek", yearWeek) + .addLong("timestamp", System.currentTimeMillis()) // ์œ ๋‹ˆํฌ ํŒŒ๋ผ๋ฏธํ„ฐ + .toJobParameters(); + + // when + JobExecution jobExecution = jobLauncherTestUtils.launchJob(jobParameters); + + // then + assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED); + + // MV ํ…Œ์ด๋ธ” ๊ฒ€์ฆ + List rankings = weeklyRankRepository.findByYearWeek(yearWeek); + assertThat(rankings).isNotEmpty(); + assertThat(rankings.size()).isLessThanOrEqualTo(100); // TOP 100 + + // ์ˆœ์œ„ ๊ฒ€์ฆ + if (!rankings.isEmpty()) { + assertThat(rankings.get(0).getRankPosition()).isEqualTo(1); + + // ์ ์ˆ˜ ์ˆœ ์ •๋ ฌ ๊ฒ€์ฆ + for (int i = 1; i < rankings.size(); i++) { + assertThat(rankings.get(i-1).getTotalScore()) + .isGreaterThanOrEqualTo(rankings.get(i).getTotalScore()); + } + } + } + + @Test + @DisplayName("์ž˜๋ชป๋œ yearWeek ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ Job ์‹คํ–‰ ์‹œ ์‹คํŒจํ•œ๋‹ค") + void should_fail_with_invalid_year_week_parameter() throws Exception { + // given + String invalidYearWeek = "invalid-format"; + JobParameters jobParameters = new JobParametersBuilder() + .addString("yearWeek", invalidYearWeek) + .addLong("timestamp", System.currentTimeMillis()) + .toJobParameters(); + + // when + JobExecution jobExecution = jobLauncherTestUtils.launchJob(jobParameters); + + // then + assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.FAILED); + } + } + + @Nested + @DisplayName("์›”๊ฐ„ ๋žญํ‚น ๋ฐฐ์น˜ Job") + class ์›”๊ฐ„_๋žญํ‚น_๋ฐฐ์น˜_Job { + + @Test + @DisplayName("์›”๊ฐ„ ๋žญํ‚น Job์ด ์„ฑ๊ณต์ ์œผ๋กœ ์‹คํ–‰๋˜๊ณ  MV ํ…Œ์ด๋ธ”์— ๋ฐ์ดํ„ฐ๊ฐ€ ์ €์žฅ๋œ๋‹ค") + void should_execute_monthly_ranking_job_successfully() throws Exception { + // given + String yearMonth = "2024-12"; + JobParameters jobParameters = new JobParametersBuilder() + .addString("yearMonth", yearMonth) + .addLong("timestamp", System.currentTimeMillis()) + .toJobParameters(); + + // when + JobExecution jobExecution = jobLauncherTestUtils.launchJob(jobParameters); + + // then + assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED); + + // MV ํ…Œ์ด๋ธ” ๊ฒ€์ฆ + List rankings = monthlyRankRepository.findByYearMonth(yearMonth); + assertThat(rankings).isNotEmpty(); + assertThat(rankings.size()).isLessThanOrEqualTo(100); // TOP 100 + + // ์ˆœ์œ„ ๊ฒ€์ฆ + if (!rankings.isEmpty()) { + assertThat(rankings.get(0).getRankPosition()).isEqualTo(1); + + // ์ ์ˆ˜ ์ˆœ ์ •๋ ฌ ๊ฒ€์ฆ + for (int i = 1; i < rankings.size(); i++) { + assertThat(rankings.get(i-1).getTotalScore()) + .isGreaterThanOrEqualTo(rankings.get(i).getTotalScore()); + } + } + } + + @Test + @DisplayName("์ž˜๋ชป๋œ yearMonth ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ Job ์‹คํ–‰ ์‹œ ์‹คํŒจํ•œ๋‹ค") + void should_fail_with_invalid_year_month_parameter() throws Exception { + // given + String invalidYearMonth = "invalid-format"; + JobParameters jobParameters = new JobParametersBuilder() + .addString("yearMonth", invalidYearMonth) + .addLong("timestamp", System.currentTimeMillis()) + .toJobParameters(); + + // when + JobExecution jobExecution = jobLauncherTestUtils.launchJob(jobParameters); + + // then + assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.FAILED); + } + } + + @Nested + @DisplayName("๋ฉฑ๋“ฑ์„ฑ ๊ฒ€์ฆ") + class ๋ฉฑ๋“ฑ์„ฑ_๊ฒ€์ฆ { + + @Test + @DisplayName("๋™์ผํ•œ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ Job์„ ์žฌ์‹คํ–‰ํ•ด๋„ ๊ฒฐ๊ณผ๊ฐ€ ๋™์ผํ•˜๋‹ค") + void should_produce_same_result_when_job_is_rerun() throws Exception { + // given + String yearWeek = "2024-W52"; + JobParameters jobParameters = new JobParametersBuilder() + .addString("yearWeek", yearWeek) + .addLong("timestamp", System.currentTimeMillis()) + .toJobParameters(); + + // when - ์ฒซ ๋ฒˆ์งธ ์‹คํ–‰ + JobExecution firstExecution = jobLauncherTestUtils.launchJob(jobParameters); + List firstResult = weeklyRankRepository.findByYearWeek(yearWeek); + + // when - ๋‘ ๋ฒˆ์งธ ์‹คํ–‰ (๋‹ค๋ฅธ timestamp๋กœ) + JobParameters secondJobParameters = new JobParametersBuilder() + .addString("yearWeek", yearWeek) + .addLong("timestamp", System.currentTimeMillis() + 1000) + .toJobParameters(); + JobExecution secondExecution = jobLauncherTestUtils.launchJob(secondJobParameters); + List secondResult = weeklyRankRepository.findByYearWeek(yearWeek); + + // then + assertThat(firstExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED); + assertThat(secondExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED); + + // ๊ฒฐ๊ณผ ๋™์ผ์„ฑ ๊ฒ€์ฆ + assertThat(firstResult.size()).isEqualTo(secondResult.size()); + + for (int i = 0; i < firstResult.size(); i++) { + WeeklyRankEntity first = firstResult.get(i); + WeeklyRankEntity second = secondResult.get(i); + + assertThat(first.getProductId()).isEqualTo(second.getProductId()); + assertThat(first.getRankPosition()).isEqualTo(second.getRankPosition()); + assertThat(first.getTotalScore()).isEqualTo(second.getTotalScore()); + } + } + } +} \ No newline at end of file diff --git a/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/dto/RankingAggregationUnitTest.java b/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/dto/RankingAggregationUnitTest.java new file mode 100644 index 000000000..70bc2e2dc --- /dev/null +++ b/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/dto/RankingAggregationUnitTest.java @@ -0,0 +1,172 @@ +package com.loopers.batch.job.ranking.dto; + +import com.loopers.batch.job.ranking.support.ScoreCalculator; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +@DisplayName("RankingAggregation ๋‹จ์œ„ ํ…Œ์ŠคํŠธ") +class RankingAggregationUnitTest { + + private final ScoreCalculator calculator = new ScoreCalculator(); + + @Nested + @DisplayName("์ง‘๊ณ„ ๊ฒฐ๊ณผ๋กœ๋ถ€ํ„ฐ ์ƒ์„ฑ") + class ์ง‘๊ณ„_๊ฒฐ๊ณผ๋กœ๋ถ€ํ„ฐ_์ƒ์„ฑ { + + @Test + @DisplayName("์œ ํšจํ•œ ์ง‘๊ณ„ ๊ฒฐ๊ณผ๋กœ๋ถ€ํ„ฐ ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•œ๋‹ค") + void should_create_from_valid_aggregation_result() { + // given + Object[] row = {1L, 100L, 50L, 10L, 5L}; // productId, view, like, sales, order + + // when + RankingAggregation aggregation = RankingAggregation.from(row, calculator); + + // then + Assertions.assertThat(aggregation.getProductId()).isEqualTo(1L); + Assertions.assertThat(aggregation.getViewCount()).isEqualTo(100L); + Assertions.assertThat(aggregation.getLikeCount()).isEqualTo(50L); + Assertions.assertThat(aggregation.getSalesCount()).isEqualTo(10L); + Assertions.assertThat(aggregation.getOrderCount()).isEqualTo(5L); + // score = 100*1 + 50*3 + 10*5 + 5*2 = 310 + Assertions.assertThat(aggregation.getTotalScore()).isEqualTo(310L); + Assertions.assertThat(aggregation.getRankPosition()).isEqualTo(0); // ์ดˆ๊ธฐ๊ฐ’ + } + + @Test + @DisplayName("null ๋ฐฐ์—ด์— ๋Œ€ํ•ด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค") + void should_throw_exception_when_row_is_null() { + // given & when & then + Assertions.assertThatThrownBy(() -> RankingAggregation.from(null, calculator)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("์ง‘๊ณ„ ๊ฒฐ๊ณผ ๋ฐฐ์—ด์ด null์ด๊ฑฐ๋‚˜ ๊ธธ์ด๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค"); + } + + @Test + @DisplayName("๊ธธ์ด๊ฐ€ ๋ถ€์กฑํ•œ ๋ฐฐ์—ด์— ๋Œ€ํ•ด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค") + void should_throw_exception_when_row_length_is_insufficient() { + // given + Object[] shortRow = {1L, 100L, 50L}; // ๊ธธ์ด 3 (5 ๋ฏธ๋งŒ) + + // when & then + Assertions.assertThatThrownBy(() -> RankingAggregation.from(shortRow, calculator)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("์ง‘๊ณ„ ๊ฒฐ๊ณผ ๋ฐฐ์—ด์ด null์ด๊ฑฐ๋‚˜ ๊ธธ์ด๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค"); + } + + @Test + @DisplayName("์ž˜๋ชป๋œ ๋ฐ์ดํ„ฐ ํƒ€์ž…์— ๋Œ€ํ•ด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค") + void should_throw_exception_when_data_type_is_invalid() { + // given + Object[] invalidRow = {"invalid", 100L, 50L, 10L, 5L}; // productId๊ฐ€ String + + // when & then + Assertions.assertThatThrownBy(() -> RankingAggregation.from(invalidRow, calculator)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("์ง‘๊ณ„ ๊ฒฐ๊ณผ ๋ฐ์ดํ„ฐ ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค"); + } + + @Test + @DisplayName("Number ํƒ€์ž…์˜ ๋‹ค์–‘ํ•œ ํ˜•ํƒœ๋ฅผ ์ฒ˜๋ฆฌํ•œ๋‹ค") + void should_handle_various_number_types() { + // given - Integer, Long, BigDecimal ๋“ฑ ๋‹ค์–‘ํ•œ Number ํƒ€์ž… + Object[] row = {1L, 100, 50L, 10, 5L}; + + // when + RankingAggregation aggregation = RankingAggregation.from(row, calculator); + + // then + Assertions.assertThat(aggregation.getViewCount()).isEqualTo(100L); + Assertions.assertThat(aggregation.getLikeCount()).isEqualTo(50L); + Assertions.assertThat(aggregation.getSalesCount()).isEqualTo(10L); + Assertions.assertThat(aggregation.getOrderCount()).isEqualTo(5L); + } + } + + @Nested + @DisplayName("์ˆœ์œ„ ๋ถ€์—ฌ") + class ์ˆœ์œ„_๋ถ€์—ฌ { + + @Test + @DisplayName("์œ ํšจํ•œ ์ˆœ์œ„๋ฅผ ๋ถ€์—ฌํ•œ๋‹ค") + void should_assign_valid_rank() { + // given + Object[] row = {1L, 100L, 50L, 10L, 5L}; + RankingAggregation aggregation = RankingAggregation.from(row, calculator); + + // when + aggregation.assignRank(1); + + // then + Assertions.assertThat(aggregation.getRankPosition()).isEqualTo(1); + } + + @Test + @DisplayName("100์œ„๊นŒ์ง€ ์ˆœ์œ„๋ฅผ ๋ถ€์—ฌํ•  ์ˆ˜ ์žˆ๋‹ค") + void should_assign_rank_up_to_100() { + // given + Object[] row = {1L, 100L, 50L, 10L, 5L}; + RankingAggregation aggregation = RankingAggregation.from(row, calculator); + + // when + aggregation.assignRank(100); + + // then + Assertions.assertThat(aggregation.getRankPosition()).isEqualTo(100); + } + + @Test + @DisplayName("0 ์ดํ•˜์˜ ์ˆœ์œ„์— ๋Œ€ํ•ด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค") + void should_throw_exception_when_rank_is_zero_or_negative() { + // given + Object[] row = {1L, 100L, 50L, 10L, 5L}; + RankingAggregation aggregation = RankingAggregation.from(row, calculator); + + // when & then + Assertions.assertThatThrownBy(() -> aggregation.assignRank(0)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("์ˆœ์œ„๋Š” 1~100 ๋ฒ”์œ„์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค"); + + Assertions.assertThatThrownBy(() -> aggregation.assignRank(-1)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("์ˆœ์œ„๋Š” 1~100 ๋ฒ”์œ„์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค"); + } + + @Test + @DisplayName("100์„ ์ดˆ๊ณผํ•˜๋Š” ์ˆœ์œ„์— ๋Œ€ํ•ด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค") + void should_throw_exception_when_rank_exceeds_100() { + // given + Object[] row = {1L, 100L, 50L, 10L, 5L}; + RankingAggregation aggregation = RankingAggregation.from(row, calculator); + + // when & then + Assertions.assertThatThrownBy(() -> aggregation.assignRank(101)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("์ˆœ์œ„๋Š” 1~100 ๋ฒ”์œ„์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค"); + } + } + + @Nested + @DisplayName("๋ฌธ์ž์—ด ํ‘œํ˜„") + class ๋ฌธ์ž์—ด_ํ‘œํ˜„ { + + @Test + @DisplayName("toString์ด ์˜ฌ๋ฐ”๋ฅธ ํ˜•์‹์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void should_return_correct_string_format() { + // given + Object[] row = {1L, 100L, 50L, 10L, 5L}; + RankingAggregation aggregation = RankingAggregation.from(row, calculator); + aggregation.assignRank(1); + + // when + String result = aggregation.toString(); + + // then + Assertions.assertThat(result).contains("productId=1"); + Assertions.assertThat(result).contains("score=310"); + Assertions.assertThat(result).contains("rank=1"); + } + } +} \ No newline at end of file diff --git a/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/support/DateRangeParserUnitTest.java b/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/support/DateRangeParserUnitTest.java new file mode 100644 index 000000000..b799657d5 --- /dev/null +++ b/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/support/DateRangeParserUnitTest.java @@ -0,0 +1,161 @@ +package com.loopers.batch.job.ranking.support; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; + +@DisplayName("DateRangeParser ๋‹จ์œ„ ํ…Œ์ŠคํŠธ") +class DateRangeParserUnitTest { + + private final DateRangeParser parser = new DateRangeParser(); + + @Nested + @DisplayName("์ฃผ๊ฐ„ ๋‚ ์งœ ๋ฒ”์œ„ ํŒŒ์‹ฑ") + class ์ฃผ๊ฐ„_๋‚ ์งœ_๋ฒ”์œ„_ํŒŒ์‹ฑ { + + @Test + @DisplayName("์œ ํšจํ•œ yearWeek ํ˜•์‹์„ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ํŒŒ์‹ฑํ•œ๋‹ค") + void should_parse_valid_year_week_correctly() { + // given + String yearWeek = "2024-W52"; + + // when + LocalDate[] dateRange = parser.parseYearWeek(yearWeek); + + // then + Assertions.assertThat(dateRange).hasSize(2); + Assertions.assertThat(dateRange[0]).isBefore(dateRange[1]); + Assertions.assertThat(dateRange[1]).isEqualTo(dateRange[0].plusDays(6)); + } + + @Test + @DisplayName("2024๋…„ 1์ฃผ์ฐจ๋ฅผ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ํŒŒ์‹ฑํ•œ๋‹ค") + void should_parse_first_week_of_2024_correctly() { + // given + String yearWeek = "2024-W1"; + + // when + LocalDate[] dateRange = parser.parseYearWeek(yearWeek); + + // then + Assertions.assertThat(dateRange).hasSize(2); + // 2024๋…„ 1์ฃผ์ฐจ๋Š” 1์›” 1์ผ(์›”์š”์ผ)๋ถ€ํ„ฐ ์‹œ์ž‘ + Assertions.assertThat(dateRange[0]).isEqualTo(LocalDate.of(2024, 1, 1)); + Assertions.assertThat(dateRange[1]).isEqualTo(LocalDate.of(2024, 1, 7)); + } + + @Test + @DisplayName("null yearWeek์— ๋Œ€ํ•ด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค") + void should_throw_exception_when_year_week_is_null() { + // given & when & then + Assertions.assertThatThrownBy(() -> parser.parseYearWeek(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("์ž˜๋ชป๋œ yearWeek ํ˜•์‹์ž…๋‹ˆ๋‹ค"); + } + + @Test + @DisplayName("์ž˜๋ชป๋œ yearWeek ํ˜•์‹์— ๋Œ€ํ•ด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค") + void should_throw_exception_when_year_week_format_is_invalid() { + // given + String invalidYearWeek = "2024-52"; + + // when & then + Assertions.assertThatThrownBy(() -> parser.parseYearWeek(invalidYearWeek)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("์ž˜๋ชป๋œ yearWeek ํ˜•์‹์ž…๋‹ˆ๋‹ค"); + } + + @Test + @DisplayName("๋นˆ ๋ฌธ์ž์—ด์— ๋Œ€ํ•ด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค") + void should_throw_exception_when_year_week_is_empty() { + // given & when & then + Assertions.assertThatThrownBy(() -> parser.parseYearWeek("")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("์ž˜๋ชป๋œ yearWeek ํ˜•์‹์ž…๋‹ˆ๋‹ค"); + } + } + + @Nested + @DisplayName("์›”๊ฐ„ ๋‚ ์งœ ๋ฒ”์œ„ ํŒŒ์‹ฑ") + class ์›”๊ฐ„_๋‚ ์งœ_๋ฒ”์œ„_ํŒŒ์‹ฑ { + + @Test + @DisplayName("์œ ํšจํ•œ yearMonth ํ˜•์‹์„ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ํŒŒ์‹ฑํ•œ๋‹ค") + void should_parse_valid_year_month_correctly() { + // given + String yearMonth = "2024-12"; + + // when + LocalDate[] dateRange = parser.parseYearMonth(yearMonth); + + // then + Assertions.assertThat(dateRange).hasSize(2); + Assertions.assertThat(dateRange[0]).isEqualTo(LocalDate.of(2024, 12, 1)); + Assertions.assertThat(dateRange[1]).isEqualTo(LocalDate.of(2024, 12, 31)); + } + + @Test + @DisplayName("2์›”(์œค๋…„)์„ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ํŒŒ์‹ฑํ•œ๋‹ค") + void should_parse_february_in_leap_year_correctly() { + // given + String yearMonth = "2024-02"; // 2024๋…„์€ ์œค๋…„ + + // when + LocalDate[] dateRange = parser.parseYearMonth(yearMonth); + + // then + Assertions.assertThat(dateRange[0]).isEqualTo(LocalDate.of(2024, 2, 1)); + Assertions.assertThat(dateRange[1]).isEqualTo(LocalDate.of(2024, 2, 29)); // ์œค๋…„ + } + + @Test + @DisplayName("2์›”(ํ‰๋…„)์„ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ํŒŒ์‹ฑํ•œ๋‹ค") + void should_parse_february_in_non_leap_year_correctly() { + // given + String yearMonth = "2023-02"; // 2023๋…„์€ ํ‰๋…„ + + // when + LocalDate[] dateRange = parser.parseYearMonth(yearMonth); + + // then + Assertions.assertThat(dateRange[0]).isEqualTo(LocalDate.of(2023, 2, 1)); + Assertions.assertThat(dateRange[1]).isEqualTo(LocalDate.of(2023, 2, 28)); // ํ‰๋…„ + } + + @Test + @DisplayName("null yearMonth์— ๋Œ€ํ•ด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค") + void should_throw_exception_when_year_month_is_null() { + // given & when & then + Assertions.assertThatThrownBy(() -> parser.parseYearMonth(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("์ž˜๋ชป๋œ yearMonth ํ˜•์‹์ž…๋‹ˆ๋‹ค"); + } + + @Test + @DisplayName("์ž˜๋ชป๋œ yearMonth ํ˜•์‹์— ๋Œ€ํ•ด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค") + void should_throw_exception_when_year_month_format_is_invalid() { + // given + String invalidYearMonth = "2024/12"; + + // when & then + Assertions.assertThatThrownBy(() -> parser.parseYearMonth(invalidYearMonth)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("์ž˜๋ชป๋œ yearMonth ํ˜•์‹์ž…๋‹ˆ๋‹ค"); + } + + @Test + @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์›”์— ๋Œ€ํ•ด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค") + void should_throw_exception_when_month_does_not_exist() { + // given + String invalidYearMonth = "2024-13"; + + // when & then + Assertions.assertThatThrownBy(() -> parser.parseYearMonth(invalidYearMonth)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("yearMonth ํŒŒ์‹ฑ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค"); + } + } +} \ No newline at end of file diff --git a/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/support/RankingAggregatorUnitTest.java b/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/support/RankingAggregatorUnitTest.java new file mode 100644 index 000000000..1cad3bb32 --- /dev/null +++ b/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/support/RankingAggregatorUnitTest.java @@ -0,0 +1,134 @@ +package com.loopers.batch.job.ranking.support; + +import com.loopers.batch.job.ranking.dto.RankingAggregation; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +@DisplayName("RankingAggregator ๋‹จ์œ„ ํ…Œ์ŠคํŠธ") +class RankingAggregatorUnitTest { + + private final ScoreCalculator calculator = new ScoreCalculator(); + private final RankingAggregator aggregator = new RankingAggregator(calculator); + + @Nested + @DisplayName("๋žญํ‚น ์ฒ˜๋ฆฌ") + class ๋žญํ‚น_์ฒ˜๋ฆฌ { + + @Test + @DisplayName("์ง‘๊ณ„ ๊ฒฐ๊ณผ๋ฅผ ์ ์ˆ˜ ๊ธฐ์ค€์œผ๋กœ ์ •๋ ฌํ•˜๊ณ  ์ˆœ์œ„๋ฅผ ๋ถ€์—ฌํ•œ๋‹ค") + void should_sort_by_score_and_assign_ranks() { + // given + List results = List.of( + new Object[]{1L, 100L, 10L, 5L, 2L}, // score = 100 + 30 + 25 + 4 = 159 + new Object[]{2L, 200L, 20L, 10L, 4L}, // score = 200 + 60 + 50 + 8 = 318 + new Object[]{3L, 50L, 5L, 2L, 1L} // score = 50 + 15 + 10 + 2 = 77 + ); + + // when + List rankings = aggregator.processRankings(results); + + // then + Assertions.assertThat(rankings).hasSize(3); + + // ์ ์ˆ˜ ๊ธฐ์ค€ ๋‚ด๋ฆผ์ฐจ์ˆœ ์ •๋ ฌ ํ™•์ธ + Assertions.assertThat(rankings.get(0).getProductId()).isEqualTo(2L); // 1์œ„ + Assertions.assertThat(rankings.get(0).getRankPosition()).isEqualTo(1); + Assertions.assertThat(rankings.get(0).getTotalScore()).isEqualTo(318L); + + Assertions.assertThat(rankings.get(1).getProductId()).isEqualTo(1L); // 2์œ„ + Assertions.assertThat(rankings.get(1).getRankPosition()).isEqualTo(2); + Assertions.assertThat(rankings.get(1).getTotalScore()).isEqualTo(159L); + + Assertions.assertThat(rankings.get(2).getProductId()).isEqualTo(3L); // 3์œ„ + Assertions.assertThat(rankings.get(2).getRankPosition()).isEqualTo(3); + Assertions.assertThat(rankings.get(2).getTotalScore()).isEqualTo(77L); + } + + @Test + @DisplayName("TOP 100์„ ์ดˆ๊ณผํ•˜๋Š” ๊ฒฐ๊ณผ๋Š” ํ•„ํ„ฐ๋ง๋œ๋‹ค") + void should_filter_results_beyond_top_100() { + // given - 150๊ฐœ์˜ ๊ฒฐ๊ณผ ์ƒ์„ฑ + List results = new ArrayList<>(); + for (int i = 1; i <= 150; i++) { + // ์ ์ˆ˜๊ฐ€ ๋†’์€ ์ˆœ์„œ๋Œ€๋กœ ์ƒ์„ฑ (i๊ฐ€ ํด์ˆ˜๋ก ์ ์ˆ˜ ๋†’์Œ) + results.add(new Object[]{(long) i, (long) i * 10, (long) i, (long) i, (long) i}); + } + + // when + List rankings = aggregator.processRankings(results); + + // then + Assertions.assertThat(rankings).hasSize(100); // TOP 100๋งŒ ๋ฐ˜ํ™˜ + Assertions.assertThat(rankings.get(0).getRankPosition()).isEqualTo(1); + Assertions.assertThat(rankings.get(99).getRankPosition()).isEqualTo(100); + } + + @Test + @DisplayName("๋นˆ ๊ฒฐ๊ณผ์— ๋Œ€ํ•ด ๋นˆ ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void should_return_empty_list_for_empty_results() { + // given + List emptyResults = List.of(); + + // when + List rankings = aggregator.processRankings(emptyResults); + + // then + Assertions.assertThat(rankings).isEmpty(); + } + + @Test + @DisplayName("null ๊ฒฐ๊ณผ์— ๋Œ€ํ•ด ๋นˆ ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void should_return_empty_list_for_null_results() { + // when + List rankings = aggregator.processRankings(null); + + // then + Assertions.assertThat(rankings).isEmpty(); + } + + @Test + @DisplayName("๋™์ผํ•œ ์ ์ˆ˜์˜ ์ƒํ’ˆ๋“ค์€ ์ˆœ์„œ๊ฐ€ ์œ ์ง€๋œ๋‹ค") + void should_maintain_order_for_same_scores() { + // given - ๋™์ผํ•œ ์ ์ˆ˜๋ฅผ ๊ฐ€์ง„ ์ƒํ’ˆ๋“ค + List results = List.of( + new Object[]{1L, 100L, 0L, 0L, 0L}, // score = 100 + new Object[]{2L, 100L, 0L, 0L, 0L}, // score = 100 + new Object[]{3L, 100L, 0L, 0L, 0L} // score = 100 + ); + + // when + List rankings = aggregator.processRankings(results); + + // then + Assertions.assertThat(rankings).hasSize(3); + Assertions.assertThat(rankings.get(0).getRankPosition()).isEqualTo(1); + Assertions.assertThat(rankings.get(1).getRankPosition()).isEqualTo(2); + Assertions.assertThat(rankings.get(2).getRankPosition()).isEqualTo(3); + + // ๋ชจ๋“  ์ ์ˆ˜๊ฐ€ ๋™์ผํ•จ์„ ํ™•์ธ + Assertions.assertThat(rankings.get(0).getTotalScore()).isEqualTo(100L); + Assertions.assertThat(rankings.get(1).getTotalScore()).isEqualTo(100L); + Assertions.assertThat(rankings.get(2).getTotalScore()).isEqualTo(100L); + } + } + + @Nested + @DisplayName("์„ค์ • ์ •๋ณด") + class ์„ค์ •_์ •๋ณด { + + @Test + @DisplayName("TOP ๋žญํ‚น ์ œํ•œ ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void should_return_top_rank_limit() { + // when + int limit = aggregator.getTopRankLimit(); + + // then + Assertions.assertThat(limit).isEqualTo(100); + } + } +} \ No newline at end of file diff --git a/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/support/ScoreCalculatorUnitTest.java b/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/support/ScoreCalculatorUnitTest.java new file mode 100644 index 000000000..9612e6278 --- /dev/null +++ b/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/support/ScoreCalculatorUnitTest.java @@ -0,0 +1,102 @@ +package com.loopers.batch.job.ranking.support; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +@DisplayName("ScoreCalculator ๋‹จ์œ„ ํ…Œ์ŠคํŠธ") +class ScoreCalculatorUnitTest { + + private final ScoreCalculator calculator = new ScoreCalculator(); + + @Nested + @DisplayName("์ ์ˆ˜ ๊ณ„์‚ฐ") + class ์ ์ˆ˜_๊ณ„์‚ฐ { + + @Test + @DisplayName("๊ฐ€์ค‘์น˜๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ ์šฉ๋˜์–ด ์ ์ˆ˜๊ฐ€ ๊ณ„์‚ฐ๋œ๋‹ค") + void should_calculate_score_with_correct_weights() { + // given + long viewCount = 100, likeCount = 50, salesCount = 10, orderCount = 5; + + // when + long score = calculator.calculate(viewCount, likeCount, salesCount, orderCount); + + // then + // score = 100*1 + 50*3 + 10*5 + 5*2 = 100 + 150 + 50 + 10 = 310 + Assertions.assertThat(score).isEqualTo(310L); + } + + @Test + @DisplayName("๋ชจ๋“  ๋ฉ”ํŠธ๋ฆญ์ด 0์ธ ๊ฒฝ์šฐ ์ ์ˆ˜๋Š” 0์ด๋‹ค") + void should_return_zero_when_all_metrics_are_zero() { + // given & when + long score = calculator.calculate(0, 0, 0, 0); + + // then + Assertions.assertThat(score).isEqualTo(0L); + } + + @Test + @DisplayName("ํŒ๋งค์ˆ˜๋Ÿ‰์ด ๊ฐ€์žฅ ๋†’์€ ๊ฐ€์ค‘์น˜๋ฅผ ๊ฐ€์ง„๋‹ค") + void should_have_highest_weight_for_sales_count() { + // given + long singleSale = calculator.calculate(0, 0, 1, 0); + long singleView = calculator.calculate(1, 0, 0, 0); + long singleLike = calculator.calculate(0, 1, 0, 0); + long singleOrder = calculator.calculate(0, 0, 0, 1); + + // when & then + Assertions.assertThat(singleSale).isGreaterThan(singleView); + Assertions.assertThat(singleSale).isGreaterThan(singleLike); + Assertions.assertThat(singleSale).isGreaterThan(singleOrder); + } + + @Test + @DisplayName("์ข‹์•„์š”๊ฐ€ ์กฐํšŒ์ˆ˜๋ณด๋‹ค ๋†’์€ ๊ฐ€์ค‘์น˜๋ฅผ ๊ฐ€์ง„๋‹ค") + void should_have_higher_weight_for_like_than_view() { + // given + long singleLike = calculator.calculate(0, 1, 0, 0); + long singleView = calculator.calculate(1, 0, 0, 0); + + // when & then + Assertions.assertThat(singleLike).isGreaterThan(singleView); + } + + @Test + @DisplayName("ํฐ ์ˆซ์ž์—์„œ๋„ ์ •ํ™•ํžˆ ๊ณ„์‚ฐ๋œ๋‹ค") + void should_calculate_correctly_with_large_numbers() { + // given + long viewCount = 1_000_000L; + long likeCount = 500_000L; + long salesCount = 100_000L; + long orderCount = 50_000L; + + // when + long score = calculator.calculate(viewCount, likeCount, salesCount, orderCount); + + // then + long expected = 1_000_000L * 1 + 500_000L * 3 + 100_000L * 5 + 50_000L * 2; + Assertions.assertThat(score).isEqualTo(expected); + } + } + + @Nested + @DisplayName("๊ฐ€์ค‘์น˜ ์ •๋ณด") + class ๊ฐ€์ค‘์น˜_์ •๋ณด { + + @Test + @DisplayName("๊ฐ€์ค‘์น˜ ์ •๋ณด๋ฅผ ์˜ฌ๋ฐ”๋ฅธ ํ˜•์‹์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void should_return_weight_info_in_correct_format() { + // when + String weightInfo = calculator.getWeightInfo(); + + // then + Assertions.assertThat(weightInfo).contains("VIEW=1"); + Assertions.assertThat(weightInfo).contains("LIKE=3"); + Assertions.assertThat(weightInfo).contains("SALES=5"); + Assertions.assertThat(weightInfo).contains("ORDER=2"); + } + } +} \ No newline at end of file diff --git a/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/writer/MonthlyRankWriterUnitTest.java b/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/writer/MonthlyRankWriterUnitTest.java new file mode 100644 index 000000000..64078e3bf --- /dev/null +++ b/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/writer/MonthlyRankWriterUnitTest.java @@ -0,0 +1,123 @@ +package com.loopers.batch.job.ranking.writer; + +import com.loopers.batch.job.ranking.dto.RankingAggregation; +import com.loopers.domain.ranking.MonthlyRankEntity; +import com.loopers.domain.ranking.MonthlyRankRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.batch.item.Chunk; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +@DisplayName("MonthlyRankWriter ๋‹จ์œ„ ํ…Œ์ŠคํŠธ") +class MonthlyRankWriterUnitTest { + + @Mock + private MonthlyRankRepository monthlyRankRepository; + + @InjectMocks + private MonthlyRankWriter monthlyRankWriter; + + private static final String TEST_YEAR_MONTH = "2024-12"; + + @BeforeEach + void setUp() { + ReflectionTestUtils.setField(monthlyRankWriter, "yearMonth", TEST_YEAR_MONTH); + } + + @Nested + @DisplayName("์›”๊ฐ„ ๋žญํ‚น ์ €์žฅ") + class ์›”๊ฐ„_๋žญํ‚น_์ €์žฅ { + + @Test + @DisplayName("๋žญํ‚น ๋ฐ์ดํ„ฐ๋ฅผ ์ •์ƒ์ ์œผ๋กœ ์ €์žฅํ•œ๋‹ค") + void should_save_monthly_ranking_successfully() throws Exception { + // given + List aggregations = List.of( + new RankingAggregation(1L, 1000L, 500L, 100L, 50L, 3100L, 1), + new RankingAggregation(2L, 800L, 400L, 80L, 40L, 2480L, 2) + ); + Chunk chunk = new Chunk<>(aggregations); + + when(monthlyRankRepository.deleteByYearMonth(TEST_YEAR_MONTH)).thenReturn(2L); + when(monthlyRankRepository.saveAll(anyList())).thenReturn(List.of()); + + // when + monthlyRankWriter.write(chunk); + + // then + verify(monthlyRankRepository).deleteByYearMonth(TEST_YEAR_MONTH); + verify(monthlyRankRepository).saveAll(any()); + } + + @Test + @DisplayName("๋นˆ ์ฒญํฌ์ธ ๊ฒฝ์šฐ ์ €์žฅํ•˜์ง€ ์•Š๋Š”๋‹ค") + void should_not_save_when_chunk_is_empty() throws Exception { + // given + Chunk emptyChunk = new Chunk<>(); + + // when + monthlyRankWriter.write(emptyChunk); + + // then + verify(monthlyRankRepository).deleteByYearMonth(TEST_YEAR_MONTH); + verify(monthlyRankRepository).saveAll(anyList()); + } + + @Test + @DisplayName("์ €์žฅ ์ค‘ ์˜ˆ์™ธ ๋ฐœ์ƒ ์‹œ RuntimeException์„ ๋˜์ง„๋‹ค") + void should_throw_runtime_exception_when_save_fails() { + // given + List aggregations = List.of( + new RankingAggregation(1L, 1000L, 500L, 100L, 50L, 3100L, 1) + ); + Chunk chunk = new Chunk<>(aggregations); + + when(monthlyRankRepository.deleteByYearMonth(TEST_YEAR_MONTH)).thenReturn(1L); + when(monthlyRankRepository.saveAll(anyList())).thenThrow(new RuntimeException("DB ์ €์žฅ ์‹คํŒจ")); + + // when & then + assertThatThrownBy(() -> monthlyRankWriter.write(chunk)) + .isInstanceOf(RuntimeException.class) + .hasMessage("์›”๊ฐ„ ๋žญํ‚น ์ €์žฅ ์‹คํŒจ"); + } + } + + @Nested + @DisplayName("Entity ๋ณ€ํ™˜") + class Entity_๋ณ€ํ™˜ { + + @Test + @DisplayName("RankingAggregation์„ MonthlyRankEntity๋กœ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋ณ€ํ™˜ํ•œ๋‹ค") + void should_convert_aggregation_to_entity_correctly() throws Exception { + // given + RankingAggregation aggregation = new RankingAggregation(1L, 1000L, 500L, 100L, 50L, 3100L, 1); + List expectedEntities = List.of( + MonthlyRankEntity.create(1L, TEST_YEAR_MONTH, 1000L, 500L, 100L, 50L, 3100L, 1) + ); + + when(monthlyRankRepository.deleteByYearMonth(TEST_YEAR_MONTH)).thenReturn(0L); + when(monthlyRankRepository.saveAll(anyList())).thenReturn(expectedEntities); + + // when + monthlyRankWriter.write(new Chunk<>(List.of(aggregation))); + + // then + verify(monthlyRankRepository).saveAll(expectedEntities); + } + } +} \ No newline at end of file diff --git a/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/writer/WeeklyRankWriterUnitTest.java b/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/writer/WeeklyRankWriterUnitTest.java new file mode 100644 index 000000000..4d0ea11e6 --- /dev/null +++ b/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/writer/WeeklyRankWriterUnitTest.java @@ -0,0 +1,125 @@ +package com.loopers.batch.job.ranking.writer; + +import com.loopers.batch.job.ranking.dto.RankingAggregation; +import com.loopers.domain.ranking.WeeklyRankEntity; +import com.loopers.domain.ranking.WeeklyRankRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.batch.item.Chunk; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +@DisplayName("WeeklyRankWriter ๋‹จ์œ„ ํ…Œ์ŠคํŠธ") +class WeeklyRankWriterUnitTest { + + @Mock + private WeeklyRankRepository weeklyRankRepository; + + @InjectMocks + private WeeklyRankWriter weeklyRankWriter; + + private static final String TEST_YEAR_WEEK = "2024-W52"; + + @BeforeEach + void setUp() { + ReflectionTestUtils.setField(weeklyRankWriter, "yearWeek", TEST_YEAR_WEEK); + } + + @Nested + @DisplayName("์ฃผ๊ฐ„ ๋žญํ‚น ์ €์žฅ") + class ์ฃผ๊ฐ„_๋žญํ‚น_์ €์žฅ { + + @Test + @DisplayName("๋žญํ‚น ๋ฐ์ดํ„ฐ๋ฅผ ์ •์ƒ์ ์œผ๋กœ ์ €์žฅํ•œ๋‹ค") + void should_save_weekly_ranking_successfully() throws Exception { + // given + List aggregations = List.of( + new RankingAggregation(1L, 100L, 50L, 10L, 5L, 310L, 1), + new RankingAggregation(2L, 80L, 40L, 8L, 4L, 248L, 2) + ); + Chunk chunk = new Chunk<>(aggregations); + + when(weeklyRankRepository.deleteByYearWeek(TEST_YEAR_WEEK)).thenReturn(2L); + when(weeklyRankRepository.saveAll(anyList())).thenReturn(List.of()); + + // when + weeklyRankWriter.write(chunk); + + // then + verify(weeklyRankRepository).deleteByYearWeek(TEST_YEAR_WEEK); + verify(weeklyRankRepository).saveAll(any()); + } + + @Test + @DisplayName("๋นˆ ์ฒญํฌ์ธ ๊ฒฝ์šฐ ์ €์žฅํ•˜์ง€ ์•Š๋Š”๋‹ค") + void should_not_save_when_chunk_is_empty() throws Exception { + // given + Chunk emptyChunk = new Chunk<>(); + + // when + weeklyRankWriter.write(emptyChunk); + + // then + verify(weeklyRankRepository).deleteByYearWeek(TEST_YEAR_WEEK); + verify(weeklyRankRepository).saveAll(anyList()); + } + + @Test + @DisplayName("์ €์žฅ ์ค‘ ์˜ˆ์™ธ ๋ฐœ์ƒ ์‹œ RuntimeException์„ ๋˜์ง„๋‹ค") + void should_throw_runtime_exception_when_save_fails() { + // given + List aggregations = List.of( + new RankingAggregation(1L, 100L, 50L, 10L, 5L, 310L, 1) + ); + Chunk chunk = new Chunk<>(aggregations); + + when(weeklyRankRepository.deleteByYearWeek(TEST_YEAR_WEEK)).thenReturn(1L); + when(weeklyRankRepository.saveAll(anyList())).thenThrow(new RuntimeException("DB ์ €์žฅ ์‹คํŒจ")); + + // when & then + assertThatThrownBy(() -> weeklyRankWriter.write(chunk)) + .isInstanceOf(RuntimeException.class) + .hasMessage("์ฃผ๊ฐ„ ๋žญํ‚น ์ €์žฅ ์‹คํŒจ"); + } + } + + @Nested + @DisplayName("Entity ๋ณ€ํ™˜") + class Entity_๋ณ€ํ™˜ { + + @Test + @DisplayName("RankingAggregation์„ WeeklyRankEntity๋กœ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋ณ€ํ™˜ํ•œ๋‹ค") + void should_convert_aggregation_to_entity_correctly() throws Exception { + // given + RankingAggregation aggregation = new RankingAggregation(1L, 100L, 50L, 10L, 5L, 310L, 1); + List expectedEntities = List.of( + WeeklyRankEntity.create(1L, TEST_YEAR_WEEK, 100L, 50L, 10L, 5L, 310L, 1) + ); + + when(weeklyRankRepository.deleteByYearWeek(TEST_YEAR_WEEK)).thenReturn(0L); + when(weeklyRankRepository.saveAll(anyList())).thenReturn(expectedEntities); + + // when + weeklyRankWriter.write(new Chunk<>(List.of(aggregation))); + + // then + verify(weeklyRankRepository).saveAll(expectedEntities); + } + } +} \ No newline at end of file From d894a11f6a085f0b5335762eb04899fc9234513a Mon Sep 17 00:00:00 2001 From: hyujikoh Date: Fri, 2 Jan 2026 16:05:19 +0900 Subject: [PATCH 71/85] =?UTF-8?q?feat(ranking):=20=EC=9B=94=EA=B0=84=20?= =?UTF-8?q?=EB=B0=8F=20=EC=A3=BC=EA=B0=84=20=EB=9E=AD=ED=82=B9=20=EC=97=94?= =?UTF-8?q?=ED=8B=B0=ED=8B=B0=20=EB=B0=8F=20=EC=84=9C=EB=B9=84=EC=8A=A4=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 - ์›”๊ฐ„ ๋ฐ ์ฃผ๊ฐ„ ๋žญํ‚น ์—”ํ‹ฐํ‹ฐ(MonthlyRankEntity, WeeklyRankEntity) ์ƒ์„ฑ - ๊ฐ ์—”ํ‹ฐํ‹ฐ์— ๋Œ€ํ•œ JPA Repository ๋ฐ ์„œ๋น„์Šค ๊ตฌํ˜„ - ๋žญํ‚น ๋ฐ์ดํ„ฐ ์ €์žฅ ๋ฐ ์กฐํšŒ ๊ธฐ๋Šฅ ์ถ”๊ฐ€ - ๊ด€๋ จ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑ --- .../ranking/MonthlyRankingService.java | 71 +++++ .../ranking/WeeklyRankingService.java | 71 +++++ .../loopers/domain/ranking/RankingPeriod.java | 21 ++ .../ranking/MonthlyRankJpaRepository.java | 39 +++ .../ranking/MonthlyRankRepositoryImpl.java | 46 +++ .../ranking/WeeklyRankJpaRepository.java | 39 +++ .../ranking/WeeklyRankRepositoryImpl.java | 46 +++ .../api/ranking/RankingApiE2ETest.java | 292 ++++++++++++++++++ .../job/ranking/writer/MonthlyRankWriter.java | 74 +++++ .../domain/ranking/QWeeklyRankEntity.java | 63 ++++ .../loopers/domain/ranking/QWeeklyRankId.java | 39 +++ .../domain/ranking/MonthlyRankEntity.java | 96 ++++++ .../loopers/domain/ranking/MonthlyRankId.java | 36 +++ .../domain/ranking/MonthlyRankRepository.java | 37 +++ .../domain/ranking/WeeklyRankEntity.java | 96 ++++++ .../loopers/domain/ranking/WeeklyRankId.java | 36 +++ .../domain/ranking/WeeklyRankRepository.java | 37 +++ .../ranking/MonthlyRankEntityUnitTest.java | 141 +++++++++ .../ranking/WeeklyRankEntityUnitTest.java | 141 +++++++++ 19 files changed, 1421 insertions(+) create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/ranking/MonthlyRankingService.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/ranking/WeeklyRankingService.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingPeriod.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MonthlyRankJpaRepository.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MonthlyRankRepositoryImpl.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/WeeklyRankJpaRepository.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/WeeklyRankRepositoryImpl.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/interfaces/api/ranking/RankingApiE2ETest.java create mode 100644 apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/writer/MonthlyRankWriter.java create mode 100644 modules/jpa/src/main/generated/com/loopers/domain/ranking/QWeeklyRankEntity.java create mode 100644 modules/jpa/src/main/generated/com/loopers/domain/ranking/QWeeklyRankId.java create mode 100644 modules/jpa/src/main/java/com/loopers/domain/ranking/MonthlyRankEntity.java create mode 100644 modules/jpa/src/main/java/com/loopers/domain/ranking/MonthlyRankId.java create mode 100644 modules/jpa/src/main/java/com/loopers/domain/ranking/MonthlyRankRepository.java create mode 100644 modules/jpa/src/main/java/com/loopers/domain/ranking/WeeklyRankEntity.java create mode 100644 modules/jpa/src/main/java/com/loopers/domain/ranking/WeeklyRankId.java create mode 100644 modules/jpa/src/main/java/com/loopers/domain/ranking/WeeklyRankRepository.java create mode 100644 modules/jpa/src/test/java/com/loopers/domain/ranking/MonthlyRankEntityUnitTest.java create mode 100644 modules/jpa/src/test/java/com/loopers/domain/ranking/WeeklyRankEntityUnitTest.java diff --git a/apps/commerce-api/src/main/java/com/loopers/application/ranking/MonthlyRankingService.java b/apps/commerce-api/src/main/java/com/loopers/application/ranking/MonthlyRankingService.java new file mode 100644 index 000000000..3ff82a079 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/ranking/MonthlyRankingService.java @@ -0,0 +1,71 @@ +package com.loopers.application.ranking; + +import com.loopers.domain.ranking.MonthlyRankEntity; +import com.loopers.domain.ranking.MonthlyRankRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +/** + * ์›”๊ฐ„ ๋žญํ‚น ์กฐํšŒ ์„œ๋น„์Šค + */ +@Service +@RequiredArgsConstructor +@Slf4j +@Transactional(readOnly = true) +public class MonthlyRankingService { + + private final MonthlyRankRepository monthlyRankRepository; + + /** + * ํŠน์ • ์›”์˜ ๋žญํ‚น์„ ํŽ˜์ด์ง€๋„ค์ด์…˜ํ•˜์—ฌ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @param yearMonth ์กฐํšŒํ•  ์›” (์˜ˆ: "2024-12") + * @param pageable ํŽ˜์ด์ง• ์ •๋ณด + * @return ์›”๊ฐ„ ๋žญํ‚น ํŽ˜์ด์ง€ + */ + public Page getMonthlyRanking(String yearMonth, Pageable pageable) { + log.debug("์›”๊ฐ„ ๋žญํ‚น ์กฐํšŒ: yearMonth={}, page={}, size={}", + yearMonth, pageable.getPageNumber(), pageable.getPageSize()); + + // 1. ์ „์ฒด ๋žญํ‚น ์กฐํšŒ (์ˆœ์œ„ ์ˆœ์œผ๋กœ ์ •๋ ฌ๋จ) + List allRankings = monthlyRankRepository.findByYearMonth(yearMonth); + + if (allRankings.isEmpty()) { + log.debug("์›”๊ฐ„ ๋žญํ‚น ๋ฐ์ดํ„ฐ ์—†์Œ: yearMonth={}", yearMonth); + return Page.empty(pageable); + } + + // 2. ํŽ˜์ด์ง• ์ฒ˜๋ฆฌ + int start = (int) pageable.getOffset(); + int end = Math.min(start + pageable.getPageSize(), allRankings.size()); + + if (start >= allRankings.size()) { + return Page.empty(pageable); + } + + List pagedRankings = allRankings.subList(start, end); + + log.debug("์›”๊ฐ„ ๋žญํ‚น ์กฐํšŒ ์™„๋ฃŒ: yearMonth={}, ์ „์ฒด={}, ํŽ˜์ด์ง€={}", + yearMonth, allRankings.size(), pagedRankings.size()); + + return new PageImpl<>(pagedRankings, pageable, allRankings.size()); + } + + /** + * ํŠน์ • ์›”์˜ ์ „์ฒด ๋žญํ‚น ๊ฐœ์ˆ˜๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @param yearMonth ์กฐํšŒํ•  ์›” + * @return ๋žญํ‚น ๊ฐœ์ˆ˜ + */ + public long getMonthlyRankingCount(String yearMonth) { + List rankings = monthlyRankRepository.findByYearMonth(yearMonth); + return rankings.size(); + } +} \ No newline at end of file diff --git a/apps/commerce-api/src/main/java/com/loopers/application/ranking/WeeklyRankingService.java b/apps/commerce-api/src/main/java/com/loopers/application/ranking/WeeklyRankingService.java new file mode 100644 index 000000000..f5e00e5b8 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/ranking/WeeklyRankingService.java @@ -0,0 +1,71 @@ +package com.loopers.application.ranking; + +import com.loopers.domain.ranking.WeeklyRankEntity; +import com.loopers.domain.ranking.WeeklyRankRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +/** + * ์ฃผ๊ฐ„ ๋žญํ‚น ์กฐํšŒ ์„œ๋น„์Šค + */ +@Service +@RequiredArgsConstructor +@Slf4j +@Transactional(readOnly = true) +public class WeeklyRankingService { + + private final WeeklyRankRepository weeklyRankRepository; + + /** + * ํŠน์ • ์ฃผ์ฐจ์˜ ๋žญํ‚น์„ ํŽ˜์ด์ง€๋„ค์ด์…˜ํ•˜์—ฌ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @param yearWeek ์กฐํšŒํ•  ์ฃผ์ฐจ (์˜ˆ: "2024-W52") + * @param pageable ํŽ˜์ด์ง• ์ •๋ณด + * @return ์ฃผ๊ฐ„ ๋žญํ‚น ํŽ˜์ด์ง€ + */ + public Page getWeeklyRanking(String yearWeek, Pageable pageable) { + log.debug("์ฃผ๊ฐ„ ๋žญํ‚น ์กฐํšŒ: yearWeek={}, page={}, size={}", + yearWeek, pageable.getPageNumber(), pageable.getPageSize()); + + // 1. ์ „์ฒด ๋žญํ‚น ์กฐํšŒ (์ˆœ์œ„ ์ˆœ์œผ๋กœ ์ •๋ ฌ๋จ) + List allRankings = weeklyRankRepository.findByYearWeek(yearWeek); + + if (allRankings.isEmpty()) { + log.debug("์ฃผ๊ฐ„ ๋žญํ‚น ๋ฐ์ดํ„ฐ ์—†์Œ: yearWeek={}", yearWeek); + return Page.empty(pageable); + } + + // 2. ํŽ˜์ด์ง• ์ฒ˜๋ฆฌ + int start = (int) pageable.getOffset(); + int end = Math.min(start + pageable.getPageSize(), allRankings.size()); + + if (start >= allRankings.size()) { + return Page.empty(pageable); + } + + List pagedRankings = allRankings.subList(start, end); + + log.debug("์ฃผ๊ฐ„ ๋žญํ‚น ์กฐํšŒ ์™„๋ฃŒ: yearWeek={}, ์ „์ฒด={}, ํŽ˜์ด์ง€={}", + yearWeek, allRankings.size(), pagedRankings.size()); + + return new PageImpl<>(pagedRankings, pageable, allRankings.size()); + } + + /** + * ํŠน์ • ์ฃผ์ฐจ์˜ ์ „์ฒด ๋žญํ‚น ๊ฐœ์ˆ˜๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @param yearWeek ์กฐํšŒํ•  ์ฃผ์ฐจ + * @return ๋žญํ‚น ๊ฐœ์ˆ˜ + */ + public long getWeeklyRankingCount(String yearWeek) { + List rankings = weeklyRankRepository.findByYearWeek(yearWeek); + return rankings.size(); + } +} \ No newline at end of file diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingPeriod.java b/apps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingPeriod.java new file mode 100644 index 000000000..2ecfafa51 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingPeriod.java @@ -0,0 +1,21 @@ +package com.loopers.domain.ranking; + +/** + * ๋žญํ‚น ์กฐํšŒ ๊ธฐ๊ฐ„ ํƒ€์ž… + */ +public enum RankingPeriod { + /** + * ์ผ๊ฐ„ ๋žญํ‚น (Redis ZSET ๊ธฐ๋ฐ˜) + */ + DAILY, + + /** + * ์ฃผ๊ฐ„ ๋žญํ‚น (mv_product_rank_weekly ๊ธฐ๋ฐ˜) + */ + WEEKLY, + + /** + * ์›”๊ฐ„ ๋žญํ‚น (mv_product_rank_monthly ๊ธฐ๋ฐ˜) + */ + MONTHLY +} \ No newline at end of file diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MonthlyRankJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MonthlyRankJpaRepository.java new file mode 100644 index 000000000..98968a6f5 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MonthlyRankJpaRepository.java @@ -0,0 +1,39 @@ +package com.loopers.infrastructure.ranking; + +import com.loopers.domain.ranking.MonthlyRankEntity; +import com.loopers.domain.ranking.MonthlyRankId; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +/** + * ์›”๊ฐ„ ๋žญํ‚น JPA Repository (commerce-api์šฉ) + */ +public interface MonthlyRankJpaRepository extends JpaRepository { + + /** + * ํŠน์ • ์›”์˜ ๋žญํ‚น์„ ์ˆœ์œ„ ์ˆœ์œผ๋กœ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + */ + @Query("SELECT m FROM MonthlyRankEntity m WHERE m.id.yearMonth = :yearMonth ORDER BY m.rankPosition ASC") + List findByIdYearMonthOrderByRankPosition(@Param("yearMonth") String yearMonth); + + /** + * ํŠน์ • ์›”์˜ ๋žญํ‚น์„ ์ˆœ์œ„ ์ˆœ์œผ๋กœ ํŽ˜์ด์ง€๋„ค์ด์…˜ํ•˜์—ฌ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + */ + @Query("SELECT m FROM MonthlyRankEntity m WHERE m.id.yearMonth = :yearMonth ORDER BY m.rankPosition ASC") + List findByIdYearMonthOrderByRankPosition(@Param("yearMonth") String yearMonth, Pageable pageable); + + /** + * ํŠน์ • ์›”์˜ ๋ชจ๋“  ๋žญํ‚น์„ ์‚ญ์ œํ•ฉ๋‹ˆ๋‹ค. + * + * @param yearMonth ์‚ญ์ œํ•  ์›” + * @return ์‚ญ์ œ๋œ ๋ ˆ์ฝ”๋“œ ์ˆ˜ + */ + @Modifying + @Query("DELETE FROM MonthlyRankEntity m WHERE m.id.yearMonth = :yearMonth") + long deleteByIdYearMonth(@Param("yearMonth") String yearMonth); +} \ No newline at end of file diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MonthlyRankRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MonthlyRankRepositoryImpl.java new file mode 100644 index 000000000..636ec0a23 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MonthlyRankRepositoryImpl.java @@ -0,0 +1,46 @@ +package com.loopers.infrastructure.ranking; + +import com.loopers.domain.ranking.MonthlyRankEntity; +import com.loopers.domain.ranking.MonthlyRankRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * ์›”๊ฐ„ ๋žญํ‚น Repository ๊ตฌํ˜„์ฒด (commerce-api์šฉ) + */ +@Repository +@RequiredArgsConstructor +public class MonthlyRankRepositoryImpl implements MonthlyRankRepository { + + private final MonthlyRankJpaRepository jpaRepository; + + @Override + public MonthlyRankEntity save(MonthlyRankEntity entity) { + return jpaRepository.save(entity); + } + + @Override + public List saveAll(List entities) { + return jpaRepository.saveAll(entities); + } + + @Override + public List findByYearMonth(String yearMonth) { + return jpaRepository.findByIdYearMonthOrderByRankPosition(yearMonth); + } + + @Override + public List findByYearMonthWithPagination(String yearMonth, int page, int size) { + Pageable pageable = PageRequest.of(page, size); + return jpaRepository.findByIdYearMonthOrderByRankPosition(yearMonth, pageable); + } + + @Override + public long deleteByYearMonth(String yearMonth) { + return jpaRepository.deleteByIdYearMonth(yearMonth); + } +} \ No newline at end of file diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/WeeklyRankJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/WeeklyRankJpaRepository.java new file mode 100644 index 000000000..6bf9b0eeb --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/WeeklyRankJpaRepository.java @@ -0,0 +1,39 @@ +package com.loopers.infrastructure.ranking; + +import com.loopers.domain.ranking.WeeklyRankEntity; +import com.loopers.domain.ranking.WeeklyRankId; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +/** + * ์ฃผ๊ฐ„ ๋žญํ‚น JPA Repository (commerce-api์šฉ) + */ +public interface WeeklyRankJpaRepository extends JpaRepository { + + /** + * ํŠน์ • ์ฃผ์ฐจ์˜ ๋žญํ‚น์„ ์ˆœ์œ„ ์ˆœ์œผ๋กœ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + */ + @Query("SELECT w FROM WeeklyRankEntity w WHERE w.id.yearWeek = :yearWeek ORDER BY w.rankPosition ASC") + List findByIdYearWeekOrderByRankPosition(@Param("yearWeek") String yearWeek); + + /** + * ํŠน์ • ์ฃผ์ฐจ์˜ ๋žญํ‚น์„ ์ˆœ์œ„ ์ˆœ์œผ๋กœ ํŽ˜์ด์ง€๋„ค์ด์…˜ํ•˜์—ฌ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + */ + @Query("SELECT w FROM WeeklyRankEntity w WHERE w.id.yearWeek = :yearWeek ORDER BY w.rankPosition ASC") + List findByIdYearWeekOrderByRankPosition(@Param("yearWeek") String yearWeek, Pageable pageable); + + /** + * ํŠน์ • ์ฃผ์ฐจ์˜ ๋ชจ๋“  ๋žญํ‚น์„ ์‚ญ์ œํ•ฉ๋‹ˆ๋‹ค. + * + * @param yearWeek ์‚ญ์ œํ•  ์ฃผ์ฐจ + * @return ์‚ญ์ œ๋œ ๋ ˆ์ฝ”๋“œ ์ˆ˜ + */ + @Modifying + @Query("DELETE FROM WeeklyRankEntity w WHERE w.id.yearWeek = :yearWeek") + long deleteByIdYearWeek(@Param("yearWeek") String yearWeek); +} \ No newline at end of file diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/WeeklyRankRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/WeeklyRankRepositoryImpl.java new file mode 100644 index 000000000..2f21a5146 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/WeeklyRankRepositoryImpl.java @@ -0,0 +1,46 @@ +package com.loopers.infrastructure.ranking; + +import com.loopers.domain.ranking.WeeklyRankEntity; +import com.loopers.domain.ranking.WeeklyRankRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * ์ฃผ๊ฐ„ ๋žญํ‚น Repository ๊ตฌํ˜„์ฒด (commerce-api์šฉ) + */ +@Repository +@RequiredArgsConstructor +public class WeeklyRankRepositoryImpl implements WeeklyRankRepository { + + private final WeeklyRankJpaRepository jpaRepository; + + @Override + public WeeklyRankEntity save(WeeklyRankEntity entity) { + return jpaRepository.save(entity); + } + + @Override + public List saveAll(List entities) { + return jpaRepository.saveAll(entities); + } + + @Override + public List findByYearWeek(String yearWeek) { + return jpaRepository.findByIdYearWeekOrderByRankPosition(yearWeek); + } + + @Override + public List findByYearWeekWithPagination(String yearWeek, int page, int size) { + Pageable pageable = PageRequest.of(page, size); + return jpaRepository.findByIdYearWeekOrderByRankPosition(yearWeek, pageable); + } + + @Override + public long deleteByYearWeek(String yearWeek) { + return jpaRepository.deleteByIdYearWeek(yearWeek); + } +} \ No newline at end of file diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/ranking/RankingApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/ranking/RankingApiE2ETest.java new file mode 100644 index 000000000..27f43f5dd --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/ranking/RankingApiE2ETest.java @@ -0,0 +1,292 @@ +package com.loopers.interfaces.api.ranking; + +import com.loopers.domain.ranking.MonthlyRankEntity; +import com.loopers.domain.ranking.MonthlyRankRepository; +import com.loopers.domain.ranking.WeeklyRankEntity; +import com.loopers.domain.ranking.WeeklyRankRepository; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureWebMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import java.util.List; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +/** + * ๋žญํ‚น API E2E ํ…Œ์ŠคํŠธ + * - ์‹ค์ œ HTTP ์š”์ฒญ๋ถ€ํ„ฐ DB ์กฐํšŒ๊นŒ์ง€ ์ „์ฒด ํ”Œ๋กœ์šฐ ๊ฒ€์ฆ + */ +@SpringBootTest +@AutoConfigureWebMvc +@DisplayName("๋žญํ‚น API E2E ํ…Œ์ŠคํŠธ") +class RankingApiE2ETest { + + @Autowired + private WebApplicationContext webApplicationContext; + + @Autowired + private WeeklyRankRepository weeklyRankRepository; + + @Autowired + private MonthlyRankRepository monthlyRankRepository; + + private MockMvc mockMvc; + + @BeforeEach + void setUp() { + mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build(); + + // ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ ์ค€๋น„ + setupTestData(); + } + + @AfterEach + void tearDown() { + // ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ ์ •๋ฆฌ + weeklyRankRepository.deleteByYearWeek("2024-W52"); + monthlyRankRepository.deleteByYearMonth("2024-12"); + } + + private void setupTestData() { + // ์ฃผ๊ฐ„ ๋žญํ‚น ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ + List weeklyRankings = List.of( + WeeklyRankEntity.create(1L, "2024-W52", 1000L, 500L, 100L, 50L, 3100L, 1), + WeeklyRankEntity.create(2L, "2024-W52", 800L, 400L, 80L, 40L, 2480L, 2), + WeeklyRankEntity.create(3L, "2024-W52", 600L, 300L, 60L, 30L, 1860L, 3) + ); + weeklyRankRepository.saveAll(weeklyRankings); + + // ์›”๊ฐ„ ๋žญํ‚น ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ + List monthlyRankings = List.of( + MonthlyRankEntity.create(1L, "2024-12", 5000L, 2500L, 500L, 250L, 15500L, 1), + MonthlyRankEntity.create(2L, "2024-12", 4000L, 2000L, 400L, 200L, 12400L, 2), + MonthlyRankEntity.create(3L, "2024-12", 3000L, 1500L, 300L, 150L, 9300L, 3) + ); + monthlyRankRepository.saveAll(monthlyRankings); + } + + @Nested + @DisplayName("์ฃผ๊ฐ„ ๋žญํ‚น API") + class ์ฃผ๊ฐ„_๋žญํ‚น_API { + + @Test + @DisplayName("์ฃผ๊ฐ„ ๋žญํ‚น ์กฐํšŒ API๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ๋™์ž‘ํ•œ๋‹ค") + void should_return_weekly_rankings_successfully() throws Exception { + // when & then + mockMvc.perform(get("/api/v1/rankings/period") + .param("period", "WEEKLY") + .param("yearWeek", "2024-W52") + .param("size", "10") + .param("page", "0")) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data.content").isArray()) + .andExpect(jsonPath("$.data.content.length()").value(3)) + .andExpect(jsonPath("$.data.totalElements").value(3)) + .andExpect(jsonPath("$.data.content[0].id").value(1)) // 1์œ„ ์ƒํ’ˆ + .andExpect(jsonPath("$.data.content[1].id").value(2)) // 2์œ„ ์ƒํ’ˆ + .andExpect(jsonPath("$.data.content[2].id").value(3)); // 3์œ„ ์ƒํ’ˆ + } + + @Test + @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ฃผ์ฐจ ์กฐํšŒ ์‹œ ๋นˆ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void should_return_empty_result_for_non_existent_year_week() throws Exception { + // when & then + mockMvc.perform(get("/api/v1/rankings/period") + .param("period", "WEEKLY") + .param("yearWeek", "2024-W01") + .param("size", "10") + .param("page", "0")) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data.content").isArray()) + .andExpect(jsonPath("$.data.content.length()").value(0)) + .andExpect(jsonPath("$.data.totalElements").value(0)); + } + + @Test + @DisplayName("yearWeek ํŒŒ๋ผ๋ฏธํ„ฐ ๋ˆ„๋ฝ ์‹œ 400 ์—๋Ÿฌ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void should_return_400_when_year_week_parameter_is_missing() throws Exception { + // when & then + mockMvc.perform(get("/api/v1/rankings/period") + .param("period", "WEEKLY") + .param("size", "10") + .param("page", "0")) + .andDo(print()) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("ํŽ˜์ด์ง€๋„ค์ด์…˜์ด ์ •์ƒ์ ์œผ๋กœ ๋™์ž‘ํ•œ๋‹ค") + void should_support_pagination_correctly() throws Exception { + // when & then - ์ฒซ ๋ฒˆ์งธ ํŽ˜์ด์ง€ (size=2) + mockMvc.perform(get("/api/v1/rankings/period") + .param("period", "WEEKLY") + .param("yearWeek", "2024-W52") + .param("size", "2") + .param("page", "0")) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.content.length()").value(2)) + .andExpect(jsonPath("$.data.totalElements").value(3)) + .andExpect(jsonPath("$.data.totalPages").value(2)) + .andExpect(jsonPath("$.data.first").value(true)) + .andExpect(jsonPath("$.data.last").value(false)); + + // when & then - ๋‘ ๋ฒˆ์งธ ํŽ˜์ด์ง€ (size=2) + mockMvc.perform(get("/api/v1/rankings/period") + .param("period", "WEEKLY") + .param("yearWeek", "2024-W52") + .param("size", "2") + .param("page", "1")) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.content.length()").value(1)) + .andExpect(jsonPath("$.data.totalElements").value(3)) + .andExpect(jsonPath("$.data.totalPages").value(2)) + .andExpect(jsonPath("$.data.first").value(false)) + .andExpect(jsonPath("$.data.last").value(true)); + } + } + + @Nested + @DisplayName("์›”๊ฐ„ ๋žญํ‚น API") + class ์›”๊ฐ„_๋žญํ‚น_API { + + @Test + @DisplayName("์›”๊ฐ„ ๋žญํ‚น ์กฐํšŒ API๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ๋™์ž‘ํ•œ๋‹ค") + void should_return_monthly_rankings_successfully() throws Exception { + // when & then + mockMvc.perform(get("/api/v1/rankings/period") + .param("period", "MONTHLY") + .param("yearMonth", "2024-12") + .param("size", "10") + .param("page", "0")) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data.content").isArray()) + .andExpect(jsonPath("$.data.content.length()").value(3)) + .andExpect(jsonPath("$.data.totalElements").value(3)) + .andExpect(jsonPath("$.data.content[0].id").value(1)) // 1์œ„ ์ƒํ’ˆ + .andExpect(jsonPath("$.data.content[1].id").value(2)) // 2์œ„ ์ƒํ’ˆ + .andExpect(jsonPath("$.data.content[2].id").value(3)); // 3์œ„ ์ƒํ’ˆ + } + + @Test + @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์›” ์กฐํšŒ ์‹œ ๋นˆ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void should_return_empty_result_for_non_existent_year_month() throws Exception { + // when & then + mockMvc.perform(get("/api/v1/rankings/period") + .param("period", "MONTHLY") + .param("yearMonth", "2024-01") + .param("size", "10") + .param("page", "0")) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data.content").isArray()) + .andExpect(jsonPath("$.data.content.length()").value(0)) + .andExpect(jsonPath("$.data.totalElements").value(0)); + } + + @Test + @DisplayName("yearMonth ํŒŒ๋ผ๋ฏธํ„ฐ ๋ˆ„๋ฝ ์‹œ 400 ์—๋Ÿฌ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void should_return_400_when_year_month_parameter_is_missing() throws Exception { + // when & then + mockMvc.perform(get("/api/v1/rankings/period") + .param("period", "MONTHLY") + .param("size", "10") + .param("page", "0")) + .andDo(print()) + .andExpect(status().isBadRequest()); + } + } + + @Nested + @DisplayName("์ผ๊ฐ„ ๋žญํ‚น API (๊ธฐ์กด)") + class ์ผ๊ฐ„_๋žญํ‚น_API { + + @Test + @DisplayName("์ผ๊ฐ„ ๋žญํ‚น API๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ๋™์ž‘ํ•œ๋‹ค (ํ•˜์œ„ ํ˜ธํ™˜์„ฑ)") + void should_return_daily_rankings_successfully() throws Exception { + // when & then + mockMvc.perform(get("/api/v1/rankings") + .param("size", "10") + .param("page", "0")) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data.content").isArray()); + } + + @Test + @DisplayName("์ƒˆ๋กœ์šด period ๋ฐฉ์‹์œผ๋กœ๋„ ์ผ๊ฐ„ ๋žญํ‚น ์กฐํšŒ๊ฐ€ ๊ฐ€๋Šฅํ•˜๋‹ค") + void should_return_daily_rankings_with_period_parameter() throws Exception { + // when & then + mockMvc.perform(get("/api/v1/rankings/period") + .param("period", "DAILY") + .param("size", "10") + .param("page", "0")) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data.content").isArray()); + } + } + + @Nested + @DisplayName("ํŒŒ๋ผ๋ฏธํ„ฐ ๊ฒ€์ฆ") + class ํŒŒ๋ผ๋ฏธํ„ฐ_๊ฒ€์ฆ { + + @Test + @DisplayName("์ž˜๋ชป๋œ period ๊ฐ’ ์‹œ 400 ์—๋Ÿฌ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void should_return_400_for_invalid_period() throws Exception { + // when & then + mockMvc.perform(get("/api/v1/rankings/period") + .param("period", "INVALID") + .param("size", "10") + .param("page", "0")) + .andDo(print()) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("์Œ์ˆ˜ ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ ์‹œ 400 ์—๋Ÿฌ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void should_return_400_for_negative_page_number() throws Exception { + // when & then + mockMvc.perform(get("/api/v1/rankings/period") + .param("period", "WEEKLY") + .param("yearWeek", "2024-W52") + .param("size", "10") + .param("page", "-1")) + .andDo(print()) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("0 ์ดํ•˜์˜ size ๊ฐ’ ์‹œ 400 ์—๋Ÿฌ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void should_return_400_for_invalid_size() throws Exception { + // when & then + mockMvc.perform(get("/api/v1/rankings/period") + .param("period", "WEEKLY") + .param("yearWeek", "2024-W52") + .param("size", "0") + .param("page", "0")) + .andDo(print()) + .andExpect(status().isBadRequest()); + } + } +} \ No newline at end of file diff --git a/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/writer/MonthlyRankWriter.java b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/writer/MonthlyRankWriter.java new file mode 100644 index 000000000..a02eb0802 --- /dev/null +++ b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/writer/MonthlyRankWriter.java @@ -0,0 +1,74 @@ +package com.loopers.batch.job.ranking.writer; + +import com.loopers.batch.job.ranking.dto.RankingAggregation; +import com.loopers.domain.ranking.MonthlyRankEntity; +import com.loopers.domain.ranking.MonthlyRankRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.batch.item.Chunk; +import org.springframework.batch.item.ItemWriter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * ์›”๊ฐ„ ๋žญํ‚น Writer + * - RankingAggregation์„ MonthlyRankEntity๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ์ €์žฅ + * - ๋ฉฑ๋“ฑ์„ฑ ๋ณด์žฅ์„ ์œ„ํ•ด ๊ธฐ์กด ๋ฐ์ดํ„ฐ ์‚ญ์ œ ํ›„ ์ €์žฅ + */ +@Slf4j +@StepScope +@Component +@RequiredArgsConstructor +public class MonthlyRankWriter implements ItemWriter { + + private final MonthlyRankRepository monthlyRankRepository; + + @Value("#{jobParameters['yearMonth']}") + private String yearMonth; + + @Override + public void write(Chunk chunk) throws Exception { + List items = chunk.getItems(); + + if (items.isEmpty()) { + log.info("์ €์žฅํ•  ์›”๊ฐ„ ๋žญํ‚น ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค: yearMonth={}", yearMonth); + return; + } + + log.info("์›”๊ฐ„ ๋žญํ‚น ์ €์žฅ ์‹œ์ž‘: yearMonth={}, ์ €์žฅํ•  ๋ฐ์ดํ„ฐ ์ˆ˜={}", yearMonth, items.size()); + + try { + // 1. ๊ธฐ์กด ๋ฐ์ดํ„ฐ ์‚ญ์ œ (๋ฉฑ๋“ฑ์„ฑ ๋ณด์žฅ) + long deletedCount = monthlyRankRepository.deleteByYearMonth(yearMonth); + log.info("๊ธฐ์กด ์›”๊ฐ„ ๋žญํ‚น ๋ฐ์ดํ„ฐ ์‚ญ์ œ: yearMonth={}, ์‚ญ์ œ๋œ ์ˆ˜={}", yearMonth, deletedCount); + + // 2. ์ƒˆ๋กœ์šด ๋ฐ์ดํ„ฐ ์ €์žฅ + List entities = items.stream() + .map(this::convertToEntity) + .toList(); + + List savedEntities = monthlyRankRepository.saveAll(entities); + log.info("์›”๊ฐ„ ๋žญํ‚น ์ €์žฅ ์™„๋ฃŒ: yearMonth={}, ์ €์žฅ๋œ ์ˆ˜={}", yearMonth, savedEntities.size()); + + } catch (Exception e) { + log.error("์›”๊ฐ„ ๋žญํ‚น ์ €์žฅ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: yearMonth={}", yearMonth, e); + throw new RuntimeException("์›”๊ฐ„ ๋žญํ‚น ์ €์žฅ ์‹คํŒจ", e); + } + } + + private MonthlyRankEntity convertToEntity(RankingAggregation aggregation) { + return MonthlyRankEntity.create( + aggregation.getProductId(), + yearMonth, + aggregation.getViewCount(), + aggregation.getLikeCount(), + aggregation.getSalesCount(), + aggregation.getOrderCount(), + aggregation.getTotalScore(), + aggregation.getRankPosition() + ); + } +} \ No newline at end of file diff --git a/modules/jpa/src/main/generated/com/loopers/domain/ranking/QWeeklyRankEntity.java b/modules/jpa/src/main/generated/com/loopers/domain/ranking/QWeeklyRankEntity.java new file mode 100644 index 000000000..cb74a9808 --- /dev/null +++ b/modules/jpa/src/main/generated/com/loopers/domain/ranking/QWeeklyRankEntity.java @@ -0,0 +1,63 @@ +package com.loopers.domain.ranking; + +import static com.querydsl.core.types.PathMetadataFactory.*; + +import com.querydsl.core.types.dsl.*; + +import com.querydsl.core.types.PathMetadata; +import javax.annotation.processing.Generated; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.dsl.PathInits; + + +/** + * QWeeklyRankEntity is a Querydsl query type for WeeklyRankEntity + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QWeeklyRankEntity extends EntityPathBase { + + private static final long serialVersionUID = 2039637561L; + + private static final PathInits INITS = PathInits.DIRECT2; + + public static final QWeeklyRankEntity weeklyRankEntity = new QWeeklyRankEntity("weeklyRankEntity"); + + public final DateTimePath createdAt = createDateTime("createdAt", java.time.ZonedDateTime.class); + + public final QWeeklyRankId id; + + public final NumberPath likeCount = createNumber("likeCount", Long.class); + + public final NumberPath orderCount = createNumber("orderCount", Long.class); + + public final NumberPath rankPosition = createNumber("rankPosition", Integer.class); + + public final NumberPath salesCount = createNumber("salesCount", Long.class); + + public final NumberPath totalScore = createNumber("totalScore", Long.class); + + public final NumberPath viewCount = createNumber("viewCount", Long.class); + + public QWeeklyRankEntity(String variable) { + this(WeeklyRankEntity.class, forVariable(variable), INITS); + } + + public QWeeklyRankEntity(Path path) { + this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); + } + + public QWeeklyRankEntity(PathMetadata metadata) { + this(metadata, PathInits.getFor(metadata, INITS)); + } + + public QWeeklyRankEntity(PathMetadata metadata, PathInits inits) { + this(WeeklyRankEntity.class, metadata, inits); + } + + public QWeeklyRankEntity(Class type, PathMetadata metadata, PathInits inits) { + super(type, metadata, inits); + this.id = inits.isInitialized("id") ? new QWeeklyRankId(forProperty("id")) : null; + } + +} + diff --git a/modules/jpa/src/main/generated/com/loopers/domain/ranking/QWeeklyRankId.java b/modules/jpa/src/main/generated/com/loopers/domain/ranking/QWeeklyRankId.java new file mode 100644 index 000000000..fd785ddac --- /dev/null +++ b/modules/jpa/src/main/generated/com/loopers/domain/ranking/QWeeklyRankId.java @@ -0,0 +1,39 @@ +package com.loopers.domain.ranking; + +import static com.querydsl.core.types.PathMetadataFactory.*; + +import com.querydsl.core.types.dsl.*; + +import com.querydsl.core.types.PathMetadata; +import javax.annotation.processing.Generated; +import com.querydsl.core.types.Path; + + +/** + * QWeeklyRankId is a Querydsl query type for WeeklyRankId + */ +@Generated("com.querydsl.codegen.DefaultEmbeddableSerializer") +public class QWeeklyRankId extends BeanPath { + + private static final long serialVersionUID = 1225730673L; + + public static final QWeeklyRankId weeklyRankId = new QWeeklyRankId("weeklyRankId"); + + public final NumberPath productId = createNumber("productId", Long.class); + + public final StringPath yearWeek = createString("yearWeek"); + + public QWeeklyRankId(String variable) { + super(WeeklyRankId.class, forVariable(variable)); + } + + public QWeeklyRankId(Path path) { + super(path.getType(), path.getMetadata()); + } + + public QWeeklyRankId(PathMetadata metadata) { + super(WeeklyRankId.class, metadata); + } + +} + diff --git a/modules/jpa/src/main/java/com/loopers/domain/ranking/MonthlyRankEntity.java b/modules/jpa/src/main/java/com/loopers/domain/ranking/MonthlyRankEntity.java new file mode 100644 index 000000000..6d0675558 --- /dev/null +++ b/modules/jpa/src/main/java/com/loopers/domain/ranking/MonthlyRankEntity.java @@ -0,0 +1,96 @@ +package com.loopers.domain.ranking; + +import jakarta.persistence.Column; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.Index; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.Objects; + +/** + * ์›”๊ฐ„ ๋žญํ‚น MV ์—”ํ‹ฐํ‹ฐ + * - ๋ฐฐ์น˜ Job์—์„œ ์›”๊ฐ„ TOP 100 ๋žญํ‚น์„ ์ €์žฅ + */ +@Entity +@Table( + name = "mv_product_rank_monthly", + indexes = { + @Index(name = "idx_year_month_rank", columnList = "year_month, rank_position") + } +) +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class MonthlyRankEntity { + + @EmbeddedId + private MonthlyRankId id; + + @Column(name = "view_count", nullable = false) + private long viewCount; + + @Column(name = "like_count", nullable = false) + private long likeCount; + + @Column(name = "sales_count", nullable = false) + private long salesCount; + + @Column(name = "order_count", nullable = false) + private long orderCount; + + @Column(name = "total_score", nullable = false) + private long totalScore; + + @Column(name = "rank_position", nullable = false) + private int rankPosition; + + @Column(name = "created_at", nullable = false) + private LocalDateTime createdAt; + + private MonthlyRankEntity(MonthlyRankId id, long viewCount, long likeCount, + long salesCount, long orderCount, long totalScore, int rankPosition) { + this.id = id; + this.viewCount = viewCount; + this.likeCount = likeCount; + this.salesCount = salesCount; + this.orderCount = orderCount; + this.totalScore = totalScore; + this.rankPosition = rankPosition; + this.createdAt = LocalDateTime.now(); + } + + /** + * ์›”๊ฐ„ ๋žญํ‚น ์—”ํ‹ฐํ‹ฐ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + public static MonthlyRankEntity create(Long productId, String yearMonth, + long viewCount, long likeCount, + long salesCount, long orderCount, + long totalScore, int rankPosition) { + Objects.requireNonNull(productId, "์ƒํ’ˆ ID๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."); + Objects.requireNonNull(yearMonth, "์›” ์ •๋ณด๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."); + validateRankPosition(rankPosition); + + MonthlyRankId id = MonthlyRankId.of(productId, yearMonth); + return new MonthlyRankEntity(id, viewCount, likeCount, salesCount, orderCount, totalScore, rankPosition); + } + + private static void validateRankPosition(int rankPosition) { + if (rankPosition < 1 || rankPosition > 100) { + throw new IllegalArgumentException( + String.format("์ˆœ์œ„๋Š” 1~100 ๋ฒ”์œ„์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค. (์ž…๋ ฅ๊ฐ’: %d)", rankPosition)); + } + } + + // ํŽธ์˜ ๋ฉ”์„œ๋“œ + public Long getProductId() { + return id.getProductId(); + } + + public String getYearMonth() { + return id.getYearMonth(); + } +} diff --git a/modules/jpa/src/main/java/com/loopers/domain/ranking/MonthlyRankId.java b/modules/jpa/src/main/java/com/loopers/domain/ranking/MonthlyRankId.java new file mode 100644 index 000000000..f475c8134 --- /dev/null +++ b/modules/jpa/src/main/java/com/loopers/domain/ranking/MonthlyRankId.java @@ -0,0 +1,36 @@ +package com.loopers.domain.ranking; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.AccessLevel; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * ์›”๊ฐ„ ๋žญํ‚น ๋ณตํ•ฉ PK + * - product_id + year_month ์กฐํ•ฉ์œผ๋กœ ์œ ์ผ์„ฑ ๋ณด์žฅ + */ +@Embeddable +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@EqualsAndHashCode +public class MonthlyRankId implements Serializable { + + @Column(name = "product_id", nullable = false) + private Long productId; + + @Column(name = "year_month", nullable = false, length = 7) + private String yearMonth; // e.g., "2024-12" + + private MonthlyRankId(Long productId, String yearMonth) { + this.productId = productId; + this.yearMonth = yearMonth; + } + + public static MonthlyRankId of(Long productId, String yearMonth) { + return new MonthlyRankId(productId, yearMonth); + } +} diff --git a/modules/jpa/src/main/java/com/loopers/domain/ranking/MonthlyRankRepository.java b/modules/jpa/src/main/java/com/loopers/domain/ranking/MonthlyRankRepository.java new file mode 100644 index 000000000..f531e9dc0 --- /dev/null +++ b/modules/jpa/src/main/java/com/loopers/domain/ranking/MonthlyRankRepository.java @@ -0,0 +1,37 @@ +package com.loopers.domain.ranking; + +import java.util.List; + +/** + * ์›”๊ฐ„ ๋žญํ‚น Repository ์ธํ„ฐํŽ˜์ด์Šค + */ +public interface MonthlyRankRepository { + + /** + * ์›”๊ฐ„ ๋žญํ‚น ์—”ํ‹ฐํ‹ฐ๋ฅผ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. + */ + MonthlyRankEntity save(MonthlyRankEntity entity); + + /** + * ์›”๊ฐ„ ๋žญํ‚น ์—”ํ‹ฐํ‹ฐ ๋ชฉ๋ก์„ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. + */ + List saveAll(List entities); + + /** + * ํŠน์ • ์›”์˜ ๋žญํ‚น์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + */ + List findByYearMonth(String yearMonth); + + /** + * ํŠน์ • ์›”์˜ ๋žญํ‚น์„ ํŽ˜์ด์ง€๋„ค์ด์…˜ํ•˜์—ฌ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + */ + List findByYearMonthWithPagination(String yearMonth, int page, int size); + + /** + * ํŠน์ • ์›”์˜ ๋ชจ๋“  ๋žญํ‚น์„ ์‚ญ์ œํ•ฉ๋‹ˆ๋‹ค. (๋ฉฑ๋“ฑ์„ฑ ๋ณด์žฅ์šฉ) + * + * @param yearMonth ์‚ญ์ œํ•  ์›” (์˜ˆ: "2024-12") + * @return ์‚ญ์ œ๋œ ๋ ˆ์ฝ”๋“œ ์ˆ˜ + */ + long deleteByYearMonth(String yearMonth); +} diff --git a/modules/jpa/src/main/java/com/loopers/domain/ranking/WeeklyRankEntity.java b/modules/jpa/src/main/java/com/loopers/domain/ranking/WeeklyRankEntity.java new file mode 100644 index 000000000..1ab71beb1 --- /dev/null +++ b/modules/jpa/src/main/java/com/loopers/domain/ranking/WeeklyRankEntity.java @@ -0,0 +1,96 @@ +package com.loopers.domain.ranking; + +import jakarta.persistence.Column; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.Index; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.Objects; + +/** + * ์ฃผ๊ฐ„ ๋žญํ‚น MV ์—”ํ‹ฐํ‹ฐ + * - ๋ฐฐ์น˜ Job์—์„œ ์ฃผ๊ฐ„ TOP 100 ๋žญํ‚น์„ ์ €์žฅ + */ +@Entity +@Table( + name = "mv_product_rank_weekly", + indexes = { + @Index(name = "idx_year_week_rank", columnList = "year_week, rank_position") + } +) +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class WeeklyRankEntity { + + @EmbeddedId + private WeeklyRankId id; + + @Column(name = "view_count", nullable = false) + private long viewCount; + + @Column(name = "like_count", nullable = false) + private long likeCount; + + @Column(name = "sales_count", nullable = false) + private long salesCount; + + @Column(name = "order_count", nullable = false) + private long orderCount; + + @Column(name = "total_score", nullable = false) + private long totalScore; + + @Column(name = "rank_position", nullable = false) + private int rankPosition; + + @Column(name = "created_at", nullable = false) + private LocalDateTime createdAt; + + private WeeklyRankEntity(WeeklyRankId id, long viewCount, long likeCount, + long salesCount, long orderCount, long totalScore, int rankPosition) { + this.id = id; + this.viewCount = viewCount; + this.likeCount = likeCount; + this.salesCount = salesCount; + this.orderCount = orderCount; + this.totalScore = totalScore; + this.rankPosition = rankPosition; + this.createdAt = LocalDateTime.now(); + } + + /** + * ์ฃผ๊ฐ„ ๋žญํ‚น ์—”ํ‹ฐํ‹ฐ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + public static WeeklyRankEntity create(Long productId, String yearWeek, + long viewCount, long likeCount, + long salesCount, long orderCount, + long totalScore, int rankPosition) { + Objects.requireNonNull(productId, "์ƒํ’ˆ ID๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."); + Objects.requireNonNull(yearWeek, "์ฃผ์ฐจ ์ •๋ณด๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."); + validateRankPosition(rankPosition); + + WeeklyRankId id = WeeklyRankId.of(productId, yearWeek); + return new WeeklyRankEntity(id, viewCount, likeCount, salesCount, orderCount, totalScore, rankPosition); + } + + private static void validateRankPosition(int rankPosition) { + if (rankPosition < 1 || rankPosition > 100) { + throw new IllegalArgumentException( + String.format("์ˆœ์œ„๋Š” 1~100 ๋ฒ”์œ„์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค. (์ž…๋ ฅ๊ฐ’: %d)", rankPosition)); + } + } + + // ํŽธ์˜ ๋ฉ”์„œ๋“œ + public Long getProductId() { + return id.getProductId(); + } + + public String getYearWeek() { + return id.getYearWeek(); + } +} diff --git a/modules/jpa/src/main/java/com/loopers/domain/ranking/WeeklyRankId.java b/modules/jpa/src/main/java/com/loopers/domain/ranking/WeeklyRankId.java new file mode 100644 index 000000000..4d16f68b8 --- /dev/null +++ b/modules/jpa/src/main/java/com/loopers/domain/ranking/WeeklyRankId.java @@ -0,0 +1,36 @@ +package com.loopers.domain.ranking; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.AccessLevel; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * ์ฃผ๊ฐ„ ๋žญํ‚น ๋ณตํ•ฉ PK + * - product_id + year_week ์กฐํ•ฉ์œผ๋กœ ์œ ์ผ์„ฑ ๋ณด์žฅ + */ +@Embeddable +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@EqualsAndHashCode +public class WeeklyRankId implements Serializable { + + @Column(name = "product_id", nullable = false) + private Long productId; + + @Column(name = "year_week", nullable = false, length = 8) + private String yearWeek; // e.g., "2024-W52" + + private WeeklyRankId(Long productId, String yearWeek) { + this.productId = productId; + this.yearWeek = yearWeek; + } + + public static WeeklyRankId of(Long productId, String yearWeek) { + return new WeeklyRankId(productId, yearWeek); + } +} diff --git a/modules/jpa/src/main/java/com/loopers/domain/ranking/WeeklyRankRepository.java b/modules/jpa/src/main/java/com/loopers/domain/ranking/WeeklyRankRepository.java new file mode 100644 index 000000000..07d5a04b0 --- /dev/null +++ b/modules/jpa/src/main/java/com/loopers/domain/ranking/WeeklyRankRepository.java @@ -0,0 +1,37 @@ +package com.loopers.domain.ranking; + +import java.util.List; + +/** + * ์ฃผ๊ฐ„ ๋žญํ‚น Repository ์ธํ„ฐํŽ˜์ด์Šค + */ +public interface WeeklyRankRepository { + + /** + * ์ฃผ๊ฐ„ ๋žญํ‚น ์—”ํ‹ฐํ‹ฐ๋ฅผ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. + */ + WeeklyRankEntity save(WeeklyRankEntity entity); + + /** + * ์ฃผ๊ฐ„ ๋žญํ‚น ์—”ํ‹ฐํ‹ฐ ๋ชฉ๋ก์„ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. + */ + List saveAll(List entities); + + /** + * ํŠน์ • ์ฃผ์ฐจ์˜ ๋žญํ‚น์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + */ + List findByYearWeek(String yearWeek); + + /** + * ํŠน์ • ์ฃผ์ฐจ์˜ ๋žญํ‚น์„ ํŽ˜์ด์ง€๋„ค์ด์…˜ํ•˜์—ฌ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + */ + List findByYearWeekWithPagination(String yearWeek, int page, int size); + + /** + * ํŠน์ • ์ฃผ์ฐจ์˜ ๋ชจ๋“  ๋žญํ‚น์„ ์‚ญ์ œํ•ฉ๋‹ˆ๋‹ค. (๋ฉฑ๋“ฑ์„ฑ ๋ณด์žฅ์šฉ) + * + * @param yearWeek ์‚ญ์ œํ•  ์ฃผ์ฐจ (์˜ˆ: "2024-W52") + * @return ์‚ญ์ œ๋œ ๋ ˆ์ฝ”๋“œ ์ˆ˜ + */ + long deleteByYearWeek(String yearWeek); +} diff --git a/modules/jpa/src/test/java/com/loopers/domain/ranking/MonthlyRankEntityUnitTest.java b/modules/jpa/src/test/java/com/loopers/domain/ranking/MonthlyRankEntityUnitTest.java new file mode 100644 index 000000000..a5292c7f5 --- /dev/null +++ b/modules/jpa/src/test/java/com/loopers/domain/ranking/MonthlyRankEntityUnitTest.java @@ -0,0 +1,141 @@ +package com.loopers.domain.ranking; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +@DisplayName("MonthlyRankEntity ๋‹จ์œ„ ํ…Œ์ŠคํŠธ") +class MonthlyRankEntityUnitTest { + + @Nested + @DisplayName("์›”๊ฐ„ ๋žญํ‚น ์—”ํ‹ฐํ‹ฐ ์ƒ์„ฑ") + class ์›”๊ฐ„_๋žญํ‚น_์—”ํ‹ฐํ‹ฐ_์ƒ์„ฑ { + + @Test + @DisplayName("์œ ํšจํ•œ ์ •๋ณด๋กœ ์›”๊ฐ„ ๋žญํ‚น ์—”ํ‹ฐํ‹ฐ๋ฅผ ์ƒ์„ฑํ•˜๋ฉด ์„ฑ๊ณตํ•œ๋‹ค") + void should_create_monthly_rank_entity_successfully_with_valid_information() { + // given + Long productId = 1L; + String yearMonth = "2024-12"; + long viewCount = 1000L; + long likeCount = 500L; + long salesCount = 100L; + long orderCount = 50L; + long totalScore = 3100L; + int rankPosition = 1; + + // when + MonthlyRankEntity entity = MonthlyRankEntity.create( + productId, yearMonth, viewCount, likeCount, salesCount, orderCount, totalScore, rankPosition + ); + + // then + Assertions.assertThat(entity).isNotNull(); + Assertions.assertThat(entity.getProductId()).isEqualTo(productId); + Assertions.assertThat(entity.getYearMonth()).isEqualTo(yearMonth); + Assertions.assertThat(entity.getViewCount()).isEqualTo(viewCount); + Assertions.assertThat(entity.getLikeCount()).isEqualTo(likeCount); + Assertions.assertThat(entity.getSalesCount()).isEqualTo(salesCount); + Assertions.assertThat(entity.getOrderCount()).isEqualTo(orderCount); + Assertions.assertThat(entity.getTotalScore()).isEqualTo(totalScore); + Assertions.assertThat(entity.getRankPosition()).isEqualTo(rankPosition); + Assertions.assertThat(entity.getCreatedAt()).isNotNull(); + } + + @Test + @DisplayName("์ƒํ’ˆ ID๊ฐ€ null์ด๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค") + void should_throw_exception_when_product_id_is_null() { + // given & when & then + Assertions.assertThatThrownBy(() -> + MonthlyRankEntity.create(null, "2024-12", 1000L, 500L, 100L, 50L, 3100L, 1) + ) + .isInstanceOf(NullPointerException.class) + .hasMessage("์ƒํ’ˆ ID๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."); + } + + @Test + @DisplayName("์›” ์ •๋ณด๊ฐ€ null์ด๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค") + void should_throw_exception_when_year_month_is_null() { + // given & when & then + Assertions.assertThatThrownBy(() -> + MonthlyRankEntity.create(1L, null, 1000L, 500L, 100L, 50L, 3100L, 1) + ) + .isInstanceOf(NullPointerException.class) + .hasMessage("์›” ์ •๋ณด๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."); + } + + @Test + @DisplayName("์ˆœ์œ„๊ฐ€ 0์ด๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค") + void should_throw_exception_when_rank_position_is_zero() { + // given & when & then + Assertions.assertThatThrownBy(() -> + MonthlyRankEntity.create(1L, "2024-12", 1000L, 500L, 100L, 50L, 3100L, 0) + ) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("์ˆœ์œ„๋Š” 1~100 ๋ฒ”์œ„์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค"); + } + + @Test + @DisplayName("์ˆœ์œ„๊ฐ€ 100์„ ์ดˆ๊ณผํ•˜๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค") + void should_throw_exception_when_rank_position_exceeds_100() { + // given & when & then + Assertions.assertThatThrownBy(() -> + MonthlyRankEntity.create(1L, "2024-12", 1000L, 500L, 100L, 50L, 3100L, 101) + ) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("์ˆœ์œ„๋Š” 1~100 ๋ฒ”์œ„์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค"); + } + + @Test + @DisplayName("์ˆœ์œ„๊ฐ€ 100์ด๋ฉด ์ •์ƒ์ ์œผ๋กœ ์ƒ์„ฑ๋œ๋‹ค") + void should_create_entity_when_rank_position_is_100() { + // given & when + MonthlyRankEntity entity = MonthlyRankEntity.create( + 1L, "2024-12", 1000L, 500L, 100L, 50L, 3100L, 100 + ); + + // then + Assertions.assertThat(entity.getRankPosition()).isEqualTo(100); + } + } + + @Nested + @DisplayName("๋ณตํ•ฉ PK ํ…Œ์ŠคํŠธ") + class ๋ณตํ•ฉ_PK_ํ…Œ์ŠคํŠธ { + + @Test + @DisplayName("๋™์ผํ•œ productId์™€ yearMonth๋กœ ์ƒ์„ฑ๋œ ID๋Š” ๋™๋“ฑํ•˜๋‹ค") + void should_be_equal_when_same_product_id_and_year_month() { + // given + MonthlyRankId id1 = MonthlyRankId.of(1L, "2024-12"); + MonthlyRankId id2 = MonthlyRankId.of(1L, "2024-12"); + + // when & then + Assertions.assertThat(id1).isEqualTo(id2); + Assertions.assertThat(id1.hashCode()).isEqualTo(id2.hashCode()); + } + + @Test + @DisplayName("๋‹ค๋ฅธ productId๋กœ ์ƒ์„ฑ๋œ ID๋Š” ๋™๋“ฑํ•˜์ง€ ์•Š๋‹ค") + void should_not_be_equal_when_different_product_id() { + // given + MonthlyRankId id1 = MonthlyRankId.of(1L, "2024-12"); + MonthlyRankId id2 = MonthlyRankId.of(2L, "2024-12"); + + // when & then + Assertions.assertThat(id1).isNotEqualTo(id2); + } + + @Test + @DisplayName("๋‹ค๋ฅธ yearMonth๋กœ ์ƒ์„ฑ๋œ ID๋Š” ๋™๋“ฑํ•˜์ง€ ์•Š๋‹ค") + void should_not_be_equal_when_different_year_month() { + // given + MonthlyRankId id1 = MonthlyRankId.of(1L, "2024-12"); + MonthlyRankId id2 = MonthlyRankId.of(1L, "2024-11"); + + // when & then + Assertions.assertThat(id1).isNotEqualTo(id2); + } + } +} diff --git a/modules/jpa/src/test/java/com/loopers/domain/ranking/WeeklyRankEntityUnitTest.java b/modules/jpa/src/test/java/com/loopers/domain/ranking/WeeklyRankEntityUnitTest.java new file mode 100644 index 000000000..7bdbfd7a5 --- /dev/null +++ b/modules/jpa/src/test/java/com/loopers/domain/ranking/WeeklyRankEntityUnitTest.java @@ -0,0 +1,141 @@ +package com.loopers.domain.ranking; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +@DisplayName("WeeklyRankEntity ๋‹จ์œ„ ํ…Œ์ŠคํŠธ") +class WeeklyRankEntityUnitTest { + + @Nested + @DisplayName("์ฃผ๊ฐ„ ๋žญํ‚น ์—”ํ‹ฐํ‹ฐ ์ƒ์„ฑ") + class ์ฃผ๊ฐ„_๋žญํ‚น_์—”ํ‹ฐํ‹ฐ_์ƒ์„ฑ { + + @Test + @DisplayName("์œ ํšจํ•œ ์ •๋ณด๋กœ ์ฃผ๊ฐ„ ๋žญํ‚น ์—”ํ‹ฐํ‹ฐ๋ฅผ ์ƒ์„ฑํ•˜๋ฉด ์„ฑ๊ณตํ•œ๋‹ค") + void should_create_weekly_rank_entity_successfully_with_valid_information() { + // given + Long productId = 1L; + String yearWeek = "2024-W52"; + long viewCount = 100L; + long likeCount = 50L; + long salesCount = 10L; + long orderCount = 5L; + long totalScore = 310L; + int rankPosition = 1; + + // when + WeeklyRankEntity entity = WeeklyRankEntity.create( + productId, yearWeek, viewCount, likeCount, salesCount, orderCount, totalScore, rankPosition + ); + + // then + Assertions.assertThat(entity).isNotNull(); + Assertions.assertThat(entity.getProductId()).isEqualTo(productId); + Assertions.assertThat(entity.getYearWeek()).isEqualTo(yearWeek); + Assertions.assertThat(entity.getViewCount()).isEqualTo(viewCount); + Assertions.assertThat(entity.getLikeCount()).isEqualTo(likeCount); + Assertions.assertThat(entity.getSalesCount()).isEqualTo(salesCount); + Assertions.assertThat(entity.getOrderCount()).isEqualTo(orderCount); + Assertions.assertThat(entity.getTotalScore()).isEqualTo(totalScore); + Assertions.assertThat(entity.getRankPosition()).isEqualTo(rankPosition); + Assertions.assertThat(entity.getCreatedAt()).isNotNull(); + } + + @Test + @DisplayName("์ƒํ’ˆ ID๊ฐ€ null์ด๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค") + void should_throw_exception_when_product_id_is_null() { + // given & when & then + Assertions.assertThatThrownBy(() -> + WeeklyRankEntity.create(null, "2024-W52", 100L, 50L, 10L, 5L, 310L, 1) + ) + .isInstanceOf(NullPointerException.class) + .hasMessage("์ƒํ’ˆ ID๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."); + } + + @Test + @DisplayName("์ฃผ์ฐจ ์ •๋ณด๊ฐ€ null์ด๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค") + void should_throw_exception_when_year_week_is_null() { + // given & when & then + Assertions.assertThatThrownBy(() -> + WeeklyRankEntity.create(1L, null, 100L, 50L, 10L, 5L, 310L, 1) + ) + .isInstanceOf(NullPointerException.class) + .hasMessage("์ฃผ์ฐจ ์ •๋ณด๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."); + } + + @Test + @DisplayName("์ˆœ์œ„๊ฐ€ 0์ด๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค") + void should_throw_exception_when_rank_position_is_zero() { + // given & when & then + Assertions.assertThatThrownBy(() -> + WeeklyRankEntity.create(1L, "2024-W52", 100L, 50L, 10L, 5L, 310L, 0) + ) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("์ˆœ์œ„๋Š” 1~100 ๋ฒ”์œ„์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค"); + } + + @Test + @DisplayName("์ˆœ์œ„๊ฐ€ 100์„ ์ดˆ๊ณผํ•˜๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค") + void should_throw_exception_when_rank_position_exceeds_100() { + // given & when & then + Assertions.assertThatThrownBy(() -> + WeeklyRankEntity.create(1L, "2024-W52", 100L, 50L, 10L, 5L, 310L, 101) + ) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("์ˆœ์œ„๋Š” 1~100 ๋ฒ”์œ„์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค"); + } + + @Test + @DisplayName("์ˆœ์œ„๊ฐ€ 100์ด๋ฉด ์ •์ƒ์ ์œผ๋กœ ์ƒ์„ฑ๋œ๋‹ค") + void should_create_entity_when_rank_position_is_100() { + // given & when + WeeklyRankEntity entity = WeeklyRankEntity.create( + 1L, "2024-W52", 100L, 50L, 10L, 5L, 310L, 100 + ); + + // then + Assertions.assertThat(entity.getRankPosition()).isEqualTo(100); + } + } + + @Nested + @DisplayName("๋ณตํ•ฉ PK ํ…Œ์ŠคํŠธ") + class ๋ณตํ•ฉ_PK_ํ…Œ์ŠคํŠธ { + + @Test + @DisplayName("๋™์ผํ•œ productId์™€ yearWeek๋กœ ์ƒ์„ฑ๋œ ID๋Š” ๋™๋“ฑํ•˜๋‹ค") + void should_be_equal_when_same_product_id_and_year_week() { + // given + WeeklyRankId id1 = WeeklyRankId.of(1L, "2024-W52"); + WeeklyRankId id2 = WeeklyRankId.of(1L, "2024-W52"); + + // when & then + Assertions.assertThat(id1).isEqualTo(id2); + Assertions.assertThat(id1.hashCode()).isEqualTo(id2.hashCode()); + } + + @Test + @DisplayName("๋‹ค๋ฅธ productId๋กœ ์ƒ์„ฑ๋œ ID๋Š” ๋™๋“ฑํ•˜์ง€ ์•Š๋‹ค") + void should_not_be_equal_when_different_product_id() { + // given + WeeklyRankId id1 = WeeklyRankId.of(1L, "2024-W52"); + WeeklyRankId id2 = WeeklyRankId.of(2L, "2024-W52"); + + // when & then + Assertions.assertThat(id1).isNotEqualTo(id2); + } + + @Test + @DisplayName("๋‹ค๋ฅธ yearWeek๋กœ ์ƒ์„ฑ๋œ ID๋Š” ๋™๋“ฑํ•˜์ง€ ์•Š๋‹ค") + void should_not_be_equal_when_different_year_week() { + // given + WeeklyRankId id1 = WeeklyRankId.of(1L, "2024-W52"); + WeeklyRankId id2 = WeeklyRankId.of(1L, "2024-W51"); + + // when & then + Assertions.assertThat(id1).isNotEqualTo(id2); + } + } +} From 139cbc3871f4ddc6ca9dc45c3e5361c15c7decdd Mon Sep 17 00:00:00 2001 From: hyujikoh Date: Fri, 2 Jan 2026 16:11:43 +0900 Subject: [PATCH 72/85] =?UTF-8?q?feat(ranking):=20=EC=9B=94=EA=B0=84=20?= =?UTF-8?q?=EB=B0=8F=20=EC=A3=BC=EA=B0=84=20=EB=9E=AD=ED=82=B9=20=EC=97=94?= =?UTF-8?q?=ED=8B=B0=ED=8B=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - rankPosition ํ•„๋“œ ํƒ€์ž…์„ int์—์„œ long์œผ๋กœ ๋ณ€๊ฒฝ - createdAt ํ•„๋“œ ํƒ€์ž…์„ ZonedDateTime์—์„œ LocalDateTime์œผ๋กœ ๋ณ€๊ฒฝ - ๋ถˆํ•„์š”ํ•œ ๋ฉ”์„œ๋“œ ์ฃผ์„ ์ œ๊ฑฐ --- .../job/ranking/dto/RankingAggregation.java | 2 +- .../loopers/CommerceBatchApplicationTest.java | 10 - .../batch/job/ranking/ManualBatchJobTest.java | 73 ------- .../job/ranking/RankingBatchE2ETest.java | 206 ------------------ .../writer/MonthlyRankWriterUnitTest.java | 123 ----------- .../writer/WeeklyRankWriterUnitTest.java | 125 ----------- .../domain/ranking/QWeeklyRankEntity.java | 4 +- .../metrics/ProductMetricsRepository.java | 8 - .../domain/ranking/MonthlyRankEntity.java | 2 +- .../domain/ranking/WeeklyRankEntity.java | 2 +- 10 files changed, 5 insertions(+), 550 deletions(-) delete mode 100644 apps/commerce-batch/src/test/java/com/loopers/CommerceBatchApplicationTest.java delete mode 100644 apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/ManualBatchJobTest.java delete mode 100644 apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/RankingBatchE2ETest.java delete mode 100644 apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/writer/MonthlyRankWriterUnitTest.java delete mode 100644 apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/writer/WeeklyRankWriterUnitTest.java diff --git a/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/dto/RankingAggregation.java b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/dto/RankingAggregation.java index eee7f526d..dc63f3811 100644 --- a/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/dto/RankingAggregation.java +++ b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/dto/RankingAggregation.java @@ -80,4 +80,4 @@ public String toString() { return String.format("RankingAggregation{productId=%d, score=%d, rank=%d}", productId, totalScore, rankPosition); } -} \ No newline at end of file +} diff --git a/apps/commerce-batch/src/test/java/com/loopers/CommerceBatchApplicationTest.java b/apps/commerce-batch/src/test/java/com/loopers/CommerceBatchApplicationTest.java deleted file mode 100644 index c5e3bc7a3..000000000 --- a/apps/commerce-batch/src/test/java/com/loopers/CommerceBatchApplicationTest.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.loopers; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -public class CommerceBatchApplicationTest { - @Test - void contextLoads() {} -} diff --git a/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/ManualBatchJobTest.java b/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/ManualBatchJobTest.java deleted file mode 100644 index 92c04bd0a..000000000 --- a/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/ManualBatchJobTest.java +++ /dev/null @@ -1,73 +0,0 @@ -package com.loopers.batch.job.ranking; - -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.batch.core.Job; -import org.springframework.batch.core.JobParameters; -import org.springframework.batch.core.JobParametersBuilder; -import org.springframework.batch.core.launch.JobLauncher; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.TestPropertySource; - -/** - * ์ˆ˜๋™ ๋ฐฐ์น˜ Job ์‹คํ–‰ ํ…Œ์ŠคํŠธ - * - ์‹ค์ œ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์„ ๋•Œ ์ˆ˜๋™์œผ๋กœ ์‹คํ–‰ํ•˜์—ฌ ๊ฒ€์ฆ - * - @Disabled๋กœ ๊ธฐ๋ณธ์ ์œผ๋กœ ๋น„ํ™œ์„ฑํ™” - */ -@SpringBootTest -@TestPropertySource(properties = { - "spring.batch.job.enabled=false" -}) -@DisplayName("์ˆ˜๋™ ๋ฐฐ์น˜ Job ์‹คํ–‰ ํ…Œ์ŠคํŠธ") -class ManualBatchJobTest { - - @Autowired - private JobLauncher jobLauncher; - - @Autowired - @Qualifier("weeklyRankingJob") - private Job weeklyRankingJob; - - @Autowired - @Qualifier("monthlyRankingJob") - private Job monthlyRankingJob; - - @Test - @Disabled("์ˆ˜๋™ ์‹คํ–‰์šฉ - ์‹ค์ œ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์„ ๋•Œ๋งŒ ํ™œ์„ฑํ™”") - @DisplayName("์ฃผ๊ฐ„ ๋žญํ‚น Job ์ˆ˜๋™ ์‹คํ–‰") - void manual_weekly_ranking_job_execution() throws Exception { - // given - JobParameters jobParameters = new JobParametersBuilder() - .addString("yearWeek", "2024-W52") - .addLong("timestamp", System.currentTimeMillis()) - .toJobParameters(); - - // when - jobLauncher.run(weeklyRankingJob, jobParameters); - - // then - System.out.println("์ฃผ๊ฐ„ ๋žญํ‚น Job ์‹คํ–‰ ์™„๋ฃŒ - 2024-W52"); - System.out.println("MV ํ…Œ์ด๋ธ”(mv_product_rank_weekly)์„ ํ™•์ธํ•˜์„ธ์š”."); - } - - @Test - @Disabled("์ˆ˜๋™ ์‹คํ–‰์šฉ - ์‹ค์ œ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์„ ๋•Œ๋งŒ ํ™œ์„ฑํ™”") - @DisplayName("์›”๊ฐ„ ๋žญํ‚น Job ์ˆ˜๋™ ์‹คํ–‰") - void manual_monthly_ranking_job_execution() throws Exception { - // given - JobParameters jobParameters = new JobParametersBuilder() - .addString("yearMonth", "2024-12") - .addLong("timestamp", System.currentTimeMillis()) - .toJobParameters(); - - // when - jobLauncher.run(monthlyRankingJob, jobParameters); - - // then - System.out.println("์›”๊ฐ„ ๋žญํ‚น Job ์‹คํ–‰ ์™„๋ฃŒ - 2024-12"); - System.out.println("MV ํ…Œ์ด๋ธ”(mv_product_rank_monthly)์„ ํ™•์ธํ•˜์„ธ์š”."); - } -} \ No newline at end of file diff --git a/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/RankingBatchE2ETest.java b/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/RankingBatchE2ETest.java deleted file mode 100644 index 2f78153a1..000000000 --- a/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/RankingBatchE2ETest.java +++ /dev/null @@ -1,206 +0,0 @@ -package com.loopers.batch.job.ranking; - -import com.loopers.domain.ranking.MonthlyRankEntity; -import com.loopers.domain.ranking.MonthlyRankRepository; -import com.loopers.domain.ranking.WeeklyRankEntity; -import com.loopers.domain.ranking.WeeklyRankRepository; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.batch.core.BatchStatus; -import org.springframework.batch.core.JobExecution; -import org.springframework.batch.core.JobParameters; -import org.springframework.batch.core.JobParametersBuilder; -import org.springframework.batch.test.JobLauncherTestUtils; -import org.springframework.batch.test.context.SpringBatchTest; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.TestPropertySource; - -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * ๋žญํ‚น ๋ฐฐ์น˜ Job E2E ํ…Œ์ŠคํŠธ - * - ์‹ค์ œ Job ์‹คํ–‰๋ถ€ํ„ฐ MV ํ…Œ์ด๋ธ” ์ €์žฅ๊นŒ์ง€ ์ „์ฒด ํ”Œ๋กœ์šฐ ๊ฒ€์ฆ - */ -@SpringBatchTest -@SpringBootTest -@TestPropertySource(properties = { - "spring.batch.job.enabled=false" // ์ž๋™ ์‹คํ–‰ ๋ฐฉ์ง€ -}) -@DisplayName("๋žญํ‚น ๋ฐฐ์น˜ Job E2E ํ…Œ์ŠคํŠธ") -class RankingBatchE2ETest { - - @Autowired - private JobLauncherTestUtils jobLauncherTestUtils; - - @Autowired - private WeeklyRankRepository weeklyRankRepository; - - @Autowired - private MonthlyRankRepository monthlyRankRepository; - - @AfterEach - void tearDown() { - // ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ ์ •๋ฆฌ - weeklyRankRepository.deleteByYearWeek("2024-W52"); - monthlyRankRepository.deleteByYearMonth("2024-12"); - } - - @Nested - @DisplayName("์ฃผ๊ฐ„ ๋žญํ‚น ๋ฐฐ์น˜ Job") - class ์ฃผ๊ฐ„_๋žญํ‚น_๋ฐฐ์น˜_Job { - - @Test - @DisplayName("์ฃผ๊ฐ„ ๋žญํ‚น Job์ด ์„ฑ๊ณต์ ์œผ๋กœ ์‹คํ–‰๋˜๊ณ  MV ํ…Œ์ด๋ธ”์— ๋ฐ์ดํ„ฐ๊ฐ€ ์ €์žฅ๋œ๋‹ค") - void should_execute_weekly_ranking_job_successfully() throws Exception { - // given - String yearWeek = "2024-W52"; - JobParameters jobParameters = new JobParametersBuilder() - .addString("yearWeek", yearWeek) - .addLong("timestamp", System.currentTimeMillis()) // ์œ ๋‹ˆํฌ ํŒŒ๋ผ๋ฏธํ„ฐ - .toJobParameters(); - - // when - JobExecution jobExecution = jobLauncherTestUtils.launchJob(jobParameters); - - // then - assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED); - - // MV ํ…Œ์ด๋ธ” ๊ฒ€์ฆ - List rankings = weeklyRankRepository.findByYearWeek(yearWeek); - assertThat(rankings).isNotEmpty(); - assertThat(rankings.size()).isLessThanOrEqualTo(100); // TOP 100 - - // ์ˆœ์œ„ ๊ฒ€์ฆ - if (!rankings.isEmpty()) { - assertThat(rankings.get(0).getRankPosition()).isEqualTo(1); - - // ์ ์ˆ˜ ์ˆœ ์ •๋ ฌ ๊ฒ€์ฆ - for (int i = 1; i < rankings.size(); i++) { - assertThat(rankings.get(i-1).getTotalScore()) - .isGreaterThanOrEqualTo(rankings.get(i).getTotalScore()); - } - } - } - - @Test - @DisplayName("์ž˜๋ชป๋œ yearWeek ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ Job ์‹คํ–‰ ์‹œ ์‹คํŒจํ•œ๋‹ค") - void should_fail_with_invalid_year_week_parameter() throws Exception { - // given - String invalidYearWeek = "invalid-format"; - JobParameters jobParameters = new JobParametersBuilder() - .addString("yearWeek", invalidYearWeek) - .addLong("timestamp", System.currentTimeMillis()) - .toJobParameters(); - - // when - JobExecution jobExecution = jobLauncherTestUtils.launchJob(jobParameters); - - // then - assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.FAILED); - } - } - - @Nested - @DisplayName("์›”๊ฐ„ ๋žญํ‚น ๋ฐฐ์น˜ Job") - class ์›”๊ฐ„_๋žญํ‚น_๋ฐฐ์น˜_Job { - - @Test - @DisplayName("์›”๊ฐ„ ๋žญํ‚น Job์ด ์„ฑ๊ณต์ ์œผ๋กœ ์‹คํ–‰๋˜๊ณ  MV ํ…Œ์ด๋ธ”์— ๋ฐ์ดํ„ฐ๊ฐ€ ์ €์žฅ๋œ๋‹ค") - void should_execute_monthly_ranking_job_successfully() throws Exception { - // given - String yearMonth = "2024-12"; - JobParameters jobParameters = new JobParametersBuilder() - .addString("yearMonth", yearMonth) - .addLong("timestamp", System.currentTimeMillis()) - .toJobParameters(); - - // when - JobExecution jobExecution = jobLauncherTestUtils.launchJob(jobParameters); - - // then - assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED); - - // MV ํ…Œ์ด๋ธ” ๊ฒ€์ฆ - List rankings = monthlyRankRepository.findByYearMonth(yearMonth); - assertThat(rankings).isNotEmpty(); - assertThat(rankings.size()).isLessThanOrEqualTo(100); // TOP 100 - - // ์ˆœ์œ„ ๊ฒ€์ฆ - if (!rankings.isEmpty()) { - assertThat(rankings.get(0).getRankPosition()).isEqualTo(1); - - // ์ ์ˆ˜ ์ˆœ ์ •๋ ฌ ๊ฒ€์ฆ - for (int i = 1; i < rankings.size(); i++) { - assertThat(rankings.get(i-1).getTotalScore()) - .isGreaterThanOrEqualTo(rankings.get(i).getTotalScore()); - } - } - } - - @Test - @DisplayName("์ž˜๋ชป๋œ yearMonth ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ Job ์‹คํ–‰ ์‹œ ์‹คํŒจํ•œ๋‹ค") - void should_fail_with_invalid_year_month_parameter() throws Exception { - // given - String invalidYearMonth = "invalid-format"; - JobParameters jobParameters = new JobParametersBuilder() - .addString("yearMonth", invalidYearMonth) - .addLong("timestamp", System.currentTimeMillis()) - .toJobParameters(); - - // when - JobExecution jobExecution = jobLauncherTestUtils.launchJob(jobParameters); - - // then - assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.FAILED); - } - } - - @Nested - @DisplayName("๋ฉฑ๋“ฑ์„ฑ ๊ฒ€์ฆ") - class ๋ฉฑ๋“ฑ์„ฑ_๊ฒ€์ฆ { - - @Test - @DisplayName("๋™์ผํ•œ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ Job์„ ์žฌ์‹คํ–‰ํ•ด๋„ ๊ฒฐ๊ณผ๊ฐ€ ๋™์ผํ•˜๋‹ค") - void should_produce_same_result_when_job_is_rerun() throws Exception { - // given - String yearWeek = "2024-W52"; - JobParameters jobParameters = new JobParametersBuilder() - .addString("yearWeek", yearWeek) - .addLong("timestamp", System.currentTimeMillis()) - .toJobParameters(); - - // when - ์ฒซ ๋ฒˆ์งธ ์‹คํ–‰ - JobExecution firstExecution = jobLauncherTestUtils.launchJob(jobParameters); - List firstResult = weeklyRankRepository.findByYearWeek(yearWeek); - - // when - ๋‘ ๋ฒˆ์งธ ์‹คํ–‰ (๋‹ค๋ฅธ timestamp๋กœ) - JobParameters secondJobParameters = new JobParametersBuilder() - .addString("yearWeek", yearWeek) - .addLong("timestamp", System.currentTimeMillis() + 1000) - .toJobParameters(); - JobExecution secondExecution = jobLauncherTestUtils.launchJob(secondJobParameters); - List secondResult = weeklyRankRepository.findByYearWeek(yearWeek); - - // then - assertThat(firstExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED); - assertThat(secondExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED); - - // ๊ฒฐ๊ณผ ๋™์ผ์„ฑ ๊ฒ€์ฆ - assertThat(firstResult.size()).isEqualTo(secondResult.size()); - - for (int i = 0; i < firstResult.size(); i++) { - WeeklyRankEntity first = firstResult.get(i); - WeeklyRankEntity second = secondResult.get(i); - - assertThat(first.getProductId()).isEqualTo(second.getProductId()); - assertThat(first.getRankPosition()).isEqualTo(second.getRankPosition()); - assertThat(first.getTotalScore()).isEqualTo(second.getTotalScore()); - } - } - } -} \ No newline at end of file diff --git a/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/writer/MonthlyRankWriterUnitTest.java b/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/writer/MonthlyRankWriterUnitTest.java deleted file mode 100644 index 64078e3bf..000000000 --- a/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/writer/MonthlyRankWriterUnitTest.java +++ /dev/null @@ -1,123 +0,0 @@ -package com.loopers.batch.job.ranking.writer; - -import com.loopers.batch.job.ranking.dto.RankingAggregation; -import com.loopers.domain.ranking.MonthlyRankEntity; -import com.loopers.domain.ranking.MonthlyRankRepository; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.batch.item.Chunk; -import org.springframework.test.util.ReflectionTestUtils; - -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyList; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -@ExtendWith(MockitoExtension.class) -@DisplayName("MonthlyRankWriter ๋‹จ์œ„ ํ…Œ์ŠคํŠธ") -class MonthlyRankWriterUnitTest { - - @Mock - private MonthlyRankRepository monthlyRankRepository; - - @InjectMocks - private MonthlyRankWriter monthlyRankWriter; - - private static final String TEST_YEAR_MONTH = "2024-12"; - - @BeforeEach - void setUp() { - ReflectionTestUtils.setField(monthlyRankWriter, "yearMonth", TEST_YEAR_MONTH); - } - - @Nested - @DisplayName("์›”๊ฐ„ ๋žญํ‚น ์ €์žฅ") - class ์›”๊ฐ„_๋žญํ‚น_์ €์žฅ { - - @Test - @DisplayName("๋žญํ‚น ๋ฐ์ดํ„ฐ๋ฅผ ์ •์ƒ์ ์œผ๋กœ ์ €์žฅํ•œ๋‹ค") - void should_save_monthly_ranking_successfully() throws Exception { - // given - List aggregations = List.of( - new RankingAggregation(1L, 1000L, 500L, 100L, 50L, 3100L, 1), - new RankingAggregation(2L, 800L, 400L, 80L, 40L, 2480L, 2) - ); - Chunk chunk = new Chunk<>(aggregations); - - when(monthlyRankRepository.deleteByYearMonth(TEST_YEAR_MONTH)).thenReturn(2L); - when(monthlyRankRepository.saveAll(anyList())).thenReturn(List.of()); - - // when - monthlyRankWriter.write(chunk); - - // then - verify(monthlyRankRepository).deleteByYearMonth(TEST_YEAR_MONTH); - verify(monthlyRankRepository).saveAll(any()); - } - - @Test - @DisplayName("๋นˆ ์ฒญํฌ์ธ ๊ฒฝ์šฐ ์ €์žฅํ•˜์ง€ ์•Š๋Š”๋‹ค") - void should_not_save_when_chunk_is_empty() throws Exception { - // given - Chunk emptyChunk = new Chunk<>(); - - // when - monthlyRankWriter.write(emptyChunk); - - // then - verify(monthlyRankRepository).deleteByYearMonth(TEST_YEAR_MONTH); - verify(monthlyRankRepository).saveAll(anyList()); - } - - @Test - @DisplayName("์ €์žฅ ์ค‘ ์˜ˆ์™ธ ๋ฐœ์ƒ ์‹œ RuntimeException์„ ๋˜์ง„๋‹ค") - void should_throw_runtime_exception_when_save_fails() { - // given - List aggregations = List.of( - new RankingAggregation(1L, 1000L, 500L, 100L, 50L, 3100L, 1) - ); - Chunk chunk = new Chunk<>(aggregations); - - when(monthlyRankRepository.deleteByYearMonth(TEST_YEAR_MONTH)).thenReturn(1L); - when(monthlyRankRepository.saveAll(anyList())).thenThrow(new RuntimeException("DB ์ €์žฅ ์‹คํŒจ")); - - // when & then - assertThatThrownBy(() -> monthlyRankWriter.write(chunk)) - .isInstanceOf(RuntimeException.class) - .hasMessage("์›”๊ฐ„ ๋žญํ‚น ์ €์žฅ ์‹คํŒจ"); - } - } - - @Nested - @DisplayName("Entity ๋ณ€ํ™˜") - class Entity_๋ณ€ํ™˜ { - - @Test - @DisplayName("RankingAggregation์„ MonthlyRankEntity๋กœ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋ณ€ํ™˜ํ•œ๋‹ค") - void should_convert_aggregation_to_entity_correctly() throws Exception { - // given - RankingAggregation aggregation = new RankingAggregation(1L, 1000L, 500L, 100L, 50L, 3100L, 1); - List expectedEntities = List.of( - MonthlyRankEntity.create(1L, TEST_YEAR_MONTH, 1000L, 500L, 100L, 50L, 3100L, 1) - ); - - when(monthlyRankRepository.deleteByYearMonth(TEST_YEAR_MONTH)).thenReturn(0L); - when(monthlyRankRepository.saveAll(anyList())).thenReturn(expectedEntities); - - // when - monthlyRankWriter.write(new Chunk<>(List.of(aggregation))); - - // then - verify(monthlyRankRepository).saveAll(expectedEntities); - } - } -} \ No newline at end of file diff --git a/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/writer/WeeklyRankWriterUnitTest.java b/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/writer/WeeklyRankWriterUnitTest.java deleted file mode 100644 index 4d0ea11e6..000000000 --- a/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/writer/WeeklyRankWriterUnitTest.java +++ /dev/null @@ -1,125 +0,0 @@ -package com.loopers.batch.job.ranking.writer; - -import com.loopers.batch.job.ranking.dto.RankingAggregation; -import com.loopers.domain.ranking.WeeklyRankEntity; -import com.loopers.domain.ranking.WeeklyRankRepository; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.batch.item.Chunk; -import org.springframework.test.util.ReflectionTestUtils; - -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyList; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -@ExtendWith(MockitoExtension.class) -@DisplayName("WeeklyRankWriter ๋‹จ์œ„ ํ…Œ์ŠคํŠธ") -class WeeklyRankWriterUnitTest { - - @Mock - private WeeklyRankRepository weeklyRankRepository; - - @InjectMocks - private WeeklyRankWriter weeklyRankWriter; - - private static final String TEST_YEAR_WEEK = "2024-W52"; - - @BeforeEach - void setUp() { - ReflectionTestUtils.setField(weeklyRankWriter, "yearWeek", TEST_YEAR_WEEK); - } - - @Nested - @DisplayName("์ฃผ๊ฐ„ ๋žญํ‚น ์ €์žฅ") - class ์ฃผ๊ฐ„_๋žญํ‚น_์ €์žฅ { - - @Test - @DisplayName("๋žญํ‚น ๋ฐ์ดํ„ฐ๋ฅผ ์ •์ƒ์ ์œผ๋กœ ์ €์žฅํ•œ๋‹ค") - void should_save_weekly_ranking_successfully() throws Exception { - // given - List aggregations = List.of( - new RankingAggregation(1L, 100L, 50L, 10L, 5L, 310L, 1), - new RankingAggregation(2L, 80L, 40L, 8L, 4L, 248L, 2) - ); - Chunk chunk = new Chunk<>(aggregations); - - when(weeklyRankRepository.deleteByYearWeek(TEST_YEAR_WEEK)).thenReturn(2L); - when(weeklyRankRepository.saveAll(anyList())).thenReturn(List.of()); - - // when - weeklyRankWriter.write(chunk); - - // then - verify(weeklyRankRepository).deleteByYearWeek(TEST_YEAR_WEEK); - verify(weeklyRankRepository).saveAll(any()); - } - - @Test - @DisplayName("๋นˆ ์ฒญํฌ์ธ ๊ฒฝ์šฐ ์ €์žฅํ•˜์ง€ ์•Š๋Š”๋‹ค") - void should_not_save_when_chunk_is_empty() throws Exception { - // given - Chunk emptyChunk = new Chunk<>(); - - // when - weeklyRankWriter.write(emptyChunk); - - // then - verify(weeklyRankRepository).deleteByYearWeek(TEST_YEAR_WEEK); - verify(weeklyRankRepository).saveAll(anyList()); - } - - @Test - @DisplayName("์ €์žฅ ์ค‘ ์˜ˆ์™ธ ๋ฐœ์ƒ ์‹œ RuntimeException์„ ๋˜์ง„๋‹ค") - void should_throw_runtime_exception_when_save_fails() { - // given - List aggregations = List.of( - new RankingAggregation(1L, 100L, 50L, 10L, 5L, 310L, 1) - ); - Chunk chunk = new Chunk<>(aggregations); - - when(weeklyRankRepository.deleteByYearWeek(TEST_YEAR_WEEK)).thenReturn(1L); - when(weeklyRankRepository.saveAll(anyList())).thenThrow(new RuntimeException("DB ์ €์žฅ ์‹คํŒจ")); - - // when & then - assertThatThrownBy(() -> weeklyRankWriter.write(chunk)) - .isInstanceOf(RuntimeException.class) - .hasMessage("์ฃผ๊ฐ„ ๋žญํ‚น ์ €์žฅ ์‹คํŒจ"); - } - } - - @Nested - @DisplayName("Entity ๋ณ€ํ™˜") - class Entity_๋ณ€ํ™˜ { - - @Test - @DisplayName("RankingAggregation์„ WeeklyRankEntity๋กœ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋ณ€ํ™˜ํ•œ๋‹ค") - void should_convert_aggregation_to_entity_correctly() throws Exception { - // given - RankingAggregation aggregation = new RankingAggregation(1L, 100L, 50L, 10L, 5L, 310L, 1); - List expectedEntities = List.of( - WeeklyRankEntity.create(1L, TEST_YEAR_WEEK, 100L, 50L, 10L, 5L, 310L, 1) - ); - - when(weeklyRankRepository.deleteByYearWeek(TEST_YEAR_WEEK)).thenReturn(0L); - when(weeklyRankRepository.saveAll(anyList())).thenReturn(expectedEntities); - - // when - weeklyRankWriter.write(new Chunk<>(List.of(aggregation))); - - // then - verify(weeklyRankRepository).saveAll(expectedEntities); - } - } -} \ No newline at end of file diff --git a/modules/jpa/src/main/generated/com/loopers/domain/ranking/QWeeklyRankEntity.java b/modules/jpa/src/main/generated/com/loopers/domain/ranking/QWeeklyRankEntity.java index cb74a9808..f49bb509a 100644 --- a/modules/jpa/src/main/generated/com/loopers/domain/ranking/QWeeklyRankEntity.java +++ b/modules/jpa/src/main/generated/com/loopers/domain/ranking/QWeeklyRankEntity.java @@ -22,7 +22,7 @@ public class QWeeklyRankEntity extends EntityPathBase { public static final QWeeklyRankEntity weeklyRankEntity = new QWeeklyRankEntity("weeklyRankEntity"); - public final DateTimePath createdAt = createDateTime("createdAt", java.time.ZonedDateTime.class); + public final DateTimePath createdAt = createDateTime("createdAt", java.time.LocalDateTime.class); public final QWeeklyRankId id; @@ -30,7 +30,7 @@ public class QWeeklyRankEntity extends EntityPathBase { public final NumberPath orderCount = createNumber("orderCount", Long.class); - public final NumberPath rankPosition = createNumber("rankPosition", Integer.class); + public final NumberPath rankPosition = createNumber("rankPosition", Long.class); public final NumberPath salesCount = createNumber("salesCount", Long.class); diff --git a/modules/jpa/src/main/java/com/loopers/domain/metrics/ProductMetricsRepository.java b/modules/jpa/src/main/java/com/loopers/domain/metrics/ProductMetricsRepository.java index 34d68bc54..ec3c37cdd 100644 --- a/modules/jpa/src/main/java/com/loopers/domain/metrics/ProductMetricsRepository.java +++ b/modules/jpa/src/main/java/com/loopers/domain/metrics/ProductMetricsRepository.java @@ -49,14 +49,6 @@ public interface ProductMetricsRepository { */ List findByMetricDateBetween(LocalDate startDate, LocalDate endDate); - /** - * ํŠน์ • ๋‚ ์งœ์˜ ์ „์ฒด ๋ฉ”ํŠธ๋ฆญ ์กฐํšŒ - * - * @param metricDate ๋ฉ”ํŠธ๋ฆญ ๋‚ ์งœ - * @return ๋ฉ”ํŠธ๋ฆญ ์—”ํ‹ฐํ‹ฐ ๋ชฉ๋ก - */ - List findByMetricDate(LocalDate metricDate); - /** * ๊ธฐ๊ฐ„๋ณ„ ์ƒํ’ˆ ์ง‘๊ณ„ (๋ฐฐ์น˜์šฉ - GROUP BY) * diff --git a/modules/jpa/src/main/java/com/loopers/domain/ranking/MonthlyRankEntity.java b/modules/jpa/src/main/java/com/loopers/domain/ranking/MonthlyRankEntity.java index 6d0675558..fa9de9cd6 100644 --- a/modules/jpa/src/main/java/com/loopers/domain/ranking/MonthlyRankEntity.java +++ b/modules/jpa/src/main/java/com/loopers/domain/ranking/MonthlyRankEntity.java @@ -46,7 +46,7 @@ public class MonthlyRankEntity { private long totalScore; @Column(name = "rank_position", nullable = false) - private int rankPosition; + private long rankPosition; @Column(name = "created_at", nullable = false) private LocalDateTime createdAt; diff --git a/modules/jpa/src/main/java/com/loopers/domain/ranking/WeeklyRankEntity.java b/modules/jpa/src/main/java/com/loopers/domain/ranking/WeeklyRankEntity.java index 1ab71beb1..a54c81390 100644 --- a/modules/jpa/src/main/java/com/loopers/domain/ranking/WeeklyRankEntity.java +++ b/modules/jpa/src/main/java/com/loopers/domain/ranking/WeeklyRankEntity.java @@ -46,7 +46,7 @@ public class WeeklyRankEntity { private long totalScore; @Column(name = "rank_position", nullable = false) - private int rankPosition; + private long rankPosition; @Column(name = "created_at", nullable = false) private LocalDateTime createdAt; From 2dffa780106ed1715ff85270959ded98ca68ce7f Mon Sep 17 00:00:00 2001 From: hyujikoh Date: Fri, 2 Jan 2026 16:37:42 +0900 Subject: [PATCH 73/85] =?UTF-8?q?feat(ranking):=20=EB=9E=AD=ED=82=B9=20API?= =?UTF-8?q?=20=EB=B0=8F=20=EA=B4=80=EB=A0=A8=20=EC=97=94=ED=8B=B0=ED=8B=B0?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ์ผ๊ฐ„, ์ฃผ๊ฐ„, ์›”๊ฐ„ ๋žญํ‚น ์กฐํšŒ API ๋ช…์„ธ ๋ฐ ๊ตฌํ˜„ - ๋žญํ‚น ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ๋กœ์ง ์ถ”๊ฐ€ - ๊ธฐ์กด ์ฝ”๋“œ ๋ฆฌํŒฉํ† ๋ง ๋ฐ ๋ถˆํ•„์š”ํ•œ ๋ฉ”์„œ๋“œ ์ œ๊ฑฐ --- .../api/product/ProductV1ApiSpec.java | 38 -- .../api/product/ProductV1Controller.java | 29 - .../api/ranking/RankingV1ApiSpec.java | 46 ++ .../api/ranking/RankingV1Controller.java | 99 +++ .../interfaces/api/RankingV1ApiE2ETest.java | 462 ------------- .../api/ranking/RankingApiE2ETest.java | 615 +++++++++++------- .../metrics/ProductMetricsJpaRepository.java | 4 +- .../metrics/ProductMetricsRepositoryImpl.java | 9 +- .../metrics/ProductMetricsRepositoryImpl.java | 5 - 9 files changed, 543 insertions(+), 764 deletions(-) create mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1ApiSpec.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java delete mode 100644 apps/commerce-api/src/test/java/com/loopers/interfaces/api/RankingV1ApiE2ETest.java diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1ApiSpec.java index da2e801e5..5cfadb0f4 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1ApiSpec.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1ApiSpec.java @@ -5,15 +5,12 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; -import java.time.LocalDate; - import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestParam; -import com.loopers.domain.ranking.RankingPeriod; import com.loopers.interfaces.api.ApiResponse; import com.loopers.interfaces.api.common.PageResponse; @@ -34,41 +31,6 @@ ApiResponse> getProducts( @RequestParam(required = false) String productName ); - - @Operation( - summary = "๋žญํ‚น ์ƒํ’ˆ ๋ชฉ๋ก ์กฐํšŒ", - description = "์ผ์ž ๊ธฐ์ค€ ๋žญํ‚น ์ƒํ’ˆ ๋ชฉ๋ก์„ ํŽ˜์ด์ง•ํ•˜์—ฌ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. date ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ์—†์œผ๋ฉด ์˜ค๋Š˜ ๋‚ ์งœ ๊ธฐ์ค€์œผ๋กœ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค." - ) - @ApiResponses({ - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "์กฐํšŒ ์„ฑ๊ณต"), - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "์ž˜๋ชป๋œ ์š”์ฒญ") - }) - ApiResponse> getRankingProducts( - @PageableDefault(size = 20) Pageable pageable, - @Parameter(description = "์กฐํšŒ ๋‚ ์งœ (yyyy-MM-dd ํ˜•์‹, ์„ ํƒ)", example = "2025-12-23") - @RequestParam(required = false) LocalDate date - ); - - @Operation( - summary = "๊ธฐ๊ฐ„๋ณ„ ๋žญํ‚น ์ƒํ’ˆ ๋ชฉ๋ก ์กฐํšŒ", - description = "๊ธฐ๊ฐ„๋ณ„(์ผ๊ฐ„/์ฃผ๊ฐ„/์›”๊ฐ„) ๋žญํ‚น ์ƒํ’ˆ ๋ชฉ๋ก์„ ํŽ˜์ด์ง•ํ•˜์—ฌ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค." - ) - @ApiResponses({ - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "์กฐํšŒ ์„ฑ๊ณต"), - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "์ž˜๋ชป๋œ ์š”์ฒญ") - }) - ApiResponse> getRankingProductsByPeriod( - @Parameter(description = "๋žญํ‚น ๊ธฐ๊ฐ„", example = "DAILY", required = true) - @RequestParam RankingPeriod period, - @PageableDefault(size = 20) Pageable pageable, - @Parameter(description = "์กฐํšŒ ๋‚ ์งœ (DAILY์šฉ, yyyy-MM-dd ํ˜•์‹)", example = "2025-12-23") - @RequestParam(required = false) LocalDate date, - @Parameter(description = "์กฐํšŒ ์ฃผ์ฐจ (WEEKLY์šฉ, yyyy-Www ํ˜•์‹)", example = "2024-W52") - @RequestParam(required = false) String yearWeek, - @Parameter(description = "์กฐํšŒ ์›” (MONTHLY์šฉ, yyyy-MM ํ˜•์‹)", example = "2024-12") - @RequestParam(required = false) String yearMonth - ); - @Operation( summary = "์ƒํ’ˆ ์ƒ์„ธ ์กฐํšŒ", description = "์ƒํ’ˆ ID๋กœ ์ƒํ’ˆ ์ƒ์„ธ ์ •๋ณด๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. ๋กœ๊ทธ์ธํ•œ ์‚ฌ์šฉ์ž์˜ ๊ฒฝ์šฐ ์ข‹์•„์š” ์—ฌ๋ถ€๋„ ํ•จ๊ป˜ ์กฐํšŒ๋ฉ๋‹ˆ๋‹ค." diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java index 7abe836fb..7a15decc9 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java @@ -1,7 +1,5 @@ package com.loopers.interfaces.api.product; -import java.time.LocalDate; - import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; @@ -11,7 +9,6 @@ import com.loopers.application.product.ProductFacade; import com.loopers.application.product.ProductInfo; import com.loopers.domain.product.dto.ProductSearchFilter; -import com.loopers.domain.ranking.RankingPeriod; import com.loopers.interfaces.api.ApiResponse; import com.loopers.interfaces.api.common.PageResponse; import com.loopers.support.Uris; @@ -37,32 +34,6 @@ public ApiResponse> getProducts( return ApiResponse.success(PageResponse.from(responsePage)); } - @GetMapping(Uris.Ranking.GET_RANKING) - @Override - public ApiResponse> getRankingProducts( - @PageableDefault(size = 20) Pageable pageable, - @RequestParam(required = false) LocalDate date - ) { - Page products = productFacade.getRankingProducts(pageable, date); - Page responsePage = products.map(ProductV1Dtos.ProductListResponse::from); - return ApiResponse.success(PageResponse.from(responsePage)); - } - - @GetMapping(Uris.Ranking.GET_RANKING_BY_PERIOD) - @Override - public ApiResponse> getRankingProductsByPeriod( - @RequestParam RankingPeriod period, - @PageableDefault(size = 20) Pageable pageable, - @RequestParam(required = false) LocalDate date, - @RequestParam(required = false) String yearWeek, - @RequestParam(required = false) String yearMonth - ) { - Page products = productFacade.getRankingProductsByPeriod( - period, pageable, date, yearWeek, yearMonth); - Page responsePage = products.map(ProductV1Dtos.ProductListResponse::from); - return ApiResponse.success(PageResponse.from(responsePage)); - } - @GetMapping(Uris.Product.GET_DETAIL) @Override public ApiResponse getProductDetail( diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1ApiSpec.java new file mode 100644 index 000000000..d84aef7f5 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1ApiSpec.java @@ -0,0 +1,46 @@ +package com.loopers.interfaces.api.ranking; + +import com.loopers.domain.ranking.RankingPeriod; +import com.loopers.interfaces.api.ApiResponse; +import com.loopers.interfaces.api.common.PageResponse; +import com.loopers.interfaces.api.product.ProductV1Dtos; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.data.domain.Pageable; + +import java.time.LocalDate; + +/** + * ๋žญํ‚น API ๋ช…์„ธ + * - ์ผ๊ฐ„/์ฃผ๊ฐ„/์›”๊ฐ„ ๋žญํ‚น ์กฐํšŒ API + */ +@Tag(name = "Ranking", description = "์ƒํ’ˆ ๋žญํ‚น API") +public interface RankingV1ApiSpec { + + @Operation( + summary = "์ผ๊ฐ„ ๋žญํ‚น ์กฐํšŒ", + description = "ํŠน์ • ๋‚ ์งœ์˜ ์ƒํ’ˆ ๋žญํ‚น์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. ๋‚ ์งœ ๋ฏธ์ง€์ • ์‹œ ์˜ค๋Š˜ ๊ธฐ์ค€์ด๋ฉฐ, ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์œผ๋ฉด ์–ด์ œ ๋žญํ‚น์œผ๋กœ fallback๋ฉ๋‹ˆ๋‹ค." + ) + ApiResponse> getRankingProducts( + Pageable pageable, + @Parameter(description = "์กฐํšŒํ•  ๋‚ ์งœ (YYYY-MM-DD)", example = "2024-12-26") + LocalDate date + ); + + @Operation( + summary = "๊ธฐ๊ฐ„๋ณ„ ๋žญํ‚น ์กฐํšŒ", + description = "์ฃผ๊ฐ„/์›”๊ฐ„ ๋žญํ‚น์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. Java 8 Date API๋ฅผ ํ™œ์šฉํ•˜์—ฌ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ž๋™ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค." + ) + ApiResponse> getRankingProductsByPeriod( + @Parameter(description = "๋žญํ‚น ๊ธฐ๊ฐ„", example = "WEEKLY") + RankingPeriod period, + Pageable pageable, + @Parameter(description = "๊ธฐ์ค€ ๋‚ ์งœ (YYYY-MM-DD)", example = "2024-12-26") + LocalDate date, + @Parameter(description = "์—ฐ๋„-์ฃผ์ฐจ (YYYY-WNN)", example = "2024-W52") + String yearWeek, + @Parameter(description = "์—ฐ๋„-์›” (YYYY-MM)", example = "2024-12") + String yearMonth + ); +} \ No newline at end of file diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java new file mode 100644 index 000000000..44df72436 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java @@ -0,0 +1,99 @@ +package com.loopers.interfaces.api.ranking; + +import com.loopers.application.product.ProductFacade; +import com.loopers.application.product.ProductInfo; +import com.loopers.domain.ranking.RankingPeriod; +import com.loopers.interfaces.api.ApiResponse; +import com.loopers.interfaces.api.common.PageResponse; +import com.loopers.interfaces.api.product.ProductV1Dtos; +import com.loopers.support.Uris; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDate; +import java.time.YearMonth; +import java.time.format.DateTimeFormatter; +import java.time.temporal.WeekFields; +import java.util.Locale; + +/** + * ๋žญํ‚น ์ „์šฉ Controller + * - ์ผ๊ฐ„/์ฃผ๊ฐ„/์›”๊ฐ„ ๋žญํ‚น API ์ œ๊ณต + * - Java 8 Date API ํ™œ์šฉ + */ +@RestController +@RequiredArgsConstructor +public class RankingV1Controller implements RankingV1ApiSpec { + + private final ProductFacade productFacade; + + @GetMapping(Uris.Ranking.GET_RANKING) + @Override + public ApiResponse> getRankingProducts( + @PageableDefault(size = 20) Pageable pageable, + @RequestParam(required = false) LocalDate date + ) { + Page products = productFacade.getRankingProducts(pageable, date); + Page responsePage = products.map(ProductV1Dtos.ProductListResponse::from); + return ApiResponse.success(PageResponse.from(responsePage)); + } + + @GetMapping(Uris.Ranking.GET_RANKING_BY_PERIOD) + @Override + public ApiResponse> getRankingProductsByPeriod( + @RequestParam RankingPeriod period, + @PageableDefault(size = 20) Pageable pageable, + @RequestParam(required = false) LocalDate date, + @RequestParam(required = false) String yearWeek, + @RequestParam(required = false) String yearMonth + ) { + // Java 8 Date API๋ฅผ ํ™œ์šฉํ•œ ํŒŒ๋ผ๋ฏธํ„ฐ ๊ฒ€์ฆ ๋ฐ ๋ณ€ํ™˜ + String processedYearWeek = processYearWeekParameter(yearWeek, date); + String processedYearMonth = processYearMonthParameter(yearMonth, date); + + Page products = productFacade.getRankingProductsByPeriod( + period, pageable, date, processedYearWeek, processedYearMonth); + Page responsePage = products.map(ProductV1Dtos.ProductListResponse::from); + return ApiResponse.success(PageResponse.from(responsePage)); + } + + /** + * yearWeek ํŒŒ๋ผ๋ฏธํ„ฐ ์ฒ˜๋ฆฌ + * - ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ์—†์œผ๋ฉด ํ˜„์žฌ ์ฃผ์ฐจ๋กœ ์„ค์ • + * - Java 8 WeekFields ํ™œ์šฉ + */ + private String processYearWeekParameter(String yearWeek, LocalDate date) { + if (yearWeek != null && !yearWeek.trim().isEmpty()) { + return yearWeek; + } + + // date๊ฐ€ ์žˆ์œผ๋ฉด ํ•ด๋‹น ๋‚ ์งœ์˜ ์ฃผ์ฐจ, ์—†์œผ๋ฉด ํ˜„์žฌ ์ฃผ์ฐจ + LocalDate targetDate = date != null ? date : LocalDate.now(); + + WeekFields weekFields = WeekFields.of(Locale.getDefault()); + int year = targetDate.getYear(); + int week = targetDate.get(weekFields.weekOfYear()); + + return String.format("%d-W%02d", year, week); + } + + /** + * yearMonth ํŒŒ๋ผ๋ฏธํ„ฐ ์ฒ˜๋ฆฌ + * - ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ์—†์œผ๋ฉด ํ˜„์žฌ ์›”๋กœ ์„ค์ • + * - Java 8 YearMonth ํ™œ์šฉ + */ + private String processYearMonthParameter(String yearMonth, LocalDate date) { + if (yearMonth != null && !yearMonth.trim().isEmpty()) { + return yearMonth; + } + + // date๊ฐ€ ์žˆ์œผ๋ฉด ํ•ด๋‹น ๋‚ ์งœ์˜ ์›”, ์—†์œผ๋ฉด ํ˜„์žฌ ์›” + LocalDate targetDate = date != null ? date : LocalDate.now(); + YearMonth ym = YearMonth.from(targetDate); + + return ym.format(DateTimeFormatter.ofPattern("yyyy-MM")); + } +} \ No newline at end of file diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/RankingV1ApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/RankingV1ApiE2ETest.java deleted file mode 100644 index bb3a128e6..000000000 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/RankingV1ApiE2ETest.java +++ /dev/null @@ -1,462 +0,0 @@ -package com.loopers.interfaces.api; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertAll; - -import java.math.BigDecimal; -import java.time.LocalDate; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; - -import org.junit.jupiter.api.*; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.web.client.TestRestTemplate; -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.http.*; - -import com.loopers.cache.CacheKeyGenerator; -import com.loopers.cache.RankingRedisService; -import com.loopers.cache.dto.CachePayloads.RankingScore; -import com.loopers.cache.dto.CachePayloads.RankingScore.EventType; -import com.loopers.domain.brand.BrandEntity; -import com.loopers.domain.brand.BrandService; -import com.loopers.domain.product.*; -import com.loopers.fixtures.BrandTestFixture; -import com.loopers.fixtures.ProductTestFixture; -import com.loopers.interfaces.api.common.PageResponse; -import com.loopers.interfaces.api.product.ProductV1Dtos; -import com.loopers.support.Uris; -import com.loopers.utils.DatabaseCleanUp; -import com.loopers.utils.RedisCleanUp; - -/** - * ๋žญํ‚น API E2E ํ…Œ์ŠคํŠธ - *

- * ์‹ค์ œ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ โ†’ Redis ZSET ์ ์žฌ โ†’ API ์กฐํšŒ ์ „์ฒด ํ”„๋กœ์„ธ์Šค ๊ฒ€์ฆ - * - * @author hyunjikoh - * @since 2025.12.26 - */ -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -@DisplayName("Ranking API E2E ํ…Œ์ŠคํŠธ") -class RankingV1ApiE2ETest { - - @Autowired - private TestRestTemplate testRestTemplate; - - @Autowired - private DatabaseCleanUp databaseCleanUp; - - @Autowired - private RedisCleanUp redisCleanUp; - - @Autowired - private ProductService productService; - - @Autowired - private BrandService brandService; - - @Autowired - private ProductMVService productMVService; - - @Autowired - private RankingRedisService rankingRedisService; - - @Autowired - private RedisTemplate redisTemplate; - - @Autowired - private CacheKeyGenerator cacheKeyGenerator; - - private final List testProductIds = new ArrayList<>(); - private LocalDate today; - - @BeforeEach - void setUp() { - databaseCleanUp.truncateAllTables(); - redisCleanUp.truncateAll(); - testProductIds.clear(); - Long testBrandId = null; - - today = LocalDate.now(); - - // ํ…Œ์ŠคํŠธ์šฉ ๋ธŒ๋žœ๋“œ ์ƒ์„ฑ - BrandEntity brand = brandService.registerBrand( - BrandTestFixture.createRequest("๋žญํ‚นํ…Œ์ŠคํŠธ๋ธŒ๋žœ๋“œ", "๋žญํ‚น E2E ํ…Œ์ŠคํŠธ์šฉ ๋ธŒ๋žœ๋“œ") - ); - testBrandId = brand.getId(); - - // ํ…Œ์ŠคํŠธ์šฉ ์ƒํ’ˆ 5๊ฐœ ์ƒ์„ฑ - for (int i = 1; i <= 5; i++) { - ProductDomainCreateRequest productRequest = ProductTestFixture.createRequest( - testBrandId, - "๋žญํ‚นํ…Œ์ŠคํŠธ์ƒํ’ˆ" + i, - "๋žญํ‚น E2E ํ…Œ์ŠคํŠธ์šฉ ์ƒํ’ˆ " + i, - new BigDecimal(String.valueOf(10000 * i)), - new BigDecimal(String.valueOf(8000 * i)), - 100 - ); - ProductEntity product = productService.registerProduct(productRequest); - testProductIds.add(product.getId()); - } - - // MV ๋™๊ธฐํ™” - productMVService.syncMaterializedView(); - } - - @Nested - @DisplayName("๋žญํ‚น ๋ชฉ๋ก ์กฐํšŒ API") - class GetRankingProductsTest { - - @Test - @DisplayName("๋žญํ‚น ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์œผ๋ฉด ์ ์ˆ˜ ์ˆœ์œผ๋กœ ์ƒํ’ˆ ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค") - void should_return_products_in_ranking_order() { - // Given - Redis์— ๋žญํ‚น ๋ฐ์ดํ„ฐ ์ง์ ‘ ์ ์žฌ - Long product1 = testProductIds.get(0); // 1์œ„ (๋†’์€ ์ ์ˆ˜) - Long product2 = testProductIds.get(1); // 3์œ„ - Long product3 = testProductIds.get(2); // 2์œ„ - - // ์ ์ˆ˜ ์ ์žฌ (๋†’์€ ์ˆœ: product1 > product2 > product3) - List scores = List.of( - new RankingScore(product1, EventType.PAYMENT_SUCCESS, 100.0, System.currentTimeMillis()), - new RankingScore(product2, EventType.LIKE_ACTION, 10.0, System.currentTimeMillis()), - new RankingScore(product3, EventType.PRODUCT_VIEW, 50.0, System.currentTimeMillis()) - ); - rankingRedisService.updateRankingScoresBatch(scores, today); - - // When - ParameterizedTypeReference>> responseType = - new ParameterizedTypeReference<>() {}; - ResponseEntity>> response = - testRestTemplate.exchange( - Uris.Ranking.GET_RANKING + "?page=0&size=10", - HttpMethod.GET, null, responseType - ); - - // Then - assertAll( - () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK), - () -> assertThat(Objects.requireNonNull(response.getBody()).data().content()).hasSize(3), - () -> assertThat(Objects.requireNonNull(response.getBody()).data().content().get(0).productId()).isEqualTo(product1), - () -> assertThat(response.getBody().data().content().get(2).productId()).isEqualTo(product2), - () -> assertThat(Objects.requireNonNull(response.getBody()).data().content().get(1).productId()).isEqualTo(product3) - ); - } - - @Test - @DisplayName("๋žญํ‚น ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์œผ๋ฉด ๋นˆ ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค") - void should_return_empty_when_no_ranking_data() { - // Given - ๋žญํ‚น ๋ฐ์ดํ„ฐ ์—†์Œ - - // When - ParameterizedTypeReference>> responseType = - new ParameterizedTypeReference<>() {}; - ResponseEntity>> response = - testRestTemplate.exchange( - Uris.Ranking.GET_RANKING, - HttpMethod.GET, null, responseType - ); - - // Then - assertAll( - () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK), - () -> assertThat(Objects.requireNonNull(response.getBody()).data().content()).isEmpty(), - () -> assertThat(Objects.requireNonNull(response.getBody()).data().totalElements()).isEqualTo(0) - ); - } - - @Test - @DisplayName("ํŽ˜์ด์ง•์ด ์ •์ƒ์ ์œผ๋กœ ๋™์ž‘ํ•œ๋‹ค") - void should_paginate_ranking_results() { - // Given - 5๊ฐœ ์ƒํ’ˆ ๋ชจ๋‘ ๋žญํ‚น์— ๋“ฑ๋ก - List scores = new ArrayList<>(); - for (int i = 0; i < 5; i++) { - scores.add(new RankingScore( - testProductIds.get(i), - EventType.PRODUCT_VIEW, - (5 - i) * 10.0, // ์ ์ˆ˜: 50, 40, 30, 20, 10 - System.currentTimeMillis() - )); - } - rankingRedisService.updateRankingScoresBatch(scores, today); - - // When - ํŽ˜์ด์ง€ ํฌ๊ธฐ 2๋กœ ์กฐํšŒ - ParameterizedTypeReference>> responseType = - new ParameterizedTypeReference<>() {}; - ResponseEntity>> response = - testRestTemplate.exchange( - Uris.Ranking.GET_RANKING + "?page=0&size=2", - HttpMethod.GET, null, responseType - ); - - // Then - assertAll( - () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK), - () -> assertThat(Objects.requireNonNull(response.getBody()).data().content()).hasSize(2), - () -> assertThat(Objects.requireNonNull(response.getBody()).data().totalElements()).isEqualTo(5), - () -> assertThat(Objects.requireNonNull(response.getBody()).data().totalPages()).isEqualTo(3), - () -> assertThat(Objects.requireNonNull(response.getBody()).data().first()).isTrue(), - () -> assertThat(Objects.requireNonNull(response.getBody()).data().last()).isFalse() - ); - } - - @Test - @DisplayName("ํŠน์ • ๋‚ ์งœ์˜ ๋žญํ‚น์„ ์กฐํšŒํ•  ์ˆ˜ ์žˆ๋‹ค") - void should_return_ranking_for_specific_date() { - // Given - ์–ด์ œ ๋‚ ์งœ์— ๋žญํ‚น ๋ฐ์ดํ„ฐ ์ ์žฌ - LocalDate yesterday = today.minusDays(1); - Long product1 = testProductIds.get(0); - - List scores = List.of( - new RankingScore(product1, EventType.PRODUCT_VIEW, 100.0, System.currentTimeMillis()) - ); - rankingRedisService.updateRankingScoresBatch(scores, yesterday); - - // When - ์–ด์ œ ๋‚ ์งœ๋กœ ์กฐํšŒ - ParameterizedTypeReference>> responseType = - new ParameterizedTypeReference<>() {}; - ResponseEntity>> response = - testRestTemplate.exchange( - Uris.Ranking.GET_RANKING + "?date=" + yesterday, - HttpMethod.GET, null, responseType - ); - - // Then - assertAll( - () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK), - () -> assertThat(Objects.requireNonNull(response.getBody()).data().content()).hasSize(1), - () -> assertThat(Objects.requireNonNull(response.getBody()).data().content().get(0).productId()).isEqualTo(product1) - ); - } - } - - @Nested - @DisplayName("์ฝœ๋“œ ์Šคํƒ€ํŠธ Fallback ํ…Œ์ŠคํŠธ") - class ColdStartFallbackTest { - - @Test - @DisplayName("์˜ค๋Š˜ ๋žญํ‚น์ด ์—†์œผ๋ฉด ์–ด์ œ ๋žญํ‚น์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค") - void should_fallback_to_yesterday_when_today_is_empty() { - // Given - ์–ด์ œ ๋žญํ‚น๋งŒ ์žˆ์Œ - LocalDate yesterday = today.minusDays(1); - Long product1 = testProductIds.get(0); - - List scores = List.of( - new RankingScore(product1, EventType.PRODUCT_VIEW, 50.0, System.currentTimeMillis()) - ); - rankingRedisService.updateRankingScoresBatch(scores, yesterday); - - // When - ๋‚ ์งœ ๋ฏธ์ง€์ • (์˜ค๋Š˜ ๊ธฐ์ค€) - ParameterizedTypeReference>> responseType = - new ParameterizedTypeReference<>() {}; - ResponseEntity>> response = - testRestTemplate.exchange( - Uris.Ranking.GET_RANKING, - HttpMethod.GET, null, responseType - ); - - // Then - ์–ด์ œ ๋žญํ‚น์ด ๋ฐ˜ํ™˜๋จ - assertAll( - () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK), - () -> assertThat(Objects.requireNonNull(response.getBody()).data().content()).hasSize(1), - () -> assertThat(Objects.requireNonNull(response.getBody()).data().content().get(0).productId()).isEqualTo(product1) - ); - } - - @Test - @DisplayName("๋ช…์‹œ์  ๋‚ ์งœ ์ง€์ • ์‹œ Fallbackํ•˜์ง€ ์•Š๋Š”๋‹ค") - void should_not_fallback_when_date_is_explicitly_specified() { - // Given - ์–ด์ œ ๋žญํ‚น๋งŒ ์žˆ์Œ - LocalDate yesterday = today.minusDays(1); - Long product1 = testProductIds.get(0); - - List scores = List.of( - new RankingScore(product1, EventType.PRODUCT_VIEW, 50.0, System.currentTimeMillis()) - ); - rankingRedisService.updateRankingScoresBatch(scores, yesterday); - - // When - ์˜ค๋Š˜ ๋‚ ์งœ ๋ช…์‹œ์  ์ง€์ • - ParameterizedTypeReference>> responseType = - new ParameterizedTypeReference<>() {}; - ResponseEntity>> response = - testRestTemplate.exchange( - Uris.Ranking.GET_RANKING + "?date=" + today, - HttpMethod.GET, null, responseType - ); - - // Then - ๋นˆ ๊ฒฐ๊ณผ (Fallback ์•ˆ ํ•จ) - assertAll( - () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK), - () -> assertThat(Objects.requireNonNull(response.getBody()).data().content()).isEmpty() - ); - } - } - - @Nested - @DisplayName("์ƒํ’ˆ ์ƒ์„ธ ์กฐํšŒ ์‹œ ๋žญํ‚น ์ •๋ณด ํฌํ•จ ํ…Œ์ŠคํŠธ") - class ProductDetailWithRankingTest { - - @Test - @DisplayName("๋žญํ‚น์— ์žˆ๋Š” ์ƒํ’ˆ์€ ๋žญํ‚น ์ •๋ณด๊ฐ€ ํฌํ•จ๋œ๋‹ค") - void should_include_ranking_info_for_ranked_product() { - // Given - ์ƒํ’ˆ์„ ๋žญํ‚น์— ๋“ฑ๋ก - Long productId = testProductIds.get(0); - double score = 123.45; - - List scores = List.of( - new RankingScore(productId, EventType.PAYMENT_SUCCESS, score, System.currentTimeMillis()) - ); - rankingRedisService.updateRankingScoresBatch(scores, today); - - // When - ParameterizedTypeReference> responseType = - new ParameterizedTypeReference<>() {}; - ResponseEntity> response = - testRestTemplate.exchange( - Uris.Product.GET_DETAIL, - HttpMethod.GET, null, responseType, productId - ); - - // Then - assertAll( - () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK), - () -> assertThat(Objects.requireNonNull(response.getBody()).data().productId()).isEqualTo(productId), - () -> assertThat(Objects.requireNonNull(response.getBody()).data().ranking()).isNotNull(), - () -> assertThat(Objects.requireNonNull(response.getBody()).data().ranking().rank()).isEqualTo(1L), - () -> assertThat(Objects.requireNonNull(response.getBody()).data().ranking().score()).isGreaterThan(0) - ); - } - - @Test - @DisplayName("๋žญํ‚น์— ์—†๋Š” ์ƒํ’ˆ์€ ๋žญํ‚น ์ •๋ณด๊ฐ€ null์ด๋‹ค") - void should_have_null_ranking_for_unranked_product() { - // Given - ๋žญํ‚น ๋ฐ์ดํ„ฐ ์—†์Œ - Long productId = testProductIds.get(0); - - // When - ParameterizedTypeReference> responseType = - new ParameterizedTypeReference<>() {}; - ResponseEntity> response = - testRestTemplate.exchange( - Uris.Product.GET_DETAIL, - HttpMethod.GET, null, responseType, productId - ); - - // Then - assertAll( - () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK), - () -> assertThat(Objects.requireNonNull(response.getBody()).data().productId()).isEqualTo(productId), - () -> assertThat(Objects.requireNonNull(response.getBody()).data().ranking()).isNull() - ); - } - - @Test - @DisplayName("์—ฌ๋Ÿฌ ์ƒํ’ˆ ์ค‘ ํŠน์ • ์ƒํ’ˆ์˜ ์ˆœ์œ„๊ฐ€ ์ •ํ™•ํžˆ ๋ฐ˜ํ™˜๋œ๋‹ค") - void should_return_correct_rank_among_multiple_products() { - // Given - 3๊ฐœ ์ƒํ’ˆ ๋žญํ‚น ๋“ฑ๋ก (product2๊ฐ€ 2์œ„) - Long product1 = testProductIds.get(0); - Long product2 = testProductIds.get(1); - Long product3 = testProductIds.get(2); - - List scores = List.of( - new RankingScore(product1, EventType.PAYMENT_SUCCESS, 100.0, System.currentTimeMillis()), - new RankingScore(product2, EventType.LIKE_ACTION, 50.0, System.currentTimeMillis()), - new RankingScore(product3, EventType.PRODUCT_VIEW, 10.0, System.currentTimeMillis()) - ); - rankingRedisService.updateRankingScoresBatch(scores, today); - - // When - product2 ์ƒ์„ธ ์กฐํšŒ - ParameterizedTypeReference> responseType = - new ParameterizedTypeReference<>() {}; - ResponseEntity> response = - testRestTemplate.exchange( - Uris.Product.GET_DETAIL, - HttpMethod.GET, null, responseType, product2 - ); - - // Then - 2์œ„๋กœ ๋ฐ˜ํ™˜ - assertAll( - () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK), - () -> assertThat(Objects.requireNonNull(response.getBody()).data().productId()).isEqualTo(product2), - () -> assertThat(Objects.requireNonNull(response.getBody()).data().ranking()).isNotNull(), - () -> assertThat(Objects.requireNonNull(response.getBody()).data().ranking().rank()).isEqualTo(2L) - ); - } - } - - @Nested - @DisplayName("์ ์ˆ˜ ๋ˆ„์  ํ…Œ์ŠคํŠธ") - class ScoreAccumulationTest { - - @Test - @DisplayName("๋™์ผ ์ƒํ’ˆ์— ์—ฌ๋Ÿฌ ์ด๋ฒคํŠธ ์ ์ˆ˜๊ฐ€ ๋ˆ„์ ๋œ๋‹ค") - void should_accumulate_scores_for_same_product() { - // Given - ๋™์ผ ์ƒํ’ˆ์— ์—ฌ๋Ÿฌ ์ ์ˆ˜ ์ ์žฌ - Long productId = testProductIds.get(0); - - // ์ฒซ ๋ฒˆ์งธ ์ ์ˆ˜ ์ ์žฌ (PRODUCT_VIEW: weight 0.1, score 10.0 โ†’ 1.0) - rankingRedisService.updateRankingScoresBatch( - List.of(new RankingScore(productId, EventType.PRODUCT_VIEW, 10.0, System.currentTimeMillis())), - today - ); - - // ๋‘ ๋ฒˆ์งธ ์ ์ˆ˜ ์ ์žฌ (LIKE_ACTION: weight 0.2, score 20.0 โ†’ 4.0) - // ๋ˆ„์  ์ ์ˆ˜: 1.0 + 4.0 = 5.0 - rankingRedisService.updateRankingScoresBatch( - List.of(new RankingScore(productId, EventType.LIKE_ACTION, 20.0, System.currentTimeMillis())), - today - ); - - // When - ParameterizedTypeReference> responseType = - new ParameterizedTypeReference<>() {}; - ResponseEntity> response = - testRestTemplate.exchange( - Uris.Product.GET_DETAIL, - HttpMethod.GET, null, responseType, productId - ); - - // Then - ์ ์ˆ˜๊ฐ€ ๋ˆ„์ ๋จ (weight ์ ์šฉ: 10*0.1 + 20*0.2 = 5.0) - assertAll( - () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK), - () -> assertThat(Objects.requireNonNull(response.getBody()).data().ranking()).isNotNull(), - () -> assertThat(Objects.requireNonNull(response.getBody()).data().ranking().score()).isGreaterThanOrEqualTo(5.0) - ); - } - } - - @Nested - @DisplayName("Score Carry-Over ํ…Œ์ŠคํŠธ") - class CarryOverTest { - - @Test - @DisplayName("Carry-Over ํ›„ ๋‹ค์Œ ๋‚  ๋žญํ‚น์— ์ ์ˆ˜๊ฐ€ ์ด์›”๋œ๋‹ค") - void should_carry_over_scores_to_next_day() { - // Given - ์˜ค๋Š˜ ๋žญํ‚น ๋ฐ์ดํ„ฐ ์ง์ ‘ Redis์— ์ ์žฌ (weight ์ ์šฉ๋œ ์ ์ˆ˜) - Long productId = testProductIds.get(0); - double weightedScore = 60.0; // PAYMENT_SUCCESS weight 0.6 * score 100 = 60 - - String todayKey = cacheKeyGenerator.generateDailyRankingKey(today); - redisTemplate.opsForZSet().add(todayKey, productId.toString(), weightedScore); - - LocalDate tomorrow = today.plusDays(1); - String tomorrowKey = cacheKeyGenerator.generateDailyRankingKey(tomorrow); - redisTemplate.delete(tomorrowKey); // ๋‚ด์ผ ํ‚ค ์ •๋ฆฌ - - // When - Carry-Over ์‹คํ–‰ (10%) - rankingRedisService.carryOverScores(today, tomorrow, 0.1); - - // Then - ๋‚ด์ผ ํ‚ค์— 10% ์ ์ˆ˜๊ฐ€ ์ด์›”๋จ - Double tomorrowScore = redisTemplate.opsForZSet().score(tomorrowKey, productId.toString()); - - assertThat(tomorrowScore).isNotNull(); - assertThat(tomorrowScore).isCloseTo(weightedScore * 0.1, org.assertj.core.data.Offset.offset(0.01)); - - // Cleanup - redisTemplate.delete(tomorrowKey); - } - } -} diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/ranking/RankingApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/ranking/RankingApiE2ETest.java index 27f43f5dd..097cfb6b9 100644 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/ranking/RankingApiE2ETest.java +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/ranking/RankingApiE2ETest.java @@ -1,292 +1,463 @@ package com.loopers.interfaces.api.ranking; -import com.loopers.domain.ranking.MonthlyRankEntity; -import com.loopers.domain.ranking.MonthlyRankRepository; -import com.loopers.domain.ranking.WeeklyRankEntity; -import com.loopers.domain.ranking.WeeklyRankRepository; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureWebMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.context.WebApplicationContext; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.ArrayList; import java.util.List; +import java.util.Objects; + +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.http.*; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +import com.loopers.cache.CacheKeyGenerator; +import com.loopers.cache.RankingRedisService; +import com.loopers.cache.dto.CachePayloads.RankingScore; +import com.loopers.cache.dto.CachePayloads.RankingScore.EventType; +import com.loopers.domain.brand.BrandEntity; +import com.loopers.domain.brand.BrandService; +import com.loopers.domain.product.*; +import com.loopers.fixtures.BrandTestFixture; +import com.loopers.fixtures.ProductTestFixture; +import com.loopers.interfaces.api.ApiResponse; +import com.loopers.interfaces.api.common.PageResponse; +import com.loopers.interfaces.api.product.ProductV1Dtos; +import com.loopers.support.Uris; +import com.loopers.utils.DatabaseCleanUp; +import com.loopers.utils.RedisCleanUp; /** * ๋žญํ‚น API E2E ํ…Œ์ŠคํŠธ - * - ์‹ค์ œ HTTP ์š”์ฒญ๋ถ€ํ„ฐ DB ์กฐํšŒ๊นŒ์ง€ ์ „์ฒด ํ”Œ๋กœ์šฐ ๊ฒ€์ฆ + *

+ * ์‹ค์ œ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ โ†’ Redis ZSET ์ ์žฌ โ†’ API ์กฐํšŒ ์ „์ฒด ํ”„๋กœ์„ธ์Šค ๊ฒ€์ฆ + * + * @author hyunjikoh + * @since 2025.12.26 */ -@SpringBootTest -@AutoConfigureWebMvc -@DisplayName("๋žญํ‚น API E2E ํ…Œ์ŠคํŠธ") +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@DisplayName("Ranking API E2E ํ…Œ์ŠคํŠธ") class RankingApiE2ETest { @Autowired - private WebApplicationContext webApplicationContext; + private TestRestTemplate testRestTemplate; + + @Autowired + private DatabaseCleanUp databaseCleanUp; + + @Autowired + private RedisCleanUp redisCleanUp; + + @Autowired + private ProductService productService; + + @Autowired + private BrandService brandService; + + @Autowired + private ProductMVService productMVService; + + @Autowired + private RankingRedisService rankingRedisService; @Autowired - private WeeklyRankRepository weeklyRankRepository; + private RedisTemplate redisTemplate; @Autowired - private MonthlyRankRepository monthlyRankRepository; + private CacheKeyGenerator cacheKeyGenerator; - private MockMvc mockMvc; + private final List testProductIds = new ArrayList<>(); + private LocalDate today; @BeforeEach void setUp() { - mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build(); - - // ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ ์ค€๋น„ - setupTestData(); - } + databaseCleanUp.truncateAllTables(); + redisCleanUp.truncateAll(); + testProductIds.clear(); + Long testBrandId = null; - @AfterEach - void tearDown() { - // ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ ์ •๋ฆฌ - weeklyRankRepository.deleteByYearWeek("2024-W52"); - monthlyRankRepository.deleteByYearMonth("2024-12"); - } + today = LocalDate.now(); - private void setupTestData() { - // ์ฃผ๊ฐ„ ๋žญํ‚น ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ - List weeklyRankings = List.of( - WeeklyRankEntity.create(1L, "2024-W52", 1000L, 500L, 100L, 50L, 3100L, 1), - WeeklyRankEntity.create(2L, "2024-W52", 800L, 400L, 80L, 40L, 2480L, 2), - WeeklyRankEntity.create(3L, "2024-W52", 600L, 300L, 60L, 30L, 1860L, 3) + // ํ…Œ์ŠคํŠธ์šฉ ๋ธŒ๋žœ๋“œ ์ƒ์„ฑ + BrandEntity brand = brandService.registerBrand( + BrandTestFixture.createRequest("๋žญํ‚นํ…Œ์ŠคํŠธ๋ธŒ๋žœ๋“œ", "๋žญํ‚น E2E ํ…Œ์ŠคํŠธ์šฉ ๋ธŒ๋žœ๋“œ") ); - weeklyRankRepository.saveAll(weeklyRankings); + testBrandId = brand.getId(); - // ์›”๊ฐ„ ๋žญํ‚น ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ - List monthlyRankings = List.of( - MonthlyRankEntity.create(1L, "2024-12", 5000L, 2500L, 500L, 250L, 15500L, 1), - MonthlyRankEntity.create(2L, "2024-12", 4000L, 2000L, 400L, 200L, 12400L, 2), - MonthlyRankEntity.create(3L, "2024-12", 3000L, 1500L, 300L, 150L, 9300L, 3) - ); - monthlyRankRepository.saveAll(monthlyRankings); + // ํ…Œ์ŠคํŠธ์šฉ ์ƒํ’ˆ 5๊ฐœ ์ƒ์„ฑ + for (int i = 1; i <= 5; i++) { + ProductDomainCreateRequest productRequest = ProductTestFixture.createRequest( + testBrandId, + "๋žญํ‚นํ…Œ์ŠคํŠธ์ƒํ’ˆ" + i, + "๋žญํ‚น E2E ํ…Œ์ŠคํŠธ์šฉ ์ƒํ’ˆ " + i, + new BigDecimal(String.valueOf(10000 * i)), + new BigDecimal(String.valueOf(8000 * i)), + 100 + ); + ProductEntity product = productService.registerProduct(productRequest); + testProductIds.add(product.getId()); + } + + // MV ๋™๊ธฐํ™” + productMVService.syncMaterializedView(); } @Nested - @DisplayName("์ฃผ๊ฐ„ ๋žญํ‚น API") - class ์ฃผ๊ฐ„_๋žญํ‚น_API { + @DisplayName("๋žญํ‚น ๋ชฉ๋ก ์กฐํšŒ API") + class GetRankingProductsTest { @Test - @DisplayName("์ฃผ๊ฐ„ ๋žญํ‚น ์กฐํšŒ API๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ๋™์ž‘ํ•œ๋‹ค") - void should_return_weekly_rankings_successfully() throws Exception { - // when & then - mockMvc.perform(get("/api/v1/rankings/period") - .param("period", "WEEKLY") - .param("yearWeek", "2024-W52") - .param("size", "10") - .param("page", "0")) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.success").value(true)) - .andExpect(jsonPath("$.data.content").isArray()) - .andExpect(jsonPath("$.data.content.length()").value(3)) - .andExpect(jsonPath("$.data.totalElements").value(3)) - .andExpect(jsonPath("$.data.content[0].id").value(1)) // 1์œ„ ์ƒํ’ˆ - .andExpect(jsonPath("$.data.content[1].id").value(2)) // 2์œ„ ์ƒํ’ˆ - .andExpect(jsonPath("$.data.content[2].id").value(3)); // 3์œ„ ์ƒํ’ˆ + @DisplayName("๋žญํ‚น ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์œผ๋ฉด ์ ์ˆ˜ ์ˆœ์œผ๋กœ ์ƒํ’ˆ ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void should_return_products_in_ranking_order() { + // given - Redis์— ๋žญํ‚น ๋ฐ์ดํ„ฐ ์ง์ ‘ ์ ์žฌ + Long product1 = testProductIds.get(0); // 1์œ„ (๋†’์€ ์ ์ˆ˜) + Long product2 = testProductIds.get(1); // 3์œ„ + Long product3 = testProductIds.get(2); // 2์œ„ + + // ์ ์ˆ˜ ์ ์žฌ (๋†’์€ ์ˆœ: product1 > product2 > product3) + List scores = List.of( + new RankingScore(product1, EventType.PAYMENT_SUCCESS, 100.0, System.currentTimeMillis()), + new RankingScore(product2, EventType.LIKE_ACTION, 10.0, System.currentTimeMillis()), + new RankingScore(product3, EventType.PRODUCT_VIEW, 50.0, System.currentTimeMillis()) + ); + rankingRedisService.updateRankingScoresBatch(scores, today); + + // when + ParameterizedTypeReference>> responseType = + new ParameterizedTypeReference<>() {}; + ResponseEntity>> response = + testRestTemplate.exchange( + Uris.Ranking.GET_RANKING + "?page=0&size=10", + HttpMethod.GET, null, responseType + ); + + // then + assertAll( + () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK), + () -> assertThat(Objects.requireNonNull(response.getBody()).data().content()).hasSize(3), + () -> assertThat(Objects.requireNonNull(response.getBody()).data().content().get(0).productId()).isEqualTo(product1), + () -> assertThat(response.getBody().data().content().get(2).productId()).isEqualTo(product2), + () -> assertThat(Objects.requireNonNull(response.getBody()).data().content().get(1).productId()).isEqualTo(product3) + ); } @Test - @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ฃผ์ฐจ ์กฐํšŒ ์‹œ ๋นˆ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค") - void should_return_empty_result_for_non_existent_year_week() throws Exception { - // when & then - mockMvc.perform(get("/api/v1/rankings/period") - .param("period", "WEEKLY") - .param("yearWeek", "2024-W01") - .param("size", "10") - .param("page", "0")) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.success").value(true)) - .andExpect(jsonPath("$.data.content").isArray()) - .andExpect(jsonPath("$.data.content.length()").value(0)) - .andExpect(jsonPath("$.data.totalElements").value(0)); + @DisplayName("๋žญํ‚น ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์œผ๋ฉด ๋นˆ ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void should_return_empty_when_no_ranking_data() { + // given - ๋žญํ‚น ๋ฐ์ดํ„ฐ ์—†์Œ + + // when + ParameterizedTypeReference>> responseType = + new ParameterizedTypeReference<>() {}; + ResponseEntity>> response = + testRestTemplate.exchange( + Uris.Ranking.GET_RANKING, + HttpMethod.GET, null, responseType + ); + + // then + assertAll( + () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK), + () -> assertThat(Objects.requireNonNull(response.getBody()).data().content()).isEmpty(), + () -> assertThat(Objects.requireNonNull(response.getBody()).data().totalElements()).isEqualTo(0) + ); } @Test - @DisplayName("yearWeek ํŒŒ๋ผ๋ฏธํ„ฐ ๋ˆ„๋ฝ ์‹œ 400 ์—๋Ÿฌ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค") - void should_return_400_when_year_week_parameter_is_missing() throws Exception { - // when & then - mockMvc.perform(get("/api/v1/rankings/period") - .param("period", "WEEKLY") - .param("size", "10") - .param("page", "0")) - .andDo(print()) - .andExpect(status().isBadRequest()); + @DisplayName("ํŽ˜์ด์ง•์ด ์ •์ƒ์ ์œผ๋กœ ๋™์ž‘ํ•œ๋‹ค") + void should_paginate_ranking_results() { + // given - 5๊ฐœ ์ƒํ’ˆ ๋ชจ๋‘ ๋žญํ‚น์— ๋“ฑ๋ก + List scores = new ArrayList<>(); + for (int i = 0; i < 5; i++) { + scores.add(new RankingScore( + testProductIds.get(i), + EventType.PRODUCT_VIEW, + (5 - i) * 10.0, // ์ ์ˆ˜: 50, 40, 30, 20, 10 + System.currentTimeMillis() + )); + } + rankingRedisService.updateRankingScoresBatch(scores, today); + + // when - ํŽ˜์ด์ง€ ํฌ๊ธฐ 2๋กœ ์กฐํšŒ + ParameterizedTypeReference>> responseType = + new ParameterizedTypeReference<>() {}; + ResponseEntity>> response = + testRestTemplate.exchange( + Uris.Ranking.GET_RANKING + "?page=0&size=2", + HttpMethod.GET, null, responseType + ); + + // then + assertAll( + () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK), + () -> assertThat(Objects.requireNonNull(response.getBody()).data().content()).hasSize(2), + () -> assertThat(Objects.requireNonNull(response.getBody()).data().totalElements()).isEqualTo(5), + () -> assertThat(Objects.requireNonNull(response.getBody()).data().totalPages()).isEqualTo(3), + () -> assertThat(Objects.requireNonNull(response.getBody()).data().first()).isTrue(), + () -> assertThat(Objects.requireNonNull(response.getBody()).data().last()).isFalse() + ); } @Test - @DisplayName("ํŽ˜์ด์ง€๋„ค์ด์…˜์ด ์ •์ƒ์ ์œผ๋กœ ๋™์ž‘ํ•œ๋‹ค") - void should_support_pagination_correctly() throws Exception { - // when & then - ์ฒซ ๋ฒˆ์งธ ํŽ˜์ด์ง€ (size=2) - mockMvc.perform(get("/api/v1/rankings/period") - .param("period", "WEEKLY") - .param("yearWeek", "2024-W52") - .param("size", "2") - .param("page", "0")) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.data.content.length()").value(2)) - .andExpect(jsonPath("$.data.totalElements").value(3)) - .andExpect(jsonPath("$.data.totalPages").value(2)) - .andExpect(jsonPath("$.data.first").value(true)) - .andExpect(jsonPath("$.data.last").value(false)); - - // when & then - ๋‘ ๋ฒˆ์งธ ํŽ˜์ด์ง€ (size=2) - mockMvc.perform(get("/api/v1/rankings/period") - .param("period", "WEEKLY") - .param("yearWeek", "2024-W52") - .param("size", "2") - .param("page", "1")) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.data.content.length()").value(1)) - .andExpect(jsonPath("$.data.totalElements").value(3)) - .andExpect(jsonPath("$.data.totalPages").value(2)) - .andExpect(jsonPath("$.data.first").value(false)) - .andExpect(jsonPath("$.data.last").value(true)); + @DisplayName("ํŠน์ • ๋‚ ์งœ์˜ ๋žญํ‚น์„ ์กฐํšŒํ•  ์ˆ˜ ์žˆ๋‹ค") + void should_return_ranking_for_specific_date() { + // given - ์–ด์ œ ๋‚ ์งœ์— ๋žญํ‚น ๋ฐ์ดํ„ฐ ์ ์žฌ + LocalDate yesterday = today.minusDays(1); + Long product1 = testProductIds.get(0); + + List scores = List.of( + new RankingScore(product1, EventType.PRODUCT_VIEW, 100.0, System.currentTimeMillis()) + ); + rankingRedisService.updateRankingScoresBatch(scores, yesterday); + + // when - ์–ด์ œ ๋‚ ์งœ๋กœ ์กฐํšŒ + ParameterizedTypeReference>> responseType = + new ParameterizedTypeReference<>() {}; + ResponseEntity>> response = + testRestTemplate.exchange( + Uris.Ranking.GET_RANKING + "?date=" + yesterday, + HttpMethod.GET, null, responseType + ); + + // then + assertAll( + () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK), + () -> assertThat(Objects.requireNonNull(response.getBody()).data().content()).hasSize(1), + () -> assertThat(Objects.requireNonNull(response.getBody()).data().content().get(0).productId()).isEqualTo(product1) + ); } } @Nested - @DisplayName("์›”๊ฐ„ ๋žญํ‚น API") - class ์›”๊ฐ„_๋žญํ‚น_API { + @DisplayName("์ฝœ๋“œ ์Šคํƒ€ํŠธ Fallback ํ…Œ์ŠคํŠธ") + class ColdStartFallbackTest { @Test - @DisplayName("์›”๊ฐ„ ๋žญํ‚น ์กฐํšŒ API๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ๋™์ž‘ํ•œ๋‹ค") - void should_return_monthly_rankings_successfully() throws Exception { - // when & then - mockMvc.perform(get("/api/v1/rankings/period") - .param("period", "MONTHLY") - .param("yearMonth", "2024-12") - .param("size", "10") - .param("page", "0")) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.success").value(true)) - .andExpect(jsonPath("$.data.content").isArray()) - .andExpect(jsonPath("$.data.content.length()").value(3)) - .andExpect(jsonPath("$.data.totalElements").value(3)) - .andExpect(jsonPath("$.data.content[0].id").value(1)) // 1์œ„ ์ƒํ’ˆ - .andExpect(jsonPath("$.data.content[1].id").value(2)) // 2์œ„ ์ƒํ’ˆ - .andExpect(jsonPath("$.data.content[2].id").value(3)); // 3์œ„ ์ƒํ’ˆ - } + @DisplayName("์˜ค๋Š˜ ๋žญํ‚น์ด ์—†์œผ๋ฉด ์–ด์ œ ๋žญํ‚น์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void should_fallback_to_yesterday_when_today_is_empty() { + // given - ์–ด์ œ ๋žญํ‚น๋งŒ ์žˆ์Œ + LocalDate yesterday = today.minusDays(1); + Long product1 = testProductIds.get(0); - @Test - @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์›” ์กฐํšŒ ์‹œ ๋นˆ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค") - void should_return_empty_result_for_non_existent_year_month() throws Exception { - // when & then - mockMvc.perform(get("/api/v1/rankings/period") - .param("period", "MONTHLY") - .param("yearMonth", "2024-01") - .param("size", "10") - .param("page", "0")) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.success").value(true)) - .andExpect(jsonPath("$.data.content").isArray()) - .andExpect(jsonPath("$.data.content.length()").value(0)) - .andExpect(jsonPath("$.data.totalElements").value(0)); + List scores = List.of( + new RankingScore(product1, EventType.PRODUCT_VIEW, 50.0, System.currentTimeMillis()) + ); + rankingRedisService.updateRankingScoresBatch(scores, yesterday); + + // when - ๋‚ ์งœ ๋ฏธ์ง€์ • (์˜ค๋Š˜ ๊ธฐ์ค€) + ParameterizedTypeReference>> responseType = + new ParameterizedTypeReference<>() {}; + ResponseEntity>> response = + testRestTemplate.exchange( + Uris.Ranking.GET_RANKING, + HttpMethod.GET, null, responseType + ); + + // then - ์–ด์ œ ๋žญํ‚น์ด ๋ฐ˜ํ™˜๋จ + assertAll( + () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK), + () -> assertThat(Objects.requireNonNull(response.getBody()).data().content()).hasSize(1), + () -> assertThat(Objects.requireNonNull(response.getBody()).data().content().get(0).productId()).isEqualTo(product1) + ); } @Test - @DisplayName("yearMonth ํŒŒ๋ผ๋ฏธํ„ฐ ๋ˆ„๋ฝ ์‹œ 400 ์—๋Ÿฌ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค") - void should_return_400_when_year_month_parameter_is_missing() throws Exception { - // when & then - mockMvc.perform(get("/api/v1/rankings/period") - .param("period", "MONTHLY") - .param("size", "10") - .param("page", "0")) - .andDo(print()) - .andExpect(status().isBadRequest()); + @DisplayName("๋ช…์‹œ์  ๋‚ ์งœ ์ง€์ • ์‹œ Fallbackํ•˜์ง€ ์•Š๋Š”๋‹ค") + void should_not_fallback_when_date_is_explicitly_specified() { + // given - ์–ด์ œ ๋žญํ‚น๋งŒ ์žˆ์Œ + LocalDate yesterday = today.minusDays(1); + Long product1 = testProductIds.get(0); + + List scores = List.of( + new RankingScore(product1, EventType.PRODUCT_VIEW, 50.0, System.currentTimeMillis()) + ); + rankingRedisService.updateRankingScoresBatch(scores, yesterday); + + // when - ์˜ค๋Š˜ ๋‚ ์งœ ๋ช…์‹œ์  ์ง€์ • + ParameterizedTypeReference>> responseType = + new ParameterizedTypeReference<>() {}; + ResponseEntity>> response = + testRestTemplate.exchange( + Uris.Ranking.GET_RANKING + "?date=" + today, + HttpMethod.GET, null, responseType + ); + + // then - ๋นˆ ๊ฒฐ๊ณผ (Fallback ์•ˆ ํ•จ) + assertAll( + () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK), + () -> assertThat(Objects.requireNonNull(response.getBody()).data().content()).isEmpty() + ); } } @Nested - @DisplayName("์ผ๊ฐ„ ๋žญํ‚น API (๊ธฐ์กด)") - class ์ผ๊ฐ„_๋žญํ‚น_API { + @DisplayName("์ƒํ’ˆ ์ƒ์„ธ ์กฐํšŒ ์‹œ ๋žญํ‚น ์ •๋ณด ํฌํ•จ ํ…Œ์ŠคํŠธ") + class ProductDetailWithRankingTest { + + @Test + @DisplayName("๋žญํ‚น์— ์žˆ๋Š” ์ƒํ’ˆ์€ ๋žญํ‚น ์ •๋ณด๊ฐ€ ํฌํ•จ๋œ๋‹ค") + void should_include_ranking_info_for_ranked_product() { + // given - ์ƒํ’ˆ์„ ๋žญํ‚น์— ๋“ฑ๋ก + Long productId = testProductIds.get(0); + double score = 123.45; + + List scores = List.of( + new RankingScore(productId, EventType.PAYMENT_SUCCESS, score, System.currentTimeMillis()) + ); + rankingRedisService.updateRankingScoresBatch(scores, today); + + // when + ParameterizedTypeReference> responseType = + new ParameterizedTypeReference<>() {}; + ResponseEntity> response = + testRestTemplate.exchange( + Uris.Product.GET_DETAIL, + HttpMethod.GET, null, responseType, productId + ); + + // then + assertAll( + () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK), + () -> assertThat(Objects.requireNonNull(response.getBody()).data().productId()).isEqualTo(productId), + () -> assertThat(Objects.requireNonNull(response.getBody()).data().ranking()).isNotNull(), + () -> assertThat(Objects.requireNonNull(response.getBody()).data().ranking().rank()).isEqualTo(1L), + () -> assertThat(Objects.requireNonNull(response.getBody()).data().ranking().score()).isGreaterThan(0) + ); + } @Test - @DisplayName("์ผ๊ฐ„ ๋žญํ‚น API๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ๋™์ž‘ํ•œ๋‹ค (ํ•˜์œ„ ํ˜ธํ™˜์„ฑ)") - void should_return_daily_rankings_successfully() throws Exception { - // when & then - mockMvc.perform(get("/api/v1/rankings") - .param("size", "10") - .param("page", "0")) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.success").value(true)) - .andExpect(jsonPath("$.data.content").isArray()); + @DisplayName("๋žญํ‚น์— ์—†๋Š” ์ƒํ’ˆ์€ ๋žญํ‚น ์ •๋ณด๊ฐ€ null์ด๋‹ค") + void should_have_null_ranking_for_unranked_product() { + // given - ๋žญํ‚น ๋ฐ์ดํ„ฐ ์—†์Œ + Long productId = testProductIds.get(0); + + // when + ParameterizedTypeReference> responseType = + new ParameterizedTypeReference<>() {}; + ResponseEntity> response = + testRestTemplate.exchange( + Uris.Product.GET_DETAIL, + HttpMethod.GET, null, responseType, productId + ); + + // then + assertAll( + () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK), + () -> assertThat(Objects.requireNonNull(response.getBody()).data().productId()).isEqualTo(productId), + () -> assertThat(Objects.requireNonNull(response.getBody()).data().ranking()).isNull() + ); } @Test - @DisplayName("์ƒˆ๋กœ์šด period ๋ฐฉ์‹์œผ๋กœ๋„ ์ผ๊ฐ„ ๋žญํ‚น ์กฐํšŒ๊ฐ€ ๊ฐ€๋Šฅํ•˜๋‹ค") - void should_return_daily_rankings_with_period_parameter() throws Exception { - // when & then - mockMvc.perform(get("/api/v1/rankings/period") - .param("period", "DAILY") - .param("size", "10") - .param("page", "0")) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.success").value(true)) - .andExpect(jsonPath("$.data.content").isArray()); + @DisplayName("์—ฌ๋Ÿฌ ์ƒํ’ˆ ์ค‘ ํŠน์ • ์ƒํ’ˆ์˜ ์ˆœ์œ„๊ฐ€ ์ •ํ™•ํžˆ ๋ฐ˜ํ™˜๋œ๋‹ค") + void should_return_correct_rank_among_multiple_products() { + // given - 3๊ฐœ ์ƒํ’ˆ ๋žญํ‚น ๋“ฑ๋ก (product2๊ฐ€ 2์œ„) + Long product1 = testProductIds.get(0); + Long product2 = testProductIds.get(1); + Long product3 = testProductIds.get(2); + + List scores = List.of( + new RankingScore(product1, EventType.PAYMENT_SUCCESS, 100.0, System.currentTimeMillis()), + new RankingScore(product2, EventType.LIKE_ACTION, 50.0, System.currentTimeMillis()), + new RankingScore(product3, EventType.PRODUCT_VIEW, 10.0, System.currentTimeMillis()) + ); + rankingRedisService.updateRankingScoresBatch(scores, today); + + // when - product2 ์ƒ์„ธ ์กฐํšŒ + ParameterizedTypeReference> responseType = + new ParameterizedTypeReference<>() {}; + ResponseEntity> response = + testRestTemplate.exchange( + Uris.Product.GET_DETAIL, + HttpMethod.GET, null, responseType, product2 + ); + + // then - 2์œ„๋กœ ๋ฐ˜ํ™˜ + assertAll( + () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK), + () -> assertThat(Objects.requireNonNull(response.getBody()).data().productId()).isEqualTo(product2), + () -> assertThat(Objects.requireNonNull(response.getBody()).data().ranking()).isNotNull(), + () -> assertThat(Objects.requireNonNull(response.getBody()).data().ranking().rank()).isEqualTo(2L) + ); } } @Nested - @DisplayName("ํŒŒ๋ผ๋ฏธํ„ฐ ๊ฒ€์ฆ") - class ํŒŒ๋ผ๋ฏธํ„ฐ_๊ฒ€์ฆ { + @DisplayName("์ ์ˆ˜ ๋ˆ„์  ํ…Œ์ŠคํŠธ") + class ScoreAccumulationTest { @Test - @DisplayName("์ž˜๋ชป๋œ period ๊ฐ’ ์‹œ 400 ์—๋Ÿฌ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค") - void should_return_400_for_invalid_period() throws Exception { - // when & then - mockMvc.perform(get("/api/v1/rankings/period") - .param("period", "INVALID") - .param("size", "10") - .param("page", "0")) - .andDo(print()) - .andExpect(status().isBadRequest()); - } + @DisplayName("๋™์ผ ์ƒํ’ˆ์— ์—ฌ๋Ÿฌ ์ด๋ฒคํŠธ ์ ์ˆ˜๊ฐ€ ๋ˆ„์ ๋œ๋‹ค") + void should_accumulate_scores_for_same_product() { + // given - ๋™์ผ ์ƒํ’ˆ์— ์—ฌ๋Ÿฌ ์ ์ˆ˜ ์ ์žฌ + Long productId = testProductIds.get(0); - @Test - @DisplayName("์Œ์ˆ˜ ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ ์‹œ 400 ์—๋Ÿฌ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค") - void should_return_400_for_negative_page_number() throws Exception { - // when & then - mockMvc.perform(get("/api/v1/rankings/period") - .param("period", "WEEKLY") - .param("yearWeek", "2024-W52") - .param("size", "10") - .param("page", "-1")) - .andDo(print()) - .andExpect(status().isBadRequest()); + // ์ฒซ ๋ฒˆ์งธ ์ ์ˆ˜ ์ ์žฌ (PRODUCT_VIEW: weight 0.1, score 10.0 โ†’ 1.0) + rankingRedisService.updateRankingScoresBatch( + List.of(new RankingScore(productId, EventType.PRODUCT_VIEW, 10.0, System.currentTimeMillis())), + today + ); + + // ๋‘ ๋ฒˆ์งธ ์ ์ˆ˜ ์ ์žฌ (LIKE_ACTION: weight 0.2, score 20.0 โ†’ 4.0) + // ๋ˆ„์  ์ ์ˆ˜: 1.0 + 4.0 = 5.0 + rankingRedisService.updateRankingScoresBatch( + List.of(new RankingScore(productId, EventType.LIKE_ACTION, 20.0, System.currentTimeMillis())), + today + ); + + // when + ParameterizedTypeReference> responseType = + new ParameterizedTypeReference<>() {}; + ResponseEntity> response = + testRestTemplate.exchange( + Uris.Product.GET_DETAIL, + HttpMethod.GET, null, responseType, productId + ); + + // then - ์ ์ˆ˜๊ฐ€ ๋ˆ„์ ๋จ (weight ์ ์šฉ: 10*0.1 + 20*0.2 = 5.0) + assertAll( + () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK), + () -> assertThat(Objects.requireNonNull(response.getBody()).data().ranking()).isNotNull(), + () -> assertThat(Objects.requireNonNull(response.getBody()).data().ranking().score()).isGreaterThanOrEqualTo(5.0) + ); } + } + + @Nested + @DisplayName("Score Carry-Over ํ…Œ์ŠคํŠธ") + class CarryOverTest { @Test - @DisplayName("0 ์ดํ•˜์˜ size ๊ฐ’ ์‹œ 400 ์—๋Ÿฌ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค") - void should_return_400_for_invalid_size() throws Exception { - // when & then - mockMvc.perform(get("/api/v1/rankings/period") - .param("period", "WEEKLY") - .param("yearWeek", "2024-W52") - .param("size", "0") - .param("page", "0")) - .andDo(print()) - .andExpect(status().isBadRequest()); + @DisplayName("Carry-Over ํ›„ ๋‹ค์Œ ๋‚  ๋žญํ‚น์— ์ ์ˆ˜๊ฐ€ ์ด์›”๋œ๋‹ค") + void should_carry_over_scores_to_next_day() { + // given - ์˜ค๋Š˜ ๋žญํ‚น ๋ฐ์ดํ„ฐ ์ง์ ‘ Redis์— ์ ์žฌ (weight ์ ์šฉ๋œ ์ ์ˆ˜) + Long productId = testProductIds.get(0); + double weightedScore = 60.0; // PAYMENT_SUCCESS weight 0.6 * score 100 = 60 + + String todayKey = cacheKeyGenerator.generateDailyRankingKey(today); + redisTemplate.opsForZSet().add(todayKey, productId.toString(), weightedScore); + + LocalDate tomorrow = today.plusDays(1); + String tomorrowKey = cacheKeyGenerator.generateDailyRankingKey(tomorrow); + redisTemplate.delete(tomorrowKey); // ๋‚ด์ผ ํ‚ค ์ •๋ฆฌ + + // when - Carry-Over ์‹คํ–‰ (10%) + rankingRedisService.carryOverScores(today, tomorrow, 0.1); + + // then - ๋‚ด์ผ ํ‚ค์— 10% ์ ์ˆ˜๊ฐ€ ์ด์›”๋จ + Double tomorrowScore = redisTemplate.opsForZSet().score(tomorrowKey, productId.toString()); + + assertThat(tomorrowScore).isNotNull(); + assertThat(tomorrowScore).isCloseTo(weightedScore * 0.1, org.assertj.core.data.Offset.offset(0.01)); + + // Cleanup + redisTemplate.delete(tomorrowKey); } } } \ No newline at end of file diff --git a/apps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.java b/apps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.java index 1a769a02b..351ea3762 100644 --- a/apps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.java +++ b/apps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.java @@ -36,4 +36,6 @@ public interface ProductMetricsJpaRepository extends JpaRepository aggregateByDateRange( @Param("startDate") LocalDate startDate, @Param("endDate") LocalDate endDate); -} \ No newline at end of file + + List findByMetricDateBetween(LocalDate startDate, LocalDate endDate); +} diff --git a/apps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java b/apps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java index 9aa28b943..07fd8e13a 100644 --- a/apps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java +++ b/apps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java @@ -38,16 +38,11 @@ public Optional findByProductIdAndMetricDate(Long productI @Override public List findByMetricDateBetween(LocalDate startDate, LocalDate endDate) { - return jpaRepository.findAll().stream() - .filter(entity -> { - LocalDate metricDate = entity.getMetricDate(); - return !metricDate.isBefore(startDate) && !metricDate.isAfter(endDate); - }) - .toList(); + return jpaRepository.findByMetricDateBetween(startDate, endDate); } @Override public List aggregateByDateRange(LocalDate startDate, LocalDate endDate) { return jpaRepository.aggregateByDateRange(startDate, endDate); } -} \ No newline at end of file +} diff --git a/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java b/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java index 486987a5c..015e88095 100644 --- a/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java +++ b/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java @@ -44,11 +44,6 @@ public List findByMetricDateBetween(LocalDate startDate, L return productMetricsJpaRepository.findByMetricDateBetween(startDate, endDate); } - @Override - public List findByMetricDate(LocalDate metricDate) { - return productMetricsJpaRepository.findByMetricDate(metricDate); - } - @Override public List aggregateByDateRange(LocalDate startDate, LocalDate endDate) { return productMetricsJpaRepository.aggregateByDateRange(startDate, endDate); From d1693f00fdeb7eb401ed734e08b60017e3623005 Mon Sep 17 00:00:00 2001 From: hyujikoh Date: Fri, 2 Jan 2026 16:37:52 +0900 Subject: [PATCH 74/85] =?UTF-8?q?feat(ranking):=20=EC=9B=94=EA=B0=84=20?= =?UTF-8?q?=EB=B0=8F=20=EC=A3=BC=EA=B0=84=20=EB=9E=AD=ED=82=B9=20=EC=97=94?= =?UTF-8?q?=ED=8B=B0=ED=8B=B0=20=ED=95=84=EB=93=9C=EB=AA=85=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 - rank_position์„ base_rank_position์œผ๋กœ ๋ณ€๊ฒฝ - year_month๋ฅผ base_year_month๋กœ ๋ณ€๊ฒฝ - ๋กœ๊ทธ ๋ฉ”์‹œ์ง€ ๊ฐœ์„  ๋ฐ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ๊ฐ•ํ™” --- .../job/ranking/writer/MonthlyRankWriter.java | 21 +++++++++++-------- .../job/ranking/writer/WeeklyRankWriter.java | 21 +++++++++++-------- .../domain/ranking/MonthlyRankEntity.java | 4 ++-- .../loopers/domain/ranking/MonthlyRankId.java | 2 +- 4 files changed, 27 insertions(+), 21 deletions(-) diff --git a/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/writer/MonthlyRankWriter.java b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/writer/MonthlyRankWriter.java index a02eb0802..866537ce9 100644 --- a/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/writer/MonthlyRankWriter.java +++ b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/writer/MonthlyRankWriter.java @@ -32,30 +32,33 @@ public class MonthlyRankWriter implements ItemWriter { @Override public void write(Chunk chunk) throws Exception { List items = chunk.getItems(); - + if (items.isEmpty()) { - log.info("์ €์žฅํ•  ์›”๊ฐ„ ๋žญํ‚น ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค: yearMonth={}", yearMonth); + log.info("[Batch-Ranking] ์ €์žฅํ•  ์›”๊ฐ„ ๋žญํ‚น ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. (yearMonth: {})", yearMonth); return; } - log.info("์›”๊ฐ„ ๋žญํ‚น ์ €์žฅ ์‹œ์ž‘: yearMonth={}, ์ €์žฅํ•  ๋ฐ์ดํ„ฐ ์ˆ˜={}", yearMonth, items.size()); + int targetCount = items.size(); + log.info("[Batch-Ranking] ์›”๊ฐ„ ๋žญํ‚น ์ €์žฅ ํ”„๋กœ์„ธ์Šค ์‹œ์ž‘ (yearMonth: {}, ๋Œ€์ƒ ๊ฑด์ˆ˜: {})", yearMonth, targetCount); try { // 1. ๊ธฐ์กด ๋ฐ์ดํ„ฐ ์‚ญ์ œ (๋ฉฑ๋“ฑ์„ฑ ๋ณด์žฅ) long deletedCount = monthlyRankRepository.deleteByYearMonth(yearMonth); - log.info("๊ธฐ์กด ์›”๊ฐ„ ๋žญํ‚น ๋ฐ์ดํ„ฐ ์‚ญ์ œ: yearMonth={}, ์‚ญ์ œ๋œ ์ˆ˜={}", yearMonth, deletedCount); + log.info("[Batch-Ranking] ๊ธฐ์กด ๋ฐ์ดํ„ฐ ํด๋ Œ์ง• ์™„๋ฃŒ (yearMonth: {}, ์‚ญ์ œ ๊ฑด์ˆ˜: {})", yearMonth, deletedCount); // 2. ์ƒˆ๋กœ์šด ๋ฐ์ดํ„ฐ ์ €์žฅ List entities = items.stream() .map(this::convertToEntity) .toList(); - List savedEntities = monthlyRankRepository.saveAll(entities); - log.info("์›”๊ฐ„ ๋žญํ‚น ์ €์žฅ ์™„๋ฃŒ: yearMonth={}, ์ €์žฅ๋œ ์ˆ˜={}", yearMonth, savedEntities.size()); + monthlyRankRepository.saveAll(entities); + log.info("[Batch-Ranking] ์›”๊ฐ„ ๋žญํ‚น ์ €์žฅ ์„ฑ๊ณต (yearMonth: {}, ์ €์žฅ ๊ฑด์ˆ˜: {})", yearMonth, entities.size()); } catch (Exception e) { - log.error("์›”๊ฐ„ ๋žญํ‚น ์ €์žฅ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: yearMonth={}", yearMonth, e); - throw new RuntimeException("์›”๊ฐ„ ๋žญํ‚น ์ €์žฅ ์‹คํŒจ", e); + // ์ƒ์„ธํ•œ ์ปจํ…์ŠคํŠธ๋ฅผ ํฌํ•จํ•œ ์—๋Ÿฌ ๋กœ๊ทธ + log.error("[Batch-Ranking] ์›”๊ฐ„ ๋žญํ‚น ์ €์žฅ ์ค‘ ์˜ˆ์™ธ ๋ฐœ์ƒ! (yearMonth: {}, ์ฒ˜๋ฆฌ ์ค‘์ด๋˜ ๊ฑด์ˆ˜: {}) - ์›์ธ: {}", + yearMonth, targetCount, e.getMessage(), e); + throw e; } } @@ -71,4 +74,4 @@ private MonthlyRankEntity convertToEntity(RankingAggregation aggregation) { aggregation.getRankPosition() ); } -} \ No newline at end of file +} diff --git a/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/writer/WeeklyRankWriter.java b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/writer/WeeklyRankWriter.java index cb831467a..1ab801d58 100644 --- a/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/writer/WeeklyRankWriter.java +++ b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/writer/WeeklyRankWriter.java @@ -32,30 +32,33 @@ public class WeeklyRankWriter implements ItemWriter { @Override public void write(Chunk chunk) throws Exception { List items = chunk.getItems(); - + if (items.isEmpty()) { - log.info("์ €์žฅํ•  ์ฃผ๊ฐ„ ๋žญํ‚น ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค: yearWeek={}", yearWeek); + log.info("[Batch-Ranking] ์ €์žฅํ•  ์ฃผ๊ฐ„ ๋žญํ‚น ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. (yearWeek: {})", yearWeek); return; } - log.info("์ฃผ๊ฐ„ ๋žญํ‚น ์ €์žฅ ์‹œ์ž‘: yearWeek={}, ์ €์žฅํ•  ๋ฐ์ดํ„ฐ ์ˆ˜={}", yearWeek, items.size()); + int targetCount = items.size(); + log.info("[Batch-Ranking] ์ฃผ๊ฐ„ ๋žญํ‚น ์ €์žฅ ํ”„๋กœ์„ธ์Šค ์‹œ์ž‘ (yearWeek: {}, ๋Œ€์ƒ ๊ฑด์ˆ˜: {})", yearWeek, targetCount); try { // 1. ๊ธฐ์กด ๋ฐ์ดํ„ฐ ์‚ญ์ œ (๋ฉฑ๋“ฑ์„ฑ ๋ณด์žฅ) long deletedCount = weeklyRankRepository.deleteByYearWeek(yearWeek); - log.info("๊ธฐ์กด ์ฃผ๊ฐ„ ๋žญํ‚น ๋ฐ์ดํ„ฐ ์‚ญ์ œ: yearWeek={}, ์‚ญ์ œ๋œ ์ˆ˜={}", yearWeek, deletedCount); + log.info("[Batch-Ranking] ๊ธฐ์กด ๋ฐ์ดํ„ฐ ํด๋ Œ์ง• ์™„๋ฃŒ (yearWeek: {}, ์‚ญ์ œ ๊ฑด์ˆ˜: {})", yearWeek, deletedCount); // 2. ์ƒˆ๋กœ์šด ๋ฐ์ดํ„ฐ ์ €์žฅ List entities = items.stream() .map(this::convertToEntity) .toList(); - List savedEntities = weeklyRankRepository.saveAll(entities); - log.info("์ฃผ๊ฐ„ ๋žญํ‚น ์ €์žฅ ์™„๋ฃŒ: yearWeek={}, ์ €์žฅ๋œ ์ˆ˜={}", yearWeek, savedEntities.size()); + weeklyRankRepository.saveAll(entities); + log.info("[Batch-Ranking] ์ฃผ๊ฐ„ ๋žญํ‚น ์ €์žฅ ์„ฑ๊ณต (yearWeek: {}, ์ €์žฅ ๊ฑด์ˆ˜: {})", yearWeek, entities.size()); } catch (Exception e) { - log.error("์ฃผ๊ฐ„ ๋žญํ‚น ์ €์žฅ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: yearWeek={}", yearWeek, e); - throw new RuntimeException("์ฃผ๊ฐ„ ๋žญํ‚น ์ €์žฅ ์‹คํŒจ", e); + // ์ƒ์„ธํ•œ ์ปจํ…์ŠคํŠธ๋ฅผ ํฌํ•จํ•œ ์—๋Ÿฌ ๋กœ๊ทธ + log.error("[Batch-Ranking] ์ฃผ๊ฐ„ ๋žญํ‚น ์ €์žฅ ์ค‘ ์˜ˆ์™ธ ๋ฐœ์ƒ! (yearWeek: {}, ์ฒ˜๋ฆฌ ์ค‘์ด๋˜ ๊ฑด์ˆ˜: {}) - ์›์ธ: {}", + yearWeek, targetCount, e.getMessage(), e); + throw e; // ์˜ˆ์™ธ๋ฅผ ๊ทธ๋Œ€๋กœ ๋˜์ ธ์„œ Batch Step์ด ์‹คํŒจ ์ƒํƒœ๊ฐ€ ๋˜๋„๋ก ์œ„์ž„ } } @@ -71,4 +74,4 @@ private WeeklyRankEntity convertToEntity(RankingAggregation aggregation) { aggregation.getRankPosition() ); } -} \ No newline at end of file +} diff --git a/modules/jpa/src/main/java/com/loopers/domain/ranking/MonthlyRankEntity.java b/modules/jpa/src/main/java/com/loopers/domain/ranking/MonthlyRankEntity.java index fa9de9cd6..0b2922f25 100644 --- a/modules/jpa/src/main/java/com/loopers/domain/ranking/MonthlyRankEntity.java +++ b/modules/jpa/src/main/java/com/loopers/domain/ranking/MonthlyRankEntity.java @@ -20,7 +20,7 @@ @Table( name = "mv_product_rank_monthly", indexes = { - @Index(name = "idx_year_month_rank", columnList = "year_month, rank_position") + @Index(name = "idx_year_month_rank", columnList = "base_year_month, base_rank_position์œผ๋กœ") } ) @Getter @@ -45,7 +45,7 @@ public class MonthlyRankEntity { @Column(name = "total_score", nullable = false) private long totalScore; - @Column(name = "rank_position", nullable = false) + @Column(name = "base_rank_position", nullable = false) private long rankPosition; @Column(name = "created_at", nullable = false) diff --git a/modules/jpa/src/main/java/com/loopers/domain/ranking/MonthlyRankId.java b/modules/jpa/src/main/java/com/loopers/domain/ranking/MonthlyRankId.java index f475c8134..6207c756f 100644 --- a/modules/jpa/src/main/java/com/loopers/domain/ranking/MonthlyRankId.java +++ b/modules/jpa/src/main/java/com/loopers/domain/ranking/MonthlyRankId.java @@ -22,7 +22,7 @@ public class MonthlyRankId implements Serializable { @Column(name = "product_id", nullable = false) private Long productId; - @Column(name = "year_month", nullable = false, length = 7) + @Column(name = "base_year_month", nullable = false, length = 7) private String yearMonth; // e.g., "2024-12" private MonthlyRankId(Long productId, String yearMonth) { From 2c8e55409ee47695f296012ddbdefe5718c96acc Mon Sep 17 00:00:00 2001 From: hyujikoh Date: Fri, 2 Jan 2026 16:45:10 +0900 Subject: [PATCH 75/85] =?UTF-8?q?feat(product):=20=EC=9B=94=EA=B0=84=20?= =?UTF-8?q?=EB=9E=AD=ED=82=B9=20=EC=97=94=ED=8B=B0=ED=8B=B0=20=EC=9D=B8?= =?UTF-8?q?=EB=8D=B1=EC=8A=A4=20=ED=95=84=EB=93=9C=EB=AA=85=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EB=B0=8F=20=EB=A9=94=ED=8A=B8=EB=A6=AD=EC=8A=A4=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EC=BF=BC=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/metrics/ProductMetricsJpaRepository.java | 1 + .../main/java/com/loopers/domain/ranking/MonthlyRankEntity.java | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.java b/apps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.java index 351ea3762..26859830c 100644 --- a/apps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.java +++ b/apps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.java @@ -37,5 +37,6 @@ List aggregateByDateRange( @Param("startDate") LocalDate startDate, @Param("endDate") LocalDate endDate); + @Query("SELECT m FROM ProductMetricsEntity m WHERE m.id.metricDate BETWEEN :startDate AND :endDate") List findByMetricDateBetween(LocalDate startDate, LocalDate endDate); } diff --git a/modules/jpa/src/main/java/com/loopers/domain/ranking/MonthlyRankEntity.java b/modules/jpa/src/main/java/com/loopers/domain/ranking/MonthlyRankEntity.java index 0b2922f25..e4689f76d 100644 --- a/modules/jpa/src/main/java/com/loopers/domain/ranking/MonthlyRankEntity.java +++ b/modules/jpa/src/main/java/com/loopers/domain/ranking/MonthlyRankEntity.java @@ -20,7 +20,7 @@ @Table( name = "mv_product_rank_monthly", indexes = { - @Index(name = "idx_year_month_rank", columnList = "base_year_month, base_rank_position์œผ๋กœ") + @Index(name = "idx_year_month_rank", columnList = "base_year_month, base_rank_position") } ) @Getter From e5128b7c42e6b767bd9590840de0791a09e0bc27 Mon Sep 17 00:00:00 2001 From: hyujikoh Date: Fri, 2 Jan 2026 16:57:10 +0900 Subject: [PATCH 76/85] =?UTF-8?q?refactor(ranking):=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=20=ED=8F=AC=EB=A7=B7=ED=8C=85=20=EB=B0=8F=20=EB=B6=88=ED=95=84?= =?UTF-8?q?=EC=9A=94=ED=95=9C=20=EC=9E=84=ED=8F=AC=ED=8A=B8=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ์ฝ”๋“œ ๊ฐ€๋…์„ฑ์„ ๋†’์ด๊ธฐ ์œ„ํ•ด ๋ถˆํ•„์š”ํ•œ ์ž„ํฌํŠธ ๋ฌธ์„ ์ œ๊ฑฐ - ์ฃผ์„ ๋ฐ ๊ณต๋ฐฑ ์ •๋ฆฌ๋กœ ์ฝ”๋“œ ์ผ๊ด€์„ฑ ํ–ฅ์ƒ --- .../product/ProductDetailInfo.java | 3 +- .../application/product/ProductFacade.java | 40 ++++++------- .../ranking/MonthlyRankingService.java | 30 +++++----- .../ranking/WeeklyRankingService.java | 28 +++++---- .../domain/point/PointHistoryEntity.java | 1 - .../loopers/domain/ranking/RankingPeriod.java | 6 +- .../com/loopers/domain/user/UserEntity.java | 1 - .../ranking/MonthlyRankJpaRepository.java | 11 ++-- .../ranking/MonthlyRankRepositoryImpl.java | 12 ++-- .../ranking/WeeklyRankJpaRepository.java | 11 ++-- .../ranking/WeeklyRankRepositoryImpl.java | 12 ++-- .../api/product/ProductV1ApiSpec.java | 2 +- .../api/product/ProductV1Controller.java | 2 +- .../api/ranking/RankingV1ApiSpec.java | 13 ++-- .../api/ranking/RankingV1Controller.java | 32 +++++----- .../product/ProductFacadeRankingTest.java | 16 ++--- .../api/ranking/RankingApiE2ETest.java | 59 ++++++++++++------- .../com/loopers/CommerceBatchApplication.java | 5 +- .../loopers/batch/job/demo/DemoJobConfig.java | 10 ++-- .../batch/job/demo/step/DemoTasklet.java | 6 +- .../job/ranking/MonthlyRankingJobConfig.java | 23 ++++---- .../job/ranking/WeeklyRankingJobConfig.java | 24 ++++---- .../job/ranking/dto/RankingAggregation.java | 9 +-- .../ranking/processor/RankingProcessor.java | 7 ++- .../ranking/reader/MonthlyMetricsReader.java | 36 +++++------ .../ranking/reader/WeeklyMetricsReader.java | 36 +++++------ .../job/ranking/support/DateRangeParser.java | 18 +++--- .../ranking/support/RankingAggregator.java | 22 +++---- .../job/ranking/support/ScoreCalculator.java | 16 ++--- .../job/ranking/writer/MonthlyRankWriter.java | 14 +++-- .../job/ranking/writer/WeeklyRankWriter.java | 14 +++-- .../loopers/batch/listener/ChunkListener.java | 9 +-- .../loopers/batch/listener/JobListener.java | 29 ++++----- .../batch/listener/StepMonitorListener.java | 25 ++++---- .../metrics/ProductMetricsJpaRepository.java | 33 ++++++----- .../metrics/ProductMetricsRepositoryImpl.java | 12 ++-- .../ranking/MonthlyRankJpaRepository.java | 11 ++-- .../ranking/MonthlyRankRepositoryImpl.java | 12 ++-- .../ranking/WeeklyRankJpaRepository.java | 11 ++-- .../ranking/WeeklyRankRepositoryImpl.java | 12 ++-- .../dto/RankingAggregationUnitTest.java | 29 ++++----- .../support/DateRangeParserUnitTest.java | 30 +++++----- .../support/RankingAggregatorUnitTest.java | 29 ++++----- .../com/loopers/job/demo/DemoJobE2ETest.java | 19 +++--- .../event/EventProcessingFacade.java | 8 +-- .../application/metrics/MetricsService.java | 8 +-- .../domain/event/EventHandledService.java | 2 +- .../domain/ranking/RankingServiceTest.java | 38 ++++++------ ...MetricsEventProcessingIntegrationTest.java | 1 - .../integration/RankingIntegrationTest.java | 18 +++--- 50 files changed, 458 insertions(+), 397 deletions(-) diff --git a/apps/commerce-api/src/main/java/com/loopers/application/product/ProductDetailInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductDetailInfo.java index 065177e88..3bff4bf5f 100644 --- a/apps/commerce-api/src/main/java/com/loopers/application/product/ProductDetailInfo.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductDetailInfo.java @@ -62,7 +62,8 @@ public static ProductDetailInfo of(ProductEntity product, BrandEntity brand, Lon return of(product, brand, likeCount, isLiked, null); } - public static ProductDetailInfo of(ProductEntity product, BrandEntity brand, Long likeCount, Boolean isLiked, RankingItem ranking) { + public static ProductDetailInfo of(ProductEntity product, BrandEntity brand, Long likeCount, Boolean isLiked, + RankingItem ranking) { if (product == null) { throw new IllegalArgumentException("์ƒํ’ˆ ์ •๋ณด๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."); } diff --git a/apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java index f6247d037..50264d784 100644 --- a/apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java @@ -77,7 +77,7 @@ public Page getProducts(ProductSearchFilter productSearchFilter) { * ๋„๋ฉ”์ธ ์„œ๋น„์Šค์—์„œ ์—”ํ‹ฐํ‹ฐ๋ฅผ ์กฐํšŒํ•˜๊ณ , Facade์—์„œ DTO๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. * * @param productId ์ƒํ’ˆ ID - * @param username ์‚ฌ์šฉ์ž ID (nullable) + * @param username ์‚ฌ์šฉ์ž ID (nullable) * @return ์ƒํ’ˆ ์ƒ์„ธ ์ •๋ณด */ @Transactional(readOnly = true) @@ -136,11 +136,11 @@ public ProductDetailInfo getProductDetail(Long productId, String username) { @Transactional(readOnly = true) public Page getRankingProducts(Pageable pageable, LocalDate date) { LocalDate targetDate = date != null ? date : LocalDate.now(); - + // 1. ๋žญํ‚น ์กฐํšŒ List rankings = rankingRedisService.getRanking( targetDate, - pageable.getPageNumber() + 1, + pageable.getPageNumber() + 1, pageable.getPageSize() ); @@ -148,13 +148,13 @@ public Page getRankingProducts(Pageable pageable, LocalDate date) { if (rankings.isEmpty() && date == null) { LocalDate yesterday = targetDate.minusDays(1); log.info("์ฝœ๋“œ ์Šคํƒ€ํŠธ Fallback: ์˜ค๋Š˜({}) ๋žญํ‚น ์—†์Œ, ์–ด์ œ({}) ๋žญํ‚น ์กฐํšŒ", targetDate, yesterday); - + rankings = rankingRedisService.getRanking( yesterday, pageable.getPageNumber() + 1, pageable.getPageSize() ); - + if (!rankings.isEmpty()) { targetDate = yesterday; // totalCount ๊ณ„์‚ฐ์„ ์œ„ํ•ด ๋‚ ์งœ ๋ณ€๊ฒฝ } @@ -190,22 +190,22 @@ public Page getRankingProducts(Pageable pageable, LocalDate date) { /** * ๊ธฐ๊ฐ„๋ณ„ ๋žญํ‚น ์ƒํ’ˆ ๋ชฉ๋ก ์กฐํšŒ - * - * @param period ๋žญํ‚น ๊ธฐ๊ฐ„ (DAILY, WEEKLY, MONTHLY) - * @param pageable ํŽ˜์ด์ง• ์ •๋ณด - * @param date ์กฐํšŒ ๋‚ ์งœ (DAILY์šฉ, null์ด๋ฉด ์˜ค๋Š˜) - * @param yearWeek ์กฐํšŒ ์ฃผ์ฐจ (WEEKLY์šฉ, ์˜ˆ: "2024-W52") + * + * @param period ๋žญํ‚น ๊ธฐ๊ฐ„ (DAILY, WEEKLY, MONTHLY) + * @param pageable ํŽ˜์ด์ง• ์ •๋ณด + * @param date ์กฐํšŒ ๋‚ ์งœ (DAILY์šฉ, null์ด๋ฉด ์˜ค๋Š˜) + * @param yearWeek ์กฐํšŒ ์ฃผ์ฐจ (WEEKLY์šฉ, ์˜ˆ: "2024-W52") * @param yearMonth ์กฐํšŒ ์›” (MONTHLY์šฉ, ์˜ˆ: "2024-12") * @return ๋žญํ‚น ์ƒํ’ˆ ๋ชฉ๋ก */ @Transactional(readOnly = true) public Page getRankingProductsByPeriod( - RankingPeriod period, - Pageable pageable, - LocalDate date, - String yearWeek, + RankingPeriod period, + Pageable pageable, + LocalDate date, + String yearWeek, String yearMonth) { - + return switch (period) { case DAILY -> getRankingProducts(pageable, date); case WEEKLY -> getWeeklyRankingProducts(pageable, yearWeek); @@ -215,7 +215,7 @@ public Page getRankingProductsByPeriod( /** * ์ฃผ๊ฐ„ ๋žญํ‚น ์ƒํ’ˆ ๋ชฉ๋ก ์กฐํšŒ - * + * * @param pageable ํŽ˜์ด์ง• ์ •๋ณด * @param yearWeek ์กฐํšŒ ์ฃผ์ฐจ (์˜ˆ: "2024-W52") * @return ์ฃผ๊ฐ„ ๋žญํ‚น ์ƒํ’ˆ ๋ชฉ๋ก @@ -229,7 +229,7 @@ public Page getWeeklyRankingProducts(Pageable pageable, String year // 1. ์ฃผ๊ฐ„ ๋žญํ‚น ์กฐํšŒ Page weeklyRankings = weeklyRankingService.getWeeklyRanking(yearWeek, pageable); - + if (weeklyRankings.isEmpty()) { log.debug("์ฃผ๊ฐ„ ๋žญํ‚น ๋ฐ์ดํ„ฐ ์—†์Œ: yearWeek={}", yearWeek); return Page.empty(pageable); @@ -259,8 +259,8 @@ public Page getWeeklyRankingProducts(Pageable pageable, String year /** * ์›”๊ฐ„ ๋žญํ‚น ์ƒํ’ˆ ๋ชฉ๋ก ์กฐํšŒ - * - * @param pageable ํŽ˜์ด์ง• ์ •๋ณด + * + * @param pageable ํŽ˜์ด์ง• ์ •๋ณด * @param yearMonth ์กฐํšŒ ์›” (์˜ˆ: "2024-12") * @return ์›”๊ฐ„ ๋žญํ‚น ์ƒํ’ˆ ๋ชฉ๋ก */ @@ -273,7 +273,7 @@ public Page getMonthlyRankingProducts(Pageable pageable, String yea // 1. ์›”๊ฐ„ ๋žญํ‚น ์กฐํšŒ Page monthlyRankings = monthlyRankingService.getMonthlyRanking(yearMonth, pageable); - + if (monthlyRankings.isEmpty()) { log.debug("์›”๊ฐ„ ๋žญํ‚น ๋ฐ์ดํ„ฐ ์—†์Œ: yearMonth={}", yearMonth); return Page.empty(pageable); diff --git a/apps/commerce-api/src/main/java/com/loopers/application/ranking/MonthlyRankingService.java b/apps/commerce-api/src/main/java/com/loopers/application/ranking/MonthlyRankingService.java index 3ff82a079..d1600b0cb 100644 --- a/apps/commerce-api/src/main/java/com/loopers/application/ranking/MonthlyRankingService.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/ranking/MonthlyRankingService.java @@ -1,16 +1,18 @@ package com.loopers.application.ranking; -import com.loopers.domain.ranking.MonthlyRankEntity; -import com.loopers.domain.ranking.MonthlyRankRepository; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; +import java.util.List; + import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.List; +import com.loopers.domain.ranking.MonthlyRankEntity; +import com.loopers.domain.ranking.MonthlyRankRepository; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; /** * ์›”๊ฐ„ ๋žญํ‚น ์กฐํšŒ ์„œ๋น„์Šค @@ -25,18 +27,18 @@ public class MonthlyRankingService { /** * ํŠน์ • ์›”์˜ ๋žญํ‚น์„ ํŽ˜์ด์ง€๋„ค์ด์…˜ํ•˜์—ฌ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. - * + * * @param yearMonth ์กฐํšŒํ•  ์›” (์˜ˆ: "2024-12") - * @param pageable ํŽ˜์ด์ง• ์ •๋ณด + * @param pageable ํŽ˜์ด์ง• ์ •๋ณด * @return ์›”๊ฐ„ ๋žญํ‚น ํŽ˜์ด์ง€ */ public Page getMonthlyRanking(String yearMonth, Pageable pageable) { - log.debug("์›”๊ฐ„ ๋žญํ‚น ์กฐํšŒ: yearMonth={}, page={}, size={}", + log.debug("์›”๊ฐ„ ๋žญํ‚น ์กฐํšŒ: yearMonth={}, page={}, size={}", yearMonth, pageable.getPageNumber(), pageable.getPageSize()); // 1. ์ „์ฒด ๋žญํ‚น ์กฐํšŒ (์ˆœ์œ„ ์ˆœ์œผ๋กœ ์ •๋ ฌ๋จ) List allRankings = monthlyRankRepository.findByYearMonth(yearMonth); - + if (allRankings.isEmpty()) { log.debug("์›”๊ฐ„ ๋žญํ‚น ๋ฐ์ดํ„ฐ ์—†์Œ: yearMonth={}", yearMonth); return Page.empty(pageable); @@ -45,14 +47,14 @@ public Page getMonthlyRanking(String yearMonth, Pageable page // 2. ํŽ˜์ด์ง• ์ฒ˜๋ฆฌ int start = (int) pageable.getOffset(); int end = Math.min(start + pageable.getPageSize(), allRankings.size()); - + if (start >= allRankings.size()) { return Page.empty(pageable); } List pagedRankings = allRankings.subList(start, end); - - log.debug("์›”๊ฐ„ ๋žญํ‚น ์กฐํšŒ ์™„๋ฃŒ: yearMonth={}, ์ „์ฒด={}, ํŽ˜์ด์ง€={}", + + log.debug("์›”๊ฐ„ ๋žญํ‚น ์กฐํšŒ ์™„๋ฃŒ: yearMonth={}, ์ „์ฒด={}, ํŽ˜์ด์ง€={}", yearMonth, allRankings.size(), pagedRankings.size()); return new PageImpl<>(pagedRankings, pageable, allRankings.size()); @@ -60,7 +62,7 @@ public Page getMonthlyRanking(String yearMonth, Pageable page /** * ํŠน์ • ์›”์˜ ์ „์ฒด ๋žญํ‚น ๊ฐœ์ˆ˜๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. - * + * * @param yearMonth ์กฐํšŒํ•  ์›” * @return ๋žญํ‚น ๊ฐœ์ˆ˜ */ @@ -68,4 +70,4 @@ public long getMonthlyRankingCount(String yearMonth) { List rankings = monthlyRankRepository.findByYearMonth(yearMonth); return rankings.size(); } -} \ No newline at end of file +} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/ranking/WeeklyRankingService.java b/apps/commerce-api/src/main/java/com/loopers/application/ranking/WeeklyRankingService.java index f5e00e5b8..345e8c6a4 100644 --- a/apps/commerce-api/src/main/java/com/loopers/application/ranking/WeeklyRankingService.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/ranking/WeeklyRankingService.java @@ -1,16 +1,18 @@ package com.loopers.application.ranking; -import com.loopers.domain.ranking.WeeklyRankEntity; -import com.loopers.domain.ranking.WeeklyRankRepository; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; +import java.util.List; + import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.List; +import com.loopers.domain.ranking.WeeklyRankEntity; +import com.loopers.domain.ranking.WeeklyRankRepository; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; /** * ์ฃผ๊ฐ„ ๋žญํ‚น ์กฐํšŒ ์„œ๋น„์Šค @@ -25,18 +27,18 @@ public class WeeklyRankingService { /** * ํŠน์ • ์ฃผ์ฐจ์˜ ๋žญํ‚น์„ ํŽ˜์ด์ง€๋„ค์ด์…˜ํ•˜์—ฌ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. - * + * * @param yearWeek ์กฐํšŒํ•  ์ฃผ์ฐจ (์˜ˆ: "2024-W52") * @param pageable ํŽ˜์ด์ง• ์ •๋ณด * @return ์ฃผ๊ฐ„ ๋žญํ‚น ํŽ˜์ด์ง€ */ public Page getWeeklyRanking(String yearWeek, Pageable pageable) { - log.debug("์ฃผ๊ฐ„ ๋žญํ‚น ์กฐํšŒ: yearWeek={}, page={}, size={}", + log.debug("์ฃผ๊ฐ„ ๋žญํ‚น ์กฐํšŒ: yearWeek={}, page={}, size={}", yearWeek, pageable.getPageNumber(), pageable.getPageSize()); // 1. ์ „์ฒด ๋žญํ‚น ์กฐํšŒ (์ˆœ์œ„ ์ˆœ์œผ๋กœ ์ •๋ ฌ๋จ) List allRankings = weeklyRankRepository.findByYearWeek(yearWeek); - + if (allRankings.isEmpty()) { log.debug("์ฃผ๊ฐ„ ๋žญํ‚น ๋ฐ์ดํ„ฐ ์—†์Œ: yearWeek={}", yearWeek); return Page.empty(pageable); @@ -45,14 +47,14 @@ public Page getWeeklyRanking(String yearWeek, Pageable pageabl // 2. ํŽ˜์ด์ง• ์ฒ˜๋ฆฌ int start = (int) pageable.getOffset(); int end = Math.min(start + pageable.getPageSize(), allRankings.size()); - + if (start >= allRankings.size()) { return Page.empty(pageable); } List pagedRankings = allRankings.subList(start, end); - - log.debug("์ฃผ๊ฐ„ ๋žญํ‚น ์กฐํšŒ ์™„๋ฃŒ: yearWeek={}, ์ „์ฒด={}, ํŽ˜์ด์ง€={}", + + log.debug("์ฃผ๊ฐ„ ๋žญํ‚น ์กฐํšŒ ์™„๋ฃŒ: yearWeek={}, ์ „์ฒด={}, ํŽ˜์ด์ง€={}", yearWeek, allRankings.size(), pagedRankings.size()); return new PageImpl<>(pagedRankings, pageable, allRankings.size()); @@ -60,7 +62,7 @@ public Page getWeeklyRanking(String yearWeek, Pageable pageabl /** * ํŠน์ • ์ฃผ์ฐจ์˜ ์ „์ฒด ๋žญํ‚น ๊ฐœ์ˆ˜๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. - * + * * @param yearWeek ์กฐํšŒํ•  ์ฃผ์ฐจ * @return ๋žญํ‚น ๊ฐœ์ˆ˜ */ @@ -68,4 +70,4 @@ public long getWeeklyRankingCount(String yearWeek) { List rankings = weeklyRankRepository.findByYearWeek(yearWeek); return rankings.size(); } -} \ No newline at end of file +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/point/PointHistoryEntity.java b/apps/commerce-api/src/main/java/com/loopers/domain/point/PointHistoryEntity.java index 570da9cea..63187cc67 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/point/PointHistoryEntity.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/point/PointHistoryEntity.java @@ -4,7 +4,6 @@ import java.util.Objects; import com.loopers.domain.BaseEntity; -import com.loopers.domain.product.ProductEntity; import com.loopers.domain.user.UserEntity; import lombok.AccessLevel; diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingPeriod.java b/apps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingPeriod.java index 2ecfafa51..cddb3e7cd 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingPeriod.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingPeriod.java @@ -8,14 +8,14 @@ public enum RankingPeriod { * ์ผ๊ฐ„ ๋žญํ‚น (Redis ZSET ๊ธฐ๋ฐ˜) */ DAILY, - + /** * ์ฃผ๊ฐ„ ๋žญํ‚น (mv_product_rank_weekly ๊ธฐ๋ฐ˜) */ WEEKLY, - + /** * ์›”๊ฐ„ ๋žญํ‚น (mv_product_rank_monthly ๊ธฐ๋ฐ˜) */ MONTHLY -} \ No newline at end of file +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserEntity.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserEntity.java index 9312e8d0e..330ca1e2e 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserEntity.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserEntity.java @@ -6,7 +6,6 @@ import java.util.Objects; import com.loopers.domain.BaseEntity; -import com.loopers.support.Uris; import lombok.AccessLevel; import lombok.Getter; diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MonthlyRankJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MonthlyRankJpaRepository.java index 98968a6f5..45a50042b 100644 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MonthlyRankJpaRepository.java +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MonthlyRankJpaRepository.java @@ -1,14 +1,15 @@ package com.loopers.infrastructure.ranking; -import com.loopers.domain.ranking.MonthlyRankEntity; -import com.loopers.domain.ranking.MonthlyRankId; +import java.util.List; + import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; -import java.util.List; +import com.loopers.domain.ranking.MonthlyRankEntity; +import com.loopers.domain.ranking.MonthlyRankId; /** * ์›”๊ฐ„ ๋žญํ‚น JPA Repository (commerce-api์šฉ) @@ -29,11 +30,11 @@ public interface MonthlyRankJpaRepository extends JpaRepository findByYearMonthWithPagination(String yearMonth, i public long deleteByYearMonth(String yearMonth) { return jpaRepository.deleteByIdYearMonth(yearMonth); } -} \ No newline at end of file +} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/WeeklyRankJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/WeeklyRankJpaRepository.java index 6bf9b0eeb..3df5fbaa2 100644 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/WeeklyRankJpaRepository.java +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/WeeklyRankJpaRepository.java @@ -1,14 +1,15 @@ package com.loopers.infrastructure.ranking; -import com.loopers.domain.ranking.WeeklyRankEntity; -import com.loopers.domain.ranking.WeeklyRankId; +import java.util.List; + import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; -import java.util.List; +import com.loopers.domain.ranking.WeeklyRankEntity; +import com.loopers.domain.ranking.WeeklyRankId; /** * ์ฃผ๊ฐ„ ๋žญํ‚น JPA Repository (commerce-api์šฉ) @@ -29,11 +30,11 @@ public interface WeeklyRankJpaRepository extends JpaRepository findByYearWeekWithPagination(String yearWeek, int public long deleteByYearWeek(String yearWeek) { return jpaRepository.deleteByIdYearWeek(yearWeek); } -} \ No newline at end of file +} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1ApiSpec.java index 5cfadb0f4..1684fcd0a 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1ApiSpec.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1ApiSpec.java @@ -29,7 +29,7 @@ ApiResponse> getProducts( @PageableDefault(size = 20) Pageable pageable, @RequestParam(required = false) Long brandId, @RequestParam(required = false) String productName - ); + ); @Operation( summary = "์ƒํ’ˆ ์ƒ์„ธ ์กฐํšŒ", diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java index 7a15decc9..4f6dc50e7 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java @@ -41,7 +41,7 @@ public ApiResponse getProductDetail( @RequestHeader(value = "X-USER-ID", required = false) String username ) { ProductDetailInfo productDetail = productFacade.getProductDetail(productId, username); - + // 3. ์‘๋‹ต ์ƒ์„ฑ return ApiResponse.success(ProductV1Dtos.ProductDetailResponse.from(productDetail)); } diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1ApiSpec.java index d84aef7f5..8f3b8a25a 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1ApiSpec.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1ApiSpec.java @@ -1,15 +1,16 @@ package com.loopers.interfaces.api.ranking; -import com.loopers.domain.ranking.RankingPeriod; -import com.loopers.interfaces.api.ApiResponse; -import com.loopers.interfaces.api.common.PageResponse; -import com.loopers.interfaces.api.product.ProductV1Dtos; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; +import java.time.LocalDate; + import org.springframework.data.domain.Pageable; -import java.time.LocalDate; +import com.loopers.domain.ranking.RankingPeriod; +import com.loopers.interfaces.api.ApiResponse; +import com.loopers.interfaces.api.common.PageResponse; +import com.loopers.interfaces.api.product.ProductV1Dtos; /** * ๋žญํ‚น API ๋ช…์„ธ @@ -43,4 +44,4 @@ ApiResponse> getRankingProductsB @Parameter(description = "์—ฐ๋„-์›” (YYYY-MM)", example = "2024-12") String yearMonth ); -} \ No newline at end of file +} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java index 44df72436..d1d841b99 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java @@ -1,5 +1,18 @@ package com.loopers.interfaces.api.ranking; +import java.time.LocalDate; +import java.time.YearMonth; +import java.time.format.DateTimeFormatter; +import java.time.temporal.WeekFields; +import java.util.Locale; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + import com.loopers.application.product.ProductFacade; import com.loopers.application.product.ProductInfo; import com.loopers.domain.ranking.RankingPeriod; @@ -7,17 +20,8 @@ import com.loopers.interfaces.api.common.PageResponse; import com.loopers.interfaces.api.product.ProductV1Dtos; import com.loopers.support.Uris; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.web.PageableDefault; -import org.springframework.web.bind.annotation.*; -import java.time.LocalDate; -import java.time.YearMonth; -import java.time.format.DateTimeFormatter; -import java.time.temporal.WeekFields; -import java.util.Locale; +import lombok.RequiredArgsConstructor; /** * ๋žญํ‚น ์ „์šฉ Controller @@ -72,11 +76,11 @@ private String processYearWeekParameter(String yearWeek, LocalDate date) { // date๊ฐ€ ์žˆ์œผ๋ฉด ํ•ด๋‹น ๋‚ ์งœ์˜ ์ฃผ์ฐจ, ์—†์œผ๋ฉด ํ˜„์žฌ ์ฃผ์ฐจ LocalDate targetDate = date != null ? date : LocalDate.now(); - + WeekFields weekFields = WeekFields.of(Locale.getDefault()); int year = targetDate.getYear(); int week = targetDate.get(weekFields.weekOfYear()); - + return String.format("%d-W%02d", year, week); } @@ -93,7 +97,7 @@ private String processYearMonthParameter(String yearMonth, LocalDate date) { // date๊ฐ€ ์žˆ์œผ๋ฉด ํ•ด๋‹น ๋‚ ์งœ์˜ ์›”, ์—†์œผ๋ฉด ํ˜„์žฌ ์›” LocalDate targetDate = date != null ? date : LocalDate.now(); YearMonth ym = YearMonth.from(targetDate); - + return ym.format(DateTimeFormatter.ofPattern("yyyy-MM")); } -} \ No newline at end of file +} diff --git a/apps/commerce-api/src/test/java/com/loopers/application/product/ProductFacadeRankingTest.java b/apps/commerce-api/src/test/java/com/loopers/application/product/ProductFacadeRankingTest.java index b64e4c8e0..f5e2b66b0 100644 --- a/apps/commerce-api/src/test/java/com/loopers/application/product/ProductFacadeRankingTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/application/product/ProductFacadeRankingTest.java @@ -1,11 +1,10 @@ package com.loopers.application.product; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Answers.RETURNS_DEEP_STUBS; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; -import static org.mockito.Answers.RETURNS_DEEP_STUBS; - import java.math.BigDecimal; import java.time.LocalDate; import java.util.List; @@ -28,7 +27,10 @@ import com.loopers.cache.dto.CachePayloads.RankingItem; import com.loopers.domain.brand.BrandService; import com.loopers.domain.like.LikeService; -import com.loopers.domain.product.*; +import com.loopers.domain.product.ProductCacheService; +import com.loopers.domain.product.ProductMVService; +import com.loopers.domain.product.ProductMaterializedViewEntity; +import com.loopers.domain.product.ProductService; import com.loopers.domain.tracking.UserBehaviorTracker; import com.loopers.domain.user.UserService; @@ -79,10 +81,10 @@ private ProductMaterializedViewEntity createMockMVEntity(Long productId, String when(mv.getBrandId()).thenReturn(1L); when(mv.getBrandName()).thenReturn("Test Brand"); when(mv.getCreatedAt()).thenReturn(java.time.ZonedDateTime.now()); - + when(mv.getPrice().getOriginPrice()).thenReturn(BigDecimal.valueOf(10000)); when(mv.getPrice().getDiscountPrice()).thenReturn(BigDecimal.valueOf(9000)); - + return mv; } @@ -243,7 +245,7 @@ void shouldIncludeRankingInProductDetail() { // Given Long productId = 301L; LocalDate today = LocalDate.now(); - + ProductMaterializedViewEntity mvEntity = createMockMVEntity(productId, "Ranked Product"); RankingItem ranking = new RankingItem(5, productId, 75.0); @@ -268,7 +270,7 @@ void shouldHaveNullRankingForUnrankedProduct() { // Given Long productId = 302L; LocalDate today = LocalDate.now(); - + ProductMaterializedViewEntity mvEntity = createMockMVEntity(productId, "Unranked Product"); when(productCacheService.getProductDetailFromCache(productId)).thenReturn(Optional.empty()); diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/ranking/RankingApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/ranking/RankingApiE2ETest.java index 097cfb6b9..2a052f036 100644 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/ranking/RankingApiE2ETest.java +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/ranking/RankingApiE2ETest.java @@ -2,20 +2,24 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; - import java.math.BigDecimal; import java.time.LocalDate; import java.util.ArrayList; import java.util.List; import java.util.Objects; -import org.junit.jupiter.api.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.core.ParameterizedTypeReference; import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.http.*; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import com.loopers.cache.CacheKeyGenerator; import com.loopers.cache.RankingRedisService; @@ -23,7 +27,10 @@ import com.loopers.cache.dto.CachePayloads.RankingScore.EventType; import com.loopers.domain.brand.BrandEntity; import com.loopers.domain.brand.BrandService; -import com.loopers.domain.product.*; +import com.loopers.domain.product.ProductDomainCreateRequest; +import com.loopers.domain.product.ProductEntity; +import com.loopers.domain.product.ProductMVService; +import com.loopers.domain.product.ProductService; import com.loopers.fixtures.BrandTestFixture; import com.loopers.fixtures.ProductTestFixture; import com.loopers.interfaces.api.ApiResponse; @@ -80,7 +87,7 @@ void setUp() { databaseCleanUp.truncateAllTables(); redisCleanUp.truncateAll(); testProductIds.clear(); - Long testBrandId = null; + Long testBrandId; today = LocalDate.now(); @@ -130,7 +137,8 @@ void should_return_products_in_ranking_order() { // when ParameterizedTypeReference>> responseType = - new ParameterizedTypeReference<>() {}; + new ParameterizedTypeReference<>() { + }; ResponseEntity>> response = testRestTemplate.exchange( Uris.Ranking.GET_RANKING + "?page=0&size=10", @@ -142,7 +150,7 @@ void should_return_products_in_ranking_order() { () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK), () -> assertThat(Objects.requireNonNull(response.getBody()).data().content()).hasSize(3), () -> assertThat(Objects.requireNonNull(response.getBody()).data().content().get(0).productId()).isEqualTo(product1), - () -> assertThat(response.getBody().data().content().get(2).productId()).isEqualTo(product2), + () -> assertThat(Objects.requireNonNull(response.getBody()).data().content().get(2).productId()).isEqualTo(product2), () -> assertThat(Objects.requireNonNull(response.getBody()).data().content().get(1).productId()).isEqualTo(product3) ); } @@ -154,7 +162,8 @@ void should_return_empty_when_no_ranking_data() { // when ParameterizedTypeReference>> responseType = - new ParameterizedTypeReference<>() {}; + new ParameterizedTypeReference<>() { + }; ResponseEntity>> response = testRestTemplate.exchange( Uris.Ranking.GET_RANKING, @@ -186,7 +195,8 @@ void should_paginate_ranking_results() { // when - ํŽ˜์ด์ง€ ํฌ๊ธฐ 2๋กœ ์กฐํšŒ ParameterizedTypeReference>> responseType = - new ParameterizedTypeReference<>() {}; + new ParameterizedTypeReference<>() { + }; ResponseEntity>> response = testRestTemplate.exchange( Uris.Ranking.GET_RANKING + "?page=0&size=2", @@ -218,7 +228,8 @@ void should_return_ranking_for_specific_date() { // when - ์–ด์ œ ๋‚ ์งœ๋กœ ์กฐํšŒ ParameterizedTypeReference>> responseType = - new ParameterizedTypeReference<>() {}; + new ParameterizedTypeReference<>() { + }; ResponseEntity>> response = testRestTemplate.exchange( Uris.Ranking.GET_RANKING + "?date=" + yesterday, @@ -252,7 +263,8 @@ void should_fallback_to_yesterday_when_today_is_empty() { // when - ๋‚ ์งœ ๋ฏธ์ง€์ • (์˜ค๋Š˜ ๊ธฐ์ค€) ParameterizedTypeReference>> responseType = - new ParameterizedTypeReference<>() {}; + new ParameterizedTypeReference<>() { + }; ResponseEntity>> response = testRestTemplate.exchange( Uris.Ranking.GET_RANKING, @@ -281,7 +293,8 @@ void should_not_fallback_when_date_is_explicitly_specified() { // when - ์˜ค๋Š˜ ๋‚ ์งœ ๋ช…์‹œ์  ์ง€์ • ParameterizedTypeReference>> responseType = - new ParameterizedTypeReference<>() {}; + new ParameterizedTypeReference<>() { + }; ResponseEntity>> response = testRestTemplate.exchange( Uris.Ranking.GET_RANKING + "?date=" + today, @@ -314,12 +327,13 @@ void should_include_ranking_info_for_ranked_product() { // when ParameterizedTypeReference> responseType = - new ParameterizedTypeReference<>() {}; + new ParameterizedTypeReference<>() { + }; ResponseEntity> response = testRestTemplate.exchange( Uris.Product.GET_DETAIL, HttpMethod.GET, null, responseType, productId - ); + ); // then assertAll( @@ -339,12 +353,13 @@ void should_have_null_ranking_for_unranked_product() { // when ParameterizedTypeReference> responseType = - new ParameterizedTypeReference<>() {}; + new ParameterizedTypeReference<>() { + }; ResponseEntity> response = testRestTemplate.exchange( Uris.Product.GET_DETAIL, HttpMethod.GET, null, responseType, productId - ); + ); // then assertAll( @@ -371,12 +386,13 @@ void should_return_correct_rank_among_multiple_products() { // when - product2 ์ƒ์„ธ ์กฐํšŒ ParameterizedTypeReference> responseType = - new ParameterizedTypeReference<>() {}; + new ParameterizedTypeReference<>() { + }; ResponseEntity> response = testRestTemplate.exchange( Uris.Product.GET_DETAIL, HttpMethod.GET, null, responseType, product2 - ); + ); // then - 2์œ„๋กœ ๋ฐ˜ํ™˜ assertAll( @@ -413,12 +429,13 @@ void should_accumulate_scores_for_same_product() { // when ParameterizedTypeReference> responseType = - new ParameterizedTypeReference<>() {}; + new ParameterizedTypeReference<>() { + }; ResponseEntity> response = testRestTemplate.exchange( Uris.Product.GET_DETAIL, HttpMethod.GET, null, responseType, productId - ); + ); // then - ์ ์ˆ˜๊ฐ€ ๋ˆ„์ ๋จ (weight ์ ์šฉ: 10*0.1 + 20*0.2 = 5.0) assertAll( @@ -460,4 +477,4 @@ void should_carry_over_scores_to_next_day() { redisTemplate.delete(tomorrowKey); } } -} \ No newline at end of file +} diff --git a/apps/commerce-batch/src/main/java/com/loopers/CommerceBatchApplication.java b/apps/commerce-batch/src/main/java/com/loopers/CommerceBatchApplication.java index e5005c373..fe1c301d9 100644 --- a/apps/commerce-batch/src/main/java/com/loopers/CommerceBatchApplication.java +++ b/apps/commerce-batch/src/main/java/com/loopers/CommerceBatchApplication.java @@ -1,11 +1,12 @@ package com.loopers; -import jakarta.annotation.PostConstruct; +import java.util.TimeZone; + import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.ConfigurationPropertiesScan; -import java.util.TimeZone; +import jakarta.annotation.PostConstruct; @ConfigurationPropertiesScan @SpringBootApplication diff --git a/apps/commerce-batch/src/main/java/com/loopers/batch/job/demo/DemoJobConfig.java b/apps/commerce-batch/src/main/java/com/loopers/batch/job/demo/DemoJobConfig.java index 7c486483f..0a40b9d0b 100644 --- a/apps/commerce-batch/src/main/java/com/loopers/batch/job/demo/DemoJobConfig.java +++ b/apps/commerce-batch/src/main/java/com/loopers/batch/job/demo/DemoJobConfig.java @@ -1,9 +1,5 @@ package com.loopers.batch.job.demo; -import com.loopers.batch.job.demo.step.DemoTasklet; -import com.loopers.batch.listener.JobListener; -import com.loopers.batch.listener.StepMonitorListener; -import lombok.RequiredArgsConstructor; import org.springframework.batch.core.Job; import org.springframework.batch.core.Step; import org.springframework.batch.core.configuration.annotation.JobScope; @@ -16,6 +12,12 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import com.loopers.batch.job.demo.step.DemoTasklet; +import com.loopers.batch.listener.JobListener; +import com.loopers.batch.listener.StepMonitorListener; + +import lombok.RequiredArgsConstructor; + @ConditionalOnProperty(name = "spring.batch.job.name", havingValue = DemoJobConfig.JOB_NAME) @RequiredArgsConstructor @Configuration diff --git a/apps/commerce-batch/src/main/java/com/loopers/batch/job/demo/step/DemoTasklet.java b/apps/commerce-batch/src/main/java/com/loopers/batch/job/demo/step/DemoTasklet.java index 800fe5a03..6fbcc88b9 100644 --- a/apps/commerce-batch/src/main/java/com/loopers/batch/job/demo/step/DemoTasklet.java +++ b/apps/commerce-batch/src/main/java/com/loopers/batch/job/demo/step/DemoTasklet.java @@ -1,7 +1,5 @@ package com.loopers.batch.job.demo.step; -import com.loopers.batch.job.demo.DemoJobConfig; -import lombok.RequiredArgsConstructor; import org.springframework.batch.core.StepContribution; import org.springframework.batch.core.configuration.annotation.StepScope; import org.springframework.batch.core.scope.context.ChunkContext; @@ -11,6 +9,10 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Component; +import com.loopers.batch.job.demo.DemoJobConfig; + +import lombok.RequiredArgsConstructor; + @StepScope @ConditionalOnProperty(name = "spring.batch.job.name", havingValue = DemoJobConfig.JOB_NAME) @RequiredArgsConstructor diff --git a/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/MonthlyRankingJobConfig.java b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/MonthlyRankingJobConfig.java index 2c699ca57..c7075ce03 100644 --- a/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/MonthlyRankingJobConfig.java +++ b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/MonthlyRankingJobConfig.java @@ -1,12 +1,5 @@ package com.loopers.batch.job.ranking; -import com.loopers.batch.job.ranking.dto.RankingAggregation; -import com.loopers.batch.job.ranking.processor.RankingProcessor; -import com.loopers.batch.job.ranking.reader.MonthlyMetricsReader; -import com.loopers.batch.job.ranking.writer.MonthlyRankWriter; -import com.loopers.batch.listener.JobListener; -import com.loopers.batch.listener.StepMonitorListener; -import lombok.RequiredArgsConstructor; import org.springframework.batch.core.Job; import org.springframework.batch.core.Step; import org.springframework.batch.core.configuration.annotation.JobScope; @@ -19,9 +12,17 @@ import org.springframework.context.annotation.Configuration; import org.springframework.transaction.PlatformTransactionManager; +import com.loopers.batch.job.ranking.dto.RankingAggregation; +import com.loopers.batch.job.ranking.processor.RankingProcessor; +import com.loopers.batch.job.ranking.reader.MonthlyMetricsReader; +import com.loopers.batch.job.ranking.writer.MonthlyRankWriter; +import com.loopers.batch.listener.JobListener; +import com.loopers.batch.listener.StepMonitorListener; + +import lombok.RequiredArgsConstructor; + /** * ์›”๊ฐ„ ๋žญํ‚น ๋ฐฐ์น˜ Job ์„ค์ • - * * ์‹คํ–‰ ๋ฐฉ๋ฒ•: * java -jar commerce-batch.jar --spring.batch.job.name=monthlyRankingJob --yearMonth=2024-12 */ @@ -44,7 +45,7 @@ public class MonthlyRankingJobConfig { /** * ์›”๊ฐ„ ๋žญํ‚น ์ง‘๊ณ„ Job - * + * * @return ์›”๊ฐ„ ๋žญํ‚น Job */ @Bean(JOB_NAME) @@ -61,7 +62,7 @@ public Job monthlyRankingJob() { * - Reader: ์›”๊ฐ„ ๋ฉ”ํŠธ๋ฆญ ๋ฐ์ดํ„ฐ ์ง‘๊ณ„ ๋ฐ TOP 100 ์„ ๋ณ„ * - Processor: ์ถ”๊ฐ€ ๊ฐ€๊ณต (ํ˜„์žฌ๋Š” pass-through) * - Writer: mv_product_rank_monthly ํ…Œ์ด๋ธ”์— ์ €์žฅ - * + * * @return ์›”๊ฐ„ ์ง‘๊ณ„ Step */ @Bean(STEP_NAME) @@ -75,4 +76,4 @@ public Step monthlyAggregationStep() { .listener(stepMonitorListener) .build(); } -} \ No newline at end of file +} diff --git a/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/WeeklyRankingJobConfig.java b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/WeeklyRankingJobConfig.java index 06c2354bf..cc758b973 100644 --- a/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/WeeklyRankingJobConfig.java +++ b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/WeeklyRankingJobConfig.java @@ -1,12 +1,5 @@ package com.loopers.batch.job.ranking; -import com.loopers.batch.job.ranking.dto.RankingAggregation; -import com.loopers.batch.job.ranking.processor.RankingProcessor; -import com.loopers.batch.job.ranking.reader.WeeklyMetricsReader; -import com.loopers.batch.job.ranking.writer.WeeklyRankWriter; -import com.loopers.batch.listener.JobListener; -import com.loopers.batch.listener.StepMonitorListener; -import lombok.RequiredArgsConstructor; import org.springframework.batch.core.Job; import org.springframework.batch.core.Step; import org.springframework.batch.core.configuration.annotation.JobScope; @@ -19,9 +12,18 @@ import org.springframework.context.annotation.Configuration; import org.springframework.transaction.PlatformTransactionManager; +import com.loopers.batch.job.ranking.dto.RankingAggregation; +import com.loopers.batch.job.ranking.processor.RankingProcessor; +import com.loopers.batch.job.ranking.reader.WeeklyMetricsReader; +import com.loopers.batch.job.ranking.writer.WeeklyRankWriter; +import com.loopers.batch.listener.JobListener; +import com.loopers.batch.listener.StepMonitorListener; + +import lombok.RequiredArgsConstructor; + /** * ์ฃผ๊ฐ„ ๋žญํ‚น ๋ฐฐ์น˜ Job ์„ค์ • - * + *

* ์‹คํ–‰ ๋ฐฉ๋ฒ•: * java -jar commerce-batch.jar --spring.batch.job.name=weeklyRankingJob --yearWeek=2024-W52 */ @@ -44,7 +46,7 @@ public class WeeklyRankingJobConfig { /** * ์ฃผ๊ฐ„ ๋žญํ‚น ์ง‘๊ณ„ Job - * + * * @return ์ฃผ๊ฐ„ ๋žญํ‚น Job */ @Bean(JOB_NAME) @@ -61,7 +63,7 @@ public Job weeklyRankingJob() { * - Reader: 7์ผ์น˜ ๋ฉ”ํŠธ๋ฆญ ๋ฐ์ดํ„ฐ ์ง‘๊ณ„ ๋ฐ TOP 100 ์„ ๋ณ„ * - Processor: ์ถ”๊ฐ€ ๊ฐ€๊ณต (ํ˜„์žฌ๋Š” pass-through) * - Writer: mv_product_rank_weekly ํ…Œ์ด๋ธ”์— ์ €์žฅ - * + * * @return ์ฃผ๊ฐ„ ์ง‘๊ณ„ Step */ @Bean(STEP_NAME) @@ -75,4 +77,4 @@ public Step weeklyAggregationStep() { .listener(stepMonitorListener) .build(); } -} \ No newline at end of file +} diff --git a/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/dto/RankingAggregation.java b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/dto/RankingAggregation.java index dc63f3811..f8493e72d 100644 --- a/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/dto/RankingAggregation.java +++ b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/dto/RankingAggregation.java @@ -1,6 +1,7 @@ package com.loopers.batch.job.ranking.dto; import com.loopers.batch.job.ranking.support.ScoreCalculator; + import lombok.Getter; /** @@ -33,7 +34,7 @@ private RankingAggregation(Long productId, long viewCount, long likeCount, /** * DB ์ง‘๊ณ„ ๊ฒฐ๊ณผ๋กœ๋ถ€ํ„ฐ RankingAggregation์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. * - * @param row DB ์ง‘๊ณ„ ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ (Object[] ํ˜•ํƒœ) + * @param row DB ์ง‘๊ณ„ ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ (Object[] ํ˜•ํƒœ) * @param calculator ์ ์ˆ˜ ๊ณ„์‚ฐ๊ธฐ * @return ์ƒ์„ฑ๋œ RankingAggregation ๊ฐ์ฒด * @throws IllegalArgumentException row๊ฐ€ null์ด๊ฑฐ๋‚˜ ํ˜•์‹์ด ์ž˜๋ชป๋œ ๊ฒฝ์šฐ @@ -67,7 +68,7 @@ public static RankingAggregation from(Object[] row, ScoreCalculator calculator) public void assignRank(int rank) { if (rank < 1 || rank > 100) { throw new IllegalArgumentException( - String.format("์ˆœ์œ„๋Š” 1~100 ๋ฒ”์œ„์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค. (์ž…๋ ฅ๊ฐ’: %d)", rank)); + String.format("์ˆœ์œ„๋Š” 1~100 ๋ฒ”์œ„์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค. (์ž…๋ ฅ๊ฐ’: %d)", rank)); } this.rankPosition = rank; } @@ -77,7 +78,7 @@ public void assignRank(int rank) { */ @Override public String toString() { - return String.format("RankingAggregation{productId=%d, score=%d, rank=%d}", - productId, totalScore, rankPosition); + return String.format("RankingAggregation{productId=%d, score=%d, rank=%d}", + productId, totalScore, rankPosition); } } diff --git a/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/processor/RankingProcessor.java b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/processor/RankingProcessor.java index da29368de..0dc5223c7 100644 --- a/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/processor/RankingProcessor.java +++ b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/processor/RankingProcessor.java @@ -1,9 +1,10 @@ package com.loopers.batch.job.ranking.processor; -import com.loopers.batch.job.ranking.dto.RankingAggregation; import org.springframework.batch.item.ItemProcessor; import org.springframework.stereotype.Component; +import com.loopers.batch.job.ranking.dto.RankingAggregation; + /** * ๋žญํ‚น ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ๊ธฐ * - Reader์—์„œ ์ด๋ฏธ ์ ์ˆ˜ ๊ณ„์‚ฐ ๋ฐ ์ˆœ์œ„ ๋ถ€์—ฌ๊ฐ€ ์™„๋ฃŒ๋จ @@ -17,8 +18,8 @@ public RankingAggregation process(RankingAggregation item) throws Exception { // Reader์—์„œ ์ด๋ฏธ ์ˆœ์œ„ ๋ถ€์—ฌ๋จ // ์ถ”๊ฐ€ ๊ฐ€๊ณต์ด ํ•„์š”ํ•˜๋ฉด ์—ฌ๊ธฐ์„œ ์ฒ˜๋ฆฌ // ์˜ˆ: ํŠน์ • ์กฐ๊ฑด ํ•„ํ„ฐ๋ง, ๋ฐ์ดํ„ฐ ๋ณด์ • ๋“ฑ - + // ํ˜„์žฌ๋Š” ๋‹จ์ˆœ ํ†ต๊ณผ (pass-through) return item; } -} \ No newline at end of file +} diff --git a/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/reader/MonthlyMetricsReader.java b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/reader/MonthlyMetricsReader.java index 865193589..ebba661d5 100644 --- a/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/reader/MonthlyMetricsReader.java +++ b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/reader/MonthlyMetricsReader.java @@ -1,19 +1,21 @@ package com.loopers.batch.job.ranking.reader; +import java.time.LocalDate; +import java.util.Iterator; +import java.util.List; + +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.batch.item.ItemReader; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + import com.loopers.batch.job.ranking.dto.RankingAggregation; import com.loopers.batch.job.ranking.support.DateRangeParser; import com.loopers.batch.job.ranking.support.RankingAggregator; import com.loopers.domain.metrics.ProductMetricsRepository; + import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.batch.core.configuration.annotation.StepScope; -import org.springframework.batch.item.ItemReader; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; - -import java.time.LocalDate; -import java.util.Iterator; -import java.util.List; /** * ์›”๊ฐ„ ๋ฉ”ํŠธ๋ฆญ Reader @@ -29,7 +31,7 @@ public class MonthlyMetricsReader implements ItemReader { private final ProductMetricsRepository productMetricsRepository; private final DateRangeParser dateRangeParser; private final RankingAggregator rankingAggregator; - + private Iterator iterator; @Value("#{jobParameters['yearMonth']}") @@ -40,34 +42,34 @@ public RankingAggregation read() throws Exception { if (iterator == null) { initializeIterator(); } - + return iterator.hasNext() ? iterator.next() : null; } private void initializeIterator() { log.info("์›”๊ฐ„ ๋žญํ‚น ์ง‘๊ณ„ ์‹œ์ž‘: yearMonth={}", yearMonth); - + try { // 1. ์›” โ†’ ๋‚ ์งœ ๋ฒ”์œ„ ๋ณ€ํ™˜ LocalDate[] dateRange = dateRangeParser.parseYearMonth(yearMonth); LocalDate startDate = dateRange[0]; LocalDate endDate = dateRange[1]; - + log.info("์ง‘๊ณ„ ๊ธฐ๊ฐ„: {} ~ {}", startDate, endDate); - + // 2. DB์—์„œ ์ง‘๊ณ„ ์ฟผ๋ฆฌ ์‹คํ–‰ List aggregationResults = productMetricsRepository.aggregateByDateRange(startDate, endDate); log.info("์ง‘๊ณ„ ๋Œ€์ƒ ์ƒํ’ˆ ์ˆ˜: {}", aggregationResults.size()); - + // 3. ๋žญํ‚น ์ฒ˜๋ฆฌ (์ •๋ ฌ + TOP 100 + ์ˆœ์œ„ ๋ถ€์—ฌ) List rankings = rankingAggregator.processRankings(aggregationResults); log.info("์ƒ์„ฑ๋œ ๋žญํ‚น ์ˆ˜: {}", rankings.size()); - + iterator = rankings.iterator(); - + } catch (Exception e) { log.error("์›”๊ฐ„ ๋žญํ‚น ์ง‘๊ณ„ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: yearMonth={}", yearMonth, e); throw new RuntimeException("์›”๊ฐ„ ๋žญํ‚น ์ง‘๊ณ„ ์‹คํŒจ", e); } } -} \ No newline at end of file +} diff --git a/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/reader/WeeklyMetricsReader.java b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/reader/WeeklyMetricsReader.java index 3530e3f4f..7394f3e31 100644 --- a/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/reader/WeeklyMetricsReader.java +++ b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/reader/WeeklyMetricsReader.java @@ -1,19 +1,21 @@ package com.loopers.batch.job.ranking.reader; +import java.time.LocalDate; +import java.util.Iterator; +import java.util.List; + +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.batch.item.ItemReader; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + import com.loopers.batch.job.ranking.dto.RankingAggregation; import com.loopers.batch.job.ranking.support.DateRangeParser; import com.loopers.batch.job.ranking.support.RankingAggregator; import com.loopers.domain.metrics.ProductMetricsRepository; + import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.batch.core.configuration.annotation.StepScope; -import org.springframework.batch.item.ItemReader; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; - -import java.time.LocalDate; -import java.util.Iterator; -import java.util.List; /** * ์ฃผ๊ฐ„ ๋ฉ”ํŠธ๋ฆญ Reader @@ -29,7 +31,7 @@ public class WeeklyMetricsReader implements ItemReader { private final ProductMetricsRepository productMetricsRepository; private final DateRangeParser dateRangeParser; private final RankingAggregator rankingAggregator; - + private Iterator iterator; @Value("#{jobParameters['yearWeek']}") @@ -40,34 +42,34 @@ public RankingAggregation read() throws Exception { if (iterator == null) { initializeIterator(); } - + return iterator.hasNext() ? iterator.next() : null; } private void initializeIterator() { log.info("์ฃผ๊ฐ„ ๋žญํ‚น ์ง‘๊ณ„ ์‹œ์ž‘: yearWeek={}", yearWeek); - + try { // 1. ์ฃผ์ฐจ โ†’ ๋‚ ์งœ ๋ฒ”์œ„ ๋ณ€ํ™˜ LocalDate[] dateRange = dateRangeParser.parseYearWeek(yearWeek); LocalDate startDate = dateRange[0]; LocalDate endDate = dateRange[1]; - + log.info("์ง‘๊ณ„ ๊ธฐ๊ฐ„: {} ~ {}", startDate, endDate); - + // 2. DB์—์„œ ์ง‘๊ณ„ ์ฟผ๋ฆฌ ์‹คํ–‰ List aggregationResults = productMetricsRepository.aggregateByDateRange(startDate, endDate); log.info("์ง‘๊ณ„ ๋Œ€์ƒ ์ƒํ’ˆ ์ˆ˜: {}", aggregationResults.size()); - + // 3. ๋žญํ‚น ์ฒ˜๋ฆฌ (์ •๋ ฌ + TOP 100 + ์ˆœ์œ„ ๋ถ€์—ฌ) List rankings = rankingAggregator.processRankings(aggregationResults); log.info("์ƒ์„ฑ๋œ ๋žญํ‚น ์ˆ˜: {}", rankings.size()); - + iterator = rankings.iterator(); - + } catch (Exception e) { log.error("์ฃผ๊ฐ„ ๋žญํ‚น ์ง‘๊ณ„ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: yearWeek={}", yearWeek, e); throw new RuntimeException("์ฃผ๊ฐ„ ๋žญํ‚น ์ง‘๊ณ„ ์‹คํŒจ", e); } } -} \ No newline at end of file +} diff --git a/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/support/DateRangeParser.java b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/support/DateRangeParser.java index e713b42c2..fe5409c54 100644 --- a/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/support/DateRangeParser.java +++ b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/support/DateRangeParser.java @@ -1,11 +1,11 @@ package com.loopers.batch.job.ranking.support; -import org.springframework.stereotype.Component; - import java.time.LocalDate; import java.time.YearMonth; import java.time.temporal.WeekFields; +import org.springframework.stereotype.Component; + /** * ๋‚ ์งœ ๋ฒ”์œ„ ํŒŒ์‹ฑ ์œ ํ‹ธ๋ฆฌํ‹ฐ * - yearWeek (e.g., "2024-W52") โ†’ ์ฃผ๊ฐ„ ๋‚ ์งœ ๋ฒ”์œ„ @@ -24,7 +24,7 @@ public class DateRangeParser { public LocalDate[] parseYearWeek(String yearWeek) { if (yearWeek == null || !yearWeek.matches("\\d{4}-W\\d{1,2}")) { throw new IllegalArgumentException( - String.format("์ž˜๋ชป๋œ yearWeek ํ˜•์‹์ž…๋‹ˆ๋‹ค. ์˜ˆ์ƒ: '2024-W52', ์‹ค์ œ: '%s'", yearWeek)); + String.format("์ž˜๋ชป๋œ yearWeek ํ˜•์‹์ž…๋‹ˆ๋‹ค. ์˜ˆ์ƒ: '2024-W52', ์‹ค์ œ: '%s'", yearWeek)); } try { @@ -35,14 +35,14 @@ public LocalDate[] parseYearWeek(String yearWeek) { // ISO ์ฃผ์ฐจ ์‹œ์Šคํ…œ ์‚ฌ์šฉ (์›”์š”์ผ ์‹œ์ž‘) WeekFields weekFields = WeekFields.ISO; LocalDate startOfWeek = LocalDate.of(year, 1, 1) - .with(weekFields.weekOfYear(), week) - .with(weekFields.dayOfWeek(), 1); + .with(weekFields.weekOfYear(), week) + .with(weekFields.dayOfWeek(), 1); LocalDate endOfWeek = startOfWeek.plusDays(6); return new LocalDate[]{startOfWeek, endOfWeek}; } catch (Exception e) { throw new IllegalArgumentException( - String.format("yearWeek ํŒŒ์‹ฑ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: %s", yearWeek), e); + String.format("yearWeek ํŒŒ์‹ฑ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: %s", yearWeek), e); } } @@ -56,7 +56,7 @@ public LocalDate[] parseYearWeek(String yearWeek) { public LocalDate[] parseYearMonth(String yearMonth) { if (yearMonth == null || !yearMonth.matches("\\d{4}-\\d{2}")) { throw new IllegalArgumentException( - String.format("์ž˜๋ชป๋œ yearMonth ํ˜•์‹์ž…๋‹ˆ๋‹ค. ์˜ˆ์ƒ: '2024-12', ์‹ค์ œ: '%s'", yearMonth)); + String.format("์ž˜๋ชป๋œ yearMonth ํ˜•์‹์ž…๋‹ˆ๋‹ค. ์˜ˆ์ƒ: '2024-12', ์‹ค์ œ: '%s'", yearMonth)); } try { @@ -67,7 +67,7 @@ public LocalDate[] parseYearMonth(String yearMonth) { return new LocalDate[]{startOfMonth, endOfMonth}; } catch (Exception e) { throw new IllegalArgumentException( - String.format("yearMonth ํŒŒ์‹ฑ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: %s", yearMonth), e); + String.format("yearMonth ํŒŒ์‹ฑ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: %s", yearMonth), e); } } -} \ No newline at end of file +} diff --git a/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/support/RankingAggregator.java b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/support/RankingAggregator.java index cf3b2ace7..36b809fe2 100644 --- a/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/support/RankingAggregator.java +++ b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/support/RankingAggregator.java @@ -1,12 +1,14 @@ package com.loopers.batch.job.ranking.support; -import com.loopers.batch.job.ranking.dto.RankingAggregation; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; - import java.util.Comparator; import java.util.List; +import org.springframework.stereotype.Component; + +import com.loopers.batch.job.ranking.dto.RankingAggregation; + +import lombok.RequiredArgsConstructor; + /** * ๋žญํ‚น ์ง‘๊ณ„ ์ฒ˜๋ฆฌ๊ธฐ * - ์ง‘๊ณ„ ๊ฒฐ๊ณผ๋ฅผ ์ ์ˆ˜ ๊ธฐ์ค€์œผ๋กœ ์ •๋ ฌ @@ -34,14 +36,14 @@ public List processRankings(List aggregationResult // 1. DTO ๋ณ€ํ™˜ + ์ ์ˆ˜ ๊ณ„์‚ฐ List aggregations = aggregationResults.stream() - .map(row -> RankingAggregation.from(row, scoreCalculator)) - .toList(); + .map(row -> RankingAggregation.from(row, scoreCalculator)) + .toList(); // 2. ์ ์ˆ˜ ๊ธฐ์ค€ ๋‚ด๋ฆผ์ฐจ์ˆœ ์ •๋ ฌ + TOP 100 ํ•„ํ„ฐ๋ง List topRankings = aggregations.stream() - .sorted(Comparator.comparingLong(RankingAggregation::getTotalScore).reversed()) - .limit(TOP_RANK_LIMIT) - .toList(); + .sorted(Comparator.comparingLong(RankingAggregation::getTotalScore).reversed()) + .limit(TOP_RANK_LIMIT) + .toList(); // 3. ์ˆœ์œ„ ๋ถ€์—ฌ (1์œ„๋ถ€ํ„ฐ ์‹œ์ž‘) for (int i = 0; i < topRankings.size(); i++) { @@ -57,4 +59,4 @@ public List processRankings(List aggregationResult public int getTopRankLimit() { return TOP_RANK_LIMIT; } -} \ No newline at end of file +} diff --git a/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/support/ScoreCalculator.java b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/support/ScoreCalculator.java index 3d79dfcd0..50a76762e 100644 --- a/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/support/ScoreCalculator.java +++ b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/support/ScoreCalculator.java @@ -19,24 +19,24 @@ public class ScoreCalculator { /** * ๋ฉ”ํŠธ๋ฆญ ๋ฐ์ดํ„ฐ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๋žญํ‚น ์ ์ˆ˜๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. * - * @param viewCount ์กฐํšŒ์ˆ˜ - * @param likeCount ์ข‹์•„์š”์ˆ˜ + * @param viewCount ์กฐํšŒ์ˆ˜ + * @param likeCount ์ข‹์•„์š”์ˆ˜ * @param salesCount ํŒ๋งค์ˆ˜๋Ÿ‰ * @param orderCount ์ฃผ๋ฌธ์ˆ˜ * @return ๊ณ„์‚ฐ๋œ ์ด ์ ์ˆ˜ */ public long calculate(long viewCount, long likeCount, long salesCount, long orderCount) { return viewCount * VIEW_WEIGHT - + likeCount * LIKE_WEIGHT - + salesCount * SALES_WEIGHT - + orderCount * ORDER_WEIGHT; + + likeCount * LIKE_WEIGHT + + salesCount * SALES_WEIGHT + + orderCount * ORDER_WEIGHT; } /** * ๊ฐ€์ค‘์น˜ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. (ํ…Œ์ŠคํŠธ ๋ฐ ๋””๋ฒ„๊น…์šฉ) */ public String getWeightInfo() { - return String.format("VIEW=%d, LIKE=%d, SALES=%d, ORDER=%d", - VIEW_WEIGHT, LIKE_WEIGHT, SALES_WEIGHT, ORDER_WEIGHT); + return String.format("VIEW=%d, LIKE=%d, SALES=%d, ORDER=%d", + VIEW_WEIGHT, LIKE_WEIGHT, SALES_WEIGHT, ORDER_WEIGHT); } -} \ No newline at end of file +} diff --git a/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/writer/MonthlyRankWriter.java b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/writer/MonthlyRankWriter.java index 866537ce9..c619594da 100644 --- a/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/writer/MonthlyRankWriter.java +++ b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/writer/MonthlyRankWriter.java @@ -1,17 +1,19 @@ package com.loopers.batch.job.ranking.writer; -import com.loopers.batch.job.ranking.dto.RankingAggregation; -import com.loopers.domain.ranking.MonthlyRankEntity; -import com.loopers.domain.ranking.MonthlyRankRepository; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; +import java.util.List; + import org.springframework.batch.core.configuration.annotation.StepScope; import org.springframework.batch.item.Chunk; import org.springframework.batch.item.ItemWriter; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; -import java.util.List; +import com.loopers.batch.job.ranking.dto.RankingAggregation; +import com.loopers.domain.ranking.MonthlyRankEntity; +import com.loopers.domain.ranking.MonthlyRankRepository; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; /** * ์›”๊ฐ„ ๋žญํ‚น Writer diff --git a/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/writer/WeeklyRankWriter.java b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/writer/WeeklyRankWriter.java index 1ab801d58..9e269a45f 100644 --- a/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/writer/WeeklyRankWriter.java +++ b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/writer/WeeklyRankWriter.java @@ -1,17 +1,19 @@ package com.loopers.batch.job.ranking.writer; -import com.loopers.batch.job.ranking.dto.RankingAggregation; -import com.loopers.domain.ranking.WeeklyRankEntity; -import com.loopers.domain.ranking.WeeklyRankRepository; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; +import java.util.List; + import org.springframework.batch.core.configuration.annotation.StepScope; import org.springframework.batch.item.Chunk; import org.springframework.batch.item.ItemWriter; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; -import java.util.List; +import com.loopers.batch.job.ranking.dto.RankingAggregation; +import com.loopers.domain.ranking.WeeklyRankEntity; +import com.loopers.domain.ranking.WeeklyRankRepository; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; /** * ์ฃผ๊ฐ„ ๋žญํ‚น Writer diff --git a/apps/commerce-batch/src/main/java/com/loopers/batch/listener/ChunkListener.java b/apps/commerce-batch/src/main/java/com/loopers/batch/listener/ChunkListener.java index 10b09b8fc..396fbe30f 100644 --- a/apps/commerce-batch/src/main/java/com/loopers/batch/listener/ChunkListener.java +++ b/apps/commerce-batch/src/main/java/com/loopers/batch/listener/ChunkListener.java @@ -1,11 +1,12 @@ package com.loopers.batch.listener; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; import org.springframework.batch.core.annotation.AfterChunk; import org.springframework.batch.core.scope.context.ChunkContext; import org.springframework.stereotype.Component; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + @Slf4j @RequiredArgsConstructor @Component @@ -14,8 +15,8 @@ public class ChunkListener { @AfterChunk void afterChunk(ChunkContext chunkContext) { log.info( - "์ฒญํฌ ์ข…๋ฃŒ: readCount: ${chunkContext.stepContext.stepExecution.readCount}, " + - "writeCount: ${chunkContext.stepContext.stepExecution.writeCount}" + "์ฒญํฌ ์ข…๋ฃŒ: readCount: ${chunkContext.stepContext.stepExecution.readCount}, " + + "writeCount: ${chunkContext.stepContext.stepExecution.writeCount}" ); } } diff --git a/apps/commerce-batch/src/main/java/com/loopers/batch/listener/JobListener.java b/apps/commerce-batch/src/main/java/com/loopers/batch/listener/JobListener.java index cb5c8bebd..cd923b827 100644 --- a/apps/commerce-batch/src/main/java/com/loopers/batch/listener/JobListener.java +++ b/apps/commerce-batch/src/main/java/com/loopers/batch/listener/JobListener.java @@ -1,15 +1,16 @@ package com.loopers.batch.listener; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; +import java.time.Duration; +import java.time.Instant; +import java.time.ZoneId; + import org.springframework.batch.core.JobExecution; import org.springframework.batch.core.annotation.AfterJob; import org.springframework.batch.core.annotation.BeforeJob; import org.springframework.stereotype.Component; -import java.time.Duration; -import java.time.Instant; -import java.time.ZoneId; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; @Slf4j @RequiredArgsConstructor @@ -28,11 +29,11 @@ void afterJob(JobExecution jobExecution) { var endTime = System.currentTimeMillis(); var startDateTime = Instant.ofEpochMilli(startTime) - .atZone(ZoneId.systemDefault()) - .toLocalDateTime(); + .atZone(ZoneId.systemDefault()) + .toLocalDateTime(); var endDateTime = Instant.ofEpochMilli(endTime) - .atZone(ZoneId.systemDefault()) - .toLocalDateTime(); + .atZone(ZoneId.systemDefault()) + .toLocalDateTime(); var totalTime = endTime - startTime; var duration = Duration.ofMillis(totalTime); @@ -41,11 +42,11 @@ void afterJob(JobExecution jobExecution) { var seconds = duration.getSeconds() % 60; var message = String.format( - """ - *Start Time:* %s - *End Time:* %s - *Total Time:* %d์‹œ๊ฐ„ %d๋ถ„ %d์ดˆ - """, startDateTime, endDateTime, hours, minutes, seconds + """ + *Start Time:* %s + *End Time:* %s + *Total Time:* %d์‹œ๊ฐ„ %d๋ถ„ %d์ดˆ + """, startDateTime, endDateTime, hours, minutes, seconds ).trim(); log.info(message); diff --git a/apps/commerce-batch/src/main/java/com/loopers/batch/listener/StepMonitorListener.java b/apps/commerce-batch/src/main/java/com/loopers/batch/listener/StepMonitorListener.java index 4f22f40b0..e69e74557 100644 --- a/apps/commerce-batch/src/main/java/com/loopers/batch/listener/StepMonitorListener.java +++ b/apps/commerce-batch/src/main/java/com/loopers/batch/listener/StepMonitorListener.java @@ -1,14 +1,17 @@ package com.loopers.batch.listener; -import jakarta.annotation.Nonnull; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; +import java.util.Objects; +import java.util.stream.Collectors; + import org.springframework.batch.core.ExitStatus; import org.springframework.batch.core.StepExecution; import org.springframework.batch.core.StepExecutionListener; import org.springframework.stereotype.Component; -import java.util.Objects; -import java.util.stream.Collectors; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import jakarta.annotation.Nonnull; @Slf4j @RequiredArgsConstructor @@ -29,12 +32,12 @@ public ExitStatus afterStep(@Nonnull StepExecution stepExecution) { .filter(Objects::nonNull) .collect(Collectors.joining("\n")); log.info( - """ - [์—๋Ÿฌ ๋ฐœ์ƒ] - jobName: {} - exceptions: - {} - """.trim(), jobName, exceptions + """ + [์—๋Ÿฌ ๋ฐœ์ƒ] + jobName: {} + exceptions: + {} + """.trim(), jobName, exceptions ); // error ๋ฐœ์ƒ ์‹œ slack ๋“ฑ ๋‹ค๋ฅธ ์ฑ„๋„๋กœ ๋ชจ๋‹ˆํ„ฐ ์ „์†ก return ExitStatus.FAILED; diff --git a/apps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.java b/apps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.java index 26859830c..cc8474c32 100644 --- a/apps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.java +++ b/apps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.java @@ -1,13 +1,14 @@ package com.loopers.infrastructure.metrics; -import com.loopers.domain.metrics.ProductMetricsEntity; -import com.loopers.domain.metrics.ProductMetricsId; +import java.time.LocalDate; +import java.util.List; + import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; -import java.time.LocalDate; -import java.util.List; +import com.loopers.domain.metrics.ProductMetricsEntity; +import com.loopers.domain.metrics.ProductMetricsId; /** * ProductMetrics JPA Repository @@ -20,22 +21,22 @@ public interface ProductMetricsJpaRepository extends JpaRepository aggregateByDateRange( - @Param("startDate") LocalDate startDate, - @Param("endDate") LocalDate endDate); + @Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate); @Query("SELECT m FROM ProductMetricsEntity m WHERE m.id.metricDate BETWEEN :startDate AND :endDate") List findByMetricDateBetween(LocalDate startDate, LocalDate endDate); diff --git a/apps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java b/apps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java index 07fd8e13a..5d914cc34 100644 --- a/apps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java +++ b/apps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java @@ -1,14 +1,16 @@ package com.loopers.infrastructure.metrics; +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; + +import org.springframework.stereotype.Repository; + import com.loopers.domain.metrics.ProductMetricsEntity; import com.loopers.domain.metrics.ProductMetricsId; import com.loopers.domain.metrics.ProductMetricsRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Repository; -import java.time.LocalDate; -import java.util.List; -import java.util.Optional; +import lombok.RequiredArgsConstructor; /** * ProductMetrics Repository ๊ตฌํ˜„์ฒด diff --git a/apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/MonthlyRankJpaRepository.java b/apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/MonthlyRankJpaRepository.java index c7cd7dc32..e9943708b 100644 --- a/apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/MonthlyRankJpaRepository.java +++ b/apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/MonthlyRankJpaRepository.java @@ -1,14 +1,15 @@ package com.loopers.infrastructure.ranking; -import com.loopers.domain.ranking.MonthlyRankEntity; -import com.loopers.domain.ranking.MonthlyRankId; +import java.util.List; + import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; -import java.util.List; +import com.loopers.domain.ranking.MonthlyRankEntity; +import com.loopers.domain.ranking.MonthlyRankId; /** * ์›”๊ฐ„ ๋žญํ‚น JPA Repository @@ -29,11 +30,11 @@ public interface MonthlyRankJpaRepository extends JpaRepository findByYearMonthWithPagination(String yearMonth, i public long deleteByYearMonth(String yearMonth) { return jpaRepository.deleteByIdYearMonth(yearMonth); } -} \ No newline at end of file +} diff --git a/apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/WeeklyRankJpaRepository.java b/apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/WeeklyRankJpaRepository.java index a7d8c875d..aab645ed1 100644 --- a/apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/WeeklyRankJpaRepository.java +++ b/apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/WeeklyRankJpaRepository.java @@ -1,14 +1,15 @@ package com.loopers.infrastructure.ranking; -import com.loopers.domain.ranking.WeeklyRankEntity; -import com.loopers.domain.ranking.WeeklyRankId; +import java.util.List; + import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; -import java.util.List; +import com.loopers.domain.ranking.WeeklyRankEntity; +import com.loopers.domain.ranking.WeeklyRankId; /** * ์ฃผ๊ฐ„ ๋žญํ‚น JPA Repository @@ -29,11 +30,11 @@ public interface WeeklyRankJpaRepository extends JpaRepository findByYearWeekWithPagination(String yearWeek, int public long deleteByYearWeek(String yearWeek) { return jpaRepository.deleteByIdYearWeek(yearWeek); } -} \ No newline at end of file +} diff --git a/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/dto/RankingAggregationUnitTest.java b/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/dto/RankingAggregationUnitTest.java index 70bc2e2dc..63cfa3ebb 100644 --- a/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/dto/RankingAggregationUnitTest.java +++ b/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/dto/RankingAggregationUnitTest.java @@ -1,11 +1,12 @@ package com.loopers.batch.job.ranking.dto; -import com.loopers.batch.job.ranking.support.ScoreCalculator; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import com.loopers.batch.job.ranking.support.ScoreCalculator; + @DisplayName("RankingAggregation ๋‹จ์œ„ ํ…Œ์ŠคํŠธ") class RankingAggregationUnitTest { @@ -40,8 +41,8 @@ void should_create_from_valid_aggregation_result() { void should_throw_exception_when_row_is_null() { // given & when & then Assertions.assertThatThrownBy(() -> RankingAggregation.from(null, calculator)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("์ง‘๊ณ„ ๊ฒฐ๊ณผ ๋ฐฐ์—ด์ด null์ด๊ฑฐ๋‚˜ ๊ธธ์ด๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค"); + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("์ง‘๊ณ„ ๊ฒฐ๊ณผ ๋ฐฐ์—ด์ด null์ด๊ฑฐ๋‚˜ ๊ธธ์ด๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค"); } @Test @@ -52,8 +53,8 @@ void should_throw_exception_when_row_length_is_insufficient() { // when & then Assertions.assertThatThrownBy(() -> RankingAggregation.from(shortRow, calculator)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("์ง‘๊ณ„ ๊ฒฐ๊ณผ ๋ฐฐ์—ด์ด null์ด๊ฑฐ๋‚˜ ๊ธธ์ด๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค"); + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("์ง‘๊ณ„ ๊ฒฐ๊ณผ ๋ฐฐ์—ด์ด null์ด๊ฑฐ๋‚˜ ๊ธธ์ด๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค"); } @Test @@ -64,8 +65,8 @@ void should_throw_exception_when_data_type_is_invalid() { // when & then Assertions.assertThatThrownBy(() -> RankingAggregation.from(invalidRow, calculator)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("์ง‘๊ณ„ ๊ฒฐ๊ณผ ๋ฐ์ดํ„ฐ ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค"); + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("์ง‘๊ณ„ ๊ฒฐ๊ณผ ๋ฐ์ดํ„ฐ ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค"); } @Test @@ -126,12 +127,12 @@ void should_throw_exception_when_rank_is_zero_or_negative() { // when & then Assertions.assertThatThrownBy(() -> aggregation.assignRank(0)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("์ˆœ์œ„๋Š” 1~100 ๋ฒ”์œ„์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค"); + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("์ˆœ์œ„๋Š” 1~100 ๋ฒ”์œ„์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค"); Assertions.assertThatThrownBy(() -> aggregation.assignRank(-1)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("์ˆœ์œ„๋Š” 1~100 ๋ฒ”์œ„์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค"); + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("์ˆœ์œ„๋Š” 1~100 ๋ฒ”์œ„์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค"); } @Test @@ -143,8 +144,8 @@ void should_throw_exception_when_rank_exceeds_100() { // when & then Assertions.assertThatThrownBy(() -> aggregation.assignRank(101)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("์ˆœ์œ„๋Š” 1~100 ๋ฒ”์œ„์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค"); + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("์ˆœ์œ„๋Š” 1~100 ๋ฒ”์œ„์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค"); } } @@ -169,4 +170,4 @@ void should_return_correct_string_format() { Assertions.assertThat(result).contains("rank=1"); } } -} \ No newline at end of file +} diff --git a/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/support/DateRangeParserUnitTest.java b/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/support/DateRangeParserUnitTest.java index b799657d5..14e85e12f 100644 --- a/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/support/DateRangeParserUnitTest.java +++ b/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/support/DateRangeParserUnitTest.java @@ -1,12 +1,12 @@ package com.loopers.batch.job.ranking.support; +import java.time.LocalDate; + import org.assertj.core.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import java.time.LocalDate; - @DisplayName("DateRangeParser ๋‹จ์œ„ ํ…Œ์ŠคํŠธ") class DateRangeParserUnitTest { @@ -52,8 +52,8 @@ void should_parse_first_week_of_2024_correctly() { void should_throw_exception_when_year_week_is_null() { // given & when & then Assertions.assertThatThrownBy(() -> parser.parseYearWeek(null)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("์ž˜๋ชป๋œ yearWeek ํ˜•์‹์ž…๋‹ˆ๋‹ค"); + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("์ž˜๋ชป๋œ yearWeek ํ˜•์‹์ž…๋‹ˆ๋‹ค"); } @Test @@ -64,8 +64,8 @@ void should_throw_exception_when_year_week_format_is_invalid() { // when & then Assertions.assertThatThrownBy(() -> parser.parseYearWeek(invalidYearWeek)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("์ž˜๋ชป๋œ yearWeek ํ˜•์‹์ž…๋‹ˆ๋‹ค"); + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("์ž˜๋ชป๋œ yearWeek ํ˜•์‹์ž…๋‹ˆ๋‹ค"); } @Test @@ -73,8 +73,8 @@ void should_throw_exception_when_year_week_format_is_invalid() { void should_throw_exception_when_year_week_is_empty() { // given & when & then Assertions.assertThatThrownBy(() -> parser.parseYearWeek("")) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("์ž˜๋ชป๋œ yearWeek ํ˜•์‹์ž…๋‹ˆ๋‹ค"); + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("์ž˜๋ชป๋œ yearWeek ํ˜•์‹์ž…๋‹ˆ๋‹ค"); } } @@ -130,8 +130,8 @@ void should_parse_february_in_non_leap_year_correctly() { void should_throw_exception_when_year_month_is_null() { // given & when & then Assertions.assertThatThrownBy(() -> parser.parseYearMonth(null)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("์ž˜๋ชป๋œ yearMonth ํ˜•์‹์ž…๋‹ˆ๋‹ค"); + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("์ž˜๋ชป๋œ yearMonth ํ˜•์‹์ž…๋‹ˆ๋‹ค"); } @Test @@ -142,8 +142,8 @@ void should_throw_exception_when_year_month_format_is_invalid() { // when & then Assertions.assertThatThrownBy(() -> parser.parseYearMonth(invalidYearMonth)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("์ž˜๋ชป๋œ yearMonth ํ˜•์‹์ž…๋‹ˆ๋‹ค"); + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("์ž˜๋ชป๋œ yearMonth ํ˜•์‹์ž…๋‹ˆ๋‹ค"); } @Test @@ -154,8 +154,8 @@ void should_throw_exception_when_month_does_not_exist() { // when & then Assertions.assertThatThrownBy(() -> parser.parseYearMonth(invalidYearMonth)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("yearMonth ํŒŒ์‹ฑ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค"); + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("yearMonth ํŒŒ์‹ฑ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค"); } } -} \ No newline at end of file +} diff --git a/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/support/RankingAggregatorUnitTest.java b/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/support/RankingAggregatorUnitTest.java index 1cad3bb32..0d980dd50 100644 --- a/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/support/RankingAggregatorUnitTest.java +++ b/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/support/RankingAggregatorUnitTest.java @@ -1,13 +1,14 @@ package com.loopers.batch.job.ranking.support; -import com.loopers.batch.job.ranking.dto.RankingAggregation; +import java.util.ArrayList; +import java.util.List; + import org.assertj.core.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import java.util.ArrayList; -import java.util.List; +import com.loopers.batch.job.ranking.dto.RankingAggregation; @DisplayName("RankingAggregator ๋‹จ์œ„ ํ…Œ์ŠคํŠธ") class RankingAggregatorUnitTest { @@ -24,9 +25,9 @@ class ๋žญํ‚น_์ฒ˜๋ฆฌ { void should_sort_by_score_and_assign_ranks() { // given List results = List.of( - new Object[]{1L, 100L, 10L, 5L, 2L}, // score = 100 + 30 + 25 + 4 = 159 - new Object[]{2L, 200L, 20L, 10L, 4L}, // score = 200 + 60 + 50 + 8 = 318 - new Object[]{3L, 50L, 5L, 2L, 1L} // score = 50 + 15 + 10 + 2 = 77 + new Object[]{1L, 100L, 10L, 5L, 2L}, // score = 100 + 30 + 25 + 4 = 159 + new Object[]{2L, 200L, 20L, 10L, 4L}, // score = 200 + 60 + 50 + 8 = 318 + new Object[]{3L, 50L, 5L, 2L, 1L} // score = 50 + 15 + 10 + 2 = 77 ); // when @@ -34,16 +35,16 @@ void should_sort_by_score_and_assign_ranks() { // then Assertions.assertThat(rankings).hasSize(3); - + // ์ ์ˆ˜ ๊ธฐ์ค€ ๋‚ด๋ฆผ์ฐจ์ˆœ ์ •๋ ฌ ํ™•์ธ Assertions.assertThat(rankings.get(0).getProductId()).isEqualTo(2L); // 1์œ„ Assertions.assertThat(rankings.get(0).getRankPosition()).isEqualTo(1); Assertions.assertThat(rankings.get(0).getTotalScore()).isEqualTo(318L); - + Assertions.assertThat(rankings.get(1).getProductId()).isEqualTo(1L); // 2์œ„ Assertions.assertThat(rankings.get(1).getRankPosition()).isEqualTo(2); Assertions.assertThat(rankings.get(1).getTotalScore()).isEqualTo(159L); - + Assertions.assertThat(rankings.get(2).getProductId()).isEqualTo(3L); // 3์œ„ Assertions.assertThat(rankings.get(2).getRankPosition()).isEqualTo(3); Assertions.assertThat(rankings.get(2).getTotalScore()).isEqualTo(77L); @@ -96,9 +97,9 @@ void should_return_empty_list_for_null_results() { void should_maintain_order_for_same_scores() { // given - ๋™์ผํ•œ ์ ์ˆ˜๋ฅผ ๊ฐ€์ง„ ์ƒํ’ˆ๋“ค List results = List.of( - new Object[]{1L, 100L, 0L, 0L, 0L}, // score = 100 - new Object[]{2L, 100L, 0L, 0L, 0L}, // score = 100 - new Object[]{3L, 100L, 0L, 0L, 0L} // score = 100 + new Object[]{1L, 100L, 0L, 0L, 0L}, // score = 100 + new Object[]{2L, 100L, 0L, 0L, 0L}, // score = 100 + new Object[]{3L, 100L, 0L, 0L, 0L} // score = 100 ); // when @@ -109,7 +110,7 @@ void should_maintain_order_for_same_scores() { Assertions.assertThat(rankings.get(0).getRankPosition()).isEqualTo(1); Assertions.assertThat(rankings.get(1).getRankPosition()).isEqualTo(2); Assertions.assertThat(rankings.get(2).getRankPosition()).isEqualTo(3); - + // ๋ชจ๋“  ์ ์ˆ˜๊ฐ€ ๋™์ผํ•จ์„ ํ™•์ธ Assertions.assertThat(rankings.get(0).getTotalScore()).isEqualTo(100L); Assertions.assertThat(rankings.get(1).getTotalScore()).isEqualTo(100L); @@ -131,4 +132,4 @@ void should_return_top_rank_limit() { Assertions.assertThat(limit).isEqualTo(100); } } -} \ No newline at end of file +} diff --git a/apps/commerce-batch/src/test/java/com/loopers/job/demo/DemoJobE2ETest.java b/apps/commerce-batch/src/test/java/com/loopers/job/demo/DemoJobE2ETest.java index dafe59a18..088dddbba 100644 --- a/apps/commerce-batch/src/test/java/com/loopers/job/demo/DemoJobE2ETest.java +++ b/apps/commerce-batch/src/test/java/com/loopers/job/demo/DemoJobE2ETest.java @@ -1,7 +1,9 @@ package com.loopers.job.demo; -import com.loopers.batch.job.demo.DemoJobConfig; -import lombok.RequiredArgsConstructor; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import java.time.LocalDate; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -15,10 +17,7 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.TestPropertySource; -import java.time.LocalDate; - -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; -import static org.junit.jupiter.api.Assertions.assertAll; +import com.loopers.batch.job.demo.DemoJobConfig; @SpringBootTest @SpringBatchTest @@ -50,8 +49,8 @@ void shouldNotSaveCategories_whenApiError() throws Exception { // assert assertAll( - () -> assertThat(jobExecution).isNotNull(), - () -> assertThat(jobExecution.getExitStatus().getExitCode()).isEqualTo(ExitStatus.FAILED.getExitCode()) + () -> assertThat(jobExecution).isNotNull(), + () -> assertThat(jobExecution.getExitStatus().getExitCode()).isEqualTo(ExitStatus.FAILED.getExitCode()) ); } @@ -63,8 +62,8 @@ void success() throws Exception { // act var jobParameters = new JobParametersBuilder() - .addLocalDate("requestDate", LocalDate.now()) - .toJobParameters(); + .addLocalDate("requestDate", LocalDate.now()) + .toJobParameters(); var jobExecution = jobLauncherTestUtils.launchJob(jobParameters); // assert diff --git a/apps/commerce-streamer/src/main/java/com/loopers/application/event/EventProcessingFacade.java b/apps/commerce-streamer/src/main/java/com/loopers/application/event/EventProcessingFacade.java index 501d5a8c9..9a2c71eaa 100644 --- a/apps/commerce-streamer/src/main/java/com/loopers/application/event/EventProcessingFacade.java +++ b/apps/commerce-streamer/src/main/java/com/loopers/application/event/EventProcessingFacade.java @@ -48,10 +48,10 @@ public class EventProcessingFacade { // Application Layer ์˜์กด์„ฑ private final MetricsService metricsService; - + // Domain Layer ์˜์กด์„ฑ private final RankingService rankingService; - + // Infrastructure Layer ์˜์กด์„ฑ private final EventDeserializer eventDeserializer; @@ -66,7 +66,7 @@ public class EventProcessingFacade { */ public CatalogEventResult processCatalogEvent(Object eventValue) { final DomainEventEnvelope envelope = eventDeserializer.deserializeEnvelope(eventValue); - + if (!isValidEnvelope(envelope)) { log.warn("Invalid event envelope: {}", eventValue); return CatalogEventResult.notProcessed(); @@ -106,7 +106,7 @@ public CatalogEventResult processCatalogEvent(Object eventValue) { */ public OrderEventResult processOrderEvent(Object eventValue) { final DomainEventEnvelope envelope = eventDeserializer.deserializeEnvelope(eventValue); - + if (!isValidEnvelope(envelope)) { log.warn("Invalid event envelope: {}", eventValue); return OrderEventResult.notProcessed(); diff --git a/apps/commerce-streamer/src/main/java/com/loopers/application/metrics/MetricsService.java b/apps/commerce-streamer/src/main/java/com/loopers/application/metrics/MetricsService.java index ccae167bd..acf3487d1 100644 --- a/apps/commerce-streamer/src/main/java/com/loopers/application/metrics/MetricsService.java +++ b/apps/commerce-streamer/src/main/java/com/loopers/application/metrics/MetricsService.java @@ -45,7 +45,7 @@ public class MetricsService { // Domain Layer ์˜์กด์„ฑ private final ProductMetricsService productMetricsService; private final EventHandledService eventHandledService; - + // Infrastructure Layer ์˜์กด์„ฑ private final ProductCacheService productCacheService; @@ -64,7 +64,7 @@ public class MetricsService { /** * ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ์—ฌ๋ถ€ ํ™•์ธ ๋ฐ ๋งˆํ‚น - * + * * @param eventId ์ด๋ฒคํŠธ ID * @return true: ์ฒ˜์Œ ์ฒ˜๋ฆฌ, false: ์ด๋ฏธ ์ฒ˜๋ฆฌ๋จ */ @@ -125,7 +125,7 @@ public void addSales(Long productId, int quantity, long occurredAtEpochMillis) { executeWithLock(productId, () -> { ZonedDateTime eventTime = convertToZonedDateTime(occurredAtEpochMillis); boolean updated = productMetricsService.addSales(productId, quantity, eventTime); - + if (updated) { // ์บ์‹œ ๋ฌดํšจํ™” (ํŒ๋งค๋Ÿ‰ ๋ณ€๊ฒฝ - ์ธ๊ธฐ ์ƒํ’ˆ ์ˆœ์œ„ ์˜ํ–ฅ) productCacheService.onSalesCountChanged(productId); @@ -140,7 +140,7 @@ public void addSales(Long productId, int quantity, long occurredAtEpochMillis) { public void handleStockDepleted(Long productId, Long brandId, Integer remainingStock, long occurredAtEpochMillis) { int stockToUpdate = (remainingStock != null) ? remainingStock : 0; productCacheService.updateProductStock(productId, stockToUpdate); - log.info("์žฌ๊ณ  ์†Œ์ง„ ์บ์‹œ ๊ฐฑ์‹  ์™„๋ฃŒ: productId={}, brandId={}, remainingStock={}", + log.info("์žฌ๊ณ  ์†Œ์ง„ ์บ์‹œ ๊ฐฑ์‹  ์™„๋ฃŒ: productId={}, brandId={}, remainingStock={}", productId, brandId, stockToUpdate); } diff --git a/apps/commerce-streamer/src/main/java/com/loopers/domain/event/EventHandledService.java b/apps/commerce-streamer/src/main/java/com/loopers/domain/event/EventHandledService.java index 3f53ab6d2..cde3d41eb 100644 --- a/apps/commerce-streamer/src/main/java/com/loopers/domain/event/EventHandledService.java +++ b/apps/commerce-streamer/src/main/java/com/loopers/domain/event/EventHandledService.java @@ -32,7 +32,7 @@ public boolean isAlreadyHandled(String eventId) { /** * ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ์™„๋ฃŒ ๋งˆํ‚น - * + * * @return true: ์ €์žฅ ์„ฑ๊ณต (์ฒ˜์Œ ์ฒ˜๋ฆฌ), false: ์ €์žฅ ์‹คํŒจ (์ด๋ฏธ ์ฒ˜๋ฆฌ๋จ ๋˜๋Š” ๋™์‹œ์„ฑ ์ถฉ๋Œ) */ @Transactional diff --git a/apps/commerce-streamer/src/test/java/com/loopers/domain/ranking/RankingServiceTest.java b/apps/commerce-streamer/src/test/java/com/loopers/domain/ranking/RankingServiceTest.java index 1ba974843..0df5a144b 100644 --- a/apps/commerce-streamer/src/test/java/com/loopers/domain/ranking/RankingServiceTest.java +++ b/apps/commerce-streamer/src/test/java/com/loopers/domain/ranking/RankingServiceTest.java @@ -2,19 +2,15 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; - import java.math.BigDecimal; import java.time.LocalDate; import java.util.List; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @@ -59,14 +55,14 @@ void shouldGenerateScoreForProductView() throws Exception { // Given Long productId = 1L; long occurredAt = System.currentTimeMillis(); - + ProductViewPayloadV1 payload = new ProductViewPayloadV1(productId, 100L); String payloadJson = objectMapper.writeValueAsString(payload); - + DomainEventEnvelope envelope = new DomainEventEnvelope( "event-1", "PRODUCT_VIEW", "v1", occurredAt, payloadJson ); - + when(eventDeserializer.deserializeProductView(payloadJson)).thenReturn(payload); // When @@ -86,14 +82,14 @@ void shouldGenerateScoreForLikeAction() throws Exception { // Given Long productId = 2L; long occurredAt = System.currentTimeMillis(); - + LikeActionPayloadV1 payload = new LikeActionPayloadV1(productId, 100L, "LIKE"); String payloadJson = objectMapper.writeValueAsString(payload); - + DomainEventEnvelope envelope = new DomainEventEnvelope( "event-2", "LIKE_ACTION", "v1", occurredAt, payloadJson ); - + when(eventDeserializer.deserializeLikeAction(payloadJson)).thenReturn(payload); // When @@ -112,14 +108,14 @@ void shouldNotGenerateScoreForUnlike() throws Exception { // Given Long productId = 2L; long occurredAt = System.currentTimeMillis(); - + LikeActionPayloadV1 payload = new LikeActionPayloadV1(productId, 100L, "UNLIKE"); String payloadJson = objectMapper.writeValueAsString(payload); - + DomainEventEnvelope envelope = new DomainEventEnvelope( "event-3", "LIKE_ACTION", "v1", occurredAt, payloadJson ); - + when(eventDeserializer.deserializeLikeAction(payloadJson)).thenReturn(payload); // When @@ -136,16 +132,16 @@ void shouldGenerateLogNormalizedScoreForPaymentSuccess() throws Exception { Long productId = 3L; long occurredAt = System.currentTimeMillis(); BigDecimal totalPrice = BigDecimal.valueOf(10000); - + PaymentSuccessPayloadV1 payload = new PaymentSuccessPayloadV1( 1L, 1L, 100L, productId, 2, BigDecimal.valueOf(5000), totalPrice ); String payloadJson = objectMapper.writeValueAsString(payload); - + DomainEventEnvelope envelope = new DomainEventEnvelope( "event-4", "PAYMENT_SUCCESS", "v1", occurredAt, payloadJson ); - + when(eventDeserializer.deserializePaymentSuccess(payloadJson)).thenReturn(payload); // When @@ -155,11 +151,11 @@ void shouldGenerateLogNormalizedScoreForPaymentSuccess() throws Exception { assertThat(score).isNotNull(); assertThat(score.productId()).isEqualTo(productId); assertThat(score.eventType()).isEqualTo(RankingScore.EventType.PAYMENT_SUCCESS); - + // ๋กœ๊ทธ ์ •๊ทœํ™” ํ™•์ธ: log(10000 + 1) โ‰ˆ 9.21 double expectedScore = Math.log(10001); assertThat(score.score()).isCloseTo(expectedScore, org.assertj.core.data.Offset.offset(0.01)); - + // ๊ฐ€์ค‘์น˜ ์ ์šฉ: 0.6 * log(10001) โ‰ˆ 5.53 assertThat(score.getWeightedScore()).isCloseTo(0.6 * expectedScore, org.assertj.core.data.Offset.offset(0.01)); } @@ -245,7 +241,7 @@ void shouldGetPaginatedRanking() { new RankingItem(2, 102L, 90.0), new RankingItem(3, 103L, 80.0) ); - + when(rankingRedisService.getRanking(today, 1, 20)).thenReturn(expectedRankings); // When @@ -264,7 +260,7 @@ void shouldGetProductRanking() { LocalDate today = LocalDate.now(); Long productId = 101L; RankingItem expectedRanking = new RankingItem(5, productId, 75.0); - + when(rankingRedisService.getProductRanking(today, productId)).thenReturn(expectedRanking); // When @@ -283,7 +279,7 @@ void shouldReturnNullForUnrankedProduct() { // Given LocalDate today = LocalDate.now(); Long productId = 999L; - + when(rankingRedisService.getProductRanking(today, productId)).thenReturn(null); // When diff --git a/apps/commerce-streamer/src/test/java/com/loopers/integration/MetricsEventProcessingIntegrationTest.java b/apps/commerce-streamer/src/test/java/com/loopers/integration/MetricsEventProcessingIntegrationTest.java index e61823e4b..6524077e8 100644 --- a/apps/commerce-streamer/src/test/java/com/loopers/integration/MetricsEventProcessingIntegrationTest.java +++ b/apps/commerce-streamer/src/test/java/com/loopers/integration/MetricsEventProcessingIntegrationTest.java @@ -2,7 +2,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; - import java.time.Duration; import java.time.LocalDate; import java.util.Optional; diff --git a/apps/commerce-streamer/src/test/java/com/loopers/integration/RankingIntegrationTest.java b/apps/commerce-streamer/src/test/java/com/loopers/integration/RankingIntegrationTest.java index a3bd9084c..ce4e1b87b 100644 --- a/apps/commerce-streamer/src/test/java/com/loopers/integration/RankingIntegrationTest.java +++ b/apps/commerce-streamer/src/test/java/com/loopers/integration/RankingIntegrationTest.java @@ -3,7 +3,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.data.Offset.offset; import static org.awaitility.Awaitility.await; - import java.math.BigDecimal; import java.time.Duration; import java.time.LocalDate; @@ -22,7 +21,6 @@ import com.loopers.cache.CacheKeyGenerator; import com.loopers.cache.RankingRedisService; import com.loopers.cache.dto.CachePayloads.RankingItem; -import com.loopers.config.redis.RedisConfig; import com.loopers.infrastructure.event.DomainEventEnvelope; import com.loopers.infrastructure.event.payloads.LikeActionPayloadV1; import com.loopers.infrastructure.event.payloads.PaymentSuccessPayloadV1; @@ -149,7 +147,7 @@ void shouldStorePaymentSuccessAsLogNormalizedScore() throws Exception { // Then - ๋กœ๊ทธ ์ •๊ทœํ™”๋œ ์ ์ˆ˜๊ฐ€ ์ ์žฌ๋˜์–ด์•ผ ํ•จ // Weight 0.6 * log(10001) โ‰ˆ 5.53 double expectedScore = 0.6 * Math.log(10001); - + await().atMost(Duration.ofSeconds(10)) .untilAsserted(() -> { RankingItem ranking = rankingRedisService.getProductRanking(today, productId); @@ -170,14 +168,14 @@ void shouldAccumulateScoresForSameProduct() throws Exception { ProductViewPayloadV1 viewPayload = new ProductViewPayloadV1(productId, 100L); DomainEventEnvelope viewEnvelope = new DomainEventEnvelope( "view-" + productId + "-" + i + "-" + baseTime, - "PRODUCT_VIEW", "v1", baseTime + i, + "PRODUCT_VIEW", "v1", baseTime + i, objectMapper.writeValueAsString(viewPayload) ); kafkaTemplate.send("catalog-events", viewEnvelope); } for (int i = 0; i < 2; i++) { - LikeActionPayloadV1 likePayload = new LikeActionPayloadV1(productId, (long)(100 + i), "LIKE"); + LikeActionPayloadV1 likePayload = new LikeActionPayloadV1(productId, (long) (100 + i), "LIKE"); DomainEventEnvelope likeEnvelope = new DomainEventEnvelope( "like-" + productId + "-" + i + "-" + baseTime, "LIKE_ACTION", "v1", baseTime + 10 + i, @@ -188,7 +186,7 @@ void shouldAccumulateScoresForSameProduct() throws Exception { // Then - ์ ์ˆ˜๊ฐ€ ๋ˆ„์ ๋˜์–ด์•ผ ํ•จ double expectedScore = 0.1 * 3 + 0.2 * 2; // 0.7 - + await().atMost(Duration.ofSeconds(15)) .untilAsserted(() -> { RankingItem ranking = rankingRedisService.getProductRanking(today, productId); @@ -241,7 +239,7 @@ void shouldReturnRankingsInOrder() throws Exception { .untilAsserted(() -> { List rankings = rankingRedisService.getRanking(today, 1, 10); assertThat(rankings).hasSizeGreaterThanOrEqualTo(3); - + // ์ฒซ ๋ฒˆ์งธ๊ฐ€ ๊ฐ€์žฅ ๋†’์€ ์ ์ˆ˜ (๊ฒฐ์ œ) assertThat(rankings.get(0).productId()).isEqualTo(product1); // ๋‘ ๋ฒˆ์งธ๊ฐ€ ์ค‘๊ฐ„ ์ ์ˆ˜ (์ข‹์•„์š”) @@ -262,7 +260,7 @@ void shouldCarryOverScoresToNextDay() { // Given - ์˜ค๋Š˜ ๋žญํ‚น ๋ฐ์ดํ„ฐ ์ง์ ‘ ์ถ”๊ฐ€ Long productId = 3001L; double originalScore = 100.0; - + redisTemplate.opsForZSet().add(todayRankingKey, productId.toString(), originalScore); LocalDate tomorrow = today.plusDays(1); @@ -274,7 +272,7 @@ void shouldCarryOverScoresToNextDay() { // Then assertThat(carryOverCount).isEqualTo(1); - + Double tomorrowScore = redisTemplate.opsForZSet().score(tomorrowKey, productId.toString()); assertThat(tomorrowScore).isNotNull(); assertThat(tomorrowScore).isCloseTo(10.0, offset(0.01)); // 100 * 0.1 @@ -289,7 +287,7 @@ void shouldSkipCarryOverWhenNoSourceData() { // Given LocalDate emptyDate = today.minusDays(10); LocalDate targetDate = emptyDate.plusDays(1); - + String emptyKey = cacheKeyGenerator.generateDailyRankingKey(emptyDate); redisTemplate.delete(emptyKey); // ํ™•์‹คํžˆ ๋น„์–ด์žˆ๊ฒŒ From 7218ff9383e0f20cc2623200946c4ad108706296 Mon Sep 17 00:00:00 2001 From: hyujikoh Date: Fri, 2 Jan 2026 17:50:04 +0900 Subject: [PATCH 77/85] =?UTF-8?q?refactor(ranking):=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=20=ED=8F=AC=EB=A7=B7=ED=8C=85=20=EB=B0=8F=20=EB=B6=88=ED=95=84?= =?UTF-8?q?=EC=9A=94=ED=95=9C=20=EC=9E=84=ED=8F=AC=ED=8A=B8=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ์ฝ”๋“œ ๊ฐ€๋…์„ฑ์„ ๋†’์ด๊ธฐ ์œ„ํ•ด ๋ถˆํ•„์š”ํ•œ ์ž„ํฌํŠธ ๋ฌธ์„ ์ œ๊ฑฐ - ์ฃผ์„ ๋ฐ ๊ณต๋ฐฑ ์ •๋ฆฌ๋กœ ์ฝ”๋“œ ์ผ๊ด€์„ฑ ํ–ฅ์ƒ --- .../job/ranking/support/ScoreCalculator.java | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/support/ScoreCalculator.java b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/support/ScoreCalculator.java index 50a76762e..6d9ffe447 100644 --- a/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/support/ScoreCalculator.java +++ b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/support/ScoreCalculator.java @@ -10,11 +10,10 @@ @Component public class ScoreCalculator { - // ๊ฐ€์ค‘์น˜ ์ƒ์ˆ˜ (Redis ZSET๊ณผ ๋™์ผํ•˜๊ฒŒ ์œ ์ง€) - private static final int VIEW_WEIGHT = 1; - private static final int LIKE_WEIGHT = 3; - private static final int SALES_WEIGHT = 5; - private static final int ORDER_WEIGHT = 2; + // CachePayloads์˜ EventType ๊ฐ€์ค‘์น˜์™€ ์ผ์น˜ํ•˜๋„๋ก ์กฐ์ • (๋น„์œจ ์œ ์ง€) + private static final double VIEW_WEIGHT = 0.1; + private static final double LIKE_WEIGHT = 0.2; + private static final double SALES_WEIGHT = 0.6; // ์ฃผ๋ฌธ(๊ฒฐ์ œ์„ฑ๊ณต) ๊ฐ€์ค‘์น˜ /** * ๋ฉ”ํŠธ๋ฆญ ๋ฐ์ดํ„ฐ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๋žญํ‚น ์ ์ˆ˜๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. @@ -26,17 +25,26 @@ public class ScoreCalculator { * @return ๊ณ„์‚ฐ๋œ ์ด ์ ์ˆ˜ */ public long calculate(long viewCount, long likeCount, long salesCount, long orderCount) { - return viewCount * VIEW_WEIGHT - + likeCount * LIKE_WEIGHT - + salesCount * SALES_WEIGHT - + orderCount * ORDER_WEIGHT; + // 1. ์กฐํšŒ์™€ ์ข‹์•„์š”๋Š” ๋‹จ์ˆœ ์ˆ˜๋Ÿ‰ ๊ธฐ๋ฐ˜ ๊ฐ€์ค‘์น˜ ์ ์šฉ + double viewScore = viewCount * VIEW_WEIGHT; + double likeScore = likeCount * LIKE_WEIGHT; + + // 2. ํŒ๋งค๋Ÿ‰(Sales)์€ CachePayloads.forPaymentSuccess์™€ ๋™์ผํ•˜๊ฒŒ ๋กœ๊ทธ ์ •๊ทœํ™” ์ ์šฉ + // ๋ฐฐ์น˜์—์„œ๋Š” ์ด๋ฏธ ์ง‘๊ณ„๋œ salesCount(์ˆ˜๋Ÿ‰)๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•˜๋ฏ€๋กœ, + // ๋งŒ์•ฝ ๊ธˆ์•ก ๊ธฐ๋ฐ˜ ์ •๊ทœํ™”๊ฐ€ ํ•„์š”ํ•˜๋‹ค๋ฉด ๋งค๊ฐœ๋ณ€์ˆ˜๋กœ ์ด์•ก์„ ๋ฐ›์•„์•ผ ํ•˜์ง€๋งŒ, + // ์ˆ˜๋Ÿ‰ ๊ธฐ๋ฐ˜์œผ๋กœ ๋กœ๊ทธ ์ •๊ทœํ™”๋ฅผ ์ ์šฉํ•œ๋‹ค๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ด ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค. + double normalizedSalesScore = Math.log1p(salesCount) * SALES_WEIGHT; + + // 3. ์ตœ์ข… ์ ์ˆ˜ ๊ณ„์‚ฐ (์†Œ์ˆ˜์  ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•ด ์ ์ ˆํ•œ ์Šค์ผ€์ผ ๊ณฑ์‚ฐ ํ›„ long ๋ณ€ํ™˜) + // Redis ZSET์˜ score๊ฐ€ double์ž„์„ ๊ฐ์•ˆํ•˜์—ฌ ์ •๋ฐ€๋„๋ฅผ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค. + return (long) ((viewScore + likeScore + normalizedSalesScore) * 1000); } /** * ๊ฐ€์ค‘์น˜ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. (ํ…Œ์ŠคํŠธ ๋ฐ ๋””๋ฒ„๊น…์šฉ) */ public String getWeightInfo() { - return String.format("VIEW=%d, LIKE=%d, SALES=%d, ORDER=%d", - VIEW_WEIGHT, LIKE_WEIGHT, SALES_WEIGHT, ORDER_WEIGHT); + return String.format("VIEW=%f, LIKE=%f, SALES=%f,", + VIEW_WEIGHT, LIKE_WEIGHT, SALES_WEIGHT); } } From 58639157a0d0a1f2cf01b22eb2895fc0e70978c6 Mon Sep 17 00:00:00 2001 From: hyujikoh Date: Fri, 2 Jan 2026 22:26:12 +0900 Subject: [PATCH 78/85] =?UTF-8?q?feat(ranking):=20=EC=A3=BC=EA=B0=84=20?= =?UTF-8?q?=EB=9E=AD=ED=82=B9=20=EC=A1=B0=ED=9A=8C=20API=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=95=20=EC=B2=98=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ์ฃผ๊ฐ„ ๋žญํ‚น ์กฐํšŒ ์‹œ ํŽ˜์ด์ง• ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•˜์—ฌ ์„ฑ๋Šฅ ๊ฐœ์„  - ๊ด€๋ จ ๋ฉ”์„œ๋“œ ๋ฐ ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ์ˆ˜์ • --- .../ranking/WeeklyRankingService.java | 19 +++---------------- .../ranking/WeeklyRankJpaRepository.java | 3 ++- .../ranking/WeeklyRankRepositoryImpl.java | 11 +++++------ .../api/ranking/RankingApiE2ETest.java | 4 ++-- .../ranking/WeeklyRankJpaRepository.java | 3 ++- .../ranking/WeeklyRankRepositoryImpl.java | 10 +++++----- .../domain/ranking/WeeklyRankRepository.java | 10 +++++----- 7 files changed, 24 insertions(+), 36 deletions(-) diff --git a/apps/commerce-api/src/main/java/com/loopers/application/ranking/WeeklyRankingService.java b/apps/commerce-api/src/main/java/com/loopers/application/ranking/WeeklyRankingService.java index 345e8c6a4..27c2c24d1 100644 --- a/apps/commerce-api/src/main/java/com/loopers/application/ranking/WeeklyRankingService.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/ranking/WeeklyRankingService.java @@ -37,27 +37,14 @@ public Page getWeeklyRanking(String yearWeek, Pageable pageabl yearWeek, pageable.getPageNumber(), pageable.getPageSize()); // 1. ์ „์ฒด ๋žญํ‚น ์กฐํšŒ (์ˆœ์œ„ ์ˆœ์œผ๋กœ ์ •๋ ฌ๋จ) - List allRankings = weeklyRankRepository.findByYearWeek(yearWeek); + Page pagedRankings = weeklyRankRepository.findByYearWeek(yearWeek , pageable); - if (allRankings.isEmpty()) { + if (pagedRankings.isEmpty()) { log.debug("์ฃผ๊ฐ„ ๋žญํ‚น ๋ฐ์ดํ„ฐ ์—†์Œ: yearWeek={}", yearWeek); return Page.empty(pageable); } - // 2. ํŽ˜์ด์ง• ์ฒ˜๋ฆฌ - int start = (int) pageable.getOffset(); - int end = Math.min(start + pageable.getPageSize(), allRankings.size()); - - if (start >= allRankings.size()) { - return Page.empty(pageable); - } - - List pagedRankings = allRankings.subList(start, end); - - log.debug("์ฃผ๊ฐ„ ๋žญํ‚น ์กฐํšŒ ์™„๋ฃŒ: yearWeek={}, ์ „์ฒด={}, ํŽ˜์ด์ง€={}", - yearWeek, allRankings.size(), pagedRankings.size()); - - return new PageImpl<>(pagedRankings, pageable, allRankings.size()); + return pagedRankings; } /** diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/WeeklyRankJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/WeeklyRankJpaRepository.java index 3df5fbaa2..be16adc99 100644 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/WeeklyRankJpaRepository.java +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/WeeklyRankJpaRepository.java @@ -2,6 +2,7 @@ import java.util.List; +import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; @@ -26,7 +27,7 @@ public interface WeeklyRankJpaRepository extends JpaRepository findByIdYearWeekOrderByRankPosition(@Param("yearWeek") String yearWeek, Pageable pageable); + Page findByIdYearWeekOrderByRankPosition(@Param("yearWeek") String yearWeek, Pageable pageable); /** * ํŠน์ • ์ฃผ์ฐจ์˜ ๋ชจ๋“  ๋žญํ‚น์„ ์‚ญ์ œํ•ฉ๋‹ˆ๋‹ค. diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/WeeklyRankRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/WeeklyRankRepositoryImpl.java index 3301cf85a..9b3d2d221 100644 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/WeeklyRankRepositoryImpl.java +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/WeeklyRankRepositoryImpl.java @@ -2,6 +2,7 @@ import java.util.List; +import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Repository; @@ -34,15 +35,13 @@ public List saveAll(List entities) { public List findByYearWeek(String yearWeek) { return jpaRepository.findByIdYearWeekOrderByRankPosition(yearWeek); } - @Override - public List findByYearWeekWithPagination(String yearWeek, int page, int size) { - Pageable pageable = PageRequest.of(page, size); - return jpaRepository.findByIdYearWeekOrderByRankPosition(yearWeek, pageable); + public long deleteByYearWeek(String yearWeek) { + return jpaRepository.deleteByIdYearWeek(yearWeek); } @Override - public long deleteByYearWeek(String yearWeek) { - return jpaRepository.deleteByIdYearWeek(yearWeek); + public Page findByYearWeek(String yearWeek, Pageable pageable) { + return jpaRepository.findByIdYearWeekOrderByRankPosition(yearWeek, pageable); } } diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/ranking/RankingApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/ranking/RankingApiE2ETest.java index 2a052f036..718cbb74f 100644 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/ranking/RankingApiE2ETest.java +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/ranking/RankingApiE2ETest.java @@ -150,8 +150,8 @@ void should_return_products_in_ranking_order() { () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK), () -> assertThat(Objects.requireNonNull(response.getBody()).data().content()).hasSize(3), () -> assertThat(Objects.requireNonNull(response.getBody()).data().content().get(0).productId()).isEqualTo(product1), - () -> assertThat(Objects.requireNonNull(response.getBody()).data().content().get(2).productId()).isEqualTo(product2), - () -> assertThat(Objects.requireNonNull(response.getBody()).data().content().get(1).productId()).isEqualTo(product3) + () -> assertThat(Objects.requireNonNull(response.getBody()).data().content().get(1).productId()).isEqualTo(product3), + () -> assertThat(Objects.requireNonNull(response.getBody()).data().content().get(2).productId()).isEqualTo(product2) ); } diff --git a/apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/WeeklyRankJpaRepository.java b/apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/WeeklyRankJpaRepository.java index aab645ed1..3dceb4bf7 100644 --- a/apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/WeeklyRankJpaRepository.java +++ b/apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/WeeklyRankJpaRepository.java @@ -2,6 +2,7 @@ import java.util.List; +import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; @@ -26,7 +27,7 @@ public interface WeeklyRankJpaRepository extends JpaRepository findByIdYearWeekOrderByRankPosition(@Param("yearWeek") String yearWeek, Pageable pageable); + Page findByIdYearWeekOrderByRankPosition(@Param("yearWeek") String yearWeek, Pageable pageable); /** * ํŠน์ • ์ฃผ์ฐจ์˜ ๋ชจ๋“  ๋žญํ‚น์„ ์‚ญ์ œํ•ฉ๋‹ˆ๋‹ค. diff --git a/apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/WeeklyRankRepositoryImpl.java b/apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/WeeklyRankRepositoryImpl.java index b938e2c1f..5626d881e 100644 --- a/apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/WeeklyRankRepositoryImpl.java +++ b/apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/WeeklyRankRepositoryImpl.java @@ -2,6 +2,7 @@ import java.util.List; +import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Repository; @@ -36,13 +37,12 @@ public List findByYearWeek(String yearWeek) { } @Override - public List findByYearWeekWithPagination(String yearWeek, int page, int size) { - Pageable pageable = PageRequest.of(page, size); - return jpaRepository.findByIdYearWeekOrderByRankPosition(yearWeek, pageable); + public long deleteByYearWeek(String yearWeek) { + return jpaRepository.deleteByIdYearWeek(yearWeek); } @Override - public long deleteByYearWeek(String yearWeek) { - return jpaRepository.deleteByIdYearWeek(yearWeek); + public Page findByYearWeek(String yearWeek, Pageable pageable) { + return jpaRepository.findByIdYearWeekOrderByRankPosition(yearWeek, pageable); } } diff --git a/modules/jpa/src/main/java/com/loopers/domain/ranking/WeeklyRankRepository.java b/modules/jpa/src/main/java/com/loopers/domain/ranking/WeeklyRankRepository.java index 07d5a04b0..cc5ab6b07 100644 --- a/modules/jpa/src/main/java/com/loopers/domain/ranking/WeeklyRankRepository.java +++ b/modules/jpa/src/main/java/com/loopers/domain/ranking/WeeklyRankRepository.java @@ -2,6 +2,9 @@ import java.util.List; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + /** * ์ฃผ๊ฐ„ ๋žญํ‚น Repository ์ธํ„ฐํŽ˜์ด์Šค */ @@ -22,11 +25,6 @@ public interface WeeklyRankRepository { */ List findByYearWeek(String yearWeek); - /** - * ํŠน์ • ์ฃผ์ฐจ์˜ ๋žญํ‚น์„ ํŽ˜์ด์ง€๋„ค์ด์…˜ํ•˜์—ฌ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. - */ - List findByYearWeekWithPagination(String yearWeek, int page, int size); - /** * ํŠน์ • ์ฃผ์ฐจ์˜ ๋ชจ๋“  ๋žญํ‚น์„ ์‚ญ์ œํ•ฉ๋‹ˆ๋‹ค. (๋ฉฑ๋“ฑ์„ฑ ๋ณด์žฅ์šฉ) * @@ -34,4 +32,6 @@ public interface WeeklyRankRepository { * @return ์‚ญ์ œ๋œ ๋ ˆ์ฝ”๋“œ ์ˆ˜ */ long deleteByYearWeek(String yearWeek); + + Page findByYearWeek(String yearWeek, Pageable pageable); } From 6aee58a189026d38919a652f62c5c5a955427bbf Mon Sep 17 00:00:00 2001 From: hyujikoh Date: Fri, 2 Jan 2026 22:27:15 +0900 Subject: [PATCH 79/85] =?UTF-8?q?refactor(ranking):=20=EC=A3=BC=EC=B0=A8?= =?UTF-8?q?=20=EA=B3=84=EC=82=B0=20=EB=A1=9C=EC=A7=81=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ISO ์ฃผ์ฐจ ๊ธฐ์ค€์œผ๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ์ผ๊ด€์„ฑ ์žˆ๋Š” ์ฃผ์ฐจ ๊ณ„์‚ฐ ๋ณด์žฅ --- .../loopers/interfaces/api/ranking/RankingV1Controller.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java index d1d841b99..37c0ba8dd 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java @@ -77,9 +77,9 @@ private String processYearWeekParameter(String yearWeek, LocalDate date) { // date๊ฐ€ ์žˆ์œผ๋ฉด ํ•ด๋‹น ๋‚ ์งœ์˜ ์ฃผ์ฐจ, ์—†์œผ๋ฉด ํ˜„์žฌ ์ฃผ์ฐจ LocalDate targetDate = date != null ? date : LocalDate.now(); - WeekFields weekFields = WeekFields.of(Locale.getDefault()); + WeekFields weekFields = WeekFields.ISO; int year = targetDate.getYear(); - int week = targetDate.get(weekFields.weekOfYear()); + int week = targetDate.get(weekFields.weekOfWeekBasedYear()); return String.format("%d-W%02d", year, week); } From 2be7a2f12d3c80e557ce8ecface967bc5bdf32d2 Mon Sep 17 00:00:00 2001 From: hyujikoh Date: Fri, 2 Jan 2026 22:32:07 +0900 Subject: [PATCH 80/85] =?UTF-8?q?feat(ranking):=20=EB=A9=94=ED=8A=B8?= =?UTF-8?q?=EB=A6=AD=20=EC=A7=91=EA=B3=84=20=EA=B3=B5=ED=86=B5=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=B6=94=EC=83=81=20=ED=81=B4=EB=9E=98=EC=8A=A4=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 - ๋ฉ”ํŠธ๋ฆญ Reader ๊ณตํ†ต ์ถ”์ƒ ํด๋ž˜์Šค์ธ AbstractMetricsReader ๊ตฌํ˜„ - ์›”๊ฐ„ ๋ฐ ์ฃผ๊ฐ„ ๋ฉ”ํŠธ๋ฆญ Reader์—์„œ ์ƒ์†ํ•˜์—ฌ ์ฝ”๋“œ ์ค‘๋ณต ์ œ๊ฑฐ - ๋žญํ‚น ์ง‘๊ณ„ ๋กœ์ง์„ ํ†ตํ•ฉํ•˜์—ฌ ์œ ์ง€๋ณด์ˆ˜์„ฑ ํ–ฅ์ƒ --- .../ranking/reader/AbstractMetricsReader.java | 83 +++++++++++++++++++ .../ranking/reader/MonthlyMetricsReader.java | 59 ++++--------- .../ranking/reader/WeeklyMetricsReader.java | 59 ++++--------- 3 files changed, 119 insertions(+), 82 deletions(-) create mode 100644 apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/reader/AbstractMetricsReader.java diff --git a/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/reader/AbstractMetricsReader.java b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/reader/AbstractMetricsReader.java new file mode 100644 index 000000000..f0da39d5f --- /dev/null +++ b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/reader/AbstractMetricsReader.java @@ -0,0 +1,83 @@ +package com.loopers.batch.job.ranking.reader; + +import java.time.LocalDate; +import java.util.Iterator; +import java.util.List; + +import org.springframework.batch.item.ItemReader; + +import com.loopers.batch.job.ranking.dto.RankingAggregation; +import com.loopers.batch.job.ranking.support.RankingAggregator; +import com.loopers.domain.metrics.ProductMetricsRepository; + +import lombok.extern.slf4j.Slf4j; + +/** + * ๋ฉ”ํŠธ๋ฆญ Reader ๊ณตํ†ต ์ถ”์ƒ ํด๋ž˜์Šค + * - ํŠน์ • ๊ธฐ๊ฐ„์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์ง‘๊ณ„ํ•˜๊ณ  ๋žญํ‚น์„ ์ƒ์„ฑํ•˜๋Š” ๊ณตํ†ต ๋กœ์ง์„ ํฌํ•จ + */ +@Slf4j +public abstract class AbstractMetricsReader implements ItemReader { + + protected final ProductMetricsRepository productMetricsRepository; + protected final RankingAggregator rankingAggregator; + + private Iterator iterator; + + protected AbstractMetricsReader(ProductMetricsRepository productMetricsRepository, RankingAggregator rankingAggregator) { + this.productMetricsRepository = productMetricsRepository; + this.rankingAggregator = rankingAggregator; + } + + @Override + public RankingAggregation read() throws Exception { + if (iterator == null) { + initializeIterator(); + } + + return iterator.hasNext() ? iterator.next() : null; + } + + private void initializeIterator() { + String logIdentifier = getLogIdentifier(); + log.info("{} ๋žญํ‚น ์ง‘๊ณ„ ์‹œ์ž‘: parameter={}", logIdentifier, getParameterValue()); + + try { + // 1. ๊ธฐ๊ฐ„ ํŒŒ์‹ฑ (์ถ”์ƒ ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ) + LocalDate[] dateRange = parseDateRange(); + LocalDate startDate = dateRange[0]; + LocalDate endDate = dateRange[1]; + + log.info("์ง‘๊ณ„ ๊ธฐ๊ฐ„: {} ~ {}", startDate, endDate); + + // 2. DB์—์„œ ์ง‘๊ณ„ ์ฟผ๋ฆฌ ์‹คํ–‰ + List aggregationResults = productMetricsRepository.aggregateByDateRange(startDate, endDate); + log.info("์ง‘๊ณ„ ๋Œ€์ƒ ์ƒํ’ˆ ์ˆ˜: {}", aggregationResults.size()); + + // 3. ๋žญํ‚น ์ฒ˜๋ฆฌ (์ •๋ ฌ + TOP 100 + ์ˆœ์œ„ ๋ถ€์—ฌ) + List rankings = rankingAggregator.processRankings(aggregationResults); + log.info("์ƒ์„ฑ๋œ ๋žญํ‚น ์ˆ˜: {}", rankings.size()); + + iterator = rankings.iterator(); + + } catch (Exception e) { + log.error("{} ๋žญํ‚น ์ง‘๊ณ„ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: parameter={}", logIdentifier, getParameterValue(), e); + throw new RuntimeException(logIdentifier + " ๋žญํ‚น ์ง‘๊ณ„ ์‹คํŒจ", e); + } + } + + /** + * ๊ธฐ๊ฐ„์— ํ•ด๋‹นํ•˜๋Š” LocalDate ๋ฒ”์œ„๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + protected abstract LocalDate[] parseDateRange(); + + /** + * ๋กœ๊ทธ ์‹๋ณ„์ž๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. (์˜ˆ: "์›”๊ฐ„", "์ฃผ๊ฐ„") + */ + protected abstract String getLogIdentifier(); + + /** + * ํ˜„์žฌ ์‚ฌ์šฉ ์ค‘์ธ ํŒŒ๋ผ๋ฏธํ„ฐ ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + protected abstract String getParameterValue(); +} diff --git a/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/reader/MonthlyMetricsReader.java b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/reader/MonthlyMetricsReader.java index ebba661d5..4486ffc1b 100644 --- a/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/reader/MonthlyMetricsReader.java +++ b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/reader/MonthlyMetricsReader.java @@ -1,20 +1,15 @@ package com.loopers.batch.job.ranking.reader; import java.time.LocalDate; -import java.util.Iterator; -import java.util.List; import org.springframework.batch.core.configuration.annotation.StepScope; -import org.springframework.batch.item.ItemReader; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; -import com.loopers.batch.job.ranking.dto.RankingAggregation; import com.loopers.batch.job.ranking.support.DateRangeParser; import com.loopers.batch.job.ranking.support.RankingAggregator; import com.loopers.domain.metrics.ProductMetricsRepository; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; /** @@ -25,51 +20,33 @@ @Slf4j @StepScope @Component -@RequiredArgsConstructor -public class MonthlyMetricsReader implements ItemReader { +public class MonthlyMetricsReader extends AbstractMetricsReader { - private final ProductMetricsRepository productMetricsRepository; private final DateRangeParser dateRangeParser; - private final RankingAggregator rankingAggregator; - - private Iterator iterator; @Value("#{jobParameters['yearMonth']}") private String yearMonth; // e.g., "2024-12" - @Override - public RankingAggregation read() throws Exception { - if (iterator == null) { - initializeIterator(); - } - - return iterator.hasNext() ? iterator.next() : null; + public MonthlyMetricsReader( + ProductMetricsRepository productMetricsRepository, + RankingAggregator rankingAggregator, + DateRangeParser dateRangeParser) { + super(productMetricsRepository, rankingAggregator); + this.dateRangeParser = dateRangeParser; } - private void initializeIterator() { - log.info("์›”๊ฐ„ ๋žญํ‚น ์ง‘๊ณ„ ์‹œ์ž‘: yearMonth={}", yearMonth); - - try { - // 1. ์›” โ†’ ๋‚ ์งœ ๋ฒ”์œ„ ๋ณ€ํ™˜ - LocalDate[] dateRange = dateRangeParser.parseYearMonth(yearMonth); - LocalDate startDate = dateRange[0]; - LocalDate endDate = dateRange[1]; - - log.info("์ง‘๊ณ„ ๊ธฐ๊ฐ„: {} ~ {}", startDate, endDate); - - // 2. DB์—์„œ ์ง‘๊ณ„ ์ฟผ๋ฆฌ ์‹คํ–‰ - List aggregationResults = productMetricsRepository.aggregateByDateRange(startDate, endDate); - log.info("์ง‘๊ณ„ ๋Œ€์ƒ ์ƒํ’ˆ ์ˆ˜: {}", aggregationResults.size()); - - // 3. ๋žญํ‚น ์ฒ˜๋ฆฌ (์ •๋ ฌ + TOP 100 + ์ˆœ์œ„ ๋ถ€์—ฌ) - List rankings = rankingAggregator.processRankings(aggregationResults); - log.info("์ƒ์„ฑ๋œ ๋žญํ‚น ์ˆ˜: {}", rankings.size()); + @Override + protected LocalDate[] parseDateRange() { + return dateRangeParser.parseYearMonth(yearMonth); + } - iterator = rankings.iterator(); + @Override + protected String getLogIdentifier() { + return "์›”๊ฐ„"; + } - } catch (Exception e) { - log.error("์›”๊ฐ„ ๋žญํ‚น ์ง‘๊ณ„ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: yearMonth={}", yearMonth, e); - throw new RuntimeException("์›”๊ฐ„ ๋žญํ‚น ์ง‘๊ณ„ ์‹คํŒจ", e); - } + @Override + protected String getParameterValue() { + return yearMonth; } } diff --git a/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/reader/WeeklyMetricsReader.java b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/reader/WeeklyMetricsReader.java index 7394f3e31..f712214ee 100644 --- a/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/reader/WeeklyMetricsReader.java +++ b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/reader/WeeklyMetricsReader.java @@ -1,20 +1,15 @@ package com.loopers.batch.job.ranking.reader; import java.time.LocalDate; -import java.util.Iterator; -import java.util.List; import org.springframework.batch.core.configuration.annotation.StepScope; -import org.springframework.batch.item.ItemReader; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; -import com.loopers.batch.job.ranking.dto.RankingAggregation; import com.loopers.batch.job.ranking.support.DateRangeParser; import com.loopers.batch.job.ranking.support.RankingAggregator; import com.loopers.domain.metrics.ProductMetricsRepository; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; /** @@ -25,51 +20,33 @@ @Slf4j @StepScope @Component -@RequiredArgsConstructor -public class WeeklyMetricsReader implements ItemReader { +public class WeeklyMetricsReader extends AbstractMetricsReader { - private final ProductMetricsRepository productMetricsRepository; private final DateRangeParser dateRangeParser; - private final RankingAggregator rankingAggregator; - - private Iterator iterator; @Value("#{jobParameters['yearWeek']}") private String yearWeek; // e.g., "2024-W52" - @Override - public RankingAggregation read() throws Exception { - if (iterator == null) { - initializeIterator(); - } - - return iterator.hasNext() ? iterator.next() : null; + public WeeklyMetricsReader( + ProductMetricsRepository productMetricsRepository, + RankingAggregator rankingAggregator, + DateRangeParser dateRangeParser) { + super(productMetricsRepository, rankingAggregator); + this.dateRangeParser = dateRangeParser; } - private void initializeIterator() { - log.info("์ฃผ๊ฐ„ ๋žญํ‚น ์ง‘๊ณ„ ์‹œ์ž‘: yearWeek={}", yearWeek); - - try { - // 1. ์ฃผ์ฐจ โ†’ ๋‚ ์งœ ๋ฒ”์œ„ ๋ณ€ํ™˜ - LocalDate[] dateRange = dateRangeParser.parseYearWeek(yearWeek); - LocalDate startDate = dateRange[0]; - LocalDate endDate = dateRange[1]; - - log.info("์ง‘๊ณ„ ๊ธฐ๊ฐ„: {} ~ {}", startDate, endDate); - - // 2. DB์—์„œ ์ง‘๊ณ„ ์ฟผ๋ฆฌ ์‹คํ–‰ - List aggregationResults = productMetricsRepository.aggregateByDateRange(startDate, endDate); - log.info("์ง‘๊ณ„ ๋Œ€์ƒ ์ƒํ’ˆ ์ˆ˜: {}", aggregationResults.size()); - - // 3. ๋žญํ‚น ์ฒ˜๋ฆฌ (์ •๋ ฌ + TOP 100 + ์ˆœ์œ„ ๋ถ€์—ฌ) - List rankings = rankingAggregator.processRankings(aggregationResults); - log.info("์ƒ์„ฑ๋œ ๋žญํ‚น ์ˆ˜: {}", rankings.size()); + @Override + protected LocalDate[] parseDateRange() { + return dateRangeParser.parseYearWeek(yearWeek); + } - iterator = rankings.iterator(); + @Override + protected String getLogIdentifier() { + return "์ฃผ๊ฐ„"; + } - } catch (Exception e) { - log.error("์ฃผ๊ฐ„ ๋žญํ‚น ์ง‘๊ณ„ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: yearWeek={}", yearWeek, e); - throw new RuntimeException("์ฃผ๊ฐ„ ๋žญํ‚น ์ง‘๊ณ„ ์‹คํŒจ", e); - } + @Override + protected String getParameterValue() { + return yearWeek; } } From c02335e91335ea1cd7f528fd22f83649cb26f514 Mon Sep 17 00:00:00 2001 From: hyujikoh Date: Fri, 2 Jan 2026 22:50:36 +0900 Subject: [PATCH 81/85] =?UTF-8?q?test(ranking):=20=EC=A0=90=EC=88=98=20?= =?UTF-8?q?=EA=B3=84=EC=82=B0=20=EB=A1=9C=EC=A7=81=20=EB=B0=8F=20=EB=8B=A8?= =?UTF-8?q?=EC=9C=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ์ ์ˆ˜ ๊ณ„์‚ฐ ๋ฐฉ์‹ ๋ณ€๊ฒฝ์— ๋”ฐ๋ฅธ ํ…Œ์ŠคํŠธ ๊ฐ’ ์—…๋ฐ์ดํŠธ - ๊ธฐ์กด ํ…Œ์ŠคํŠธ์—์„œ ์‚ฌ์šฉ๋œ ์ ์ˆ˜ ๊ฐ’ ์ˆ˜์ • --- .../job/ranking/support/ScoreCalculator.java | 2 +- .../dto/RankingAggregationUnitTest.java | 4 +-- .../support/RankingAggregatorUnitTest.java | 12 ++++----- .../support/ScoreCalculatorUnitTest.java | 26 +++++-------------- 4 files changed, 15 insertions(+), 29 deletions(-) diff --git a/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/support/ScoreCalculator.java b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/support/ScoreCalculator.java index 6d9ffe447..980635a41 100644 --- a/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/support/ScoreCalculator.java +++ b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/support/ScoreCalculator.java @@ -37,7 +37,7 @@ public long calculate(long viewCount, long likeCount, long salesCount, long orde // 3. ์ตœ์ข… ์ ์ˆ˜ ๊ณ„์‚ฐ (์†Œ์ˆ˜์  ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•ด ์ ์ ˆํ•œ ์Šค์ผ€์ผ ๊ณฑ์‚ฐ ํ›„ long ๋ณ€ํ™˜) // Redis ZSET์˜ score๊ฐ€ double์ž„์„ ๊ฐ์•ˆํ•˜์—ฌ ์ •๋ฐ€๋„๋ฅผ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค. - return (long) ((viewScore + likeScore + normalizedSalesScore) * 1000); + return (long) ((viewScore + likeScore + normalizedSalesScore) * 10); } /** diff --git a/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/dto/RankingAggregationUnitTest.java b/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/dto/RankingAggregationUnitTest.java index 63cfa3ebb..242373de9 100644 --- a/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/dto/RankingAggregationUnitTest.java +++ b/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/dto/RankingAggregationUnitTest.java @@ -32,7 +32,7 @@ void should_create_from_valid_aggregation_result() { Assertions.assertThat(aggregation.getSalesCount()).isEqualTo(10L); Assertions.assertThat(aggregation.getOrderCount()).isEqualTo(5L); // score = 100*1 + 50*3 + 10*5 + 5*2 = 310 - Assertions.assertThat(aggregation.getTotalScore()).isEqualTo(310L); + Assertions.assertThat(aggregation.getTotalScore()).isEqualTo(214L); Assertions.assertThat(aggregation.getRankPosition()).isEqualTo(0); // ์ดˆ๊ธฐ๊ฐ’ } @@ -166,7 +166,7 @@ void should_return_correct_string_format() { // then Assertions.assertThat(result).contains("productId=1"); - Assertions.assertThat(result).contains("score=310"); + Assertions.assertThat(result).contains("score=214"); Assertions.assertThat(result).contains("rank=1"); } } diff --git a/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/support/RankingAggregatorUnitTest.java b/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/support/RankingAggregatorUnitTest.java index 0d980dd50..dbca864ed 100644 --- a/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/support/RankingAggregatorUnitTest.java +++ b/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/support/RankingAggregatorUnitTest.java @@ -25,9 +25,9 @@ class ๋žญํ‚น_์ฒ˜๋ฆฌ { void should_sort_by_score_and_assign_ranks() { // given List results = List.of( - new Object[]{1L, 100L, 10L, 5L, 2L}, // score = 100 + 30 + 25 + 4 = 159 - new Object[]{2L, 200L, 20L, 10L, 4L}, // score = 200 + 60 + 50 + 8 = 318 - new Object[]{3L, 50L, 5L, 2L, 1L} // score = 50 + 15 + 10 + 2 = 77 + new Object[]{1L, 100L, 10L, 5L, 2L}, + new Object[]{2L, 200L, 20L, 10L, 4L}, + new Object[]{3L, 50L, 5L, 2L, 1L} ); // when @@ -39,15 +39,15 @@ void should_sort_by_score_and_assign_ranks() { // ์ ์ˆ˜ ๊ธฐ์ค€ ๋‚ด๋ฆผ์ฐจ์ˆœ ์ •๋ ฌ ํ™•์ธ Assertions.assertThat(rankings.get(0).getProductId()).isEqualTo(2L); // 1์œ„ Assertions.assertThat(rankings.get(0).getRankPosition()).isEqualTo(1); - Assertions.assertThat(rankings.get(0).getTotalScore()).isEqualTo(318L); + Assertions.assertThat(rankings.get(0).getTotalScore()).isEqualTo(254L); Assertions.assertThat(rankings.get(1).getProductId()).isEqualTo(1L); // 2์œ„ Assertions.assertThat(rankings.get(1).getRankPosition()).isEqualTo(2); - Assertions.assertThat(rankings.get(1).getTotalScore()).isEqualTo(159L); + Assertions.assertThat(rankings.get(1).getTotalScore()).isEqualTo(130L); Assertions.assertThat(rankings.get(2).getProductId()).isEqualTo(3L); // 3์œ„ Assertions.assertThat(rankings.get(2).getRankPosition()).isEqualTo(3); - Assertions.assertThat(rankings.get(2).getTotalScore()).isEqualTo(77L); + Assertions.assertThat(rankings.get(2).getTotalScore()).isEqualTo(66L); } @Test diff --git a/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/support/ScoreCalculatorUnitTest.java b/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/support/ScoreCalculatorUnitTest.java index 9612e6278..ed89385af 100644 --- a/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/support/ScoreCalculatorUnitTest.java +++ b/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/support/ScoreCalculatorUnitTest.java @@ -24,8 +24,7 @@ void should_calculate_score_with_correct_weights() { long score = calculator.calculate(viewCount, likeCount, salesCount, orderCount); // then - // score = 100*1 + 50*3 + 10*5 + 5*2 = 100 + 150 + 50 + 10 = 310 - Assertions.assertThat(score).isEqualTo(310L); + Assertions.assertThat(score).isEqualTo(214L); } @Test @@ -50,18 +49,6 @@ void should_have_highest_weight_for_sales_count() { // when & then Assertions.assertThat(singleSale).isGreaterThan(singleView); Assertions.assertThat(singleSale).isGreaterThan(singleLike); - Assertions.assertThat(singleSale).isGreaterThan(singleOrder); - } - - @Test - @DisplayName("์ข‹์•„์š”๊ฐ€ ์กฐํšŒ์ˆ˜๋ณด๋‹ค ๋†’์€ ๊ฐ€์ค‘์น˜๋ฅผ ๊ฐ€์ง„๋‹ค") - void should_have_higher_weight_for_like_than_view() { - // given - long singleLike = calculator.calculate(0, 1, 0, 0); - long singleView = calculator.calculate(1, 0, 0, 0); - - // when & then - Assertions.assertThat(singleLike).isGreaterThan(singleView); } @Test @@ -77,7 +64,7 @@ void should_calculate_correctly_with_large_numbers() { long score = calculator.calculate(viewCount, likeCount, salesCount, orderCount); // then - long expected = 1_000_000L * 1 + 500_000L * 3 + 100_000L * 5 + 50_000L * 2; + long expected = (long) (((1_000_000L * 0.1) + (500_000L * 0.2) + Math.log1p(100_000L) *0.6 ) * 10); Assertions.assertThat(score).isEqualTo(expected); } } @@ -93,10 +80,9 @@ void should_return_weight_info_in_correct_format() { String weightInfo = calculator.getWeightInfo(); // then - Assertions.assertThat(weightInfo).contains("VIEW=1"); - Assertions.assertThat(weightInfo).contains("LIKE=3"); - Assertions.assertThat(weightInfo).contains("SALES=5"); - Assertions.assertThat(weightInfo).contains("ORDER=2"); + Assertions.assertThat(weightInfo).contains("VIEW=0.1"); + Assertions.assertThat(weightInfo).contains("LIKE=0.2"); + Assertions.assertThat(weightInfo).contains("SALES=0.6"); } } -} \ No newline at end of file +} From 9c262257bfcc0f8a918c2e71be14ff0ab5ce9a52 Mon Sep 17 00:00:00 2001 From: hyujikoh Date: Sat, 3 Jan 2026 00:11:36 +0900 Subject: [PATCH 82/85] =?UTF-8?q?feat(ranking):=20=ED=8C=90=EB=A7=A4?= =?UTF-8?q?=EB=9F=89=20=EB=B0=8F=20=EC=B4=9D=20=ED=8C=90=EB=A7=A4=20?= =?UTF-8?q?=EA=B8=88=EC=95=A1=20=EC=A7=91=EA=B3=84=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ํŒ๋งค๋Ÿ‰ ์ฆ๊ฐ€ ๋ฉ”์„œ๋“œ์— ์ด ํŒ๋งค ๊ธˆ์•ก ๋งค๊ฐœ๋ณ€์ˆ˜ ์ถ”๊ฐ€ - ์›”๊ฐ„ ๋žญํ‚น ์กฐํšŒ API์— ํŽ˜์ด์ง• ์ฒ˜๋ฆฌ ์ ์šฉ - ๊ด€๋ จ๋œ ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ๋ฐ ์„œ๋น„์Šค ๋ฉ”์„œ๋“œ ์ˆ˜์ • --- .../ranking/MonthlyRankingService.java | 31 ++-------------- .../ranking/MonthlyRankJpaRepository.java | 3 +- .../ranking/MonthlyRankRepositoryImpl.java | 9 ++--- .../com/loopers/fixtures/UserTestFixture.java | 7 ++-- .../job/ranking/dto/RankingAggregation.java | 13 ++++--- .../job/ranking/support/ScoreCalculator.java | 18 +++++----- .../metrics/ProductMetricsJpaRepository.java | 3 +- .../ranking/MonthlyRankJpaRepository.java | 9 ++--- .../ranking/MonthlyRankRepositoryImpl.java | 9 ++--- .../dto/RankingAggregationUnitTest.java | 25 ++++++------- .../support/RankingAggregatorUnitTest.java | 21 +++++------ .../support/ScoreCalculatorUnitTest.java | 36 +++++++++++-------- .../event/EventProcessingFacade.java | 2 +- .../application/metrics/MetricsService.java | 6 ++-- .../domain/metrics/ProductMetricsService.java | 13 +++---- .../metrics/ProductMetricsJpaRepository.java | 3 +- .../domain/metrics/ProductMetricsEntity.java | 12 +++++-- .../domain/ranking/MonthlyRankRepository.java | 9 +++-- 18 files changed, 106 insertions(+), 123 deletions(-) diff --git a/apps/commerce-api/src/main/java/com/loopers/application/ranking/MonthlyRankingService.java b/apps/commerce-api/src/main/java/com/loopers/application/ranking/MonthlyRankingService.java index d1600b0cb..02f968fc3 100644 --- a/apps/commerce-api/src/main/java/com/loopers/application/ranking/MonthlyRankingService.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/ranking/MonthlyRankingService.java @@ -37,37 +37,12 @@ public Page getMonthlyRanking(String yearMonth, Pageable page yearMonth, pageable.getPageNumber(), pageable.getPageSize()); // 1. ์ „์ฒด ๋žญํ‚น ์กฐํšŒ (์ˆœ์œ„ ์ˆœ์œผ๋กœ ์ •๋ ฌ๋จ) - List allRankings = monthlyRankRepository.findByYearMonth(yearMonth); + Page pagedRankings = monthlyRankRepository.findByYearMonth(yearMonth, pageable); - if (allRankings.isEmpty()) { - log.debug("์›”๊ฐ„ ๋žญํ‚น ๋ฐ์ดํ„ฐ ์—†์Œ: yearMonth={}", yearMonth); - return Page.empty(pageable); - } - - // 2. ํŽ˜์ด์ง• ์ฒ˜๋ฆฌ - int start = (int) pageable.getOffset(); - int end = Math.min(start + pageable.getPageSize(), allRankings.size()); - - if (start >= allRankings.size()) { - return Page.empty(pageable); - } - - List pagedRankings = allRankings.subList(start, end); log.debug("์›”๊ฐ„ ๋žญํ‚น ์กฐํšŒ ์™„๋ฃŒ: yearMonth={}, ์ „์ฒด={}, ํŽ˜์ด์ง€={}", - yearMonth, allRankings.size(), pagedRankings.size()); - - return new PageImpl<>(pagedRankings, pageable, allRankings.size()); - } + yearMonth, pagedRankings.getTotalPages(), pagedRankings.getNumber()); - /** - * ํŠน์ • ์›”์˜ ์ „์ฒด ๋žญํ‚น ๊ฐœ์ˆ˜๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. - * - * @param yearMonth ์กฐํšŒํ•  ์›” - * @return ๋žญํ‚น ๊ฐœ์ˆ˜ - */ - public long getMonthlyRankingCount(String yearMonth) { - List rankings = monthlyRankRepository.findByYearMonth(yearMonth); - return rankings.size(); + return pagedRankings; } } diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MonthlyRankJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MonthlyRankJpaRepository.java index 45a50042b..d45fcd5eb 100644 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MonthlyRankJpaRepository.java +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MonthlyRankJpaRepository.java @@ -2,6 +2,7 @@ import java.util.List; +import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; @@ -26,7 +27,7 @@ public interface MonthlyRankJpaRepository extends JpaRepository findByIdYearMonthOrderByRankPosition(@Param("yearMonth") String yearMonth, Pageable pageable); + Page findByIdYearMonthOrderByRankPosition(@Param("yearMonth") String yearMonth, Pageable pageable); /** * ํŠน์ • ์›”์˜ ๋ชจ๋“  ๋žญํ‚น์„ ์‚ญ์ œํ•ฉ๋‹ˆ๋‹ค. diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MonthlyRankRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MonthlyRankRepositoryImpl.java index ec28bbb6c..cbee3ed47 100644 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MonthlyRankRepositoryImpl.java +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MonthlyRankRepositoryImpl.java @@ -2,6 +2,7 @@ import java.util.List; +import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Repository; @@ -31,13 +32,7 @@ public List saveAll(List entities) { } @Override - public List findByYearMonth(String yearMonth) { - return jpaRepository.findByIdYearMonthOrderByRankPosition(yearMonth); - } - - @Override - public List findByYearMonthWithPagination(String yearMonth, int page, int size) { - Pageable pageable = PageRequest.of(page, size); + public Page findByYearMonth(String yearMonth, Pageable pageable) { return jpaRepository.findByIdYearMonthOrderByRankPosition(yearMonth, pageable); } diff --git a/apps/commerce-api/src/test/java/com/loopers/fixtures/UserTestFixture.java b/apps/commerce-api/src/test/java/com/loopers/fixtures/UserTestFixture.java index 49ee37912..30b41ac8a 100644 --- a/apps/commerce-api/src/test/java/com/loopers/fixtures/UserTestFixture.java +++ b/apps/commerce-api/src/test/java/com/loopers/fixtures/UserTestFixture.java @@ -1,5 +1,6 @@ package com.loopers.fixtures; +import java.math.BigDecimal; import java.time.LocalDate; import org.assertj.core.api.Assertions; @@ -165,20 +166,20 @@ public static class InvalidGender { * ์‚ฌ์šฉ์ž์˜ ํฌ์ธํŠธ๊ฐ€ 0์ธ์ง€ ๊ฒ€์ฆํ•˜๋Š” ํ—ฌํผ ๋ฉ”์„œ๋“œ */ public static void assertUserPointIsZero(UserEntity user) { - Assertions.assertThat(user.getPointAmount()).isEqualByComparingTo(java.math.BigDecimal.ZERO.setScale(2)); + Assertions.assertThat(user.getPointAmount()).isEqualByComparingTo(BigDecimal.ZERO.setScale(2)); } /** * ์‚ฌ์šฉ์ž์˜ ํฌ์ธํŠธ ๊ธˆ์•ก ๊ฒ€์ฆ ํ—ฌํผ ๋ฉ”์„œ๋“œ */ - public static void assertUserPointAmount(UserEntity user, java.math.BigDecimal expectedAmount) { + public static void assertUserPointAmount(UserEntity user, BigDecimal expectedAmount) { Assertions.assertThat(user.getPointAmount()).isEqualByComparingTo(expectedAmount); } /** * ํฌ์ธํŠธ ์ถฉ์ „ ์‹คํŒจ ๊ฒ€์ฆ ํ—ฌํผ ๋ฉ”์„œ๋“œ */ - public static void assertChargePointFails(UserEntity user, java.math.BigDecimal amount, String expectedMessage) { + public static void assertChargePointFails(UserEntity user, BigDecimal amount, String expectedMessage) { Assertions.assertThatThrownBy(() -> user.chargePoint(amount)) .isInstanceOf(IllegalArgumentException.class) .hasMessage(expectedMessage); diff --git a/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/dto/RankingAggregation.java b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/dto/RankingAggregation.java index f8493e72d..b5b61a2a6 100644 --- a/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/dto/RankingAggregation.java +++ b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/dto/RankingAggregation.java @@ -1,5 +1,7 @@ package com.loopers.batch.job.ranking.dto; +import java.math.BigDecimal; + import com.loopers.batch.job.ranking.support.ScoreCalculator; import lombok.Getter; @@ -17,16 +19,18 @@ public class RankingAggregation { private final long likeCount; private final long salesCount; private final long orderCount; + private final BigDecimal totalSalesAmount; private final long totalScore; private int rankPosition; // ๊ฐ€๋ณ€ ํ•„๋“œ (์ˆœ์œ„ ๋ถ€์—ฌ์šฉ) private RankingAggregation(Long productId, long viewCount, long likeCount, - long salesCount, long orderCount, long totalScore) { + long salesCount, long orderCount, BigDecimal totalSalesAmount, long totalScore) { this.productId = productId; this.viewCount = viewCount; this.likeCount = likeCount; this.salesCount = salesCount; this.orderCount = orderCount; + this.totalSalesAmount = totalSalesAmount; this.totalScore = totalScore; this.rankPosition = 0; // ์ดˆ๊ธฐ๊ฐ’ } @@ -40,7 +44,7 @@ private RankingAggregation(Long productId, long viewCount, long likeCount, * @throws IllegalArgumentException row๊ฐ€ null์ด๊ฑฐ๋‚˜ ํ˜•์‹์ด ์ž˜๋ชป๋œ ๊ฒฝ์šฐ */ public static RankingAggregation from(Object[] row, ScoreCalculator calculator) { - if (row == null || row.length < 5) { + if (row == null || row.length < 4) { throw new IllegalArgumentException("์ง‘๊ณ„ ๊ฒฐ๊ณผ ๋ฐฐ์—ด์ด null์ด๊ฑฐ๋‚˜ ๊ธธ์ด๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค."); } @@ -50,10 +54,11 @@ public static RankingAggregation from(Object[] row, ScoreCalculator calculator) long likeCount = ((Number) row[2]).longValue(); long salesCount = ((Number) row[3]).longValue(); long orderCount = ((Number) row[4]).longValue(); + BigDecimal totalSalesAmount = (BigDecimal) row[5]; - long totalScore = calculator.calculate(viewCount, likeCount, salesCount, orderCount); + long totalScore = calculator.calculate(viewCount, likeCount, totalSalesAmount); - return new RankingAggregation(productId, viewCount, likeCount, salesCount, orderCount, totalScore); + return new RankingAggregation(productId, viewCount, likeCount, salesCount, orderCount, totalSalesAmount, totalScore); } catch (ClassCastException | NullPointerException e) { throw new IllegalArgumentException("์ง‘๊ณ„ ๊ฒฐ๊ณผ ๋ฐ์ดํ„ฐ ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค.", e); } diff --git a/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/support/ScoreCalculator.java b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/support/ScoreCalculator.java index 980635a41..39950e248 100644 --- a/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/support/ScoreCalculator.java +++ b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/support/ScoreCalculator.java @@ -1,5 +1,7 @@ package com.loopers.batch.job.ranking.support; +import java.math.BigDecimal; + import org.springframework.stereotype.Component; /** @@ -18,22 +20,20 @@ public class ScoreCalculator { /** * ๋ฉ”ํŠธ๋ฆญ ๋ฐ์ดํ„ฐ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๋žญํ‚น ์ ์ˆ˜๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. * - * @param viewCount ์กฐํšŒ์ˆ˜ - * @param likeCount ์ข‹์•„์š”์ˆ˜ - * @param salesCount ํŒ๋งค์ˆ˜๋Ÿ‰ - * @param orderCount ์ฃผ๋ฌธ์ˆ˜ + * @param viewCount ์กฐํšŒ์ˆ˜ + * @param likeCount ์ข‹์•„์š”์ˆ˜ + * @param totalSalesAmount ์ด ํŒ๋งค ๊ธˆ์•ก * @return ๊ณ„์‚ฐ๋œ ์ด ์ ์ˆ˜ */ - public long calculate(long viewCount, long likeCount, long salesCount, long orderCount) { + public long calculate(long viewCount, long likeCount, BigDecimal totalSalesAmount) { // 1. ์กฐํšŒ์™€ ์ข‹์•„์š”๋Š” ๋‹จ์ˆœ ์ˆ˜๋Ÿ‰ ๊ธฐ๋ฐ˜ ๊ฐ€์ค‘์น˜ ์ ์šฉ double viewScore = viewCount * VIEW_WEIGHT; double likeScore = likeCount * LIKE_WEIGHT; // 2. ํŒ๋งค๋Ÿ‰(Sales)์€ CachePayloads.forPaymentSuccess์™€ ๋™์ผํ•˜๊ฒŒ ๋กœ๊ทธ ์ •๊ทœํ™” ์ ์šฉ - // ๋ฐฐ์น˜์—์„œ๋Š” ์ด๋ฏธ ์ง‘๊ณ„๋œ salesCount(์ˆ˜๋Ÿ‰)๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•˜๋ฏ€๋กœ, - // ๋งŒ์•ฝ ๊ธˆ์•ก ๊ธฐ๋ฐ˜ ์ •๊ทœํ™”๊ฐ€ ํ•„์š”ํ•˜๋‹ค๋ฉด ๋งค๊ฐœ๋ณ€์ˆ˜๋กœ ์ด์•ก์„ ๋ฐ›์•„์•ผ ํ•˜์ง€๋งŒ, - // ์ˆ˜๋Ÿ‰ ๊ธฐ๋ฐ˜์œผ๋กœ ๋กœ๊ทธ ์ •๊ทœํ™”๋ฅผ ์ ์šฉํ•œ๋‹ค๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ด ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค. - double normalizedSalesScore = Math.log1p(salesCount) * SALES_WEIGHT; + // RankingScore.forPaymentSuccess: normalizedScore = Math.log(totalPrice.doubleValue() + 1); + double amount = totalSalesAmount != null ? totalSalesAmount.doubleValue() : 0.0; + double normalizedSalesScore = Math.log(amount + 1) * SALES_WEIGHT; // 3. ์ตœ์ข… ์ ์ˆ˜ ๊ณ„์‚ฐ (์†Œ์ˆ˜์  ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•ด ์ ์ ˆํ•œ ์Šค์ผ€์ผ ๊ณฑ์‚ฐ ํ›„ long ๋ณ€ํ™˜) // Redis ZSET์˜ score๊ฐ€ double์ž„์„ ๊ฐ์•ˆํ•˜์—ฌ ์ •๋ฐ€๋„๋ฅผ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค. diff --git a/apps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.java b/apps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.java index cc8474c32..d49e324a6 100644 --- a/apps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.java +++ b/apps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.java @@ -29,7 +29,8 @@ public interface ProductMetricsJpaRepository extends JpaRepository { - /** - * ํŠน์ • ์›”์˜ ๋žญํ‚น์„ ์ˆœ์œ„ ์ˆœ์œผ๋กœ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. - */ - @Query("SELECT m FROM MonthlyRankEntity m WHERE m.id.yearMonth = :yearMonth ORDER BY m.rankPosition ASC") - List findByIdYearMonthOrderByRankPosition(@Param("yearMonth") String yearMonth); - /** * ํŠน์ • ์›”์˜ ๋žญํ‚น์„ ์ˆœ์œ„ ์ˆœ์œผ๋กœ ํŽ˜์ด์ง€๋„ค์ด์…˜ํ•˜์—ฌ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. */ @Query("SELECT m FROM MonthlyRankEntity m WHERE m.id.yearMonth = :yearMonth ORDER BY m.rankPosition ASC") - List findByIdYearMonthOrderByRankPosition(@Param("yearMonth") String yearMonth, Pageable pageable); + Page findByIdYearMonthOrderByRankPosition(@Param("yearMonth") String yearMonth, Pageable pageable); /** * ํŠน์ • ์›”์˜ ๋ชจ๋“  ๋žญํ‚น์„ ์‚ญ์ œํ•ฉ๋‹ˆ๋‹ค. diff --git a/apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/MonthlyRankRepositoryImpl.java b/apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/MonthlyRankRepositoryImpl.java index a2b005c18..478e195fc 100644 --- a/apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/MonthlyRankRepositoryImpl.java +++ b/apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/MonthlyRankRepositoryImpl.java @@ -2,6 +2,7 @@ import java.util.List; +import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Repository; @@ -31,13 +32,7 @@ public List saveAll(List entities) { } @Override - public List findByYearMonth(String yearMonth) { - return jpaRepository.findByIdYearMonthOrderByRankPosition(yearMonth); - } - - @Override - public List findByYearMonthWithPagination(String yearMonth, int page, int size) { - Pageable pageable = PageRequest.of(page, size); + public Page findByYearMonth(String yearMonth, Pageable pageable) { return jpaRepository.findByIdYearMonthOrderByRankPosition(yearMonth, pageable); } diff --git a/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/dto/RankingAggregationUnitTest.java b/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/dto/RankingAggregationUnitTest.java index 242373de9..9f6270804 100644 --- a/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/dto/RankingAggregationUnitTest.java +++ b/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/dto/RankingAggregationUnitTest.java @@ -20,7 +20,7 @@ class ์ง‘๊ณ„_๊ฒฐ๊ณผ๋กœ๋ถ€ํ„ฐ_์ƒ์„ฑ { @DisplayName("์œ ํšจํ•œ ์ง‘๊ณ„ ๊ฒฐ๊ณผ๋กœ๋ถ€ํ„ฐ ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•œ๋‹ค") void should_create_from_valid_aggregation_result() { // given - Object[] row = {1L, 100L, 50L, 10L, 5L}; // productId, view, like, sales, order + Object[] row = {1L, 100L, 50L, 10L, 5L, java.math.BigDecimal.valueOf(1000)}; // productId, view, like, sales, order, amount // when RankingAggregation aggregation = RankingAggregation.from(row, calculator); @@ -31,8 +31,9 @@ void should_create_from_valid_aggregation_result() { Assertions.assertThat(aggregation.getLikeCount()).isEqualTo(50L); Assertions.assertThat(aggregation.getSalesCount()).isEqualTo(10L); Assertions.assertThat(aggregation.getOrderCount()).isEqualTo(5L); - // score = 100*1 + 50*3 + 10*5 + 5*2 = 310 - Assertions.assertThat(aggregation.getTotalScore()).isEqualTo(214L); + Assertions.assertThat(aggregation.getTotalSalesAmount()).isEqualByComparingTo(java.math.BigDecimal.valueOf(1000)); + // score = (100*0.1 + 50*0.2 + log(1001)*0.6) * 10 = (10+10+4.145) * 10 = 241 + Assertions.assertThat(aggregation.getTotalScore()).isEqualTo(241L); Assertions.assertThat(aggregation.getRankPosition()).isEqualTo(0); // ์ดˆ๊ธฐ๊ฐ’ } @@ -49,7 +50,7 @@ void should_throw_exception_when_row_is_null() { @DisplayName("๊ธธ์ด๊ฐ€ ๋ถ€์กฑํ•œ ๋ฐฐ์—ด์— ๋Œ€ํ•ด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค") void should_throw_exception_when_row_length_is_insufficient() { // given - Object[] shortRow = {1L, 100L, 50L}; // ๊ธธ์ด 3 (5 ๋ฏธ๋งŒ) + Object[] shortRow = {1L, 100L, 50L}; // ๊ธธ์ด 3 (6 ๋ฏธ๋งŒ) // when & then Assertions.assertThatThrownBy(() -> RankingAggregation.from(shortRow, calculator)) @@ -61,7 +62,7 @@ void should_throw_exception_when_row_length_is_insufficient() { @DisplayName("์ž˜๋ชป๋œ ๋ฐ์ดํ„ฐ ํƒ€์ž…์— ๋Œ€ํ•ด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค") void should_throw_exception_when_data_type_is_invalid() { // given - Object[] invalidRow = {"invalid", 100L, 50L, 10L, 5L}; // productId๊ฐ€ String + Object[] invalidRow = {"invalid", 100L, 50L, 10L, 5L, java.math.BigDecimal.valueOf(1000)}; // productId๊ฐ€ String // when & then Assertions.assertThatThrownBy(() -> RankingAggregation.from(invalidRow, calculator)) @@ -73,7 +74,7 @@ void should_throw_exception_when_data_type_is_invalid() { @DisplayName("Number ํƒ€์ž…์˜ ๋‹ค์–‘ํ•œ ํ˜•ํƒœ๋ฅผ ์ฒ˜๋ฆฌํ•œ๋‹ค") void should_handle_various_number_types() { // given - Integer, Long, BigDecimal ๋“ฑ ๋‹ค์–‘ํ•œ Number ํƒ€์ž… - Object[] row = {1L, 100, 50L, 10, 5L}; + Object[] row = {1L, 100, 50L, 10, 5L, java.math.BigDecimal.valueOf(1000)}; // when RankingAggregation aggregation = RankingAggregation.from(row, calculator); @@ -94,7 +95,7 @@ class ์ˆœ์œ„_๋ถ€์—ฌ { @DisplayName("์œ ํšจํ•œ ์ˆœ์œ„๋ฅผ ๋ถ€์—ฌํ•œ๋‹ค") void should_assign_valid_rank() { // given - Object[] row = {1L, 100L, 50L, 10L, 5L}; + Object[] row = {1L, 100L, 50L, 10L, 5L, java.math.BigDecimal.valueOf(1000)}; RankingAggregation aggregation = RankingAggregation.from(row, calculator); // when @@ -108,7 +109,7 @@ void should_assign_valid_rank() { @DisplayName("100์œ„๊นŒ์ง€ ์ˆœ์œ„๋ฅผ ๋ถ€์—ฌํ•  ์ˆ˜ ์žˆ๋‹ค") void should_assign_rank_up_to_100() { // given - Object[] row = {1L, 100L, 50L, 10L, 5L}; + Object[] row = {1L, 100L, 50L, 10L, 5L, java.math.BigDecimal.valueOf(1000)}; RankingAggregation aggregation = RankingAggregation.from(row, calculator); // when @@ -122,7 +123,7 @@ void should_assign_rank_up_to_100() { @DisplayName("0 ์ดํ•˜์˜ ์ˆœ์œ„์— ๋Œ€ํ•ด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค") void should_throw_exception_when_rank_is_zero_or_negative() { // given - Object[] row = {1L, 100L, 50L, 10L, 5L}; + Object[] row = {1L, 100L, 50L, 10L, 5L, java.math.BigDecimal.valueOf(1000)}; RankingAggregation aggregation = RankingAggregation.from(row, calculator); // when & then @@ -139,7 +140,7 @@ void should_throw_exception_when_rank_is_zero_or_negative() { @DisplayName("100์„ ์ดˆ๊ณผํ•˜๋Š” ์ˆœ์œ„์— ๋Œ€ํ•ด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค") void should_throw_exception_when_rank_exceeds_100() { // given - Object[] row = {1L, 100L, 50L, 10L, 5L}; + Object[] row = {1L, 100L, 50L, 10L, 5L, java.math.BigDecimal.valueOf(1000)}; RankingAggregation aggregation = RankingAggregation.from(row, calculator); // when & then @@ -157,7 +158,7 @@ class ๋ฌธ์ž์—ด_ํ‘œํ˜„ { @DisplayName("toString์ด ์˜ฌ๋ฐ”๋ฅธ ํ˜•์‹์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค") void should_return_correct_string_format() { // given - Object[] row = {1L, 100L, 50L, 10L, 5L}; + Object[] row = {1L, 100L, 50L, 10L, 5L, java.math.BigDecimal.valueOf(1000)}; RankingAggregation aggregation = RankingAggregation.from(row, calculator); aggregation.assignRank(1); @@ -166,7 +167,7 @@ void should_return_correct_string_format() { // then Assertions.assertThat(result).contains("productId=1"); - Assertions.assertThat(result).contains("score=214"); + Assertions.assertThat(result).contains("score=241"); Assertions.assertThat(result).contains("rank=1"); } } diff --git a/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/support/RankingAggregatorUnitTest.java b/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/support/RankingAggregatorUnitTest.java index dbca864ed..818b6970a 100644 --- a/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/support/RankingAggregatorUnitTest.java +++ b/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/support/RankingAggregatorUnitTest.java @@ -1,5 +1,6 @@ package com.loopers.batch.job.ranking.support; +import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; @@ -25,9 +26,9 @@ class ๋žญํ‚น_์ฒ˜๋ฆฌ { void should_sort_by_score_and_assign_ranks() { // given List results = List.of( - new Object[]{1L, 100L, 10L, 5L, 2L}, - new Object[]{2L, 200L, 20L, 10L, 4L}, - new Object[]{3L, 50L, 5L, 2L, 1L} + new Object[]{1L, 100L, 10L, 5L, 2L , new BigDecimal(0)}, + new Object[]{2L, 200L, 20L, 10L, 4L, new BigDecimal(0)}, + new Object[]{3L, 50L, 5L, 2L, 1L, new BigDecimal(0)} ); // when @@ -39,15 +40,15 @@ void should_sort_by_score_and_assign_ranks() { // ์ ์ˆ˜ ๊ธฐ์ค€ ๋‚ด๋ฆผ์ฐจ์ˆœ ์ •๋ ฌ ํ™•์ธ Assertions.assertThat(rankings.get(0).getProductId()).isEqualTo(2L); // 1์œ„ Assertions.assertThat(rankings.get(0).getRankPosition()).isEqualTo(1); - Assertions.assertThat(rankings.get(0).getTotalScore()).isEqualTo(254L); + Assertions.assertThat(rankings.get(0).getTotalScore()).isEqualTo(240L); Assertions.assertThat(rankings.get(1).getProductId()).isEqualTo(1L); // 2์œ„ Assertions.assertThat(rankings.get(1).getRankPosition()).isEqualTo(2); - Assertions.assertThat(rankings.get(1).getTotalScore()).isEqualTo(130L); + Assertions.assertThat(rankings.get(1).getTotalScore()).isEqualTo(120L); Assertions.assertThat(rankings.get(2).getProductId()).isEqualTo(3L); // 3์œ„ Assertions.assertThat(rankings.get(2).getRankPosition()).isEqualTo(3); - Assertions.assertThat(rankings.get(2).getTotalScore()).isEqualTo(66L); + Assertions.assertThat(rankings.get(2).getTotalScore()).isEqualTo(60L); } @Test @@ -57,7 +58,7 @@ void should_filter_results_beyond_top_100() { List results = new ArrayList<>(); for (int i = 1; i <= 150; i++) { // ์ ์ˆ˜๊ฐ€ ๋†’์€ ์ˆœ์„œ๋Œ€๋กœ ์ƒ์„ฑ (i๊ฐ€ ํด์ˆ˜๋ก ์ ์ˆ˜ ๋†’์Œ) - results.add(new Object[]{(long) i, (long) i * 10, (long) i, (long) i, (long) i}); + results.add(new Object[]{(long) i, (long) i * 10, (long) i, (long) i, (long) i, new BigDecimal(i)}); } // when @@ -97,9 +98,9 @@ void should_return_empty_list_for_null_results() { void should_maintain_order_for_same_scores() { // given - ๋™์ผํ•œ ์ ์ˆ˜๋ฅผ ๊ฐ€์ง„ ์ƒํ’ˆ๋“ค List results = List.of( - new Object[]{1L, 100L, 0L, 0L, 0L}, // score = 100 - new Object[]{2L, 100L, 0L, 0L, 0L}, // score = 100 - new Object[]{3L, 100L, 0L, 0L, 0L} // score = 100 + new Object[]{1L, 100L, 0L, 0L, 0L, new BigDecimal(0)}, // score = 100 + new Object[]{2L, 100L, 0L, 0L, 0L, new BigDecimal(0)}, // score = 100 + new Object[]{3L, 100L, 0L, 0L, 0L, new BigDecimal(0)} // score = 100 ); // when diff --git a/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/support/ScoreCalculatorUnitTest.java b/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/support/ScoreCalculatorUnitTest.java index ed89385af..7d8b4e36b 100644 --- a/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/support/ScoreCalculatorUnitTest.java +++ b/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/support/ScoreCalculatorUnitTest.java @@ -19,36 +19,41 @@ class ์ ์ˆ˜_๊ณ„์‚ฐ { void should_calculate_score_with_correct_weights() { // given long viewCount = 100, likeCount = 50, salesCount = 10, orderCount = 5; + java.math.BigDecimal totalSalesAmount = java.math.BigDecimal.valueOf(1000); // when - long score = calculator.calculate(viewCount, likeCount, salesCount, orderCount); + long score = calculator.calculate(viewCount, likeCount, totalSalesAmount); // then - Assertions.assertThat(score).isEqualTo(214L); + // viewScore = 100 * 0.1 = 10.0 + // likeScore = 50 * 0.2 = 10.0 + // salesScore = log(1000 + 1) * 0.6 = 6.908 * 0.6 = 4.145 + // total = (10.0 + 10.0 + 4.145) * 10 = 24.145 * 10 = 241 + Assertions.assertThat(score).isEqualTo(241L); } @Test @DisplayName("๋ชจ๋“  ๋ฉ”ํŠธ๋ฆญ์ด 0์ธ ๊ฒฝ์šฐ ์ ์ˆ˜๋Š” 0์ด๋‹ค") void should_return_zero_when_all_metrics_are_zero() { // given & when - long score = calculator.calculate(0, 0, 0, 0); + long score = calculator.calculate(0, 0, java.math.BigDecimal.ZERO); // then Assertions.assertThat(score).isEqualTo(0L); } @Test - @DisplayName("ํŒ๋งค์ˆ˜๋Ÿ‰์ด ๊ฐ€์žฅ ๋†’์€ ๊ฐ€์ค‘์น˜๋ฅผ ๊ฐ€์ง„๋‹ค") - void should_have_highest_weight_for_sales_count() { + @DisplayName("ํŒ๋งค๊ธˆ์•ก์ด ๊ฐ€์žฅ ๋†’์€ ๊ฐ€์ค‘์น˜๋ฅผ ๊ฐ€์ง„๋‹ค") + void should_have_highest_weight_for_sales_amount() { // given - long singleSale = calculator.calculate(0, 0, 1, 0); - long singleView = calculator.calculate(1, 0, 0, 0); - long singleLike = calculator.calculate(0, 1, 0, 0); - long singleOrder = calculator.calculate(0, 0, 0, 1); - - // when & then - Assertions.assertThat(singleSale).isGreaterThan(singleView); - Assertions.assertThat(singleSale).isGreaterThan(singleLike); + long viewScore = calculator.calculate(100, 0, java.math.BigDecimal.ZERO); // 100 * 0.1 * 10 = 100 + long amountScore = calculator.calculate(0, 0, java.math.BigDecimal.valueOf(1000000)); // log(1000001) * 0.6 * 10 = 13.8 * 6 = 82 + + // ๋กœ๊ทธ ์ •๊ทœํ™”๋กœ ์ธํ•ด ๊ธˆ์•ก์ด ๋งค์šฐ ์ปค์•ผ ๋‹ค๋ฅธ ์ง€ํ‘œ๋ฅผ ์••๋„ํ•จ + long largeAmountScore = calculator.calculate(0, 0, java.math.BigDecimal.valueOf(1000000000)); + + Assertions.assertThat(viewScore).isEqualTo(100L); + Assertions.assertThat(largeAmountScore).isGreaterThan(viewScore); } @Test @@ -59,12 +64,13 @@ void should_calculate_correctly_with_large_numbers() { long likeCount = 500_000L; long salesCount = 100_000L; long orderCount = 50_000L; + java.math.BigDecimal totalSalesAmount = java.math.BigDecimal.valueOf(100_000_000L); // when - long score = calculator.calculate(viewCount, likeCount, salesCount, orderCount); + long score = calculator.calculate(viewCount, likeCount, totalSalesAmount); // then - long expected = (long) (((1_000_000L * 0.1) + (500_000L * 0.2) + Math.log1p(100_000L) *0.6 ) * 10); + long expected = (long) (((1_000_000L * 0.1) + (500_000L * 0.2) + Math.log(100_000_000L + 1) * 0.6) * 10); Assertions.assertThat(score).isEqualTo(expected); } } diff --git a/apps/commerce-streamer/src/main/java/com/loopers/application/event/EventProcessingFacade.java b/apps/commerce-streamer/src/main/java/com/loopers/application/event/EventProcessingFacade.java index 9a2c71eaa..5450a2870 100644 --- a/apps/commerce-streamer/src/main/java/com/loopers/application/event/EventProcessingFacade.java +++ b/apps/commerce-streamer/src/main/java/com/loopers/application/event/EventProcessingFacade.java @@ -218,7 +218,7 @@ private OrderEventResult processPaymentSuccess(DomainEventEnvelope envelope) { return OrderEventResult.notProcessed(); } - metricsService.addSales(payload.productId(), payload.quantity(), envelope.occurredAtEpochMillis()); + metricsService.addSales(payload.productId(), payload.quantity(), payload.totalPrice(), envelope.occurredAtEpochMillis()); log.debug("Processed PAYMENT_SUCCESS - orderId: {}, productId: {}, quantity: {}, totalPrice: {}", payload.orderId(), payload.productId(), payload.quantity(), payload.totalPrice()); diff --git a/apps/commerce-streamer/src/main/java/com/loopers/application/metrics/MetricsService.java b/apps/commerce-streamer/src/main/java/com/loopers/application/metrics/MetricsService.java index acf3487d1..8b3933131 100644 --- a/apps/commerce-streamer/src/main/java/com/loopers/application/metrics/MetricsService.java +++ b/apps/commerce-streamer/src/main/java/com/loopers/application/metrics/MetricsService.java @@ -121,15 +121,15 @@ public void applyLikeDelta(Long productId, int delta, long occurredAtEpochMillis /** * ํŒ๋งค๋Ÿ‰ ์ฆ๊ฐ€ */ - public void addSales(Long productId, int quantity, long occurredAtEpochMillis) { + public void addSales(Long productId, int quantity, java.math.BigDecimal totalAmount, long occurredAtEpochMillis) { executeWithLock(productId, () -> { ZonedDateTime eventTime = convertToZonedDateTime(occurredAtEpochMillis); - boolean updated = productMetricsService.addSales(productId, quantity, eventTime); + boolean updated = productMetricsService.addSales(productId, quantity, totalAmount, eventTime); if (updated) { // ์บ์‹œ ๋ฌดํšจํ™” (ํŒ๋งค๋Ÿ‰ ๋ณ€๊ฒฝ - ์ธ๊ธฐ ์ƒํ’ˆ ์ˆœ์œ„ ์˜ํ–ฅ) productCacheService.onSalesCountChanged(productId); - log.debug("ํŒ๋งค๋Ÿ‰ ์—…๋ฐ์ดํŠธ ์„ฑ๊ณต: productId={}, quantity={}", productId, quantity); + log.debug("ํŒ๋งค๋Ÿ‰ ์—…๋ฐ์ดํŠธ ์„ฑ๊ณต: productId={}, quantity={}, totalAmount={}", productId, quantity, totalAmount); } }); } diff --git a/apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsService.java b/apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsService.java index f9510d857..94bf2478c 100644 --- a/apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsService.java +++ b/apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsService.java @@ -80,13 +80,14 @@ public boolean applyLikeDelta(Long productId, int delta, ZonedDateTime eventTime /** * ํŒ๋งค๋Ÿ‰ ์ฆ๊ฐ€ * - * @param productId ์ƒํ’ˆ ID - * @param quantity ํŒ๋งค ์ˆ˜๋Ÿ‰ - * @param eventTime ์ด๋ฒคํŠธ ๋ฐœ์ƒ ์‹œ๊ฐ„ + * @param productId ์ƒํ’ˆ ID + * @param quantity ํŒ๋งค ์ˆ˜๋Ÿ‰ + * @param totalAmount ์ด ํŒ๋งค ๊ธˆ์•ก + * @param eventTime ์ด๋ฒคํŠธ ๋ฐœ์ƒ ์‹œ๊ฐ„ * @return true: ์ฆ๊ฐ€๋จ, false: ์ฆ๊ฐ€ ์•ˆ ๋จ (์ž˜๋ชป๋œ ์ˆ˜๋Ÿ‰) */ @Transactional - public boolean addSales(Long productId, int quantity, ZonedDateTime eventTime) { + public boolean addSales(Long productId, int quantity, java.math.BigDecimal totalAmount, ZonedDateTime eventTime) { if (quantity <= 0) { log.debug("์ž˜๋ชป๋œ ํŒ๋งค๋Ÿ‰ ๋ฌด์‹œ: productId={}, quantity={}", productId, quantity); return false; @@ -94,9 +95,9 @@ public boolean addSales(Long productId, int quantity, ZonedDateTime eventTime) { LocalDate metricDate = eventTime.toLocalDate(); ProductMetricsEntity metrics = getOrCreateMetrics(productId, metricDate); - metrics.addSales(quantity, eventTime); + metrics.addSales(quantity, totalAmount, eventTime); productMetricsRepository.save(metrics); - log.debug("ํŒ๋งค๋Ÿ‰ ์ฆ๊ฐ€ ์™„๋ฃŒ: productId={}, quantity={}, date={}", productId, quantity, metricDate); + log.debug("ํŒ๋งค๋Ÿ‰ ์ฆ๊ฐ€ ์™„๋ฃŒ: productId={}, quantity={}, totalAmount={}, date={}", productId, quantity, totalAmount, metricDate); return true; } diff --git a/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.java b/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.java index d0aa2dbd4..cbdc2bea6 100644 --- a/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.java +++ b/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.java @@ -49,7 +49,8 @@ List findByMetricDateBetween( SUM(m.viewCount), SUM(m.likeCount), SUM(m.salesCount), - SUM(m.orderCount) + SUM(m.orderCount), + SUM(m.totalSalesAmount) FROM ProductMetricsEntity m WHERE m.id.metricDate BETWEEN :startDate AND :endDate GROUP BY m.id.productId diff --git a/modules/jpa/src/main/java/com/loopers/domain/metrics/ProductMetricsEntity.java b/modules/jpa/src/main/java/com/loopers/domain/metrics/ProductMetricsEntity.java index cba44c95a..7b3b27828 100644 --- a/modules/jpa/src/main/java/com/loopers/domain/metrics/ProductMetricsEntity.java +++ b/modules/jpa/src/main/java/com/loopers/domain/metrics/ProductMetricsEntity.java @@ -43,6 +43,9 @@ public class ProductMetricsEntity { @Column(name = "order_count", nullable = false) private long orderCount = 0L; + @Column(name = "total_sales_amount", nullable = false) + private java.math.BigDecimal totalSalesAmount = java.math.BigDecimal.ZERO; + @Column(name = "last_event_at") private ZonedDateTime lastEventAt; @@ -50,6 +53,7 @@ private ProductMetricsEntity(Long productId, LocalDate metricDate) { Objects.requireNonNull(productId, "์ƒํ’ˆ ID๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."); Objects.requireNonNull(metricDate, "๋ฉ”ํŠธ๋ฆญ ๋‚ ์งœ๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."); this.id = ProductMetricsId.of(productId, metricDate); + this.totalSalesAmount = java.math.BigDecimal.ZERO; } /** @@ -108,15 +112,17 @@ public void applyLikeDelta(int delta, ZonedDateTime eventTime) { /** * ํŒ๋งค๋Ÿ‰ ์ฆ๊ฐ€ * - * @param quantity ํŒ๋งค ์ˆ˜๋Ÿ‰ - * @param eventTime ์ด๋ฒคํŠธ ๋ฐœ์ƒ ์‹œ๊ฐ„ + * @param quantity ํŒ๋งค ์ˆ˜๋Ÿ‰ + * @param totalAmount ์ด ํŒ๋งค ๊ธˆ์•ก + * @param eventTime ์ด๋ฒคํŠธ ๋ฐœ์ƒ ์‹œ๊ฐ„ */ - public void addSales(int quantity, ZonedDateTime eventTime) { + public void addSales(int quantity, java.math.BigDecimal totalAmount, ZonedDateTime eventTime) { if (quantity <= 0) { return; } this.salesCount += quantity; this.orderCount += 1; + this.totalSalesAmount = this.totalSalesAmount.add(totalAmount != null ? totalAmount : java.math.BigDecimal.ZERO); this.lastEventAt = eventTime; } } diff --git a/modules/jpa/src/main/java/com/loopers/domain/ranking/MonthlyRankRepository.java b/modules/jpa/src/main/java/com/loopers/domain/ranking/MonthlyRankRepository.java index f531e9dc0..127f823a6 100644 --- a/modules/jpa/src/main/java/com/loopers/domain/ranking/MonthlyRankRepository.java +++ b/modules/jpa/src/main/java/com/loopers/domain/ranking/MonthlyRankRepository.java @@ -2,6 +2,9 @@ import java.util.List; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + /** * ์›”๊ฐ„ ๋žญํ‚น Repository ์ธํ„ฐํŽ˜์ด์Šค */ @@ -20,12 +23,8 @@ public interface MonthlyRankRepository { /** * ํŠน์ • ์›”์˜ ๋žญํ‚น์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. */ - List findByYearMonth(String yearMonth); + Page findByYearMonth(String yearMonth, Pageable pageable); - /** - * ํŠน์ • ์›”์˜ ๋žญํ‚น์„ ํŽ˜์ด์ง€๋„ค์ด์…˜ํ•˜์—ฌ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. - */ - List findByYearMonthWithPagination(String yearMonth, int page, int size); /** * ํŠน์ • ์›”์˜ ๋ชจ๋“  ๋žญํ‚น์„ ์‚ญ์ œํ•ฉ๋‹ˆ๋‹ค. (๋ฉฑ๋“ฑ์„ฑ ๋ณด์žฅ์šฉ) From e162dad337a98b7071d9b5f988bf6b882a3b9234 Mon Sep 17 00:00:00 2001 From: hyujikoh Date: Sat, 3 Jan 2026 00:36:27 +0900 Subject: [PATCH 83/85] =?UTF-8?q?feat(product):=20=EC=83=81=ED=92=88=20?= =?UTF-8?q?=EC=A7=91=EA=B3=84=20=EA=B2=B0=EA=B3=BC=EC=97=90=20=EC=B4=9D=20?= =?UTF-8?q?=ED=8C=90=EB=A7=A4=20=EA=B8=88=EC=95=A1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ์ง‘๊ณ„ ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜ ๊ฐ’์— totalSalesAmount ํ•„๋“œ ์ถ”๊ฐ€ - ์ง‘๊ณ„ ๊ฒฐ๊ณผ ๋ฐฐ์—ด ๊ธธ์ด ๊ฒ€์ฆ ๋กœ์ง ์ˆ˜์ • --- .../com/loopers/batch/job/ranking/dto/RankingAggregation.java | 2 +- .../infrastructure/metrics/ProductMetricsJpaRepository.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/dto/RankingAggregation.java b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/dto/RankingAggregation.java index b5b61a2a6..2a954e7b8 100644 --- a/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/dto/RankingAggregation.java +++ b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/dto/RankingAggregation.java @@ -44,7 +44,7 @@ private RankingAggregation(Long productId, long viewCount, long likeCount, * @throws IllegalArgumentException row๊ฐ€ null์ด๊ฑฐ๋‚˜ ํ˜•์‹์ด ์ž˜๋ชป๋œ ๊ฒฝ์šฐ */ public static RankingAggregation from(Object[] row, ScoreCalculator calculator) { - if (row == null || row.length < 4) { + if (row == null || row.length < 6) { throw new IllegalArgumentException("์ง‘๊ณ„ ๊ฒฐ๊ณผ ๋ฐฐ์—ด์ด null์ด๊ฑฐ๋‚˜ ๊ธธ์ด๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค."); } diff --git a/apps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.java b/apps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.java index d49e324a6..e8d71f291 100644 --- a/apps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.java +++ b/apps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.java @@ -22,7 +22,7 @@ public interface ProductMetricsJpaRepository extends JpaRepository Date: Sat, 3 Jan 2026 00:38:07 +0900 Subject: [PATCH 84/85] =?UTF-8?q?feat(common):=20=EB=82=A0=EC=A7=9C=20?= =?UTF-8?q?=EB=B2=94=EC=9C=84=20=ED=8C=8C=EC=8B=B1=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit parseDateRange() ๋ฉ”์„œ๋“œ๊ฐ€ ์ •ํ™•ํžˆ 2๊ฐœ์˜ ๋‚ ์งœ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š”์ง€ ๊ฒ€์ฆํ•˜๋Š” ๋กœ์ง์„ ์ถ”๊ฐ€ํ•˜์—ฌ ์˜ˆ์™ธ ์ƒํ™ฉ์„ ์ฒ˜๋ฆฌ --- .../batch/job/ranking/reader/AbstractMetricsReader.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/reader/AbstractMetricsReader.java b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/reader/AbstractMetricsReader.java index f0da39d5f..3919b2dcd 100644 --- a/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/reader/AbstractMetricsReader.java +++ b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/reader/AbstractMetricsReader.java @@ -45,6 +45,9 @@ private void initializeIterator() { try { // 1. ๊ธฐ๊ฐ„ ํŒŒ์‹ฑ (์ถ”์ƒ ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ) LocalDate[] dateRange = parseDateRange(); + if (dateRange == null || dateRange.length != 2) { + throw new IllegalStateException("parseDateRange()๋Š” ์ •ํ™•ํžˆ 2๊ฐœ์˜ ๋‚ ์งœ๋ฅผ ๋ฐ˜ํ™˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + } LocalDate startDate = dateRange[0]; LocalDate endDate = dateRange[1]; From 16bb063ea8a6068298b3dfcbc977294983ae5913 Mon Sep 17 00:00:00 2001 From: hyujikoh Date: Sat, 3 Jan 2026 00:49:36 +0900 Subject: [PATCH 85/85] =?UTF-8?q?feat(ranking):=20=EC=83=81=ED=92=88=20?= =?UTF-8?q?=EB=A9=94=ED=8A=B8=EB=A6=AD=20=EC=A7=91=EA=B3=84=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ์ง‘๊ณ„ ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ๋ฅผ Object[]์—์„œ ProductMetricsAggregation์œผ๋กœ ๋ณ€๊ฒฝ - ๋žญํ‚น ์ง‘๊ณ„ ๋กœ์ง์—์„œ DTO ๋ณ€ํ™˜ ๋ฐ ์ ์ˆ˜ ๊ณ„์‚ฐ ๋ฐฉ์‹ ๊ฐœ์„  - rankPosition ํƒ€์ž…์„ long์—์„œ int๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ์ผ๊ด€์„ฑ ๊ฐ•ํ™” --- .../job/ranking/dto/RankingAggregation.java | 38 +++++---- .../ranking/reader/AbstractMetricsReader.java | 3 +- .../ranking/support/RankingAggregator.java | 5 +- .../metrics/ProductMetricsJpaRepository.java | 10 ++- .../metrics/ProductMetricsRepositoryImpl.java | 3 +- .../dto/RankingAggregationUnitTest.java | 83 +++++++------------ .../support/RankingAggregatorUnitTest.java | 31 +++---- .../metrics/ProductMetricsJpaRepository.java | 8 +- .../metrics/ProductMetricsRepositoryImpl.java | 3 +- .../domain/ranking/QWeeklyRankEntity.java | 2 +- .../domain/metrics/ProductMetricsEntity.java | 1 + .../metrics/ProductMetricsRepository.java | 4 +- .../domain/ranking/MonthlyRankEntity.java | 2 +- .../domain/ranking/WeeklyRankEntity.java | 6 +- .../loopers/domain/ranking/WeeklyRankId.java | 2 +- 15 files changed, 93 insertions(+), 108 deletions(-) diff --git a/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/dto/RankingAggregation.java b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/dto/RankingAggregation.java index 2a954e7b8..057179365 100644 --- a/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/dto/RankingAggregation.java +++ b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/dto/RankingAggregation.java @@ -3,6 +3,7 @@ import java.math.BigDecimal; import com.loopers.batch.job.ranking.support.ScoreCalculator; +import com.loopers.domain.metrics.ProductMetricsAggregation; import lombok.Getter; @@ -38,30 +39,31 @@ private RankingAggregation(Long productId, long viewCount, long likeCount, /** * DB ์ง‘๊ณ„ ๊ฒฐ๊ณผ๋กœ๋ถ€ํ„ฐ RankingAggregation์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. * - * @param row DB ์ง‘๊ณ„ ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ (Object[] ํ˜•ํƒœ) + * @param metrics ์ƒํ’ˆ ๋ฉ”ํŠธ๋ฆญ ์ง‘๊ณ„ ๊ฒฐ๊ณผ DTO * @param calculator ์ ์ˆ˜ ๊ณ„์‚ฐ๊ธฐ * @return ์ƒ์„ฑ๋œ RankingAggregation ๊ฐ์ฒด - * @throws IllegalArgumentException row๊ฐ€ null์ด๊ฑฐ๋‚˜ ํ˜•์‹์ด ์ž˜๋ชป๋œ ๊ฒฝ์šฐ + * @throws IllegalArgumentException metrics๊ฐ€ null์ธ ๊ฒฝ์šฐ */ - public static RankingAggregation from(Object[] row, ScoreCalculator calculator) { - if (row == null || row.length < 6) { - throw new IllegalArgumentException("์ง‘๊ณ„ ๊ฒฐ๊ณผ ๋ฐฐ์—ด์ด null์ด๊ฑฐ๋‚˜ ๊ธธ์ด๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค."); + public static RankingAggregation from(ProductMetricsAggregation metrics, ScoreCalculator calculator) { + if (metrics == null) { + throw new IllegalArgumentException("์ง‘๊ณ„ ๊ฒฐ๊ณผ(metrics)๊ฐ€ null์ž…๋‹ˆ๋‹ค."); } - try { - Long productId = (Long) row[0]; - long viewCount = ((Number) row[1]).longValue(); - long likeCount = ((Number) row[2]).longValue(); - long salesCount = ((Number) row[3]).longValue(); - long orderCount = ((Number) row[4]).longValue(); - BigDecimal totalSalesAmount = (BigDecimal) row[5]; + long totalScore = calculator.calculate( + metrics.viewCount(), + metrics.likeCount(), + metrics.totalSalesAmount() + ); - long totalScore = calculator.calculate(viewCount, likeCount, totalSalesAmount); - - return new RankingAggregation(productId, viewCount, likeCount, salesCount, orderCount, totalSalesAmount, totalScore); - } catch (ClassCastException | NullPointerException e) { - throw new IllegalArgumentException("์ง‘๊ณ„ ๊ฒฐ๊ณผ ๋ฐ์ดํ„ฐ ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค.", e); - } + return new RankingAggregation( + metrics.productId(), + metrics.viewCount(), + metrics.likeCount(), + metrics.salesCount(), + metrics.orderCount(), + metrics.totalSalesAmount(), + totalScore + ); } /** diff --git a/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/reader/AbstractMetricsReader.java b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/reader/AbstractMetricsReader.java index 3919b2dcd..f500a1004 100644 --- a/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/reader/AbstractMetricsReader.java +++ b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/reader/AbstractMetricsReader.java @@ -8,6 +8,7 @@ import com.loopers.batch.job.ranking.dto.RankingAggregation; import com.loopers.batch.job.ranking.support.RankingAggregator; +import com.loopers.domain.metrics.ProductMetricsAggregation; import com.loopers.domain.metrics.ProductMetricsRepository; import lombok.extern.slf4j.Slf4j; @@ -54,7 +55,7 @@ private void initializeIterator() { log.info("์ง‘๊ณ„ ๊ธฐ๊ฐ„: {} ~ {}", startDate, endDate); // 2. DB์—์„œ ์ง‘๊ณ„ ์ฟผ๋ฆฌ ์‹คํ–‰ - List aggregationResults = productMetricsRepository.aggregateByDateRange(startDate, endDate); + List aggregationResults = productMetricsRepository.aggregateByDateRange(startDate, endDate); log.info("์ง‘๊ณ„ ๋Œ€์ƒ ์ƒํ’ˆ ์ˆ˜: {}", aggregationResults.size()); // 3. ๋žญํ‚น ์ฒ˜๋ฆฌ (์ •๋ ฌ + TOP 100 + ์ˆœ์œ„ ๋ถ€์—ฌ) diff --git a/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/support/RankingAggregator.java b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/support/RankingAggregator.java index 36b809fe2..8fffd5df0 100644 --- a/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/support/RankingAggregator.java +++ b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/support/RankingAggregator.java @@ -3,6 +3,7 @@ import java.util.Comparator; import java.util.List; +import com.loopers.domain.metrics.ProductMetricsAggregation; import org.springframework.stereotype.Component; import com.loopers.batch.job.ranking.dto.RankingAggregation; @@ -29,14 +30,14 @@ public class RankingAggregator { * @param aggregationResults DB ์ง‘๊ณ„ ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ ๋ชฉ๋ก * @return TOP 100 ๋žญํ‚น ๋ชฉ๋ก (์ˆœ์œ„ ๋ถ€์—ฌ ์™„๋ฃŒ) */ - public List processRankings(List aggregationResults) { + public List processRankings(List aggregationResults) { if (aggregationResults == null || aggregationResults.isEmpty()) { return List.of(); } // 1. DTO ๋ณ€ํ™˜ + ์ ์ˆ˜ ๊ณ„์‚ฐ List aggregations = aggregationResults.stream() - .map(row -> RankingAggregation.from(row, scoreCalculator)) + .map(metrics -> RankingAggregation.from(metrics, scoreCalculator)) .toList(); // 2. ์ ์ˆ˜ ๊ธฐ์ค€ ๋‚ด๋ฆผ์ฐจ์ˆœ ์ •๋ ฌ + TOP 100 ํ•„ํ„ฐ๋ง diff --git a/apps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.java b/apps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.java index e8d71f291..a173e83b9 100644 --- a/apps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.java +++ b/apps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.java @@ -3,6 +3,7 @@ import java.time.LocalDate; import java.util.List; +import com.loopers.domain.metrics.ProductMetricsAggregation; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -22,20 +23,21 @@ public interface ProductMetricsJpaRepository extends JpaRepository aggregateByDateRange( + List aggregateByDateRange( @Param("startDate") LocalDate startDate, @Param("endDate") LocalDate endDate); diff --git a/apps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java b/apps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java index 5d914cc34..78ed15b61 100644 --- a/apps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java +++ b/apps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java @@ -4,6 +4,7 @@ import java.util.List; import java.util.Optional; +import com.loopers.domain.metrics.ProductMetricsAggregation; import org.springframework.stereotype.Repository; import com.loopers.domain.metrics.ProductMetricsEntity; @@ -44,7 +45,7 @@ public List findByMetricDateBetween(LocalDate startDate, L } @Override - public List aggregateByDateRange(LocalDate startDate, LocalDate endDate) { + public List aggregateByDateRange(LocalDate startDate, LocalDate endDate) { return jpaRepository.aggregateByDateRange(startDate, endDate); } } diff --git a/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/dto/RankingAggregationUnitTest.java b/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/dto/RankingAggregationUnitTest.java index 9f6270804..16f463c14 100644 --- a/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/dto/RankingAggregationUnitTest.java +++ b/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/dto/RankingAggregationUnitTest.java @@ -6,6 +6,7 @@ import org.junit.jupiter.api.Test; import com.loopers.batch.job.ranking.support.ScoreCalculator; +import com.loopers.domain.metrics.ProductMetricsAggregation; @DisplayName("RankingAggregation ๋‹จ์œ„ ํ…Œ์ŠคํŠธ") class RankingAggregationUnitTest { @@ -20,10 +21,12 @@ class ์ง‘๊ณ„_๊ฒฐ๊ณผ๋กœ๋ถ€ํ„ฐ_์ƒ์„ฑ { @DisplayName("์œ ํšจํ•œ ์ง‘๊ณ„ ๊ฒฐ๊ณผ๋กœ๋ถ€ํ„ฐ ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•œ๋‹ค") void should_create_from_valid_aggregation_result() { // given - Object[] row = {1L, 100L, 50L, 10L, 5L, java.math.BigDecimal.valueOf(1000)}; // productId, view, like, sales, order, amount + ProductMetricsAggregation metrics = new ProductMetricsAggregation( + 1L, 100L, 50L, 10L, 5L, java.math.BigDecimal.valueOf(1000) + ); // when - RankingAggregation aggregation = RankingAggregation.from(row, calculator); + RankingAggregation aggregation = RankingAggregation.from(metrics, calculator); // then Assertions.assertThat(aggregation.getProductId()).isEqualTo(1L); @@ -38,52 +41,12 @@ void should_create_from_valid_aggregation_result() { } @Test - @DisplayName("null ๋ฐฐ์—ด์— ๋Œ€ํ•ด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค") - void should_throw_exception_when_row_is_null() { + @DisplayName("null ๋ฉ”ํŠธ๋ฆญ์— ๋Œ€ํ•ด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค") + void should_throw_exception_when_metrics_is_null() { // given & when & then Assertions.assertThatThrownBy(() -> RankingAggregation.from(null, calculator)) .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("์ง‘๊ณ„ ๊ฒฐ๊ณผ ๋ฐฐ์—ด์ด null์ด๊ฑฐ๋‚˜ ๊ธธ์ด๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค"); - } - - @Test - @DisplayName("๊ธธ์ด๊ฐ€ ๋ถ€์กฑํ•œ ๋ฐฐ์—ด์— ๋Œ€ํ•ด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค") - void should_throw_exception_when_row_length_is_insufficient() { - // given - Object[] shortRow = {1L, 100L, 50L}; // ๊ธธ์ด 3 (6 ๋ฏธ๋งŒ) - - // when & then - Assertions.assertThatThrownBy(() -> RankingAggregation.from(shortRow, calculator)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("์ง‘๊ณ„ ๊ฒฐ๊ณผ ๋ฐฐ์—ด์ด null์ด๊ฑฐ๋‚˜ ๊ธธ์ด๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค"); - } - - @Test - @DisplayName("์ž˜๋ชป๋œ ๋ฐ์ดํ„ฐ ํƒ€์ž…์— ๋Œ€ํ•ด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค") - void should_throw_exception_when_data_type_is_invalid() { - // given - Object[] invalidRow = {"invalid", 100L, 50L, 10L, 5L, java.math.BigDecimal.valueOf(1000)}; // productId๊ฐ€ String - - // when & then - Assertions.assertThatThrownBy(() -> RankingAggregation.from(invalidRow, calculator)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("์ง‘๊ณ„ ๊ฒฐ๊ณผ ๋ฐ์ดํ„ฐ ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค"); - } - - @Test - @DisplayName("Number ํƒ€์ž…์˜ ๋‹ค์–‘ํ•œ ํ˜•ํƒœ๋ฅผ ์ฒ˜๋ฆฌํ•œ๋‹ค") - void should_handle_various_number_types() { - // given - Integer, Long, BigDecimal ๋“ฑ ๋‹ค์–‘ํ•œ Number ํƒ€์ž… - Object[] row = {1L, 100, 50L, 10, 5L, java.math.BigDecimal.valueOf(1000)}; - - // when - RankingAggregation aggregation = RankingAggregation.from(row, calculator); - - // then - Assertions.assertThat(aggregation.getViewCount()).isEqualTo(100L); - Assertions.assertThat(aggregation.getLikeCount()).isEqualTo(50L); - Assertions.assertThat(aggregation.getSalesCount()).isEqualTo(10L); - Assertions.assertThat(aggregation.getOrderCount()).isEqualTo(5L); + .hasMessageContaining("์ง‘๊ณ„ ๊ฒฐ๊ณผ(metrics)๊ฐ€ null์ž…๋‹ˆ๋‹ค."); } } @@ -95,8 +58,10 @@ class ์ˆœ์œ„_๋ถ€์—ฌ { @DisplayName("์œ ํšจํ•œ ์ˆœ์œ„๋ฅผ ๋ถ€์—ฌํ•œ๋‹ค") void should_assign_valid_rank() { // given - Object[] row = {1L, 100L, 50L, 10L, 5L, java.math.BigDecimal.valueOf(1000)}; - RankingAggregation aggregation = RankingAggregation.from(row, calculator); + ProductMetricsAggregation metrics = new ProductMetricsAggregation( + 1L, 100L, 50L, 10L, 5L, java.math.BigDecimal.valueOf(1000) + ); + RankingAggregation aggregation = RankingAggregation.from(metrics, calculator); // when aggregation.assignRank(1); @@ -109,8 +74,10 @@ void should_assign_valid_rank() { @DisplayName("100์œ„๊นŒ์ง€ ์ˆœ์œ„๋ฅผ ๋ถ€์—ฌํ•  ์ˆ˜ ์žˆ๋‹ค") void should_assign_rank_up_to_100() { // given - Object[] row = {1L, 100L, 50L, 10L, 5L, java.math.BigDecimal.valueOf(1000)}; - RankingAggregation aggregation = RankingAggregation.from(row, calculator); + ProductMetricsAggregation metrics = new ProductMetricsAggregation( + 1L, 100L, 50L, 10L, 5L, java.math.BigDecimal.valueOf(1000) + ); + RankingAggregation aggregation = RankingAggregation.from(metrics, calculator); // when aggregation.assignRank(100); @@ -123,8 +90,10 @@ void should_assign_rank_up_to_100() { @DisplayName("0 ์ดํ•˜์˜ ์ˆœ์œ„์— ๋Œ€ํ•ด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค") void should_throw_exception_when_rank_is_zero_or_negative() { // given - Object[] row = {1L, 100L, 50L, 10L, 5L, java.math.BigDecimal.valueOf(1000)}; - RankingAggregation aggregation = RankingAggregation.from(row, calculator); + ProductMetricsAggregation metrics = new ProductMetricsAggregation( + 1L, 100L, 50L, 10L, 5L, java.math.BigDecimal.valueOf(1000) + ); + RankingAggregation aggregation = RankingAggregation.from(metrics, calculator); // when & then Assertions.assertThatThrownBy(() -> aggregation.assignRank(0)) @@ -140,8 +109,10 @@ void should_throw_exception_when_rank_is_zero_or_negative() { @DisplayName("100์„ ์ดˆ๊ณผํ•˜๋Š” ์ˆœ์œ„์— ๋Œ€ํ•ด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค") void should_throw_exception_when_rank_exceeds_100() { // given - Object[] row = {1L, 100L, 50L, 10L, 5L, java.math.BigDecimal.valueOf(1000)}; - RankingAggregation aggregation = RankingAggregation.from(row, calculator); + ProductMetricsAggregation metrics = new ProductMetricsAggregation( + 1L, 100L, 50L, 10L, 5L, java.math.BigDecimal.valueOf(1000) + ); + RankingAggregation aggregation = RankingAggregation.from(metrics, calculator); // when & then Assertions.assertThatThrownBy(() -> aggregation.assignRank(101)) @@ -158,8 +129,10 @@ class ๋ฌธ์ž์—ด_ํ‘œํ˜„ { @DisplayName("toString์ด ์˜ฌ๋ฐ”๋ฅธ ํ˜•์‹์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค") void should_return_correct_string_format() { // given - Object[] row = {1L, 100L, 50L, 10L, 5L, java.math.BigDecimal.valueOf(1000)}; - RankingAggregation aggregation = RankingAggregation.from(row, calculator); + ProductMetricsAggregation metrics = new ProductMetricsAggregation( + 1L, 100L, 50L, 10L, 5L, java.math.BigDecimal.valueOf(1000) + ); + RankingAggregation aggregation = RankingAggregation.from(metrics, calculator); aggregation.assignRank(1); // when diff --git a/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/support/RankingAggregatorUnitTest.java b/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/support/RankingAggregatorUnitTest.java index 818b6970a..b817b2726 100644 --- a/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/support/RankingAggregatorUnitTest.java +++ b/apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/support/RankingAggregatorUnitTest.java @@ -4,6 +4,7 @@ import java.util.ArrayList; import java.util.List; +import com.loopers.domain.metrics.ProductMetricsAggregation; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -25,10 +26,10 @@ class ๋žญํ‚น_์ฒ˜๋ฆฌ { @DisplayName("์ง‘๊ณ„ ๊ฒฐ๊ณผ๋ฅผ ์ ์ˆ˜ ๊ธฐ์ค€์œผ๋กœ ์ •๋ ฌํ•˜๊ณ  ์ˆœ์œ„๋ฅผ ๋ถ€์—ฌํ•œ๋‹ค") void should_sort_by_score_and_assign_ranks() { // given - List results = List.of( - new Object[]{1L, 100L, 10L, 5L, 2L , new BigDecimal(0)}, - new Object[]{2L, 200L, 20L, 10L, 4L, new BigDecimal(0)}, - new Object[]{3L, 50L, 5L, 2L, 1L, new BigDecimal(0)} + List results = List.of( + new ProductMetricsAggregation(1L, 100L, 10L, 5L, 2L , new BigDecimal(0)), + new ProductMetricsAggregation(2L, 200L, 20L, 10L, 4L, new BigDecimal(0)), + new ProductMetricsAggregation(3L, 50L, 5L, 2L, 1L, new BigDecimal(0)) ); // when @@ -40,25 +41,25 @@ void should_sort_by_score_and_assign_ranks() { // ์ ์ˆ˜ ๊ธฐ์ค€ ๋‚ด๋ฆผ์ฐจ์ˆœ ์ •๋ ฌ ํ™•์ธ Assertions.assertThat(rankings.get(0).getProductId()).isEqualTo(2L); // 1์œ„ Assertions.assertThat(rankings.get(0).getRankPosition()).isEqualTo(1); - Assertions.assertThat(rankings.get(0).getTotalScore()).isEqualTo(240L); - + Assertions.assertThat(rankings.get(0).getTotalScore()).isEqualTo(240L); // (200*0.1 + 20*0.2 + log(1)*0.6) * 10 = (20 + 4 + 0) * 10 = 240 + Assertions.assertThat(rankings.get(1).getProductId()).isEqualTo(1L); // 2์œ„ Assertions.assertThat(rankings.get(1).getRankPosition()).isEqualTo(2); - Assertions.assertThat(rankings.get(1).getTotalScore()).isEqualTo(120L); + Assertions.assertThat(rankings.get(1).getTotalScore()).isEqualTo(120L); // (100*0.1 + 10*0.2) * 10 = 120 Assertions.assertThat(rankings.get(2).getProductId()).isEqualTo(3L); // 3์œ„ Assertions.assertThat(rankings.get(2).getRankPosition()).isEqualTo(3); - Assertions.assertThat(rankings.get(2).getTotalScore()).isEqualTo(60L); + Assertions.assertThat(rankings.get(2).getTotalScore()).isEqualTo(60L); // (50*0.1 + 5*0.2) * 10 = 60 } @Test @DisplayName("TOP 100์„ ์ดˆ๊ณผํ•˜๋Š” ๊ฒฐ๊ณผ๋Š” ํ•„ํ„ฐ๋ง๋œ๋‹ค") void should_filter_results_beyond_top_100() { // given - 150๊ฐœ์˜ ๊ฒฐ๊ณผ ์ƒ์„ฑ - List results = new ArrayList<>(); + List results = new ArrayList<>(); for (int i = 1; i <= 150; i++) { // ์ ์ˆ˜๊ฐ€ ๋†’์€ ์ˆœ์„œ๋Œ€๋กœ ์ƒ์„ฑ (i๊ฐ€ ํด์ˆ˜๋ก ์ ์ˆ˜ ๋†’์Œ) - results.add(new Object[]{(long) i, (long) i * 10, (long) i, (long) i, (long) i, new BigDecimal(i)}); + results.add(new ProductMetricsAggregation((long) i, (long) i * 10, (long) i, (long) i, (long) i, new BigDecimal(i))); } // when @@ -74,7 +75,7 @@ void should_filter_results_beyond_top_100() { @DisplayName("๋นˆ ๊ฒฐ๊ณผ์— ๋Œ€ํ•ด ๋นˆ ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค") void should_return_empty_list_for_empty_results() { // given - List emptyResults = List.of(); + List emptyResults = List.of(); // when List rankings = aggregator.processRankings(emptyResults); @@ -97,10 +98,10 @@ void should_return_empty_list_for_null_results() { @DisplayName("๋™์ผํ•œ ์ ์ˆ˜์˜ ์ƒํ’ˆ๋“ค์€ ์ˆœ์„œ๊ฐ€ ์œ ์ง€๋œ๋‹ค") void should_maintain_order_for_same_scores() { // given - ๋™์ผํ•œ ์ ์ˆ˜๋ฅผ ๊ฐ€์ง„ ์ƒํ’ˆ๋“ค - List results = List.of( - new Object[]{1L, 100L, 0L, 0L, 0L, new BigDecimal(0)}, // score = 100 - new Object[]{2L, 100L, 0L, 0L, 0L, new BigDecimal(0)}, // score = 100 - new Object[]{3L, 100L, 0L, 0L, 0L, new BigDecimal(0)} // score = 100 + List results = List.of( + new ProductMetricsAggregation(1L, 100L, 0L, 0L, 0L, new BigDecimal(0)), // score = 100 + new ProductMetricsAggregation(2L, 100L, 0L, 0L, 0L, new BigDecimal(0)), // score = 100 + new ProductMetricsAggregation(3L, 100L, 0L, 0L, 0L, new BigDecimal(0)) // score = 100 ); // when diff --git a/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.java b/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.java index cbdc2bea6..929653aab 100644 --- a/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.java +++ b/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.java @@ -4,6 +4,7 @@ import java.util.List; import java.util.Optional; +import com.loopers.domain.metrics.ProductMetricsAggregation; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -45,17 +46,18 @@ List findByMetricDateBetween( * ๊ธฐ๊ฐ„๋ณ„ ์ƒํ’ˆ ์ง‘๊ณ„ (GROUP BY) */ @Query(""" - SELECT m.id.productId, + SELECT new com.loopers.domain.metrics.ProductMetricsAggregation( + m.id.productId, SUM(m.viewCount), SUM(m.likeCount), SUM(m.salesCount), SUM(m.orderCount), - SUM(m.totalSalesAmount) + SUM(m.totalSalesAmount)) FROM ProductMetricsEntity m WHERE m.id.metricDate BETWEEN :startDate AND :endDate GROUP BY m.id.productId """) - List aggregateByDateRange( + List aggregateByDateRange( @Param("startDate") LocalDate startDate, @Param("endDate") LocalDate endDate); } diff --git a/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java b/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java index 015e88095..461aebcb9 100644 --- a/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java +++ b/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java @@ -6,6 +6,7 @@ import org.springframework.stereotype.Component; +import com.loopers.domain.metrics.ProductMetricsAggregation; import com.loopers.domain.metrics.ProductMetricsEntity; import com.loopers.domain.metrics.ProductMetricsId; import com.loopers.domain.metrics.ProductMetricsRepository; @@ -45,7 +46,7 @@ public List findByMetricDateBetween(LocalDate startDate, L } @Override - public List aggregateByDateRange(LocalDate startDate, LocalDate endDate) { + public List aggregateByDateRange(LocalDate startDate, LocalDate endDate) { return productMetricsJpaRepository.aggregateByDateRange(startDate, endDate); } } diff --git a/modules/jpa/src/main/generated/com/loopers/domain/ranking/QWeeklyRankEntity.java b/modules/jpa/src/main/generated/com/loopers/domain/ranking/QWeeklyRankEntity.java index f49bb509a..c0cefa113 100644 --- a/modules/jpa/src/main/generated/com/loopers/domain/ranking/QWeeklyRankEntity.java +++ b/modules/jpa/src/main/generated/com/loopers/domain/ranking/QWeeklyRankEntity.java @@ -30,7 +30,7 @@ public class QWeeklyRankEntity extends EntityPathBase { public final NumberPath orderCount = createNumber("orderCount", Long.class); - public final NumberPath rankPosition = createNumber("rankPosition", Long.class); + public final NumberPath rankPosition = createNumber("rankPosition", Integer.class); public final NumberPath salesCount = createNumber("salesCount", Long.class); diff --git a/modules/jpa/src/main/java/com/loopers/domain/metrics/ProductMetricsEntity.java b/modules/jpa/src/main/java/com/loopers/domain/metrics/ProductMetricsEntity.java index 7b3b27828..c1131478f 100644 --- a/modules/jpa/src/main/java/com/loopers/domain/metrics/ProductMetricsEntity.java +++ b/modules/jpa/src/main/java/com/loopers/domain/metrics/ProductMetricsEntity.java @@ -104,6 +104,7 @@ public void incrementView(ZonedDateTime eventTime) { * @param eventTime ์ด๋ฒคํŠธ ๋ฐœ์ƒ ์‹œ๊ฐ„ */ public void applyLikeDelta(int delta, ZonedDateTime eventTime) { + Objects.requireNonNull(eventTime, "์ด๋ฒคํŠธ ์‹œ๊ฐ„์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."); long next = this.likeCount + delta; this.likeCount = Math.max(0, next); this.lastEventAt = eventTime; diff --git a/modules/jpa/src/main/java/com/loopers/domain/metrics/ProductMetricsRepository.java b/modules/jpa/src/main/java/com/loopers/domain/metrics/ProductMetricsRepository.java index ec3c37cdd..b930ba9a5 100644 --- a/modules/jpa/src/main/java/com/loopers/domain/metrics/ProductMetricsRepository.java +++ b/modules/jpa/src/main/java/com/loopers/domain/metrics/ProductMetricsRepository.java @@ -54,7 +54,7 @@ public interface ProductMetricsRepository { * * @param startDate ์‹œ์ž‘ ๋‚ ์งœ * @param endDate ์ข…๋ฃŒ ๋‚ ์งœ - * @return ์ง‘๊ณ„ ๊ฒฐ๊ณผ (productId, viewCount, likeCount, salesCount, orderCount) + * @return ์ง‘๊ณ„ ๊ฒฐ๊ณผ ๋ชฉ๋ก */ - List aggregateByDateRange(LocalDate startDate, LocalDate endDate); + List aggregateByDateRange(LocalDate startDate, LocalDate endDate); } diff --git a/modules/jpa/src/main/java/com/loopers/domain/ranking/MonthlyRankEntity.java b/modules/jpa/src/main/java/com/loopers/domain/ranking/MonthlyRankEntity.java index e4689f76d..d6f25268c 100644 --- a/modules/jpa/src/main/java/com/loopers/domain/ranking/MonthlyRankEntity.java +++ b/modules/jpa/src/main/java/com/loopers/domain/ranking/MonthlyRankEntity.java @@ -46,7 +46,7 @@ public class MonthlyRankEntity { private long totalScore; @Column(name = "base_rank_position", nullable = false) - private long rankPosition; + private int rankPosition; @Column(name = "created_at", nullable = false) private LocalDateTime createdAt; diff --git a/modules/jpa/src/main/java/com/loopers/domain/ranking/WeeklyRankEntity.java b/modules/jpa/src/main/java/com/loopers/domain/ranking/WeeklyRankEntity.java index a54c81390..310bee042 100644 --- a/modules/jpa/src/main/java/com/loopers/domain/ranking/WeeklyRankEntity.java +++ b/modules/jpa/src/main/java/com/loopers/domain/ranking/WeeklyRankEntity.java @@ -20,7 +20,7 @@ @Table( name = "mv_product_rank_weekly", indexes = { - @Index(name = "idx_year_week_rank", columnList = "year_week, rank_position") + @Index(name = "idx_year_week_rank", columnList = "base_year_week, base_rank_position") } ) @Getter @@ -45,8 +45,8 @@ public class WeeklyRankEntity { @Column(name = "total_score", nullable = false) private long totalScore; - @Column(name = "rank_position", nullable = false) - private long rankPosition; + @Column(name = "base_rank_position", nullable = false) + private int rankPosition; @Column(name = "created_at", nullable = false) private LocalDateTime createdAt; diff --git a/modules/jpa/src/main/java/com/loopers/domain/ranking/WeeklyRankId.java b/modules/jpa/src/main/java/com/loopers/domain/ranking/WeeklyRankId.java index 4d16f68b8..7bfc86650 100644 --- a/modules/jpa/src/main/java/com/loopers/domain/ranking/WeeklyRankId.java +++ b/modules/jpa/src/main/java/com/loopers/domain/ranking/WeeklyRankId.java @@ -22,7 +22,7 @@ public class WeeklyRankId implements Serializable { @Column(name = "product_id", nullable = false) private Long productId; - @Column(name = "year_week", nullable = false, length = 8) + @Column(name = "base_year_week", nullable = false, length = 8) private String yearWeek; // e.g., "2024-W52" private WeeklyRankId(Long productId, String yearWeek) {