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/76] =?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/76] =?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/76] =?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/76] =?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/76] =?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/76] =?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/76] =?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/76] =?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/76] =?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/76] =?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/76] =?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/76] =?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/76] =?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/76] =?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/76] =?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/76] =?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/76] =?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/76] =?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/76] =?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/76] =?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/76] =?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/76] =?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/76] =?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/76] 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/76] =?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/76] =?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/76] =?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/76] =?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/76] =?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/76] =?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/76] =?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/76] =?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/76] =?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/76] =?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/76] =?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/76] =?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/76] =?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/76] =?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/76] 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/76] =?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/76] =?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/76] =?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/76] =?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/76] =?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/76] =?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/76] =?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/76] =?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/76] =?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/76] =?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/76] =?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/76] =?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/76] =?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/76] =?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/76] =?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/76] =?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/76] =?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/76] =?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/76] =?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/76] =?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/76] =?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/76] =?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/76] =?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/76] =?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/76] =?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/76] 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 72517a88b6e0807431eef51ce464544d2f300ce5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A7=80=ED=99=98?= <91196025+Kimjipang@users.noreply.github.com> Date: Thu, 25 Dec 2025 04:41:10 +0900 Subject: [PATCH 66/76] =?UTF-8?q?feat:=20=EC=83=81=ED=92=88=20=EC=A7=91?= =?UTF-8?q?=EA=B3=84=20=EC=A0=95=EB=B3=B4=20Redis=20ZSET=EC=97=90=20?= =?UTF-8?q?=EC=A0=81=EC=9E=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../consumer/KafkaOutboxConsumer.java | 44 +++++++++++++++++-- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/KafkaOutboxConsumer.java b/apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/KafkaOutboxConsumer.java index 086880a45..d08f9c1bb 100644 --- a/apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/KafkaOutboxConsumer.java +++ b/apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/KafkaOutboxConsumer.java @@ -9,21 +9,33 @@ import com.loopers.domain.ProductMetricRepository; import lombok.RequiredArgsConstructor; import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.core.ZSetOperations; import org.springframework.kafka.annotation.KafkaListener; import org.springframework.kafka.support.Acknowledgment; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; +import java.time.Duration; +import java.time.LocalDate; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; import java.util.List; +import java.util.Map; @Component @RequiredArgsConstructor public class KafkaOutboxConsumer { private final ProductMetricRepository productMetricRepository; private final ObjectMapper objectMapper; + private final StringRedisTemplate redisTemplate; + + private static final ZoneId KST = ZoneId.of("Asia/Seoul"); + private static final DateTimeFormatter YYYYMMDD = DateTimeFormatter.BASIC_ISO_DATE; @KafkaListener( - topics = {"product-viewed", "product-liked"}, + topics = {"product-viewed", "product-liked", "product-sales"}, containerFactory = KafkaConfig.BATCH_LISTENER ) @Transactional @@ -31,23 +43,47 @@ public void productViewedListener( List> messages, Acknowledgment acknowledgment ) throws JsonProcessingException { + + // 1) ๋ฐฐ์น˜ ๋‚ด productId๋ณ„ ์ ์ˆ˜ ๋ˆ„์  + Map scoreDelta = new HashMap<>(); + for (var record : messages) { OutboxEvent value = objectMapper.readValue(record.value(), OutboxEvent.class); Long productId = value.aggregateId(); - String eventType = value.eventType(); + ProductEventType eventType = ProductEventType.valueOf(value.eventType()); + + scoreDelta.merge(productId, weight(eventType), Double::sum); ProductMetric productMetric = productMetricRepository.findByProductId(productId); if (productMetric == null) { - ProductMetric newProductMetric = ProductMetric.of(productId, ProductEventType.valueOf(eventType)); + ProductMetric newProductMetric = ProductMetric.of(productId, eventType); productMetricRepository.save(newProductMetric); } else { - productMetric.increaseProductMetric(ProductEventType.valueOf(eventType)); + productMetric.increaseProductMetric(eventType); } + } + String key = "ranking:all:" + LocalDate.now(KST).format(YYYYMMDD); + ZSetOperations zset = redisTemplate.opsForZSet(); + + for (var e : scoreDelta.entrySet()) { + zset.incrementScore(key, String.valueOf(e.getKey()), e.getValue()); // ZINCRBY } + + // 3) ์ผ๊ฐ„ ํ‚ค๋Š” TTL ๊ฑธ์–ด๋‘๋Š” ๊ฒŒ ์šด์˜์— ์œ ๋ฆฌ (์˜ˆ: 8์ผ ๋ณด๊ด€) + redisTemplate.expire(key, Duration.ofDays(8)); + acknowledgment.acknowledge(); } + + double weight(ProductEventType eventType) { + return switch (eventType) { + case PRODUCT_VIEWED -> 0.1; + case PRODUCT_LIKED -> 0.3; + case PRODUCT_SALES -> 0.6; + }; + } } From 1cfeaa3cab455286ad2d9ba6fff0ebf04b858939 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A7=80=ED=99=98?= <91196025+Kimjipang@users.noreply.github.com> Date: Fri, 26 Dec 2025 16:52:26 +0900 Subject: [PATCH 67/76] =?UTF-8?q?feat:=20custom=20exception=20=EC=A0=95?= =?UTF-8?q?=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../loopers/support/error/CoreException.java | 19 +++++++++++++++++++ .../com/loopers/support/error/ErrorType.java | 19 +++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 apps/commerce-streamer/src/main/java/com/loopers/support/error/CoreException.java create mode 100644 apps/commerce-streamer/src/main/java/com/loopers/support/error/ErrorType.java diff --git a/apps/commerce-streamer/src/main/java/com/loopers/support/error/CoreException.java b/apps/commerce-streamer/src/main/java/com/loopers/support/error/CoreException.java new file mode 100644 index 000000000..bc3017f7e --- /dev/null +++ b/apps/commerce-streamer/src/main/java/com/loopers/support/error/CoreException.java @@ -0,0 +1,19 @@ +package com.loopers.support.error; + +import lombok.Getter; + +@Getter +public class CoreException extends RuntimeException { + private final ErrorType errorType; + private final String customMessage; + + public CoreException(ErrorType errorType) { + this(errorType, null); + } + + public CoreException(ErrorType errorType, String customMessage) { + super(customMessage != null ? customMessage : errorType.getMessage()); + this.errorType = errorType; + this.customMessage = customMessage; + } +} diff --git a/apps/commerce-streamer/src/main/java/com/loopers/support/error/ErrorType.java b/apps/commerce-streamer/src/main/java/com/loopers/support/error/ErrorType.java new file mode 100644 index 000000000..5200f7dcd --- /dev/null +++ b/apps/commerce-streamer/src/main/java/com/loopers/support/error/ErrorType.java @@ -0,0 +1,19 @@ +package com.loopers.support.error; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@Getter +@RequiredArgsConstructor +public enum ErrorType { + /** ๋ฒ”์šฉ ์—๋Ÿฌ */ + INTERNAL_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase(), "์ผ์‹œ์ ์ธ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."), + BAD_REQUEST(HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST.getReasonPhrase(), "์ž˜๋ชป๋œ ์š”์ฒญ์ž…๋‹ˆ๋‹ค."), + NOT_FOUND(HttpStatus.NOT_FOUND, HttpStatus.NOT_FOUND.getReasonPhrase(), "์กด์žฌํ•˜์ง€ ์•Š๋Š” ์š”์ฒญ์ž…๋‹ˆ๋‹ค."), + CONFLICT(HttpStatus.CONFLICT, HttpStatus.CONFLICT.getReasonPhrase(), "์ด๋ฏธ ์กด์žฌํ•˜๋Š” ๋ฆฌ์†Œ์Šค์ž…๋‹ˆ๋‹ค."); + + private final HttpStatus status; + private final String code; + private final String message; +} From e102c1d365dd31ea6cbdb11d96a3a0abdc17264f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A7=80=ED=99=98?= <91196025+Kimjipang@users.noreply.github.com> Date: Fri, 26 Dec 2025 16:53:40 +0900 Subject: [PATCH 68/76] =?UTF-8?q?feat:=20custom=20response=20body=20?= =?UTF-8?q?=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../interfaces/api/ApiControllerAdvice.java | 126 ++++++++++++++++++ .../loopers/interfaces/api/ApiResponse.java | 32 +++++ 2 files changed, 158 insertions(+) create mode 100644 apps/commerce-streamer/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java create mode 100644 apps/commerce-streamer/src/main/java/com/loopers/interfaces/api/ApiResponse.java diff --git a/apps/commerce-streamer/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java b/apps/commerce-streamer/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java new file mode 100644 index 000000000..afb3f13f5 --- /dev/null +++ b/apps/commerce-streamer/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java @@ -0,0 +1,126 @@ +package com.loopers.interfaces.api; + +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.exc.InvalidFormatException; +import com.fasterxml.jackson.databind.exc.MismatchedInputException; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.web.bind.MissingServletRequestParameterException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import org.springframework.web.server.ServerWebInputException; +import org.springframework.web.servlet.resource.NoResourceFoundException; + +import java.util.Arrays; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +@RestControllerAdvice +@Slf4j +public class ApiControllerAdvice { + @ExceptionHandler + public ResponseEntity> handle(CoreException e) { + log.warn("CoreException : {}", e.getCustomMessage() != null ? e.getCustomMessage() : e.getMessage(), e); + return failureResponse(e.getErrorType(), e.getCustomMessage()); + } + + @ExceptionHandler + public ResponseEntity> handleBadRequest(MethodArgumentTypeMismatchException e) { + String name = e.getName(); + String type = e.getRequiredType() != null ? e.getRequiredType().getSimpleName() : "unknown"; + String value = e.getValue() != null ? e.getValue().toString() : "null"; + String message = String.format("์š”์ฒญ ํŒŒ๋ผ๋ฏธํ„ฐ '%s' (ํƒ€์ž…: %s)์˜ ๊ฐ’ '%s'์ด(๊ฐ€) ์ž˜๋ชป๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", name, type, value); + return failureResponse(ErrorType.BAD_REQUEST, message); + } + + @ExceptionHandler + public ResponseEntity> handleBadRequest(MissingServletRequestParameterException e) { + String name = e.getParameterName(); + String type = e.getParameterType(); + String message = String.format("ํ•„์ˆ˜ ์š”์ฒญ ํŒŒ๋ผ๋ฏธํ„ฐ '%s' (ํƒ€์ž…: %s)๊ฐ€ ๋ˆ„๋ฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", name, type); + return failureResponse(ErrorType.BAD_REQUEST, message); + } + + @ExceptionHandler + public ResponseEntity> handleBadRequest(HttpMessageNotReadableException e) { + String errorMessage; + Throwable rootCause = e.getRootCause(); + + if (rootCause instanceof InvalidFormatException invalidFormat) { + String fieldName = invalidFormat.getPath().stream() + .map(ref -> ref.getFieldName() != null ? ref.getFieldName() : "?") + .collect(Collectors.joining(".")); + + String valueIndicationMessage = ""; + if (invalidFormat.getTargetType().isEnum()) { + Class enumClass = invalidFormat.getTargetType(); + String enumValues = Arrays.stream(enumClass.getEnumConstants()) + .map(Object::toString) + .collect(Collectors.joining(", ")); + valueIndicationMessage = "์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๊ฐ’ : [" + enumValues + "]"; + } + + String expectedType = invalidFormat.getTargetType().getSimpleName(); + Object value = invalidFormat.getValue(); + + errorMessage = String.format("ํ•„๋“œ '%s'์˜ ๊ฐ’ '%s'์ด(๊ฐ€) ์˜ˆ์ƒ ํƒ€์ž…(%s)๊ณผ ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. %s", + fieldName, value, expectedType, valueIndicationMessage); + + } else if (rootCause instanceof MismatchedInputException mismatchedInput) { + String fieldPath = mismatchedInput.getPath().stream() + .map(ref -> ref.getFieldName() != null ? ref.getFieldName() : "?") + .collect(Collectors.joining(".")); + errorMessage = String.format("ํ•„์ˆ˜ ํ•„๋“œ '%s'์ด(๊ฐ€) ๋ˆ„๋ฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", fieldPath); + + } else if (rootCause instanceof JsonMappingException jsonMapping) { + String fieldPath = jsonMapping.getPath().stream() + .map(ref -> ref.getFieldName() != null ? ref.getFieldName() : "?") + .collect(Collectors.joining(".")); + errorMessage = String.format("ํ•„๋“œ '%s'์—์„œ JSON ๋งคํ•‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: %s", + fieldPath, jsonMapping.getOriginalMessage()); + + } else { + errorMessage = "์š”์ฒญ ๋ณธ๋ฌธ์„ ์ฒ˜๋ฆฌํ•˜๋Š” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. JSON ๋ฉ”์„ธ์ง€ ๊ทœ๊ฒฉ์„ ํ™•์ธํ•ด์ฃผ์„ธ์š”."; + } + + return failureResponse(ErrorType.BAD_REQUEST, errorMessage); + } + + @ExceptionHandler + public ResponseEntity> handleBadRequest(ServerWebInputException e) { + String missingParams = extractMissingParameter(e.getReason() != null ? e.getReason() : ""); + if (!missingParams.isEmpty()) { + String message = String.format("ํ•„์ˆ˜ ์š”์ฒญ ๊ฐ’ '%s'๊ฐ€ ๋ˆ„๋ฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", missingParams); + return failureResponse(ErrorType.BAD_REQUEST, message); + } else { + return failureResponse(ErrorType.BAD_REQUEST, null); + } + } + + @ExceptionHandler + public ResponseEntity> handleNotFound(NoResourceFoundException e) { + return failureResponse(ErrorType.NOT_FOUND, null); + } + + @ExceptionHandler + public ResponseEntity> handle(Throwable e) { + log.error("Exception : {}", e.getMessage(), e); + return failureResponse(ErrorType.INTERNAL_ERROR, null); + } + + private String extractMissingParameter(String message) { + Pattern pattern = Pattern.compile("'(.+?)'"); + Matcher matcher = pattern.matcher(message); + return matcher.find() ? matcher.group(1) : ""; + } + + private ResponseEntity> failureResponse(ErrorType errorType, String errorMessage) { + return ResponseEntity.status(errorType.getStatus()) + .body(ApiResponse.fail(errorType.getCode(), errorMessage != null ? errorMessage : errorType.getMessage())); + } +} diff --git a/apps/commerce-streamer/src/main/java/com/loopers/interfaces/api/ApiResponse.java b/apps/commerce-streamer/src/main/java/com/loopers/interfaces/api/ApiResponse.java new file mode 100644 index 000000000..2e3b8b282 --- /dev/null +++ b/apps/commerce-streamer/src/main/java/com/loopers/interfaces/api/ApiResponse.java @@ -0,0 +1,32 @@ +package com.loopers.interfaces.api; + +public record ApiResponse(Metadata meta, T data) { + public record Metadata(Result result, String errorCode, String message) { + public enum Result { + SUCCESS, FAIL + } + + public static Metadata success() { + return new Metadata(Result.SUCCESS, null, null); + } + + public static Metadata fail(String errorCode, String errorMessage) { + return new Metadata(Result.FAIL, errorCode, errorMessage); + } + } + + public static ApiResponse success() { + return new ApiResponse<>(Metadata.success(), null); + } + + public static ApiResponse success(T data) { + return new ApiResponse<>(Metadata.success(), data); + } + + public static ApiResponse fail(String errorCode, String errorMessage) { + return new ApiResponse<>( + Metadata.fail(errorCode, errorMessage), + null + ); + } +} From cc0c13919bfd3ad08d0ca74666e785e0faeb4398 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A7=80=ED=99=98?= <91196025+Kimjipang@users.noreply.github.com> Date: Fri, 26 Dec 2025 16:55:32 +0900 Subject: [PATCH 69/76] =?UTF-8?q?chore:=20=EC=A3=BC=EC=84=9D=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/interfaces/consumer/KafkaOutboxConsumer.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/KafkaOutboxConsumer.java b/apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/KafkaOutboxConsumer.java index d08f9c1bb..79f4a5843 100644 --- a/apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/KafkaOutboxConsumer.java +++ b/apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/KafkaOutboxConsumer.java @@ -44,7 +44,6 @@ public void productViewedListener( Acknowledgment acknowledgment ) throws JsonProcessingException { - // 1) ๋ฐฐ์น˜ ๋‚ด productId๋ณ„ ์ ์ˆ˜ ๋ˆ„์  Map scoreDelta = new HashMap<>(); for (var record : messages) { @@ -73,8 +72,7 @@ public void productViewedListener( zset.incrementScore(key, String.valueOf(e.getKey()), e.getValue()); // ZINCRBY } - // 3) ์ผ๊ฐ„ ํ‚ค๋Š” TTL ๊ฑธ์–ด๋‘๋Š” ๊ฒŒ ์šด์˜์— ์œ ๋ฆฌ (์˜ˆ: 8์ผ ๋ณด๊ด€) - redisTemplate.expire(key, Duration.ofDays(8)); + redisTemplate.expire(key, Duration.ofDays(2)); acknowledgment.acknowledge(); } From a75c7efeab70c888f20170d6c7ec377330383099 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A7=80=ED=99=98?= <91196025+Kimjipang@users.noreply.github.com> Date: Fri, 26 Dec 2025 16:56:06 +0900 Subject: [PATCH 70/76] =?UTF-8?q?feat:=20=EC=9D=B8=EA=B8=B0=EC=83=81?= =?UTF-8?q?=ED=92=88=20=EC=A1=B0=ED=9A=8C=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../loopers/application/RankingFacade.java | 70 +++++++++++++++++++ .../api/ranking/RankingV1ApiSpec.java | 9 +++ .../api/ranking/RankingV1Controller.java | 27 +++++++ .../interfaces/api/ranking/RankingV1Dto.java | 20 ++++++ 4 files changed, 126 insertions(+) create mode 100644 apps/commerce-streamer/src/main/java/com/loopers/application/RankingFacade.java create mode 100644 apps/commerce-streamer/src/main/java/com/loopers/interfaces/api/ranking/RankingV1ApiSpec.java create mode 100644 apps/commerce-streamer/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java create mode 100644 apps/commerce-streamer/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Dto.java diff --git a/apps/commerce-streamer/src/main/java/com/loopers/application/RankingFacade.java b/apps/commerce-streamer/src/main/java/com/loopers/application/RankingFacade.java new file mode 100644 index 000000000..f60579071 --- /dev/null +++ b/apps/commerce-streamer/src/main/java/com/loopers/application/RankingFacade.java @@ -0,0 +1,70 @@ +package com.loopers.application; + +import com.loopers.interfaces.api.ranking.RankingV1Dto; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.core.ZSetOperations; +import org.springframework.stereotype.Component; + +import java.time.LocalDate; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +@Component +@RequiredArgsConstructor +public class RankingFacade { + private final StringRedisTemplate redisTemplate; + private static final ZoneId KST = ZoneId.of("Asia/Seoul"); + private static final DateTimeFormatter YYYYMMDD = DateTimeFormatter.BASIC_ISO_DATE; + + public RankingV1Dto.ProductRankingPageResponse getDailyProductRanking(int page, int size) { + if (page < 1) page = 1; + if (size < 1) size = 20; + + String date = LocalDate.now(KST).format(YYYYMMDD); + + String key = "ranking:all:" + date; + ZSetOperations zset = redisTemplate.opsForZSet(); + + Long total = zset.size(key); + long totalElements = (total == null) ? 0 : total; + + if (totalElements == 0) { + return new RankingV1Dto.ProductRankingPageResponse(date, page, size, 0, 0, List.of()); + } + + long start = (long) (page - 1) * size; + long end = start + size - 1; + + if (start >= totalElements) { + int totalPages = (int) Math.ceil((double) totalElements / size); + return new RankingV1Dto.ProductRankingPageResponse(date, page, size, totalElements, totalPages, List.of()); + } + + Set> tuples = + zset.reverseRangeWithScores(key, start, end); + + List items = new ArrayList<>(); + if (tuples != null) { + long rank = start + 1; + for (var t : tuples) { + String member = t.getValue(); + Double score = t.getScore(); + if (member == null || score == null) continue; + + items.add(new RankingV1Dto.ProductRankingResponse( + rank++, + Long.parseLong(member), + score + )); + } + } + + int totalPages = (int) Math.ceil((double) totalElements / size); + return new RankingV1Dto.ProductRankingPageResponse(date, page, size, totalElements, totalPages, items); + } + +} diff --git a/apps/commerce-streamer/src/main/java/com/loopers/interfaces/api/ranking/RankingV1ApiSpec.java b/apps/commerce-streamer/src/main/java/com/loopers/interfaces/api/ranking/RankingV1ApiSpec.java new file mode 100644 index 000000000..7a4b6e4ba --- /dev/null +++ b/apps/commerce-streamer/src/main/java/com/loopers/interfaces/api/ranking/RankingV1ApiSpec.java @@ -0,0 +1,9 @@ +package com.loopers.interfaces.api.ranking; + +import com.loopers.interfaces.api.ApiResponse; + +public interface RankingV1ApiSpec { + + ApiResponse getDailyProductRanking(int size, int page); + +} diff --git a/apps/commerce-streamer/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java b/apps/commerce-streamer/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java new file mode 100644 index 000000000..44a81a981 --- /dev/null +++ b/apps/commerce-streamer/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java @@ -0,0 +1,27 @@ +package com.loopers.interfaces.api.ranking; + +import com.loopers.application.RankingFacade; +import com.loopers.interfaces.api.ApiResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/rankings") +@RequiredArgsConstructor +public class RankingV1Controller implements RankingV1ApiSpec { + private final RankingFacade rankingFacade; + + @GetMapping + @Override + public ApiResponse getDailyProductRanking( + @RequestParam int size, + @RequestParam int page + ) { + RankingV1Dto.ProductRankingPageResponse response = rankingFacade.getDailyProductRanking(page, size); + + return ApiResponse.success(response); + } +} diff --git a/apps/commerce-streamer/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Dto.java b/apps/commerce-streamer/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Dto.java new file mode 100644 index 000000000..6d210553b --- /dev/null +++ b/apps/commerce-streamer/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Dto.java @@ -0,0 +1,20 @@ +package com.loopers.interfaces.api.ranking; + +import java.util.List; + +public class RankingV1Dto { + public record ProductRankingResponse( + Long rank, + Long productId, + double score + ) {} + + public record ProductRankingPageResponse( + String date, + int page, + int size, + long totalElements, + int totalPages, + List items + ) {} +} From b397ce4a9cb1c15bcb653bb0530fcb2648b2ded5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A7=80=ED=99=98?= <91196025+Kimjipang@users.noreply.github.com> Date: Fri, 26 Dec 2025 17:53:04 +0900 Subject: [PATCH 71/76] =?UTF-8?q?feat:=20=EC=83=81=ED=92=88=20=EB=9E=AD?= =?UTF-8?q?=ED=82=B9=20=EC=A0=95=EB=B3=B4=20=EC=A1=B0=ED=9A=8C=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Redis ZSET์œผ๋กœ๋ถ€ํ„ฐ ์ƒํ’ˆ์˜ ๋žญํ‚น ์ •๋ณด๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. - ์ˆœ์œ„(rank) - ์ ์ˆ˜(score) --- .../product/RankingRedisReader.java | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/product/RankingRedisReader.java diff --git a/apps/commerce-api/src/main/java/com/loopers/application/product/RankingRedisReader.java b/apps/commerce-api/src/main/java/com/loopers/application/product/RankingRedisReader.java new file mode 100644 index 000000000..c700c71c9 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/product/RankingRedisReader.java @@ -0,0 +1,40 @@ +package com.loopers.application.product; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisCallback; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.serializer.RedisSerializer; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +@RequiredArgsConstructor +public class RankingRedisReader { + private final StringRedisTemplate redisTemplate; + + public RankingInfo getDailyRanking(String date, Long productId) { + String key = "ranking:all:" + date; + String member = String.valueOf(productId); + + RedisSerializer serializer = redisTemplate.getStringSerializer(); + byte[] keyBytes = serializer.serialize(key); + byte[] memberBytes = serializer.serialize(member); + + @SuppressWarnings("unchecked") + List results = (List) redisTemplate.executePipelined((RedisCallback) connection -> { + connection.zScore(keyBytes, memberBytes); // -> Double (or null) + connection.zRevRank(keyBytes, memberBytes); // -> Long (or null) 0-base + connection.zCard(keyBytes); // -> Long + return null; + }); + + Double score = (Double) results.get(0); + Long revRank0 = (Long) results.get(1); + Long total = (Long) results.get(2); + + Integer rank = (revRank0 == null) ? null : Math.toIntExact(revRank0 + 1); + + return new RankingInfo(date, score, rank, total); + } +} From ccc015efc10ce520454309618340dd2800435030 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A7=80=ED=99=98?= <91196025+Kimjipang@users.noreply.github.com> Date: Fri, 26 Dec 2025 17:55:05 +0900 Subject: [PATCH 72/76] =?UTF-8?q?refactor:=20=EC=83=81=ED=92=88=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=EC=A0=95=EB=B3=B4=20API=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 * ์ƒํ’ˆ ์ƒ์„ธ ์ •๋ณด ์กฐํšŒ ์‹œ ๋žญํ‚น ์ •๋ณด๋„ ํ•จ๊ป˜ ์กฐํšŒํ•˜๋„๋ก ์ˆ˜์ •ํ•˜๋ฉด์„œ ๊ธฐ์กด API ์ˆ˜์ • - ๋ฐ˜ํ™˜ํƒ€์ž… ์ˆ˜์ • - Facade ์ˆ˜์ • - DTO ์ˆ˜์ • --- .../application/product/ProductFacade.java | 14 +++++++++++-- .../product/ProductRankingInfo.java | 20 +++++++++++++++++++ .../application/product/RankingInfo.java | 9 +++++++++ .../api/product/ProductV1ApiSpec.java | 2 +- .../api/product/ProductV1Controller.java | 7 ++++--- .../interfaces/api/product/ProductV1Dto.java | 14 +++++++++++++ 6 files changed, 60 insertions(+), 6 deletions(-) create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/product/ProductRankingInfo.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/product/RankingInfo.java 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 3ca1a15af..a7d22c673 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 @@ -17,6 +17,8 @@ import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; import java.util.List; @Component @@ -25,6 +27,7 @@ public class ProductFacade { private final ProductRepository productRepository; private final BrandRepository brandRepository; private final OutboxRepository outBoxRepository; + private final RankingRedisReader rankingRedisReader; @Transactional public ProductInfo registerProduct(ProductV1Dto.ProductRequest request) { @@ -52,7 +55,7 @@ public List findAllProducts() { @Transactional @Cacheable(value = "product", key = "#id") - public ProductInfo findProductById(Long id) { + public ProductRankingInfo findProductById(Long id) { Product product = productRepository.findById(id).orElseThrow( () -> new CoreException(ErrorType.NOT_FOUND, "์ฐพ๊ณ ์ž ํ•˜๋Š” ์ƒํ’ˆ์ด ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.") ); @@ -65,7 +68,14 @@ public ProductInfo findProductById(Long id) { outBoxRepository.save(outBoxEvent); - return ProductInfo.from(product); + RankingInfo ranking = null; + + try { + String date = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE); + ranking = rankingRedisReader.getDailyRanking(date, product.getId()); + } catch (Exception ignored) {} + + return ProductRankingInfo.from(product, ranking); } @Transactional(readOnly = true) diff --git a/apps/commerce-api/src/main/java/com/loopers/application/product/ProductRankingInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductRankingInfo.java new file mode 100644 index 000000000..8447e4ad7 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductRankingInfo.java @@ -0,0 +1,20 @@ +package com.loopers.application.product; + +import com.loopers.domain.product.Product; + +import java.math.BigDecimal; + +public record ProductRankingInfo(Long id, Long brandId, String name, BigDecimal price, int stock, int likeCount, int rank, double score) { + public static ProductRankingInfo from(Product product, RankingInfo ranking) { + return new ProductRankingInfo( + product.getId(), + product.getBrandId(), + product.getName(), + product.getPrice(), + product.getStock(), + product.getLikeCount(), + ranking.rank(), + ranking.score() + ); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/product/RankingInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/product/RankingInfo.java new file mode 100644 index 000000000..e5dde6afb --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/product/RankingInfo.java @@ -0,0 +1,9 @@ +package com.loopers.application.product; + +public record RankingInfo( + String date, + double score, + int rank, + Long total +) { +} 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 fabd557cb..fd735a62c 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 @@ -15,7 +15,7 @@ public interface ProductV1ApiSpec { ApiResponse> findAllProducts(); @Operation(summary = "์ƒํ’ˆ ์ƒ์„ธ ์กฐํšŒ") - ApiResponse findProductById(Long id); + ApiResponse findProductById(Long id); @Operation(summary = "์ƒํ’ˆ ์ •๋ ฌ ์กฐํšŒ") ApiResponse> findProductsBySortCondition(ProductV1Dto.SearchProductRequest request); 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 a0eb89d65..3cdf5dc61 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 @@ -2,6 +2,7 @@ import com.loopers.application.product.ProductFacade; import com.loopers.application.product.ProductInfo; +import com.loopers.application.product.ProductRankingInfo; import com.loopers.interfaces.api.ApiResponse; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; @@ -42,9 +43,9 @@ public ApiResponse> findAllProducts() { @GetMapping("/{id}") @Override - public ApiResponse findProductById(@PathVariable Long id) { - ProductInfo info = productFacade.findProductById(id); - ProductV1Dto.ProductResponse response = ProductV1Dto.ProductResponse.from(info); + public ApiResponse findProductById(@PathVariable Long id) { + ProductRankingInfo info = productFacade.findProductById(id); + ProductV1Dto.ProductRankingResponse response = ProductV1Dto.ProductRankingResponse.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 index f581a3827..9c7cbd805 100644 --- 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 @@ -1,6 +1,7 @@ package com.loopers.interfaces.api.product; import com.loopers.application.product.ProductInfo; +import com.loopers.application.product.ProductRankingInfo; import com.loopers.domain.product.Product; import com.loopers.support.error.CoreException; import com.loopers.support.error.ErrorType; @@ -8,6 +9,19 @@ import java.math.BigDecimal; public class ProductV1Dto { + public record ProductRankingResponse(Long id, Long brandId, String name, BigDecimal price, int stock, int rank, double score) { + public static ProductRankingResponse from(ProductRankingInfo info) { + return new ProductRankingResponse( + info.id(), + info.brandId(), + info.name(), + info.price(), + info.stock(), + info.rank(), + info.score() + ); + } + } public record ProductResponse(Long id, Long brandId, String name, BigDecimal price, int stock, int likeCount) { public static ProductResponse from(ProductInfo info) { return new ProductResponse( From d2586f9215370f232b015c300d0bfd10eb401b61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A7=80=ED=99=98?= <91196025+Kimjipang@users.noreply.github.com> Date: Sun, 28 Dec 2025 02:00:33 +0900 Subject: [PATCH 73/76] =?UTF-8?q?refactor:=20=EB=9E=AD=ED=82=B9=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20API=20path=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **AS-IS** commerce-streamer์—์„œ ๋žญํ‚น ์ •๋ณด API ์ •์˜ **TO-BE** commerce-streamer์—์„œ๋Š” ์ง‘๊ณ„๋งŒ ํ•˜๋„๋ก ํ•˜๊ธฐ ์œ„ํ•ด commerce-api ๋ชจ๋“ˆ๋กœ ๋žญํ‚น ์ •๋ณด API ์ˆ˜์ • --- .../application/ranking}/RankingFacade.java | 2 +- .../api/ranking/RankingV1ApiSpec.java | 0 .../api/ranking/RankingV1Controller.java | 2 +- .../interfaces/api/ranking/RankingV1Dto.java | 0 .../interfaces/api/ApiControllerAdvice.java | 126 ------------------ .../loopers/interfaces/api/ApiResponse.java | 32 ----- .../loopers/support/error/CoreException.java | 19 --- .../com/loopers/support/error/ErrorType.java | 19 --- 8 files changed, 2 insertions(+), 198 deletions(-) rename apps/{commerce-streamer/src/main/java/com/loopers/application => commerce-api/src/main/java/com/loopers/application/ranking}/RankingFacade.java (95%) rename apps/{commerce-streamer => commerce-api}/src/main/java/com/loopers/interfaces/api/ranking/RankingV1ApiSpec.java (100%) rename apps/{commerce-streamer => commerce-api}/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java (91%) rename apps/{commerce-streamer => commerce-api}/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Dto.java (100%) delete mode 100644 apps/commerce-streamer/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java delete mode 100644 apps/commerce-streamer/src/main/java/com/loopers/interfaces/api/ApiResponse.java delete mode 100644 apps/commerce-streamer/src/main/java/com/loopers/support/error/CoreException.java delete mode 100644 apps/commerce-streamer/src/main/java/com/loopers/support/error/ErrorType.java diff --git a/apps/commerce-streamer/src/main/java/com/loopers/application/RankingFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java similarity index 95% rename from apps/commerce-streamer/src/main/java/com/loopers/application/RankingFacade.java rename to apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java index f60579071..2781eceea 100644 --- a/apps/commerce-streamer/src/main/java/com/loopers/application/RankingFacade.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java @@ -1,4 +1,4 @@ -package com.loopers.application; +package com.loopers.application.ranking; import com.loopers.interfaces.api.ranking.RankingV1Dto; import lombok.RequiredArgsConstructor; diff --git a/apps/commerce-streamer/src/main/java/com/loopers/interfaces/api/ranking/RankingV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1ApiSpec.java similarity index 100% rename from apps/commerce-streamer/src/main/java/com/loopers/interfaces/api/ranking/RankingV1ApiSpec.java rename to apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1ApiSpec.java diff --git a/apps/commerce-streamer/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java similarity index 91% rename from apps/commerce-streamer/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java rename to apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java index 44a81a981..004f48972 100644 --- a/apps/commerce-streamer/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,6 +1,6 @@ package com.loopers.interfaces.api.ranking; -import com.loopers.application.RankingFacade; +import com.loopers.application.ranking.RankingFacade; import com.loopers.interfaces.api.ApiResponse; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; diff --git a/apps/commerce-streamer/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Dto.java similarity index 100% rename from apps/commerce-streamer/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Dto.java rename to apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Dto.java diff --git a/apps/commerce-streamer/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java b/apps/commerce-streamer/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java deleted file mode 100644 index afb3f13f5..000000000 --- a/apps/commerce-streamer/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java +++ /dev/null @@ -1,126 +0,0 @@ -package com.loopers.interfaces.api; - -import com.fasterxml.jackson.databind.JsonMappingException; -import com.fasterxml.jackson.databind.exc.InvalidFormatException; -import com.fasterxml.jackson.databind.exc.MismatchedInputException; -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.ResponseEntity; -import org.springframework.http.converter.HttpMessageNotReadableException; -import org.springframework.web.bind.MissingServletRequestParameterException; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.RestControllerAdvice; -import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; -import org.springframework.web.server.ServerWebInputException; -import org.springframework.web.servlet.resource.NoResourceFoundException; - -import java.util.Arrays; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Collectors; - -@RestControllerAdvice -@Slf4j -public class ApiControllerAdvice { - @ExceptionHandler - public ResponseEntity> handle(CoreException e) { - log.warn("CoreException : {}", e.getCustomMessage() != null ? e.getCustomMessage() : e.getMessage(), e); - return failureResponse(e.getErrorType(), e.getCustomMessage()); - } - - @ExceptionHandler - public ResponseEntity> handleBadRequest(MethodArgumentTypeMismatchException e) { - String name = e.getName(); - String type = e.getRequiredType() != null ? e.getRequiredType().getSimpleName() : "unknown"; - String value = e.getValue() != null ? e.getValue().toString() : "null"; - String message = String.format("์š”์ฒญ ํŒŒ๋ผ๋ฏธํ„ฐ '%s' (ํƒ€์ž…: %s)์˜ ๊ฐ’ '%s'์ด(๊ฐ€) ์ž˜๋ชป๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", name, type, value); - return failureResponse(ErrorType.BAD_REQUEST, message); - } - - @ExceptionHandler - public ResponseEntity> handleBadRequest(MissingServletRequestParameterException e) { - String name = e.getParameterName(); - String type = e.getParameterType(); - String message = String.format("ํ•„์ˆ˜ ์š”์ฒญ ํŒŒ๋ผ๋ฏธํ„ฐ '%s' (ํƒ€์ž…: %s)๊ฐ€ ๋ˆ„๋ฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", name, type); - return failureResponse(ErrorType.BAD_REQUEST, message); - } - - @ExceptionHandler - public ResponseEntity> handleBadRequest(HttpMessageNotReadableException e) { - String errorMessage; - Throwable rootCause = e.getRootCause(); - - if (rootCause instanceof InvalidFormatException invalidFormat) { - String fieldName = invalidFormat.getPath().stream() - .map(ref -> ref.getFieldName() != null ? ref.getFieldName() : "?") - .collect(Collectors.joining(".")); - - String valueIndicationMessage = ""; - if (invalidFormat.getTargetType().isEnum()) { - Class enumClass = invalidFormat.getTargetType(); - String enumValues = Arrays.stream(enumClass.getEnumConstants()) - .map(Object::toString) - .collect(Collectors.joining(", ")); - valueIndicationMessage = "์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๊ฐ’ : [" + enumValues + "]"; - } - - String expectedType = invalidFormat.getTargetType().getSimpleName(); - Object value = invalidFormat.getValue(); - - errorMessage = String.format("ํ•„๋“œ '%s'์˜ ๊ฐ’ '%s'์ด(๊ฐ€) ์˜ˆ์ƒ ํƒ€์ž…(%s)๊ณผ ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. %s", - fieldName, value, expectedType, valueIndicationMessage); - - } else if (rootCause instanceof MismatchedInputException mismatchedInput) { - String fieldPath = mismatchedInput.getPath().stream() - .map(ref -> ref.getFieldName() != null ? ref.getFieldName() : "?") - .collect(Collectors.joining(".")); - errorMessage = String.format("ํ•„์ˆ˜ ํ•„๋“œ '%s'์ด(๊ฐ€) ๋ˆ„๋ฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", fieldPath); - - } else if (rootCause instanceof JsonMappingException jsonMapping) { - String fieldPath = jsonMapping.getPath().stream() - .map(ref -> ref.getFieldName() != null ? ref.getFieldName() : "?") - .collect(Collectors.joining(".")); - errorMessage = String.format("ํ•„๋“œ '%s'์—์„œ JSON ๋งคํ•‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: %s", - fieldPath, jsonMapping.getOriginalMessage()); - - } else { - errorMessage = "์š”์ฒญ ๋ณธ๋ฌธ์„ ์ฒ˜๋ฆฌํ•˜๋Š” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. JSON ๋ฉ”์„ธ์ง€ ๊ทœ๊ฒฉ์„ ํ™•์ธํ•ด์ฃผ์„ธ์š”."; - } - - return failureResponse(ErrorType.BAD_REQUEST, errorMessage); - } - - @ExceptionHandler - public ResponseEntity> handleBadRequest(ServerWebInputException e) { - String missingParams = extractMissingParameter(e.getReason() != null ? e.getReason() : ""); - if (!missingParams.isEmpty()) { - String message = String.format("ํ•„์ˆ˜ ์š”์ฒญ ๊ฐ’ '%s'๊ฐ€ ๋ˆ„๋ฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", missingParams); - return failureResponse(ErrorType.BAD_REQUEST, message); - } else { - return failureResponse(ErrorType.BAD_REQUEST, null); - } - } - - @ExceptionHandler - public ResponseEntity> handleNotFound(NoResourceFoundException e) { - return failureResponse(ErrorType.NOT_FOUND, null); - } - - @ExceptionHandler - public ResponseEntity> handle(Throwable e) { - log.error("Exception : {}", e.getMessage(), e); - return failureResponse(ErrorType.INTERNAL_ERROR, null); - } - - private String extractMissingParameter(String message) { - Pattern pattern = Pattern.compile("'(.+?)'"); - Matcher matcher = pattern.matcher(message); - return matcher.find() ? matcher.group(1) : ""; - } - - private ResponseEntity> failureResponse(ErrorType errorType, String errorMessage) { - return ResponseEntity.status(errorType.getStatus()) - .body(ApiResponse.fail(errorType.getCode(), errorMessage != null ? errorMessage : errorType.getMessage())); - } -} diff --git a/apps/commerce-streamer/src/main/java/com/loopers/interfaces/api/ApiResponse.java b/apps/commerce-streamer/src/main/java/com/loopers/interfaces/api/ApiResponse.java deleted file mode 100644 index 2e3b8b282..000000000 --- a/apps/commerce-streamer/src/main/java/com/loopers/interfaces/api/ApiResponse.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.loopers.interfaces.api; - -public record ApiResponse(Metadata meta, T data) { - public record Metadata(Result result, String errorCode, String message) { - public enum Result { - SUCCESS, FAIL - } - - public static Metadata success() { - return new Metadata(Result.SUCCESS, null, null); - } - - public static Metadata fail(String errorCode, String errorMessage) { - return new Metadata(Result.FAIL, errorCode, errorMessage); - } - } - - public static ApiResponse success() { - return new ApiResponse<>(Metadata.success(), null); - } - - public static ApiResponse success(T data) { - return new ApiResponse<>(Metadata.success(), data); - } - - public static ApiResponse fail(String errorCode, String errorMessage) { - return new ApiResponse<>( - Metadata.fail(errorCode, errorMessage), - null - ); - } -} diff --git a/apps/commerce-streamer/src/main/java/com/loopers/support/error/CoreException.java b/apps/commerce-streamer/src/main/java/com/loopers/support/error/CoreException.java deleted file mode 100644 index bc3017f7e..000000000 --- a/apps/commerce-streamer/src/main/java/com/loopers/support/error/CoreException.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.loopers.support.error; - -import lombok.Getter; - -@Getter -public class CoreException extends RuntimeException { - private final ErrorType errorType; - private final String customMessage; - - public CoreException(ErrorType errorType) { - this(errorType, null); - } - - public CoreException(ErrorType errorType, String customMessage) { - super(customMessage != null ? customMessage : errorType.getMessage()); - this.errorType = errorType; - this.customMessage = customMessage; - } -} diff --git a/apps/commerce-streamer/src/main/java/com/loopers/support/error/ErrorType.java b/apps/commerce-streamer/src/main/java/com/loopers/support/error/ErrorType.java deleted file mode 100644 index 5200f7dcd..000000000 --- a/apps/commerce-streamer/src/main/java/com/loopers/support/error/ErrorType.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.loopers.support.error; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; - -@Getter -@RequiredArgsConstructor -public enum ErrorType { - /** ๋ฒ”์šฉ ์—๋Ÿฌ */ - INTERNAL_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase(), "์ผ์‹œ์ ์ธ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."), - BAD_REQUEST(HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST.getReasonPhrase(), "์ž˜๋ชป๋œ ์š”์ฒญ์ž…๋‹ˆ๋‹ค."), - NOT_FOUND(HttpStatus.NOT_FOUND, HttpStatus.NOT_FOUND.getReasonPhrase(), "์กด์žฌํ•˜์ง€ ์•Š๋Š” ์š”์ฒญ์ž…๋‹ˆ๋‹ค."), - CONFLICT(HttpStatus.CONFLICT, HttpStatus.CONFLICT.getReasonPhrase(), "์ด๋ฏธ ์กด์žฌํ•˜๋Š” ๋ฆฌ์†Œ์Šค์ž…๋‹ˆ๋‹ค."); - - private final HttpStatus status; - private final String code; - private final String message; -} From dc745c99e9c6aaaf31f77df3cf753c66f350504c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A7=80=ED=99=98?= <91196025+Kimjipang@users.noreply.github.com> Date: Tue, 30 Dec 2025 02:23:16 +0900 Subject: [PATCH 74/76] =?UTF-8?q?refactor:=20product=5Fmetrics=20=ED=85=8C?= =?UTF-8?q?=EC=9D=B4=EB=B8=94=20=EC=BB=AC=EB=9F=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ์ฃผ๊ฐ„/์›”๊ฐ„ ๋žญํ‚น ์ •๋ณด ์ง‘๊ณ„๋ฅผ ์šฉ์ดํ•˜๊ฒŒ ํ•˜๊ธฐ ์œ„ํ•ด metric_date ์ปฌ๋Ÿผ ์ถ”๊ฐ€ --- .../com/loopers/domain/ProductMetric.java | 26 +++++++++++-------- .../domain/ProductMetricRepository.java | 2 +- .../ProductMetricJpaRepository.java | 2 ++ .../ProductMetricRepositoryImpl.java | 4 +-- .../consumer/KafkaOutboxConsumer.java | 9 ++++--- 5 files changed, 26 insertions(+), 17 deletions(-) diff --git a/apps/commerce-streamer/src/main/java/com/loopers/domain/ProductMetric.java b/apps/commerce-streamer/src/main/java/com/loopers/domain/ProductMetric.java index d4224a809..a0ebba5eb 100644 --- a/apps/commerce-streamer/src/main/java/com/loopers/domain/ProductMetric.java +++ b/apps/commerce-streamer/src/main/java/com/loopers/domain/ProductMetric.java @@ -22,43 +22,47 @@ public class ProductMetric extends BaseEntity { @Column(name = "sales_volume", nullable = false) private Long salesVolume; + @Column(name = "metric_date", nullable = false) + private String metricDate; + @Enumerated(EnumType.STRING) @Column(name = "event_type", nullable = false) private ProductEventType eventType; - public ProductMetric(Long productId, Long viewCount, Long likeCount, Long salesVolume, ProductEventType eventType) { + public ProductMetric(Long productId, Long viewCount, Long likeCount, Long salesVolume, String metricDate, ProductEventType eventType) { this.productId = productId; this.viewCount = viewCount; this.likeCount = likeCount; this.salesVolume = salesVolume; + this.metricDate = metricDate; this.eventType = eventType; } /* - [ ] ๋ฆฌํŒฉํ† ๋ง ์˜ˆ์ • */ - public static ProductMetric of(Long productId, ProductEventType eventType) { + public static ProductMetric of(Long productId, String date, ProductEventType eventType) { if (eventType == null) { throw new IllegalArgumentException("์ •์˜๋˜์ง€ ์•Š์€ event type์ž…๋‹ˆ๋‹ค."); } return switch (eventType) { - case PRODUCT_VIEWED -> ofProductViewed(productId); - case PRODUCT_LIKED -> ofProductLiked(productId); - case PRODUCT_SALES -> ofProductSales(productId); + case PRODUCT_VIEWED -> ofProductViewed(productId, date); + case PRODUCT_LIKED -> ofProductLiked(productId, date); + case PRODUCT_SALES -> ofProductSales(productId, date); }; } - public static ProductMetric ofProductViewed(Long productId) { - return new ProductMetric(productId, 1L, 0L, 0L, ProductEventType.PRODUCT_VIEWED); + public static ProductMetric ofProductViewed(Long productId, String date) { + return new ProductMetric(productId, 1L, 0L, 0L, date, ProductEventType.PRODUCT_VIEWED); } - public static ProductMetric ofProductLiked(Long productId) { - return new ProductMetric(productId, 0L, 1L, 0L, ProductEventType.PRODUCT_LIKED); + public static ProductMetric ofProductLiked(Long productId, String date) { + return new ProductMetric(productId, 0L, 1L, 0L, date, ProductEventType.PRODUCT_LIKED); } - public static ProductMetric ofProductSales(Long productId) { - return new ProductMetric(productId, 0L, 0L, 1L, ProductEventType.PRODUCT_SALES); + public static ProductMetric ofProductSales(Long productId, String date) { + return new ProductMetric(productId, 0L, 0L, 1L, date, ProductEventType.PRODUCT_SALES); } public void increaseProductMetric(ProductEventType eventType) { diff --git a/apps/commerce-streamer/src/main/java/com/loopers/domain/ProductMetricRepository.java b/apps/commerce-streamer/src/main/java/com/loopers/domain/ProductMetricRepository.java index ff451da2c..c38ac9886 100644 --- a/apps/commerce-streamer/src/main/java/com/loopers/domain/ProductMetricRepository.java +++ b/apps/commerce-streamer/src/main/java/com/loopers/domain/ProductMetricRepository.java @@ -2,7 +2,7 @@ public interface ProductMetricRepository { - ProductMetric findByProductId(Long productId); + ProductMetric findByProductIdAndDate(Long productId, String date); ProductMetric save(ProductMetric of); } diff --git a/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/ProductMetricJpaRepository.java b/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/ProductMetricJpaRepository.java index fec349610..8e7f2f34b 100644 --- a/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/ProductMetricJpaRepository.java +++ b/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/ProductMetricJpaRepository.java @@ -6,4 +6,6 @@ public interface ProductMetricJpaRepository extends JpaRepository { ProductMetric findByProductId(Long productId); + + ProductMetric findByProductIdAndMetricDate(Long productId, String date); } diff --git a/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/ProductMetricRepositoryImpl.java b/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/ProductMetricRepositoryImpl.java index 20aef6b90..d8d89c4d2 100644 --- a/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/ProductMetricRepositoryImpl.java +++ b/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/ProductMetricRepositoryImpl.java @@ -12,8 +12,8 @@ public class ProductMetricRepositoryImpl implements ProductMetricRepository { private final ProductMetricJpaRepository productMetricJpaRepository; @Override - public ProductMetric findByProductId(Long productId) { - return productMetricJpaRepository.findByProductId(productId); + public ProductMetric findByProductIdAndDate(Long productId, String date) { + return productMetricJpaRepository.findByProductIdAndMetricDate(productId, date); } @Override diff --git a/apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/KafkaOutboxConsumer.java b/apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/KafkaOutboxConsumer.java index 79f4a5843..447acd9de 100644 --- a/apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/KafkaOutboxConsumer.java +++ b/apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/KafkaOutboxConsumer.java @@ -46,6 +46,8 @@ public void productViewedListener( Map scoreDelta = new HashMap<>(); + String date = LocalDate.now(KST).format(YYYYMMDD); + for (var record : messages) { OutboxEvent value = objectMapper.readValue(record.value(), OutboxEvent.class); Long productId = value.aggregateId(); @@ -53,10 +55,10 @@ public void productViewedListener( scoreDelta.merge(productId, weight(eventType), Double::sum); - ProductMetric productMetric = productMetricRepository.findByProductId(productId); + ProductMetric productMetric = productMetricRepository.findByProductIdAndDate(productId, date); if (productMetric == null) { - ProductMetric newProductMetric = ProductMetric.of(productId, eventType); + ProductMetric newProductMetric = ProductMetric.of(productId, date, eventType); productMetricRepository.save(newProductMetric); } @@ -65,7 +67,7 @@ public void productViewedListener( } } - String key = "ranking:all:" + LocalDate.now(KST).format(YYYYMMDD); + String key = "ranking:all:" + date; ZSetOperations zset = redisTemplate.opsForZSet(); for (var e : scoreDelta.entrySet()) { @@ -85,3 +87,4 @@ public void productViewedListener( }; } } + From da195a3964c6d383b3d475cce20a4263dae9fac4 Mon Sep 17 00:00:00 2001 From: hubtwork Date: Wed, 31 Dec 2025 16:09:54 +0900 Subject: [PATCH 75/76] =?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 76/76] =?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