Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
026d966
docs : 컴포넌트 다이어그램 문서 생성.
YoHanKi Feb 15, 2026
b23196c
refactor(member) : 인터페이스 영역이 어플리케이션 영역을 거치지 않고 도메인 영역을 침범하는 것 수정.
YoHanKi Feb 15, 2026
7838d7a
refactor(security) : 시큐리티 의존성 분리.
YoHanKi Feb 15, 2026
b8cae51
refactor(member) : member vo 패키지 이동.
YoHanKi Feb 15, 2026
4e20ddb
test(architecture) : archunit 의존 추가 및 아키텍쳐 방향 검사 추가.
YoHanKi Feb 16, 2026
8f90f49
remove : example 도메인 클래스 및 테스트 삭제
YoHanKi Feb 16, 2026
2e20b20
test(architecture) : 레이어드 아키텍쳐 테스트 추가 검증.
YoHanKi Feb 16, 2026
2f28020
docs(claude) : CLAUDE.md 내 도메인/아키텍처 전략 추가.
YoHanKi Feb 16, 2026
dcea02b
feat(Brand) : Brand VO 및 Entity 구현.
YoHanKi Feb 16, 2026
4136b3a
feat(brand): Service 및 Repository 구현.
YoHanKi Feb 16, 2026
40f47ed
feat(brand): REST API 구현
YoHanKi Feb 16, 2026
3bafbec
feat(product): VO 및 Entity 구현
YoHanKi Feb 16, 2026
a17d7e7
refactor: 테스트 형식 수정 및 테스트 관련 SKILL 수정.
YoHanKi Feb 16, 2026
62db086
feat(product): Service 및 Repository 구현 및 단위/통합 테스트 작성.
YoHanKi Feb 16, 2026
fcae9f8
feat(product): REST API 구현
YoHanKi Feb 16, 2026
3e7f0be
feat(like): VO 및 Entity 구현
YoHanKi Feb 16, 2026
329ba8f
refactor(product): FK 참조에 RefBrandId VO 적용
YoHanKi Feb 16, 2026
59863d2
refactor : Reader 제거, Repository 통합.
YoHanKi Feb 16, 2026
be00880
feat(like) : Like 기능 구현 및 단위/통합 테스트 구축.
YoHanKi Feb 16, 2026
f692bb6
feat(like): REST API 및 테스트 구축.
YoHanKi Feb 16, 2026
b9ef992
feat(order): VO, Entity 및 Enum 구현.
YoHanKi Feb 16, 2026
dccc531
feat(order): 주문 생성 기능 구현
YoHanKi Feb 16, 2026
dfb2212
feat(product): likes_desc 정렬 구현 (좋아요 수 기준 내림차순)
YoHanKi Feb 16, 2026
f028ab7
feat(product): ProductInfo에 브랜드 정보와 좋아요 수 추가
YoHanKi Feb 16, 2026
3b9e15f
test(archunit): 테스트 코드는 제외하도록 수정.
YoHanKi Feb 16, 2026
213dd28
docs(requirements) : 락 전략 변경.
YoHanKi Feb 20, 2026
81828fd
docs : 구현에 맞게 문서 최신화.
YoHanKi Feb 20, 2026
c2d38d8
feat(like): API 경로를 /products/{productId}/likes로 변경
YoHanKi Feb 20, 2026
138314d
feat(brand): 브랜드 단건 조회 API 추가
YoHanKi Feb 20, 2026
5628a43
feat(product): 상품 단건 조회 및 수정 API 추가
YoHanKi Feb 20, 2026
e99274b
feat(order): 주문 생성/취소 도메인 및 API 구현
YoHanKi Feb 20, 2026
8952cc0
docs: 요구사항 명세 업데이트
YoHanKi Feb 20, 2026
8b80f7d
feat(brand): 브랜드 삭제 시 상품 연쇄 soft delete 구현
YoHanKi Feb 20, 2026
870bda7
feat(order): 주문 상세 조회 API 구현
YoHanKi Feb 20, 2026
ffb25b3
feat(order): 내 주문 목록 조회 API 구현
YoHanKi Feb 20, 2026
db85687
feat(like): 내 좋아요 목록 조회 API 구현
YoHanKi Feb 20, 2026
bc9a61f
feat(product): 어드민 상품 수정 API 구현
YoHanKi Feb 20, 2026
5b29fa6
refactor(arch): Facade → Repository 직접 의존 제거 및 cascade delete를 Applic…
YoHanKi Feb 22, 2026
a33c020
refactor(infra): EntityManager 직접 사용 제거, JpaRepository @Query로 대체
YoHanKi Feb 22, 2026
3a27b48
refactor(style): var 키워드 제거 및 DbId → RefId 메서드명 통일
YoHanKi Feb 22, 2026
98e1211
docs(arch): 아키텍처 확정 규칙 문서화 (Facade 규칙, VO 소유권, @Query 패턴, 코딩 컨벤션)
YoHanKi Feb 22, 2026
ec86ccf
refactor(domain): Ref*Id VO를 domain.common.vo로 통합하고 도메인별 중복 삭제
YoHanKi Feb 22, 2026
3c23b8b
test: common.vo ArchUnit 룰 추가, 테스트 수정
YoHanKi Feb 22, 2026
a1e302f
docs : application 내 app 클래스 문서화 추가.
YoHanKi Feb 23, 2026
3db244d
refactor(application): MemberFacade를 MemberApp으로 전환
YoHanKi Feb 23, 2026
429b354
refactor(application): OrderFacade를 OrderApp으로 전환
YoHanKi Feb 23, 2026
a28cd64
refactor(application): Brand/Product 도메인 App/Facade 분리
YoHanKi Feb 23, 2026
60ed95b
refactor(like): LikeApp 도입 및 LikeFacade를 App 조합 방식으로 전환
YoHanKi Feb 23, 2026
ed22994
refactor(service): 트랜잭션 경계를 Service에서 App 레이어로 이관
YoHanKi Feb 23, 2026
6d672bd
refactor: Service 단순 조회를 App에서 Repository 직접 호출로 전환
YoHanKi Feb 23, 2026
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
171 changes: 147 additions & 24 deletions .claude/skills/architecture/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ allowed-tools: Read, Grep
┌─────────────────────────────────────────┐
│ Application Layer │ ← 유스케이스 조합
(Facade, Info)
(App, Facade, Info) │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
Expand Down Expand Up @@ -233,38 +233,75 @@ public interface MemberRepository {
## Application Layer (응용 계층)

### 책임
- 여러 도메인 서비스 조합
- 유스케이스 구현
- 트랜잭션 경계 설정 (선택적)
- **App**: 단일 도메인의 유스케이스 처리, Service 호출 및 Model → Info 변환
- **Facade**: **2개 이상의 App을 조합**할 때만 사용, 크로스 도메인 오케스트레이션

### Facade 예시
### App 예시 (단일 도메인 — 기본 패턴)
```java
@Component
@RequiredArgsConstructor
public class MemberFacade {
public class MemberApp {
private final MemberService memberService;
private final PointService pointService;
private final NotificationService notificationService;

@Transactional
public MemberInfo registerMemberWithWelcomePoint(MemberInfo.RegisterRequest request) {
// 1. 회원 가입
MemberModel member = memberService.register(
request.memberId(), request.password(), request.email(),
request.birthDate(), request.name(), request.gender()
);
public MemberInfo register(String memberId, String password, String email,
String birthDate, String name, Gender gender) {
MemberModel member = memberService.register(memberId, password, email, birthDate, name, gender);
return MemberInfo.from(member);
}

// 2. 웰컴 포인트 지급
pointService.grantWelcomePoint(member.getMemberId());
@Transactional(readOnly = true)
public MemberInfo getMe(String loginId, String loginPw) {
MemberModel member = memberService.authenticate(loginId, loginPw);
return MemberInfo.from(member);
}

// 3. 가입 환영 알림 발송
notificationService.sendWelcomeNotification(member.getEmail());
public void changePassword(String loginId, String loginPw,
String currentPassword, String newPassword) {
memberService.changePassword(loginId, loginPw, currentPassword, newPassword);
}
}
```

return MemberInfo.from(member);
**App 의존성 규칙**:
```
✅ App → Service (허용)
❌ App → Repository 직접 의존 (금지)
❌ App → App 의존 (금지 — 크로스 도메인은 Facade 책임)
❌ App → Facade 의존 (금지)
```

### Facade 예시 (크로스 도메인 — 2개 이상 App 조합 시에만)
```java
@Component
@RequiredArgsConstructor
public class OrderFacade {
private final MemberApp memberApp;
private final ProductApp productApp;
private final OrderApp orderApp;

@Transactional
public OrderInfo placeOrder(String memberId, String productId, int quantity) {
// 1. 회원 확인
MemberInfo member = memberApp.getMe(memberId, /* ... */);

// 2. 상품 재고 확인 및 차감
productApp.decreaseStock(productId, quantity);

// 3. 주문 생성
return orderApp.createOrder(member.id(), productId, quantity);
}
}
```

**Facade 의존성 규칙**:
```
✅ Facade → App (허용, 반드시 2개 이상)
❌ Facade → Service 직접 호출 (금지 — 반드시 App 경유)
❌ Facade → Repository 직접 의존 (금지)
❌ Facade → Facade 의존 (금지)
❌ 단일 도메인만 처리하는 Facade 생성 (금지 — App을 사용할 것)
```

---

## Interfaces Layer (인터페이스 계층)
Expand All @@ -282,18 +319,18 @@ public class MemberFacade {
@RequestMapping("/api/v1/members")
public class MemberV1Controller implements MemberV1ApiSpec {

private final MemberService memberService;
private final MemberApp memberApp;

@PostMapping("/register")
@Override
public ApiResponse<MemberV1Dto.MemberResponse> register(
@Valid @RequestBody MemberV1Dto.RegisterRequest request) {
MemberModel member = memberService.register(
MemberInfo info = memberApp.register(
request.memberId(), request.password(), request.email(),
request.birthDate(), request.name(), request.gender()
);

MemberV1Dto.MemberResponse response = MemberV1Dto.MemberResponse.from(member);
MemberV1Dto.MemberResponse response = MemberV1Dto.MemberResponse.from(info);
return ApiResponse.success(response);
}
}
Expand Down Expand Up @@ -374,8 +411,12 @@ com.loopers
│ │ └── PasswordHasher.java # Interface
├── application # 응용 계층
│ ├── member
│ │ ├── MemberFacade.java # Facade
│ │ ├── MemberApp.java # App (단일 도메인 유스케이스)
│ │ └── MemberInfo.java # Info
│ ├── order # 크로스 도메인 예시
│ │ ├── OrderApp.java # App (order 도메인)
│ │ ├── OrderFacade.java # Facade (MemberApp + ProductApp + OrderApp 조합)
│ │ └── OrderInfo.java # Info
├── infrastructure # 인프라 계층
│ ├── member
│ │ ├── MemberRepositoryImpl.java
Expand Down Expand Up @@ -423,3 +464,85 @@ com.loopers
- ❌ 순환 참조
- ❌ 도메인 로직 누수 (Controller에 비즈니스 로직)
- ❌ God Service (하나의 Service에 모든 로직)

---

## Application Layer 규칙 (확정 결정)

### 어노테이션
- App: **`@Component`** 사용 (절대 `@Service` 사용 금지)
- Facade: **`@Component`** 사용 (절대 `@Service` 사용 금지)
- Service: **`@Service`** 사용

### App 의존성 규칙
```
✅ App → Service (허용)
❌ App → Repository 직접 의존 (금지)
❌ App → App 의존 (금지)
❌ App → Facade 의존 (금지)
```

### Facade 사용 조건 및 의존성 규칙
```
✅ Facade → App (허용, 반드시 2개 이상의 App 사용 시에만 Facade 생성)
❌ Facade → Service 직접 호출 (금지 — 반드시 App 경유)
❌ Facade → Repository 직접 의존 (금지)
❌ Facade → Facade 의존 (금지)
❌ 단일 App만 사용하는 Facade 생성 (금지 — App을 직접 사용할 것)
```

### 크로스 도메인 오케스트레이션은 Facade 책임 (App 경유)
```java
// ✅ 올바른 예: BrandFacade에서 cascade delete 처리 (App 경유)
@Transactional
public void deleteBrand(String brandId) {
brandApp.deleteBrand(brandId); // BrandService 호출
productApp.deleteProductsByBrandRefId(brandId); // ProductService 호출
}

// ❌ 잘못된 예: Facade에서 Service 직접 호출
@Transactional
public void deleteBrand(String brandId) {
brandService.deleteBrand(brandId); // 금지: Facade → Service 직접 호출
productService.deleteProductsByBrandRefId(brand.getId()); // 금지
}
```

도메인 간 연쇄 처리(cascade)가 필요하면 Service에 두지 말고 Facade에서 App을 통해 조율할 것.

---

## 도메인 간 의존성 규칙 (확정 결정)

### Model과 Repository 인터페이스: 자기 도메인 VO만 사용
```java
// ✅ OrderModel은 order.vo만 import
import com.loopers.domain.order.vo.OrderId;
import com.loopers.domain.order.vo.RefMemberId; // order 도메인 소유

// ❌ OrderModel이 like.vo를 import하는 것은 금지
import com.loopers.domain.like.vo.RefMemberId;
```

### Service: 타 도메인 Repository 호출 시 해당 도메인 VO import 허용
```java
// ✅ LikeService가 ProductRepository를 호출하기 위해 ProductId VO import
import com.loopers.domain.product.ProductRepository;
import com.loopers.domain.product.vo.ProductId; // 관계가 있으므로 허용

// ✅ ProductService가 BrandRepository를 호출하기 위해 BrandId VO import
import com.loopers.domain.brand.BrandRepository;
import com.loopers.domain.brand.vo.BrandId; // 관계가 있으므로 허용
```

Service 계층에서 타 도메인 Repository를 직접 사용하는 것은 **트랜잭션 원자성 보장** 목적으로 허용됨.
단, 이는 의도적 설계 결정이며, 단순 조회 위임이라면 타 도메인 Service를 통하는 것을 검토할 것.

### 참조 VO (RefOOOId) 소유권
- `RefMemberId`, `RefProductId` 등 타 도메인의 PK를 참조하는 VO는 **사용하는 도메인이 자기 vo 패키지에 별도 정의**
- 예: `order.vo.RefMemberId`, `like.vo.RefMemberId` — 같은 이름이어도 별개의 독립 VO
- 한 도메인의 VO를 다른 도메인 Model/Repository가 import하는 것은 도메인 경계 위반

### Converter 소유권
- 각 도메인의 VO에 대응하는 Converter는 해당 도메인의 VO를 사용하는 Entity 맥락에 맞게 별도 정의
- 예: `RefMemberIdConverter` (like.vo용), `OrderRefMemberIdConverter` (order.vo용)
13 changes: 13 additions & 0 deletions .claude/skills/chat/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
name: chat
description: 파일을 수정하지 않고 질문에만 답변하는 채팅 모드. 코드 분석, 개념 설명, 아키텍처 논의 등 순수 대화가 필요할 때 사용.
disable-model-invocation: true
allowed-tools: Read, Grep, Glob
---

채팅 모드가 활성화되었습니다.

이 모드에서는 파일 수정, 생성, 삭제, 명령어 실행을 하지 않습니다.
코드 읽기와 검색만 허용되며, 질문에 대한 답변과 분석에 집중합니다.

$ARGUMENTS
45 changes: 44 additions & 1 deletion .claude/skills/coding-standards/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,17 @@ allowed-tools: Read, Grep
| Controller | `{Domain}V{version}Controller` | `MemberV1Controller` | `interfaces.api.{domain}` |
| API Spec | `{Domain}V{version}ApiSpec` | `MemberV1ApiSpec` | `interfaces.api.{domain}` |
| DTO | `{Domain}V{version}Dto` | `MemberV1Dto` | `interfaces.api.{domain}` |
| Facade | `{Domain}Facade` | `MemberFacade` | `application.{domain}` |
| **App** | `{Domain}App` | `MemberApp`, `OrderApp` | `application.{domain}` |
| **Facade** | `{Domain}Facade` | `OrderFacade` | `application.{domain}` |
| Info | `{Domain}Info` | `MemberInfo` | `application.{domain}` |
| Exception | `{Concept}Exception` | `CoreException` | `support.error` |
| Converter | `{ValueObject}Converter` | `MemberIdConverter` | `infrastructure.jpa.converter` |

> **App vs Facade 선택 기준**:
> - 단일 도메인 유스케이스 → **`{Domain}App`** 사용
> - 2개 이상의 App을 조합하는 크로스 도메인 → **`{Domain}Facade`** 사용
> - Facade는 반드시 2개 이상의 App을 호출할 때만 생성 (단일 App만 쓰는 Facade 금지)

### 메서드 네이밍

#### Repository
Expand All @@ -39,6 +45,15 @@ allowed-tools: Read, Grep

#### Service
- 도메인 용어 사용: `register`, `getMemberByMemberId`, `updateProfile`, `withdraw`
- 타 도메인 PK(DB id)를 파라미터로 받는 메서드: **`RefId` 접미사** 사용 (`DbId` 사용 금지)
```java
// ✅ 올바름
ProductModel getProductByRefId(Long id)
void deleteProductsByBrandRefId(Long brandDbId)
BrandModel getBrandByRefId(Long id)
// ❌ 금지
ProductModel getProductByDbId(Long id)
```

#### Controller
- RESTful 원칙: GET (조회), POST (생성), PUT (전체 수정), PATCH (부분 수정), DELETE (삭제)
Expand Down Expand Up @@ -401,6 +416,34 @@ public record Email(String address) {
5. **Unused Import**: 사용하지 않는 import 제거
6. **Raw Type**: 제네릭 타입 명시
7. **Exception Swallowing**: 예외를 무시하지 말 것
8. **`var` 키워드 사용 금지**: 반드시 명시적 타입 사용
```java
// ❌ 금지
var product = productRepository.findById(id);
// ✅ 허용
Optional<ProductModel> product = productRepository.findById(id);
```
9. **중첩 클래스/레코드 정의 금지**: 클래스나 record 내부에 다른 record/class 정의 금지 → 별도 파일로 분리
```java
// ❌ 금지
public class OrderApp {
public record OrderCommand(String productId, int qty) {}
}
// ✅ 허용: OrderCommand.java 별도 파일로 생성
```
10. **단일 도메인에 Facade 생성 금지**: 단일 도메인은 App으로 처리
```java
// ❌ 금지: MemberFacade가 MemberApp 하나만 사용하는 경우
public class MemberFacade {
private final MemberApp memberApp; // 단일 App만 사용 → Facade 불필요
}
// ✅ 허용: App 직접 사용
public class MemberV1Controller {
private final MemberApp memberApp;
}
```
11. **App/Facade에서 Repository 직접 의존 금지**: 반드시 Service 경유
12. **Facade에서 Service 직접 호출 금지**: 반드시 App 경유

### ✅ Best Practices
1. **불변 객체 선호**: `record`, `final` 활용
Expand Down
61 changes: 61 additions & 0 deletions .claude/skills/jpa-database/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,66 @@ class MemberServiceIntegrationTest {

---

## @Query 패턴 (확정 결정)

### EntityManager 직접 사용 금지
프로덕션 코드에서 `EntityManager`를 직접 사용하는 것은 금지. 반드시 JpaRepository의 `@Query`로 대체.

```java
// ❌ 금지: EntityManager 직접 사용
@Autowired
private EntityManager entityManager;

public Page<ProductModel> findProducts(...) {
Query query = entityManager.createNativeQuery("SELECT ...", ProductModel.class);
// ...
}

// ✅ 올바름: JpaRepository에 @Query 정의
public interface ProductJpaRepository extends JpaRepository<ProductModel, Long> {
@Query(value = "SELECT * FROM products WHERE ...", nativeQuery = true)
Page<ProductModel> findActiveProducts(...);
}
```

### 페이징 네이티브 쿼리: countQuery 필수
```java
// ✅ 페이징 native query는 반드시 countQuery 명시
@Query(
value = "SELECT p.* FROM products p LEFT JOIN likes l ON p.id = l.ref_product_id " +
"WHERE p.deleted_at IS NULL GROUP BY p.id ORDER BY COUNT(l.id) DESC",
countQuery = "SELECT COUNT(*) FROM products p WHERE p.deleted_at IS NULL",
nativeQuery = true
)
Page<ProductModel> findActiveSortByLikesDesc(Pageable pageable);
```

### 조건부 UPDATE: @Modifying + @Query
```java
// ✅ 재고 차감처럼 조건부 UPDATE는 @Modifying 사용
@Modifying
@Query(value = "UPDATE products SET stock_quantity = stock_quantity - :quantity " +
"WHERE id = :productId AND stock_quantity >= :quantity", nativeQuery = true)
int decreaseStockIfAvailable(@Param("productId") Long productId, @Param("quantity") int quantity);
```

### nullable 파라미터 조건 필터링: JPQL 활용
```java
// ✅ null 가능한 파라미터는 JPQL의 조건부 표현식으로 처리
@Query("SELECT o FROM OrderModel o WHERE o.refMemberId = :refMemberId " +
"AND (:startDateTime IS NULL OR o.createdAt >= :startDateTime) " +
"AND (:endDateTime IS NULL OR o.createdAt <= :endDateTime) " +
"ORDER BY o.createdAt DESC")
Page<OrderModel> findByRefMemberIdWithDateFilter(
@Param("refMemberId") RefMemberId refMemberId,
@Param("startDateTime") LocalDateTime startDateTime,
@Param("endDateTime") LocalDateTime endDateTime,
Pageable pageable
);
```

---

## 주의사항

### Entity 설계
Expand All @@ -462,6 +522,7 @@ class MemberServiceIntegrationTest {

### Repository 설계
- ❌ **Service에서 JpaRepository 직접 사용 금지**: RepositoryImpl 경유
- ❌ **EntityManager 직접 사용 금지**: `@Query` 어노테이션으로 대체
- ✅ **Domain Repository 인터페이스**: 도메인 용어 사용
- ✅ **쿼리 메서드 활용**: 간단한 조회는 메서드명으로

Expand Down
Loading