Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 137 additions & 0 deletions .docs/design/01-requirements.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
# **요구사항 명세서**

## **1. 상품 목록 조회**

### 1.1. 유저 시나리오
> "사용자가 상품을 둘러보기 위해 사이트에 접속했다. 그는 어떤 제품이 인기가 많은지 보기 위해 상품 목록을 조회한다."

### 1.2. 핵심 기능 정의
- **F-1:** 상품 목록을 조회할 수 있다.
- **F-2:** 상품 목록을 '좋아요순'으로 정렬할 수 있다.
- **F-3:** 상품 목록을 '최신순', '가격순' 등 다른 기준으로도 정렬할 수 있다.
- **F-4:** 상품 목록을 페이지 단위로 나누어 볼 수 있다.

### 1.3. 유스케이스 흐름
* **Main Flow (기본 조회):**
1. 사용자가 상품 목록 페이지에 진입한다.
2. 최신순으로 첫 번째 페이지의 상품 목록을 반환한다.
3. 화면에 상품 목록이 표시된다.
* **Alternate Flow (정렬 변경):**
1. 사용자가 '인기순' 정렬 버튼을 클릭한다.
2. 인기순으로 상품 목록을 다시 조회하여 반환한다.
3. 화면에 인기순으로 정렬된 목록이 표시된다.
* **Alternate Flow (페이징):**
1. 사용자가 다른 페이지를 클릭한다.
2. 다른 페이지의 상품 목록을 조회하여 반환한다.
3. 화면에 다음 상품 목록이 이어서 표시된다.
* **Exception Flow (결과 없음):**
1. 사용자가 특정 필터를 적용했으나, 해당하는 상품이 하나도 없다.
2. "조회된 상품이 없습니다."라는 메시지를 반환한다.

---

## **2. 상품 상세 조회**

### 2.1. 유저 시나리오
> "사용자가 마음에 드는 상품이 있어서 해당 상품을 클릭했다. 그는 해당 상품의 상세 정보를 확인하고 싶어 한다."

### 2.2. 핵심 기능 정의
- **F-1:** 상품 상세정보를 조회할 수 있다.
- **F-2:** 상품의 총 좋아요 수와 나의 좋아요 여부를 조회할 수 있다.
- **F-3:** 구매할 상품의 수량을 선택할 수 있다.

### 2.3. 유스케이스 흐름
* **Main Flow (상세 조회):**
1. 사용자가 상품 목록에서 특정 상품을 클릭한다.
2. 해당 상푸의 상세 정보(이름, 가격, 설명, 총 좋아요 수, 나의 좋아요 여부 등)를 조회하여 반환한다.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

타이포 수정: "상푸" → "상품".

-    2.  해당 상푸의 상세 정보(이름, 가격, 설명, 총 좋아요 수, 나의 좋아요 여부 등)를 조회하여 반환한다.
+    2.  해당 상품의 상세 정보(이름, 가격, 설명, 총 좋아요 수, 나의 좋아요 여부 등)를 조회하여 반환한다.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
2. 해당 상푸의 상세 정보(이름, 가격, 설명, 총 좋아요 수, 나의 좋아요 여부 등)를 조회하여 반환한다.
2. 해당 상품의 상세 정보(이름, 가격, 설명, 총 좋아요 수, 나의 좋아요 여부 등)를 조회하여 반환한다.
🤖 Prompt for AI Agents
In .docs/design/01-requirements.md around line 46, there is a typo "상푸" that
should be corrected to "상품"; update the text to read "해당 상품의 상세 정보(이름, 가격, 설명, 총
좋아요 수, 나의 좋아요 여부 등)를 조회하여 반환한다." ensuring only the single word is fixed and
punctuation/spacing remains unchanged.

3. 화면에 상품 상세 정보가 표시되고, 수량은 '1'로 기본 설정된다.
* **Alternate Flow (수량 변경):**
1. 사용자가 '+' 버튼을 눌러 수량을 '3'으로 변경한다.
2. 화면에 수량이 '3'으로 갱신된다.
* **Exception Flow (상품 없음):**
1. 사용자가 존재하지 않는 상품 ID의 URL로 직접 접근한다.
2. 상품을 찾을 수 없으므로, "상품을 찾을 수 없습니다."라는 오류를 반환한다.

---

## **3. 브랜드별 상품 조회**

### 3.1. 유저 시나리오
> "사용자가 마음에 드는 특정 브랜드를 찾고 있다. 그는 해당 브랜드에서 나오는 상품만 모아서 조회하고 싶어 한다."

### 3.2. 핵심 기능 정의
- **F-1:** 특정 브랜드에 속한 상품만 필터링하여 조회할 수 있다.
- **F-2:** 상품 목록을 '인기순'으로 정렬할 수 있다.
- **F-3:** 상품 목록을 '최신순', '가격순' 등 다른 기준으로도 정렬할 수 있다.
- **F-4:** 상품 목록을 페이지 단위로 나누어 볼 수 있다.

### 3.3. 유스케이스 흐름
* **Main Flow (브랜드별 조회):**
1. 사용자가 'A 브랜드'를 조회회한다.
2. 해당 브랜드드 상품 목록을 반환한다.
Comment on lines +70 to +71
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

타이포 수정: 문법 오류 2건.

-    1.  사용자가 'A 브랜드'를 조회회한다.
-    2.  해당 브랜드드 상품 목록을 반환한다.
+    1.  사용자가 'A 브랜드'를 조회한다.
+    2.  해당 브랜드의 상품 목록을 반환한다.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
1. 사용자가 'A 브랜드'를 조회회한다.
2. 해당 브랜드드 상품 목록을 반환한다.
1. 사용자가 'A 브랜드'를 조회한다.
2. 해당 브랜드의 상품 목록을 반환한다.
🤖 Prompt for AI Agents
In .docs/design/01-requirements.md around lines 70 to 71, fix two typos: change
"조회회한다" to "조회한다" and change "해당 브랜드드 상품 목록을 반환한다." to "해당 브랜드의 상품 목록을 반환한다."

3. 화면에 'A 브랜드'의 상품 목록만 표시된다.
* **Exception Flow (브랜드 없음):**
1. 사용자가 존재하지 않는 브랜드 ID의 URL로 직접 접근한다.
2. "브랜드를 찾을 수 없습니다."라는 오류(404 Not Found)를 반환한다.

---

## **4. 상품 좋아요 (등록/취소)**

### 4.1. 유저 시나리오
> (등록) "사용자가 마음에 드는 상품을 발견했다. 그는 나중에 마음에 드는 상품을 모아보기 위해 '좋아요'를 누른다."
> (취소) "사용자가 이전에 '좋아요' 했던 상품이 더 이상 마음에 들지 않아 '좋아요'를 취소한다."

### 4.2. 핵심 기능 정의
- **F-1:** 상품에 대해 '좋아요'를 등록할 수 있다.
- **F-2:** 이미 '좋아요'가 등록된 상품의 '좋아요'를 취소할 수 있다.

### 4.3. 유스케이스 흐름
* **Main Flow (좋아요 등록):**
1. 사용자가 좋아요 아이콘을 클릭한다.
2. 좋아요 여부를 판단한다.
3. 해당 상품이 좋아요가 안되어있으면 좋아요 등록이 된다.
* **Alternate Flow (좋아요 취소):**
1. 사용자가 좋아요 아이콘을 클릭한다.
2. 좋아요 여부를 판단한다.
3. 해당 상품이 좋아요가 되어있으면 좋아요가 취소된다.
* **Exception Flow (비회원):**
1. 비로그인 사용자가 좋아요 아이콘을 클릭한다.
2. "로그인이 필요한 기능입니다."라는 오류 메시지를 반환한다.

---

## **5. 주문 생성 (결제)**

### 5.1. 유저 시나리오
> "사용자가 특정 상품을 구매하고 싶어, 해당 상품을 결제한다."

### 5.2. 핵심 기능 정의
- **F-1:** 사용자가 선택한 상품을 구매할 수 있다.
- **F-2:** 사용자의 포인트가 충분할 경우 결제할 수 있으며, 결제 시 포인트가 차감된다.
- **F-3:** 상품의 재고가 충분할 경우 결제할 수 있으며, 결제 시 재고가 차감된다.
- **F-4:** 결제 성공 시 주문 정보가 외부 시스템으로 전송된다.

### 5.3. 유스케이스 흐름
* **Main Flow (주문 성공):**
1. 로그인한 사용자가 상품과 수량을 선택하고 '결제하기'를 요청한다.
2. 상품 재고가 1개 이상인지 확인한다.
3. 사용자 포인트가 총 결제 금액 이상인지 확인한다.
4. 상품 재고를 1 차감한다.
5. 사용자 포인트를 차감한다.
6. '주문' 및 '주문 항목'을 저장한다.
7. 주문 정보를 외부 시스템으로 전송한다.
8. 사용자에게 "주문 완료" 응답을 반환한다.
* **Alternate Flow (여러 수량 구매):**
1. 사용자가 수량을 '3'으로 선택하고 '결제하기'를 요청한다.
2. 재고 및 포인트를 수량 기준으로 검증, 차감, 저장한다.
3. 주문에 성공한다.
* **Exception Flow (재고 부족):**
1. 시스템이 재고를 확인했으나, 요청 수량보다 재고가 부족하다.
2. "재고가 부족하여 주문할 수 없습니다."라는 오류 메시지를 반환한다.
* **Exception Flow (포인트 부족):**
1. 시스템이 사용자의 포인트를 확인했으나, 총 결제 금액보다 포인트가 부족하다.
2. "포인트가 부족합니다."라는 오류 메시지를 반환한다.
* **Exception Flow (비회원):**
1. 비로그인 사용자가 '결제하기'를 요청한다.
2. "로그인이 필요한 기능입니다."라는 오류 메시지를 반환한다.
64 changes: 64 additions & 0 deletions .docs/design/02-sequence-diagrams.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@

### 1. 상품 목록/상세 조회 및 브랜드 조회
```mermaid
sequenceDiagram
participant User
participant ProductController
participant ProductReader

%% 상품 목록 조회 %%
User->>ProductController: GET /api/v1/products?sort=likes_desc
ProductController->>ProductReader: getProducts(sort)
ProductReader-->>ProductController: productList
ProductController-->>User: productList

%% 상품 상세 조회 %%
User->>ProductController: GET /api/v1/products/{productId}
ProductController->>ProductReader: getProduct(productId)
ProductReader-->>ProductController: product
ProductController-->>User: product

participant BrandController
participant BrandReader

%% 브랜드 조회 %%
User->>BrandController: GET /api/v1/brands/{brandId}
BrandController->>BrandReader: getBrand({brandId})
BrandReader-->>BrandController: brand
BrandController-->>User: brand
```
### 2. 주문 생성

```mermaid
sequenceDiagram
participant User
participant OrderController
participant OrderService
participant ProductReader
participant PointReader
participant ProductService
participant PointService
participant OrderRepository

User->>OrderController: POST /api/v1/orders (body: {productId, quantity})
OrderController->>OrderService: setOrder(userId, {productId, quantity})

%% 조회 및 검증 %%
OrderService->>ProductReader: getProduct({productId})
ProductReader -->>OrderService: product(현재가격, 재고)

OrderService->>PointReader: getPoint(userId)
PointReader-->>OrderService: point (현재 잔여 포인트)

%% 재고 및 포인트 차감 %%
critical Transaction Block
OrderService ->>ProductService: decreaseStock(productId, quantity)
OrderService ->>PointService: deductPoint(productPrice * quantity)
OrderService->> OrderRepository: save(new Order(...))
OrderRepository-->>OrderService: orderInfo
end

%% 응답 %%
OrderService -->> OrderController:orderInfo
OrderController-->> User:orderInfo
```
46 changes: 46 additions & 0 deletions .docs/design/03-class-diagram.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
## 1. 클래스 다이어그램램

```mermaid
classDiagram
class User {
Long id
String name
int point
}
class Product {
Long id
String name
int price
int quantity
}
Comment on lines +10 to +15
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

필드명 일관성 검토: ERD와 클래스 다이어그램 동기화 필요.

Product 클래스의 필드명이 ERD와 다릅니다:

  • 클래스 다이어그램: quantity
  • ERD: stock_quantity

요구사항 및 시퀀스 다이어그램에서 사용할 정확한 필드명을 결정하고 전체 설계 문서에서 일관되게 사용하세요.



---

🏁 Script executed:

```shell
#!/bin/bash
# Find all design documents in the .docs/design directory
find .docs/design -type f \( -name "*.md" -o -name "*.mmd" -o -name "*.txt" \) | sort

Length of output: 124


🏁 Script executed:

#!/bin/bash
# Search for ERD-related files
fd -e md -e mmd -e txt . .docs/design | grep -i erd

Length of output: 22


🏁 Script executed:

#!/bin/bash
# Examine the class diagram file to see Product class definition
cat ".docs/design/03-class-diagram.md"

Length of output: 838


🏁 Script executed:

#!/bin/bash
# Search for field name references (quantity, stock_quantity, stock) in all design docs
rg -n "(quantity|stock_quantity|stock)" .docs/design --type md

Length of output: 677


🏁 Script executed:

#!/bin/bash
# List all files in .docs/design to understand document structure
ls -la .docs/design/

Length of output: 380



---

🏁 Script executed:

```shell
#!/bin/bash
# Examine the ERD file completely to see Product entity field names
cat ".docs/design/04-erd.md"

Length of output: 918


🏁 Script executed:

#!/bin/bash
# Examine requirements to see specified field names
cat ".docs/design/01-requirements.md"

Length of output: 3680


🏁 Script executed:

#!/bin/bash
# Examine sequence diagrams for context
cat ".docs/design/02-sequence-diagrams.md"

Length of output: 1977


ERD와 클래스 다이어그램의 필드명 불일치 수정 필요

검증 결과, 설계 문서 간 필드명 불일치가 확인되었습니다:

  • ERD (04-erd.md 라인 14): products 테이블에 stock_quantity 정의
  • 클래스 다이어그램 (03-class-diagram.md 라인 14): Product 클래스에 quantity 정의

orderitemsquantity(주문 수량)와 구분하기 위해, Product의 재고 필드명을 명확히 결정하고 ERD, 클래스 다이어그램, 요구사항, 시퀀스 다이어그램 전체에서 일관되게 적용하세요.

🤖 Prompt for AI Agents
In .docs/design/03-class-diagram.md around lines 10 to 15, the Product class
uses the field name "quantity" which conflicts with the ERD's "stock_quantity"
and can be confused with orderitems.quantity; rename Product.quantity to
stock_quantity in this file and update the wording to match the ERD, then review
and make the same rename across related artifacts (04-erd.md, requirements,
sequence diagrams, and any references in docs/tests) so all diagrams and docs
use "stock_quantity" consistently.

class Brand {
Long id
String name
}
class Like {
User user
Product product
}
class Order {
Long id
User user
int totalPrice
Timestamp orderDate
}
Comment on lines +24 to +29
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

필드명 일관성 검토: Order 클래스의 시간 필드.

Order 클래스의 필드명이 ERD와 다릅니다:

  • 클래스 다이어그램: orderDate
  • ERD: created_at

데이터베이스 스키마와 도메인 모델의 네이밍을 통일하고, 시퀀스 다이어그램과도 일관성 있게 유지하세요.


🏁 Script executed:

#!/bin/bash
# List all files in .docs/design directory to identify ERD and sequence diagram files
find .docs/design -type f -name "*.md" | sort

Length of output: 124


🏁 Script executed:

#!/bin/bash
# Search for ERD-related content showing database field names
fd -e md . .docs/design -x cat {}

Length of output: 7413


🏁 Script executed:

#!/bin/bash
# Search for "created_at" or "orderDate" references in design docs
rg -n -i "(created_at|orderDate)" .docs/design/

Length of output: 118


🏁 Script executed:

#!/bin/bash
# Look for Order class definitions in the codebase
rg -n -C5 "class Order" --type=java --type=ts --type=js --type=py

Length of output: 0


Order 클래스 시간 필드 네이밍 불일치 확인됨 - 수정 필요

검증 결과 설계 문서에서 다음과 같은 불일치가 확인되었습니다:

  • ERD (.docs/design/04-erd.md, 29줄): created_at
  • 클래스 다이어그램 (.docs/design/03-class-diagram.md, 28줄): orderDate

데이터베이스 스키마(snake_case)와 도메인 모델(camelCase) 간 명확한 네이밍 규칙을 수립하거나, 둘 중 하나로 통일하세요. 다른 엔티티(User, Product 등)의 시간 필드도 동일한 원칙으로 일관성 있게 정의되어야 합니다.

🤖 Prompt for AI Agents
In .docs/design/03-class-diagram.md around lines 24–29, the Order class uses
"orderDate" which conflicts with the ERD (.docs/design/04-erd.md line 29) that
defines the timestamp as "created_at"; decide on a consistent convention (either
DB snake_case and domain camelCase mapping rule, or unify both to one style) and
apply it: update the class diagram to use the chosen name (e.g., change
orderDate → createdAt if you keep domain camelCase, or → created_at if you want
exact DB names), then update the ERD or other class diagrams (User, Product,
etc.) accordingly to reflect the same rule and add a brief note in the design
docs describing the naming convention for timestamp fields.

class OrderItem {
Order order
Product product
int quantity
int orderPrice
}

%% --- 관계 정의 ---
Product --> Brand : (상품은 브랜드를 가짐)

Order --> User : (주문은 유저를 가짐)

OrderItem --> Order : (주문 항목은 주문에 속함)
OrderItem --> Product : (주문 항목은 상품을 가짐)

Like --> User : (좋아요는 유저를 가짐)
Like -- > Product : (좋아요는 상품을 가짐)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Mermaid 문법 오류: 관계 정의 수정 필요.

라인 46에서 관계 정의에 오타가 있습니다. 스페이스가 불필요하게 포함되어 있습니다.

-    Like -- > Product : (좋아요는 상품을 가짐)
+    Like --> Product : (좋아요는 상품을 가짐)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Like -- > Product : (좋아요는 상품을 가짐)
Like --> Product : (좋아요는 상품을 가짐)
🤖 Prompt for AI Agents
.docs/design/03-class-diagram.md around line 46: the Mermaid relationship has an
extra space in "Like -- > Product : (좋아요는 상품을 가짐)"; remove the space so the
arrow is "Like --> Product : (좋아요는 상품을 가짐)" to follow correct Mermaid syntax.

48 changes: 48 additions & 0 deletions .docs/design/04-erd.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
## 1. ERD

```mermaid
erDiagram
users {
bigint id PK
varchar name
int point
}
products {
bigint id PK
varchar name
int price
int stock_quantity
bigint brand_id FK
}
brands {
bigint id PK
varchar name
}
likes {
bigint user_id PK, FK
bigint product_id PK, FK
}
orders {
bigint id PK
bigint user_id FK
int total_price
Timestamp created_at
}
orderitems {
bigint id PK
bigint order_id FK
bigint product_id FK
int quantity
int order_price
}

%% --- 관계 정의 (1:N) ---
users ||--o{ likes : "likes"
users ||--o{ orders : "places"

products ||--o{ likes : "is_liked"
products ||--o{ orderitems : "is_in"

brands ||--o{ products : "has"

orders ||--o{ orderitems : "contains"
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.loopers.application.point;

import com.loopers.domain.point.PointModel;
import com.loopers.domain.point.PointService;
import com.loopers.domain.user.UserModel;
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 PointService pointService;
private final UserService userService;

public PointInfo getPoint(String userId) {
UserModel user = userService.getUser(userId);
if (user == null) {
throw new CoreException(ErrorType.NOT_FOUND, "존재하지 않는 요청입니다.");
}
PointModel pointModel = new PointModel(user, 0);
PointModel point = pointService.findPoint(pointModel);

if (point == null) {
throw new CoreException(ErrorType.NOT_FOUND, "포인트 정보가 없습니다.");
}

return PointInfo.from(point);
}

public PointInfo chargePoint(String userId, int amount) {
UserModel user = userService.getUser(userId);
if (user == null) {
throw new CoreException(ErrorType.NOT_FOUND, "존재하지 않는 요청입니다.");
}
PointModel pointModel = new PointModel(user, amount);
pointService.charge(pointModel);

PointModel charged = pointService.findPoint(new PointModel(user, 0));
return PointInfo.from(charged);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.loopers.application.point;

import com.loopers.domain.point.PointModel;
import com.loopers.domain.user.UserModel;

public record PointInfo(Long id, UserModel user, int point) {
public static PointInfo from(PointModel model) {
return new PointInfo(model.getId(), model.getUser(), model.getPoint());
}
public int getPoint() {
return point;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.loopers.application.user;

import com.loopers.domain.user.UserModel;
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 signup(String userId, String email, String birthDate) {
UserModel userModel = new UserModel(userId, email, birthDate);
UserModel savedUser = userService.signUp(userModel);
return UserInfo.from(savedUser);
}

public UserInfo getUser(String userId) {
UserModel user = userService.getUser(userId);
if (user == null) {
throw new CoreException(ErrorType.NOT_FOUND, "존재하지 않는 요청입니다.");
}
return UserInfo.from(user);
}
}
Loading