Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
7440a13
feat: ๋ธŒ๋žœ๋“œ crud ๊ธฐ๋Šฅ ๊ตฌํ˜„
hey-sion Feb 19, 2026
b086151
refactor: E2E ํ…Œ์ŠคํŠธ DisplayName ๋ฐ ๊ตฌ์กฐ ๊ฐœ์„ 
hey-sion Feb 19, 2026
06ea3af
test: Product ๋„๋ฉ”์ธ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ (Red phase)
hey-sion Feb 20, 2026
c5be832
refactor: ํ…Œ์ŠคํŠธ ๋ฉ”์„œ๋“œ๋ช… ๋ณ€๊ฒฝ
hey-sion Feb 20, 2026
028667f
refactor: product service, sort enum ํด๋ž˜์Šค ์œ„์น˜ ๋ณ€๊ฒฝ
hey-sion Feb 20, 2026
f554f54
feat: product crud ๊ธฐ๋Šฅ ๊ตฌํ˜„
hey-sion Feb 20, 2026
8c03901
refactor: ๋ ˆ์ด์–ด ์—ญ์ „ ๋ฐฉ์ง€๋ฅผ ์œ„ํ•ด application/domain ์˜์กด์„ฑ ๊ตฌ์กฐ ์ •๋ฆฌ
hey-sion Feb 20, 2026
7ae7e67
feat: Product JPA repository ๊ตฌํ˜„์ฒด ์ถ”๊ฐ€
hey-sion Feb 20, 2026
386d3b6
refactor: Service ํ…Œ์ŠคํŠธ ํŒจํ‚ค์ง€๋ฅผ application ๋ ˆ์ด์–ด์— ๋งž๊ฒŒ ์ •๋ ฌ
hey-sion Feb 20, 2026
cb9b90d
feat: PageResponse DTO ๋„์ž… ๋ฐ ์–ด๋“œ๋ฏผ Brand ๋ชฉ๋ก ์‘๋‹ต/ํ…Œ์ŠคํŠธ ๊ฐœ์„ 
hey-sion Feb 23, 2026
0cf10c5
feat: Product CRUD api ๊ตฌํ˜„ ๋ฐ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ถ”๊ฐ€
hey-sion Feb 23, 2026
a332af6
refactor: Service/Facade ๋ฐ˜ํ™˜ ํƒ€์ž…์„ application ๋ ˆ์ด์–ด Info DTO๋กœ ํ†ต์ผ
hey-sion Feb 23, 2026
7f35955
feat: ๋ˆ„๋ฝ ํŒŒ์ผ ์ถ”๊ฐ€
hey-sion Feb 23, 2026
52fd81f
feat: ProductFacade ๋„์ž… ๋ฐ ์„œ๋น„์Šค๊ฐ„ ์˜์กด ์ œ๊ฑฐ
hey-sion Feb 24, 2026
bde6a33
refactor: ๋ ˆ์ด์–ด ์˜์กด ์ •๋ฆฌ (ProductOrder ๋„์ž…)
hey-sion Feb 24, 2026
32a3819
refactor: Product ์กฐํšŒ ๋ฉ”์„œ๋“œ๋ช…์„ ๋™์ž‘ ๊ธฐ์ค€์œผ๋กœ ๋ณ€๊ฒฝ, ์ž‘์—…๊ธฐ๋ก ์—…๋ฐ์ดํŠธ
hey-sion Feb 24, 2026
7d90015
feat: brandId๋กœ product๋ฅผ ์กฐํšŒํ•˜๋Š” ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€
hey-sion Feb 24, 2026
2a1bc65
feat: userId๋ฅผ ํ†ตํ•œ user ์กฐํšŒ ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€
hey-sion Feb 24, 2026
d4d0055
feat: ์ƒํ’ˆ ์ข‹์•„์š” ๊ด€๋ จ ๊ตฌํ˜„
hey-sion Feb 25, 2026
200af51
feat: ์ข‹์•„์š” ๋ชฉ๋ก ์กฐํšŒ ์‹œ ์ƒํ’ˆ ์ •๋ณด ํฌํ•จ ์‘๋‹ต์œผ๋กœ ๋ณ€๊ฒฝ
hey-sion Feb 25, 2026
61a9f72
refactor: SignUpValidator๋ฅผ ์ œ๊ฑฐํ•˜๊ณ  ๊ฒ€์ฆ ๋กœ์ง์„ ๋ ˆ์ด์–ด์— ๋งž๊ฒŒ ๋ถ„๋ฆฌ
hey-sion Feb 25, 2026
feb3cf4
feat: ํ—ค๋” ๊ธฐ๋ฐ˜ ์ธ์ฆ ์ธํ„ฐ์…‰ํ„ฐ/๋ฆฌ์กธ๋ฒ„ ๋„์ž… ๋ฐ ๊ด€๋ จ ๋กœ์ง ์ˆ˜์ •
hey-sion Feb 25, 2026
31e9814
feat: product์— likeCount ์ถ”๊ฐ€ ๋ฐ ๊ด€๋ จ ๋กœ์ง ๊ตฌํ˜„
hey-sion Feb 25, 2026
0dadedf
refactor: ์ข‹์•„์š” ์ทจ์†Œ ๋กœ์ง ์ˆ˜์ •
hey-sion Feb 25, 2026
9f34d58
test: ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ๋‚ด ์ž˜๋ชป๋œ ๊ฒ€์ฆ ๋ฉ”์„œ๋“œ ๋ณ€๊ฒฝ
hey-sion Feb 25, 2026
ad96818
feat: ์ข‹์•„์š” ์ทจ์†Œ ๊ด€๋ จ ํ…Œ์ŠคํŠธ ์ˆ˜์ • ๋ฐ ๋ฌธ์„œ ์—…๋ฐ์ดํŠธ
hey-sion Feb 25, 2026
a4b1704
feat: ์ƒํ’ˆ ์‚ญ์ œ์‹œ ์ข‹์•„์š”๋„ ์‚ญ์ œํ•˜๋Š” ๋กœ์ง ์ถ”๊ฐ€
hey-sion Feb 25, 2026
1dd8627
feat: ์ฃผ๋ฌธ api ๊ตฌํ˜„ ๋ฐ ๊ด€๋ จ ํ…Œ์ŠคํŠธ ์ถ”๊ฐ€
hey-sion Feb 26, 2026
27a18e0
feat: ์ƒํ’ˆ ๋ชฉ๋ก ์ข‹์•„์š”์ˆœ ์ •๋ ฌ ์ถ”๊ฐ€
hey-sion Feb 26, 2026
34f8657
refactor: ์ƒํ’ˆ์˜ ๋…ธ์ถœ์—ฌ๋ถ€์™€ ์‚ญ์ œ์—ฌ๋ถ€๋ฅผ ๋ฌถ์–ด์„œ active ๋„ค์ด๋ฐ ์‚ฌ์šฉํ•˜๋„๋ก ๋ณ€๊ฒฝ
hey-sion Feb 27, 2026
ad1d5d8
refactor: ์ƒํ’ˆ ๋ฐ˜ํ™˜์‹œ ๋ธŒ๋žœ๋“œ ์ด๋ฆ„์„ ํฌํ•จํ•˜๋„๋ก ๊ฐœ์„ 
hey-sion Feb 27, 2026
a0ba6f4
refactor: ๋ธŒ๋žœ๋“œ ํ™•์žฅ์„ฑ์„ ์œ„ํ•ด ์ƒํ’ˆ ๋‚ด๋ธŒ๋žœ๋“œ ์ •๋ณด๋ฅผ BrandSummary VO๋กœ ์บก์Аํ™”
hey-sion Feb 27, 2026
24cdc9b
refactor: ๋ฉ”์„œ๋“œ๋ช… ๋ณ€๊ฒฝ ๋ฐ ๋ˆ„๋ฝ๋œ @Transactional ์ถ”๊ฐ€
hey-sion Feb 27, 2026
4b8d048
refactor: application ๋ ˆ์ด์–ด ์„œ๋น„์Šค ํด๋ž˜์Šค๋ช… ApplicationService๋กœ ํ†ต์ผ
hey-sion Feb 27, 2026
9f79bc6
refactor: ์ƒํ’ˆ์กฐํšŒ์‹œ ์‚ญ์ œ์ƒํ’ˆ์€ ๊ธฐ๋ณธ์ ์œผ๋กœ ์กฐํšŒํ•˜์ง€ ์•Š๋„๋ก ์ˆ˜์ •
hey-sion Feb 27, 2026
76f4133
refactor: ์ฝ”๋“œ ๋ผ์ธ ์ •๋ฆฌ
hey-sion Mar 3, 2026
2a2c883
refactor: Order๊ฐ€ ์ฃผ๋ฌธ๊ธˆ์•ก ๊ณ„์‚ฐ ์ฑ…์ž„์„ ์ง์ ‘ ๋‹ด๋‹นํ•˜๋„๋ก ๊ฐœ์„ 
hey-sion Mar 3, 2026
87e7467
refactor: ํด๋ž˜์Šค ๋ ˆ๋ฒจ @Transactional์„ ๋ฉ”์„œ๋“œ ๋ ˆ๋ฒจ๋กœ ์ด๋™
hey-sion Mar 3, 2026
a53ca80
refactor: Facade๋Š” ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ด์…˜ ๋กœ์ง๋งŒ ๋‹ด๋‹นํ•˜๋„๋ก ๋‹จ์ˆœ ์œ„์ž„ ๋ฉ”์„œ๋“œ ์ œ๊ฑฐ
hey-sion Mar 3, 2026
296b460
refactor: ApplicationService ๋„ค์ด๋ฐ์„ Service๋กœ ๋ณ€๊ฒฝ
hey-sion Mar 3, 2026
632e9bd
refactor: OrderCreateCommand์—์„œ interfaces ๊ณ„์ธต ์˜์กด ์ œ๊ฑฐ, ๋งคํ•‘ ์ฑ…์ž„์„ Controllerโ€ฆ
hey-sion Mar 3, 2026
3686462
fix: decreaseStock 0/์Œ์ˆ˜ ์ˆ˜๋Ÿ‰ ์ฐจ๋‹จ ๋ฐ ํ…Œ์ŠคํŠธ ์ถ”๊ฐ€
hey-sion Mar 3, 2026
65602cb
refactor: visibleํ•œ ์ƒํƒœ์˜ ์ƒํ’ˆ๋งŒ ์žฌ๊ณ ์ฐจ๊ฐ ๊ฐ€๋Šฅํ•˜๋„๋ก ์ฟผ๋ฆฌ ์ˆ˜์ •
hey-sion Mar 4, 2026
32b3558
refactor: ์žฌ๊ณ  ์Œ์ˆ˜๊ฐ’ ๊ฒ€์ฆ ๋กœ์ง ์ถ”๊ฐ€, ๋ถˆํ•„์š”ํ•œ ๋ฉ”์„œ๋“œ ์ œ๊ฑฐ
hey-sion Mar 4, 2026
ac936d3
refactor: ์ž‘์—…๋กœ๊ทธ ์—…๋ฐ์ดํŠธ
hey-sion Mar 4, 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
2 changes: 1 addition & 1 deletion .docs/design/01-requirements.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@
### 4.2 ์ข‹์•„์š”

- ์ด๋ฏธ ์ข‹์•„์š”ํ•œ ์ƒํ’ˆ์— ๋‹ค์‹œ ์ข‹์•„์š”๋ฅผ ๋ˆ„๋ฅผ ๊ฒฝ์šฐ ์˜ค๋ฅ˜๋กœ ์ฒ˜๋ฆฌํ•œ๋‹ค.
- ์ข‹์•„์š”ํ•˜์ง€ ์•Š์€ ์ƒํ’ˆ์— ์ข‹์•„์š” ์ทจ์†Œ๋ฅผ ์š”์ฒญํ•  ๊ฒฝ์šฐ ์˜ค๋ฅ˜๋กœ ์ฒ˜๋ฆฌํ•œ๋‹ค.
- ์ข‹์•„์š”ํ•˜์ง€ ์•Š์€ ์ƒํ’ˆ์— ์ข‹์•„์š” ์ทจ์†Œ๋ฅผ ์š”์ฒญํ•  ๊ฒฝ์šฐ ์•„๋ฌด ๋™์ž‘ ์—†์ด ์„ฑ๊ณต์œผ๋กœ ์ฒ˜๋ฆฌํ•œ๋‹ค. (๋ฉฑ๋“ฑ)
- ์‚ญ์ œ๋œ ์ƒํ’ˆ์— ๋Œ€ํ•œ ์ข‹์•„์š”๋Š” ๋ชฉ๋ก์—์„œ ์ œ์™ธํ•œ๋‹ค.

### 4.3 ์ฃผ๋ฌธ
Expand Down
1,288 changes: 1,288 additions & 0 deletions .docs/design/work-log3.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.loopers.application.brand;

import com.loopers.application.like.LikeService;
import com.loopers.application.product.ProductService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@RequiredArgsConstructor
@Service
public class BrandFacade {
private final BrandService brandService;
private final ProductService productService;
private final LikeService likeService;

@Transactional
public void delete(Long brandId) {
List<Long> productIds = productService.getProductIdsByBrandId(brandId);
likeService.deleteAllByProductIds(productIds);
productService.deleteAllByBrandId(brandId);
brandService.delete(brandId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.loopers.application.brand;

import com.loopers.domain.brand.Brand;

import java.time.ZonedDateTime;

public record BrandInfo(
Long id,
String name,
String description,
ZonedDateTime createdAt,
ZonedDateTime updatedAt,
ZonedDateTime deletedAt
) {
public static BrandInfo from(Brand brand) {
return new BrandInfo(
brand.getId(),
brand.getName(),
brand.getDescription(),
brand.getCreatedAt(),
brand.getUpdatedAt(),
brand.getDeletedAt()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package com.loopers.application.brand;

import com.loopers.domain.brand.Brand;
import com.loopers.domain.brand.BrandRepository;
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.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Collection;
import java.util.Map;
import java.util.stream.Collectors;

@RequiredArgsConstructor
@Service
public class BrandService {
private final BrandRepository brandRepository;

@Transactional
public BrandInfo register(String name, String description) {
Brand brand = Brand.create(name, description);
return BrandInfo.from(brandRepository.save(brand));
}

@Transactional(readOnly = true)
public BrandInfo getBrand(Long id) {
Brand brand = findById(id);
if (brand.getDeletedAt() != null) {
throw new CoreException(ErrorType.NOT_FOUND, "[brandId = " + id + "] ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.");
}
return BrandInfo.from(brand);
}

@Transactional(readOnly = true)
public Page<BrandInfo> getBrands(Pageable pageable) {
return brandRepository.findAllByDeletedAtIsNull(pageable).map(BrandInfo::from);
}

@Transactional
public BrandInfo update(Long id, String name, String description) {
Brand brand = findNonDeletedById(id);
brand.update(name, description);
return BrandInfo.from(brand);
}

@Transactional(readOnly = true)
public Map<Long, String> getBrandNameMap(Collection<Long> brandIds) {
return brandRepository.findAllByIdIn(brandIds).stream()
.collect(Collectors.toMap(Brand::getId, Brand::getName));
}

@Transactional
public void delete(Long id) {
Brand brand = findById(id);
brand.delete();
}

private Brand findNonDeletedById(Long id) {
Brand brand = findById(id);
if (brand.getDeletedAt() != null) {
throw new CoreException(ErrorType.NOT_FOUND, "[brandId = " + id + "] ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.");
}
return brand;
}

private Brand findById(Long id) {
return brandRepository.findById(id)
.orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND,
"[brandId = " + id + "] ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.loopers.application.like;

import com.loopers.application.product.ProductInfo;
import com.loopers.application.product.ProductService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

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

@RequiredArgsConstructor
@Service
public class LikeFacade {
private final LikeService likeService;
private final ProductService productService;

@Transactional
public LikeInfo register(Long userId, Long productId) {
LikeInfo like = likeService.register(userId, productId);
productService.increaseLikeCount(productId);
return like;
}

@Transactional
public void cancel(Long userId, Long productId) {
if (likeService.cancel(userId, productId)) {
productService.decreaseLikeCount(productId);
}
}

public List<LikedProductInfo> getLikedProductsByUserId(Long userId) {
List<LikeInfo> likes = likeService.getLikesByUserId(userId);
if (likes.isEmpty()) return List.of();

List<Long> productIds = likes.stream().map(LikeInfo::productId).toList();
Map<Long, ProductInfo> productMap = productService.getActiveProductsByIds(productIds)
.stream()
.collect(Collectors.toMap(ProductInfo::id, p -> p));

return likes.stream()
.filter(like -> productMap.containsKey(like.productId()))
.map(like -> new LikedProductInfo(like.id(), productMap.get(like.productId()), like.createdAt()))
.toList();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.loopers.application.like;

import com.loopers.domain.like.Like;

import java.time.ZonedDateTime;

public record LikeInfo(
Long id,
Long userId,
Long productId,
ZonedDateTime createdAt
) {
public static LikeInfo from(Like like) {
return new LikeInfo(
like.getId(),
like.getUserId(),
like.getProductId(),
like.getCreatedAt()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.loopers.application.like;

import com.loopers.domain.like.Like;
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.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@RequiredArgsConstructor
@Service
public class LikeService {
private final LikeRepository likeRepository;

@Transactional
public LikeInfo register(Long userId, Long productId) {
likeRepository.findByUserIdAndProductId(userId, productId).ifPresent(like -> {
throw new CoreException(ErrorType.ALREADY_LIKED, "์ด๋ฏธ ์ข‹์•„์š”ํ•œ ์ƒํ’ˆ์ž…๋‹ˆ๋‹ค.");
});

Like like = Like.create(userId, productId);
return LikeInfo.from(likeRepository.save(like));
}

@Transactional
public boolean cancel(Long userId, Long productId) {
return likeRepository.deleteByUserIdAndProductId(userId, productId) > 0;
}

@Transactional(readOnly = true)
public List<LikeInfo> getLikesByUserId(Long userId) {
return likeRepository.findByUserId(userId)
.stream()
.map(LikeInfo::from)
.toList();
}

@Transactional
public void deleteAllByProductIds(List<Long> productIds) {
if (productIds.isEmpty()) {
return;
}

likeRepository.deleteAllByProductIdIn(productIds);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.loopers.application.like;

import com.loopers.application.product.ProductInfo;

import java.time.ZonedDateTime;

public record LikedProductInfo(
Long likeId,
ProductInfo product,
ZonedDateTime likedAt
) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.loopers.application.order;

import java.util.List;

public record OrderCreateCommand(Long userId, List<OrderItemCommand> items) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.loopers.application.order;

import com.loopers.application.product.ProductService;
import com.loopers.application.product.ProductInfo;
import com.loopers.domain.order.OrderItemSnapshot;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

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

@RequiredArgsConstructor
@Service
public class OrderFacade {
private final OrderService orderService;
private final ProductService productService;

@Transactional
public OrderInfo createOrder(OrderCreateCommand command) {
orderService.validateItems(command.items());
List<Long> productIds = command.items().stream()
.map(OrderItemCommand::productId)
.toList();
List<ProductInfo> products = productService.getActiveProductsByIdsOrThrow(productIds);
productService.decreaseStock(command.items());

Map<Long, ProductInfo> productMap = products.stream()
.collect(Collectors.toMap(ProductInfo::id, p -> p));
List<OrderItemSnapshot> snapshots = command.items().stream()
.map(item -> {
ProductInfo p = productMap.get(item.productId());
return new OrderItemSnapshot(p.id(), p.name(), p.price(), item.quantity());
})
.toList();
return orderService.placeOrder(command.userId(), snapshots);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.loopers.application.order;

import com.loopers.domain.order.Order;

import java.time.ZonedDateTime;

public record OrderInfo(
Long id,
Long userId,
Order.Status status,
Long totalAmount,
ZonedDateTime createdAt
) {
public static OrderInfo from(Order order) {
return new OrderInfo(
order.getId(),
order.getUserId(),
order.getStatus(),
order.getTotalAmount(),
order.getCreatedAt()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.loopers.application.order;

public record OrderItemCommand(Long productId, Integer quantity) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.loopers.application.order;

import com.loopers.domain.order.OrderItem;

public record OrderItemInfo(
Long id,
Long orderId,
Long productId,
String productName,
Long price,
Integer quantity
) {
public static OrderItemInfo from(OrderItem item) {
return new OrderItemInfo(
item.getId(),
item.getOrderId(),
item.getProductId(),
item.getProductName(),
item.getUnitPrice(),
item.getQuantity()
);
}
}
Loading