Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
38795db
test: ArchUnit 아키텍처 검증 테스트 추가
juoklee Feb 22, 2026
04884ed
feat: Admin 인증 필터 구현 (X-Loopers-Ldap 헤더 검증)
juoklee Feb 22, 2026
93cb7ce
feat: Brand 도메인 전체 구현
juoklee Feb 22, 2026
9199679
refactor: MemberService에 @Transactional 적용
juoklee Feb 22, 2026
ed64783
feat: Member 확장 - gender/phone 추가, 전화번호 수정/회원 탈퇴 API + DIP 리팩토링
juoklee Feb 22, 2026
e86c2fb
feat: Product 도메인 전체 구현
juoklee Feb 22, 2026
0217669
feat: Address 도메인 구현 (배송지 CRUD + 기본 배송지 관리)
juoklee Feb 24, 2026
23f411b
refactor: Interfaces → Domain 직접 접근 차단 (ArchUnit 강화)
juoklee Feb 24, 2026
a053483
refactor: 계층별 책임 분리 개선 (Entity 불변식 내재화, N+1 해소)
juoklee Feb 24, 2026
ee5233b
feat: Like 도메인 구현 (단일 다형 구조 — 상품/브랜드 좋아요 토글 + 내 좋아요 목록)
juoklee Feb 24, 2026
d8639ec
feat: Order 도메인 구현 (주문 생성/취소/조회 + 재고 선차감 + 계층 책임 분리)
juoklee Feb 26, 2026
25dba34
feat: Admin 회원 관리 API 구현 (AD-8 목록 조회 + AD-9 상세 조회)
juoklee Feb 26, 2026
3b2df7e
refactor: 비즈니스 규칙 유출 수정 (Facade → Domain/Service 위임)
juoklee Feb 26, 2026
9faf042
refactor: ArchUnit 검증 강화 (Domain Repository 순수성 + setter 금지 규칙 추가)
juoklee Feb 26, 2026
7197560
refactor: Domain Reader 인터페이스 Spring Data 의존 제거 (Page/Pageable → Page…
juoklee Feb 26, 2026
192e724
refactor: Facade→Reader 직접 의존 제거 + Controller 인증 로직 공통화
juoklee Feb 26, 2026
c688447
Merge branch 'Loopers-dev-lab:main' into round3
juoklee Feb 26, 2026
2d22d5d
refactor: AuthFilter를 Support→Interfaces 계층으로 이동 + 인증 로직 계층화
juoklee Mar 2, 2026
ac64038
docs: 설계 문서 현행화 (리팩토링 후 코드 기준 반영)
juoklee Mar 2, 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
3 changes: 3 additions & 0 deletions apps/commerce-api/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ dependencies {
annotationProcessor("jakarta.persistence:jakarta.persistence-api")
annotationProcessor("jakarta.annotation:jakarta.annotation-api")

// architecture test
testImplementation("com.tngtech.archunit:archunit-junit5:1.4.0")

// test-fixtures
testImplementation(testFixtures(project(":modules:jpa")))
testImplementation(testFixtures(project(":modules:redis")))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.loopers.application;

import java.util.List;

public record PagedInfo<T>(
List<T> content,
long totalElements,
int totalPages,
int page,
int size
) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.loopers.application.address;

import com.loopers.domain.address.Address;
import com.loopers.domain.address.AddressService;
import com.loopers.domain.member.Member;
import com.loopers.domain.member.MemberService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

import java.util.List;

@RequiredArgsConstructor
@Component
public class AddressFacade {

private final AddressService addressService;
private final MemberService memberService;

public AddressInfo register(String loginId, String label, String recipientName, String recipientPhone,
String zipCode, String address1, String address2) {
Long memberId = getMemberId(loginId);
Address address = addressService.register(memberId, label, recipientName, recipientPhone,
zipCode, address1, address2);
return AddressInfo.from(address);
}

public AddressInfo getAddress(String loginId, Long addressId) {
Long memberId = getMemberId(loginId);
Address address = addressService.getAddress(addressId, memberId);
return AddressInfo.from(address);
}

public List<AddressInfo> getAddresses(String loginId) {
Long memberId = getMemberId(loginId);
return addressService.getAddresses(memberId).stream()
.map(AddressInfo::from)
.toList();
}

public void update(String loginId, Long addressId, String label, String recipientName, String recipientPhone,
String zipCode, String address1, String address2) {
Long memberId = getMemberId(loginId);
addressService.update(addressId, memberId, label, recipientName, recipientPhone,
zipCode, address1, address2);
}

public void delete(String loginId, Long addressId) {
Long memberId = getMemberId(loginId);
addressService.delete(addressId, memberId);
}

public void changeDefault(String loginId, Long addressId) {
Long memberId = getMemberId(loginId);
addressService.changeDefault(addressId, memberId);
}

private Long getMemberId(String loginId) {
Member member = memberService.getMemberByLoginId(loginId);
return member.getId();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.loopers.application.address;

import com.loopers.domain.address.Address;

public record AddressInfo(Long id, String label, String recipientName, String recipientPhone,
String zipCode, String address1, String address2, boolean isDefault) {

public static AddressInfo from(Address address) {
return new AddressInfo(
address.getId(),
address.getLabel(),
address.getRecipientName(),
address.getRecipientPhone(),
address.getZipCode(),
address.getAddress1(),
address.getAddress2(),
address.getIsDefault()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.loopers.application.brand;

import com.loopers.application.PagedInfo;
import com.loopers.domain.PageResult;
import com.loopers.domain.brand.BrandService;
import com.loopers.domain.brand.Brand;
import com.loopers.domain.product.ProductService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

@RequiredArgsConstructor
@Component
public class BrandFacade {

private final BrandService brandService;
private final ProductService productService;

public BrandInfo register(String name, String description) {
Brand brand = brandService.register(name, description);
return BrandInfo.from(brand);
}

public BrandInfo getBrand(Long id) {
Brand brand = brandService.getBrand(id);
return BrandInfo.from(brand);
}

public PagedInfo<BrandInfo> getBrands(String keyword, int page, int size) {
PageResult<Brand> result = brandService.getBrands(keyword, page, size);
return new PagedInfo<>(
result.content().stream().map(BrandInfo::from).toList(),
result.totalElements(),
result.totalPages(),
result.page(),
result.size()
);
}

public void update(Long id, String name, String description) {
brandService.update(id, name, description);
}

@Transactional
public void delete(Long id) {
brandService.delete(id);
productService.deleteAllByBrandId(id);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.loopers.application.brand;

import com.loopers.domain.brand.Brand;

public record BrandInfo(Long id, String name, String description, int likeCount) {
public static BrandInfo from(Brand brand) {
return new BrandInfo(
brand.getId(),
brand.getName(),
brand.getDescription(),
brand.getLikeCount()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.loopers.application.like;

import com.loopers.application.brand.BrandInfo;
import com.loopers.domain.brand.Brand;
import com.loopers.domain.like.Like;

import java.time.ZonedDateTime;

public record BrandLikeInfo(
BrandInfo brand,
ZonedDateTime likedAt
) {
public static BrandLikeInfo of(Like like, Brand brand) {
return new BrandLikeInfo(
BrandInfo.from(brand),
like.getCreatedAt()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package com.loopers.application.like;

import com.loopers.application.PagedInfo;
import com.loopers.domain.PageResult;
import com.loopers.domain.brand.Brand;
import com.loopers.domain.brand.BrandService;
import com.loopers.domain.like.Like;
import com.loopers.domain.like.LikeService;
import com.loopers.domain.like.LikeTargetType;
import com.loopers.domain.member.Member;
import com.loopers.domain.member.MemberService;
import com.loopers.domain.product.Product;
import com.loopers.domain.product.ProductService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;

@RequiredArgsConstructor
@Component
public class LikeFacade {

private final MemberService memberService;
private final ProductService productService;
private final BrandService brandService;
private final LikeService likeService;

@Transactional
public LikeToggleInfo toggleProductLike(String loginId, Long productId) {
Long memberId = getMemberId(loginId);
productService.getProduct(productId);

boolean liked = likeService.toggleLike(memberId, LikeTargetType.PRODUCT, productId);

int likeCount = liked
? productService.increaseLikeCount(productId)
: productService.decreaseLikeCount(productId);

return new LikeToggleInfo(liked, likeCount);
}

@Transactional
public LikeToggleInfo toggleBrandLike(String loginId, Long brandId) {
Long memberId = getMemberId(loginId);
brandService.getBrand(brandId);

boolean liked = likeService.toggleLike(memberId, LikeTargetType.BRAND, brandId);

int likeCount = liked
? brandService.increaseLikeCount(brandId)
: brandService.decreaseLikeCount(brandId);

return new LikeToggleInfo(liked, likeCount);
}

@Transactional(readOnly = true)
public PagedInfo<ProductLikeInfo> getMyLikedProducts(String loginId, int page, int size) {
Long memberId = getMemberId(loginId);
PageResult<Like> likes = likeService.getMyLikes(memberId, LikeTargetType.PRODUCT, page, size);

List<Long> productIds = likes.content().stream()
.map(Like::getTargetId).toList();
Map<Long, Product> productMap = productService.getProductsByIds(productIds).stream()
.collect(Collectors.toMap(Product::getId, Function.identity()));

List<Long> brandIds = productMap.values().stream()
.map(Product::getBrandId).distinct().toList();
Map<Long, Brand> brandMap = brandService.getBrandsByIds(brandIds).stream()
.collect(Collectors.toMap(Brand::getId, Function.identity()));

List<ProductLikeInfo> content = likes.content().stream()
.filter(like -> productMap.containsKey(like.getTargetId()))
.map(like -> {
Product product = productMap.get(like.getTargetId());
Brand brand = brandMap.get(product.getBrandId());
return ProductLikeInfo.of(like, product, brand);
})
.toList();

return new PagedInfo<>(content, likes.totalElements(), likes.totalPages(), likes.page(), likes.size());
}

@Transactional(readOnly = true)
public PagedInfo<BrandLikeInfo> getMyLikedBrands(String loginId, int page, int size) {
Long memberId = getMemberId(loginId);
PageResult<Like> likes = likeService.getMyLikes(memberId, LikeTargetType.BRAND, page, size);

List<Long> brandIds = likes.content().stream()
.map(Like::getTargetId).toList();
Map<Long, Brand> brandMap = brandService.getBrandsByIds(brandIds).stream()
.collect(Collectors.toMap(Brand::getId, Function.identity()));

List<BrandLikeInfo> content = likes.content().stream()
.filter(like -> brandMap.containsKey(like.getTargetId()))
.map(like -> {
Brand brand = brandMap.get(like.getTargetId());
return BrandLikeInfo.of(like, brand);
})
.toList();

return new PagedInfo<>(content, likes.totalElements(), likes.totalPages(), likes.page(), likes.size());
}

private Long getMemberId(String loginId) {
Member member = memberService.getMemberByLoginId(loginId);
return member.getId();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.loopers.application.like;

public record LikeToggleInfo(boolean liked, int likeCount) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.loopers.application.like;

import com.loopers.application.brand.BrandInfo;
import com.loopers.application.product.ProductInfo;
import com.loopers.domain.brand.Brand;
import com.loopers.domain.like.Like;
import com.loopers.domain.product.Product;

import java.time.ZonedDateTime;

public record ProductLikeInfo(
ProductInfo product,
ZonedDateTime likedAt
) {
public static ProductLikeInfo of(Like like, Product product, Brand brand) {
return new ProductLikeInfo(
ProductInfo.of(product, BrandInfo.from(brand)),
like.getCreatedAt()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.loopers.application.member;

import com.loopers.domain.member.Member;

import java.time.LocalDate;
import java.time.ZonedDateTime;

public record AdminMemberInfo(
Long memberId,
String loginId,
String name,
LocalDate birthDate,
String gender,
String email,
String phone,
ZonedDateTime createdAt
) {
public static AdminMemberInfo from(Member member) {
return new AdminMemberInfo(
member.getId(),
member.getLoginId(),
member.getName(),
member.getBirthDate(),
member.getGender().name(),
member.getEmail(),
member.getPhone(),
member.getCreatedAt()
);
}
}
Loading