Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
a3390c8
feat: LOOK_NOT_FOUND 예외 메시지 추가 및 PRODUCT_NOT_FOUND 포맷 수정 #16
codrin2 Aug 12, 2025
ffedebf
refactor: Look 엔티티 구조 개편 및 연관관계 변경 #16
codrin2 Aug 12, 2025
612de51
refactor: LookImage 엔티티 생성 팩토리 메서드 추가 및 불필요 필드 제거 #16
codrin2 Aug 12, 2025
9b3bead
remove: LookLike 엔티티 삭제 #16
codrin2 Aug 12, 2025
4c2da78
feat: LookReaction 엔티티 추가 #16
codrin2 Aug 12, 2025
c860c6d
remove: LookReactionId 임베디드 키 클래스 삭제 #16
codrin2 Aug 12, 2025
8a1fed2
remove: ProductLike 엔티티 삭제 #16
codrin2 Aug 12, 2025
1fe7b14
feat: ProductLike 엔티티 추가 #16
codrin2 Aug 12, 2025
da0a8cb
remove: ProductLikeId 임베디드 키 클래스 삭제 #16
codrin2 Aug 12, 2025
546b7bd
refactor: ProductQueryService 정리 및 유틸 메서드 추가 #16
codrin2 Aug 12, 2025
3685612
feat: UserService에 사용자 존재 여부 검증 메서드 추가 #16
codrin2 Aug 12, 2025
efa0a06
feat: LookController 추가 — 생성/리액션/스와이프/상세 조회 API #16
codrin2 Aug 12, 2025
aa8e41d
feat: LookCreateCommand DTO 추가 #16
codrin2 Aug 12, 2025
68bb459
feat: LookCreateRequest DTO 추가 #16
codrin2 Aug 12, 2025
3191536
feat: LookCreateResponse DTO 추가 #16
codrin2 Aug 12, 2025
5a69f7b
feat: LookDetailResponse DTO 추가 #16
codrin2 Aug 12, 2025
04527c4
feat: LookDto DTO 추가 #16
codrin2 Aug 12, 2025
b646730
feat: LookFacade 추가 #16
codrin2 Aug 12, 2025
1df3330
feat: LookImageRepository 추가 #16
codrin2 Aug 12, 2025
29b2aac
feat: LookImageService 추가 #16
codrin2 Aug 12, 2025
3482d77
feat: LookProduct 엔티티 추가 #16
codrin2 Aug 12, 2025
a6374e4
feat: LookProductRepository 추가 #16
codrin2 Aug 12, 2025
e4314ec
feat: LookProductService 추가 #16
codrin2 Aug 12, 2025
5f7e33f
feat: LookReactionFacade 추가 #16
codrin2 Aug 12, 2025
4af2691
feat: LookReactionRepository 추가 #16
codrin2 Aug 12, 2025
3b27667
feat: LookReactionService 추가 #16
codrin2 Aug 12, 2025
706129f
feat: LookRepository 추가 #16
codrin2 Aug 12, 2025
2e1cec9
feat: LookService 추가 #16
codrin2 Aug 12, 2025
c258be5
feat: ReactionRequest DTO 추가 #16
codrin2 Aug 12, 2025
d4962e0
feat: ReactionType enum 추가 #16
codrin2 Aug 12, 2025
2aa9ea0
refactor: Look 엔티티 필드 접근 제어 강화 및 getter 추가 #16
codrin2 Aug 12, 2025
28b21d6
remove: 사용하지 않는 메서드 정리 #16
codrin2 Aug 12, 2025
94330e8
refactor: Look 엔티티에 정렬된 이미지·상품 조회 기능 추가 및 캡슐화 강화 #16
codrin2 Aug 12, 2025
691acc7
refactor: product column getter 접근으로 변경 #16
codrin2 Aug 12, 2025
85f2a17
feat: LookRepository에 이미지·상품 fetch join 조회 메서드 추가 #16
codrin2 Aug 12, 2025
6f892da
refactor: ProductImage 엔티티에 팩토리 메서드 추가 #16
codrin2 Aug 12, 2025
f01c4cf
test: productFixture 리팩토링 addImages/addCategory 메서드 활용 #16
codrin2 Aug 12, 2025
769f9c2
test: product getter 사용하도록 변경 #16
codrin2 Aug 12, 2025
d25e900
test: LookReactionService 테스트 추가 — LIKE/DISLIKE 동작 및 멱등성 검증 (Describe…
codrin2 Aug 12, 2025
57633a5
test: LookService 테스트 추가 — 생성/상세/스와이프/도메인 동작 검증 #16
codrin2 Aug 12, 2025
4f37abf
test: lookFixture 유틸 추가 #16
codrin2 Aug 12, 2025
228026f
feat: LookImageDto 추가 #16
codrin2 Aug 12, 2025
6e89244
feat: LookProductDto 추가 #16
codrin2 Aug 12, 2025
95693b5
docs: LookSwagger 인터페이스 추가 — Look API 스웨거 문서화 #16
codrin2 Aug 12, 2025
59cea01
refactor: LookController에 LookSwagger 인터페이스 구현 적용 #16
codrin2 Aug 12, 2025
e41f6af
fix: CORS 설정 개선 — credentials 지원 및 Origin 패턴 적용 #16
codrin2 Aug 12, 2025
4fce6a9
fix: 쿠키 설정 변경 #16
codrin2 Aug 12, 2025
b52fff4
fix: 상품 생성 시 images 를 추가할 수 있도록 수정 #16
codrin2 Aug 12, 2025
b4c24c8
feat(Product): addCategories 메서드 추가 #16
codrin2 Aug 12, 2025
6a6157c
docs(ProductSwagger): 불필요한 내용 삭제 #16
codrin2 Aug 12, 2025
bf5b287
fix: 코드 리뷰 반영 #16
codrin2 Aug 12, 2025
27de4bc
fix: 코드 리뷰 반영 #16
codrin2 Aug 12, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ data class CartResponse(
private fun CartItem.toResponse() = CartItemResponse(
itemId = id,
productId = product.id,
productName = product.name,
productThumbnailUrl = product.thumbnailUrl,
price = product.price,
productName = product.getName(),
productThumbnailUrl = product.getThumbnailUrl(),
price = product.getPrice(),
quantity = quantity,
subtotal = product.price
subtotal = product.getPrice()
.multiply(BigDecimal(quantity))
.setScale(SCALE_NONE, RoundingMode.HALF_UP)
)
Expand Down
5 changes: 4 additions & 1 deletion src/main/kotlin/com/dh/baro/core/ErrorMessage.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ enum class ErrorMessage(val message: String) {
KAKAO_USER_FETCH_FAILED("카카오 사용자 정보 조회 실패: %s"),

// Product
PRODUCT_NOT_FOUND("상품을 찾을 수 없습니다: %d"),
PRODUCT_NOT_FOUND("상품을 찾을 수 없습니다: %s"),
INVALID_POPULAR_PRODUCT_CURSOR("cursorLikes와 cursorId는 함께 지정하거나 함께 생략해야 합니다."),
OUT_OF_STOCK("재고가 모두 소진되었습니다: %d"),

Expand All @@ -28,6 +28,9 @@ enum class ErrorMessage(val message: String) {

// Order
ORDER_NOT_FOUND("주문을 찾을 수 없습니다: %d"),

// Look
LOOK_NOT_FOUND("룩을 찾을 수 없습니다: %d"),
;

fun format(vararg args: Any): String =
Expand Down
8 changes: 4 additions & 4 deletions src/main/kotlin/com/dh/baro/core/config/WebConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,16 @@ internal class WebConfig(

override fun addCorsMappings(registry: CorsRegistry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedHeaders("**")
.allowedOriginPatterns("*")
.allowedMethods(*ALLOWED_METHOD_NAMES)
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600)
}

companion object {
private val ALLOWED_METHOD_NAMES = arrayOf(
"GET", "HEAD", "POST", "PUT", "DELETE", "TRACE",
"OPTIONS", "PATCH"
"GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS",
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ class UserService(
private val socialAccountRepository: SocialAccountRepository,
) {

fun checkUserExists(userId: Long) {
require(userRepository.existsById(userId)) {
ErrorMessage.USER_NOT_FOUND.format(userId)
}
}

fun getUserById(userId: Long): User =
userRepository.findByIdOrNull(userId)
?: throw IllegalArgumentException(ErrorMessage.USER_NOT_FOUND.format(userId))
Expand Down
10 changes: 10 additions & 0 deletions src/main/kotlin/com/dh/baro/look/application/LookCreateCommand.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.dh.baro.look.application

data class LookCreateCommand(
val creatorId: Long,
val title: String,
val description: String?,
val thumbnailUrl: String,
val imageUrls: List<String>,
val productIds: List<Long>,
)
41 changes: 41 additions & 0 deletions src/main/kotlin/com/dh/baro/look/application/LookFacade.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.dh.baro.look.application

import com.dh.baro.identity.domain.service.UserService
import com.dh.baro.look.domain.*
import com.dh.baro.look.domain.service.LookService
import com.dh.baro.look.presentation.dto.LookDetailResponse
import com.dh.baro.product.domain.service.ProductQueryService
import org.springframework.data.domain.Slice
import org.springframework.stereotype.Service

@Service
class LookFacade(
private val userService: UserService,
private val productQueryService: ProductQueryService,
private val lookService: LookService,
) {

fun createLook(cmd: LookCreateCommand): Look {
userService.checkUserExists(cmd.creatorId)
productQueryService.checkProductsExists(cmd.productIds)
return lookService.createLook(cmd)
}

fun getSwipeLooks(userId: Long, cursorId: Long?, size: Int): Slice<Look> =
lookService.getSwipeLooks(userId, cursorId, size)

fun getLookDetail(lookId: Long): LookDetailResponse {
val look = lookService.getLookDetail(lookId)
val orderedProducts = look.getOrderedProductViews()

val productIds = orderedProducts.map { it.productId }
val products = productQueryService.getAllByIds(productIds)

return LookDetailResponse.of(
look = look,
images = look.getOrderedImageViews(),
lookProducts = orderedProducts,
products = products,
)
}
}
25 changes: 25 additions & 0 deletions src/main/kotlin/com/dh/baro/look/application/LookReactionFacade.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.dh.baro.look.application

import com.dh.baro.identity.domain.service.UserService
import com.dh.baro.look.domain.ReactionType
import com.dh.baro.look.domain.service.LookReactionService
import org.springframework.stereotype.Service

@Service
class LookReactionFacade(
private val userService: UserService,
private val lookReactionService: LookReactionService,
) {

fun recordReaction(userId: Long, lookId: Long, reactionType: ReactionType) {
userService.checkUserExists(userId)
lookReactionService.createReactionIfAbsent(
userId = userId,
lookId = lookId,
reactionType = reactionType,
)
}

fun cancelReaction(userId: Long, lookId: Long) =
lookReactionService.deleteReaction(userId, lookId)
}
118 changes: 101 additions & 17 deletions src/main/kotlin/com/dh/baro/look/domain/Look.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package com.dh.baro.look.domain

import com.dh.baro.core.AbstractTime
import com.dh.baro.identity.domain.User
import com.dh.baro.product.domain.Product
import com.dh.baro.core.IdGenerator
import com.dh.baro.look.domain.vo.LookImageView
import com.dh.baro.look.domain.vo.LookProductView
import jakarta.persistence.*

@Entity
Expand All @@ -12,27 +13,110 @@ class Look(
@Column(name = "id")
val id: Long,

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "creator_id")
val creator: User? = null,
@Column(name = "creator_id", nullable = false)
val creatorId: Long,

@Column(name = "title", nullable = false)
var title: String,
private var title: String,

@Lob @Column(name = "description")
var description: String? = null,
private var description: String? = null,

@Column(name = "likes_count", nullable = false)
var likesCount: Int = 0,
private var likesCount: Int = 0,

@OneToMany(mappedBy = "look", cascade = [CascadeType.ALL], orphanRemoval = true)
val images: MutableList<LookImage> = mutableListOf(),
@Column(name = "thumbnail_url", nullable = false, length = 300)
private var thumbnailUrl: String,

@ManyToMany
@JoinTable(
name = "look_products",
joinColumns = [JoinColumn(name = "look_id")],
inverseJoinColumns = [JoinColumn(name = "product_id")]
@OneToMany(
mappedBy = "look",
fetch = FetchType.LAZY,
cascade = [CascadeType.ALL],
orphanRemoval = true,
)
val products: MutableSet<Product> = mutableSetOf()
) : AbstractTime()
private val images: MutableSet<LookImage> = mutableSetOf(),

@OneToMany(
mappedBy = "look",
fetch = FetchType.LAZY,
cascade = [CascadeType.ALL],
orphanRemoval = true,
)
private val products: MutableSet<LookProduct> = mutableSetOf(),
) : AbstractTime() {

fun getTitle() = title

fun getDescription() = description

fun getLikesCount() = likesCount

fun getThumbnailUrl() = thumbnailUrl

fun getOrderedImageViews(): List<LookImageView> =
images.asSequence()
.sortedBy { it.displayOrder }
.map { LookImageView(it.imageUrl, it.displayOrder) }
.toList()

fun getOrderedProductViews(): List<LookProductView> =
products.asSequence()
.sortedBy { it.displayOrder }
.map { LookProductView(it.productId, it.displayOrder) }
.toList()

fun addImages(imageUrls: List<String>) {
val start = images.size
imageUrls.forEachIndexed { idx, url ->
addImage(
imageUrl = url,
displayOrder = start + idx + 1,
)
}
}
Comment on lines +68 to +76
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

이미지 추가 로직 검증

Product 도메인과 동일한 패턴으로 구현되어 일관성이 있습니다.

다음 스크립트로 LookImage 엔티티의 구조를 확인해보세요:


🏁 Script executed:

#!/bin/bash
# LookImage 엔티티 구조 확인
cat src/main/kotlin/com/dh/baro/look/domain/LookImage.kt

# displayOrder 필드의 유니크 제약 확인
rg -A 3 "displayOrder" src/main/kotlin/com/dh/baro/look/domain/LookImage.kt

Length of output: 1297


displayOrder 복합 유니크 제약 추가 검토 필요

현재 addImages 로직으로 순서(displayOrder)를 관리하지만, LookImage 엔티티에는 DB 레벨에서 (look_id, display_order)에 대한 유니크 제약이 없습니다. 순서 중복 삽입을 방지하려면 아래 항목을 검토해주세요.

  • 파일: src/main/kotlin/com/dh/baro/look/domain/LookImage.kt
    @Table 애너테이션에 uniqueConstraints 추가
    @Entity
    @Table(
      name = "look_images",
      uniqueConstraints = [
        UniqueConstraint(columnNames = ["look_id", "display_order"])
      ]
    )
    class LookImage(…)
  • 마이그레이션 스크립트에도 동일한 제약을 반영해 주세요.
🤖 Prompt for AI Agents
In src/main/kotlin/com/dh/baro/look/domain/Look.kt around lines 68 to 76, the
addImages logic assigns displayOrder but the LookImage entity/table lacks a
DB-level unique constraint on (look_id, display_order); update
src/main/kotlin/com/dh/baro/look/domain/LookImage.kt to add a @Table annotation
with uniqueConstraints = [UniqueConstraint(columnNames = ["look_id",
"display_order"])] and then add a corresponding migration script change to the
database migration (SQL or migration file) to create the unique index/constraint
on look_images(look_id, display_order) so duplicate display_order for the same
look cannot be inserted.


private fun addImage(imageUrl: String, displayOrder: Int) =
images.add(
LookImage.of(
look = this,
imageUrl = imageUrl,
displayOrder = displayOrder,
)
)

fun addProducts(productIds: List<Long>) {
val nextOrderStart = products.size
productIds
.distinct()
.filterNot { id ->
products.any { it.productId == id }
}
.forEachIndexed { idx, id ->
addProduct(id, nextOrderStart + idx + 1)
}
}

private fun addProduct(productId: Long, displayOrder: Int) {
products += LookProduct.of(
look = this,
productId = productId,
displayOrder = displayOrder,
)
}

companion object {
fun newLook(
creatorId: Long,
title: String,
description: String?,
thumbnailUrl: String,
): Look =
Look(
id = IdGenerator.generate(),
creatorId = creatorId,
title = title,
description = description,
thumbnailUrl = thumbnailUrl,
)
}
}
19 changes: 16 additions & 3 deletions src/main/kotlin/com/dh/baro/look/domain/LookImage.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.dh.baro.look.domain

import com.dh.baro.core.AbstractTime
import com.dh.baro.core.IdGenerator
import jakarta.persistence.*

@Entity
Expand All @@ -19,7 +20,19 @@ class LookImage(

@Column(name = "display_order", nullable = false)
val displayOrder: Int,
) : AbstractTime() {

@Column(name = "is_thumbnail", nullable = false)
val isThumbnail: Boolean = false
) : AbstractTime()
companion object {
fun of(
look: Look,
imageUrl: String,
displayOrder: Int,
): LookImage =
LookImage(
id = IdGenerator.generate(),
look = look,
imageUrl = imageUrl,
displayOrder = displayOrder,
)
}
}
42 changes: 42 additions & 0 deletions src/main/kotlin/com/dh/baro/look/domain/LookProduct.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.dh.baro.look.domain

import com.dh.baro.core.IdGenerator
import jakarta.persistence.*

@Entity
@Table(
name = "look_products",
uniqueConstraints = [
UniqueConstraint(columnNames = ["look_id", "product_id"]),
UniqueConstraint(columnNames = ["look_id", "display_order"])
],
)
class LookProduct(
@Id
@Column(name = "id")
val id: Long,

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "look_id", nullable = false, updatable = false)
val look: Look,

@Column(name = "product_id", nullable = false, updatable = false)
val productId: Long,
Comment on lines +23 to +24
Copy link

@coderabbitai coderabbitai bot Aug 12, 2025

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

productId 필드 설계 검토

productId를 Long 타입으로 저장하고 있는데, Product 엔티티와의 직접적인 연관관계 대신 ID만 저장하는 이유를 확인해보세요. 일반적으로는 @manytoone으로 Product 엔티티와 연관관계를 맺는 것이 일반적입니다.

다음 스크립트를 실행하여 Product 엔티티와의 관계 설정 패턴을 확인해보세요:


🏁 Script executed:

#!/bin/bash
# Product와 다른 엔티티들의 연관관계 패턴 확인
echo "=== Product 엔티티와의 연관관계 패턴 확인 ==="
rg -A 5 -B 5 "Product" --type kotlin | rg -A 10 -B 10 "@ManyToOne|@OneToMany"

echo -e "\n=== productId 필드 사용 패턴 확인 ==="
rg "productId.*Long" --type kotlin

Length of output: 12699


LookProduct의 productId 필드 설계 검토 및 문서화 필요

LookProduct에서 Product 엔티티를 @manytoone으로 직접 연관관계 매핑하지 않고 productId: Long만 저장하고 있습니다.
이는 Look 도메인과 Product 도메인을 분리된 Bounded Context로 유지하거나 불필요한 JPA 로딩을 피하기 위한 의도적 설계일 수 있습니다.
해당 선택이 의도된 것이라면, 설계 의도를 코드에 주석 또는 README 등에 명시하여 다른 개발자가 이해할 수 있도록 문서화해 주세요.
만약 도메인 간 결합을 허용하고 JPA의 연관관계를 활용하고자 한다면, 아래 위치를 참고하여 @manytoone 매핑으로 전환을 검토해 보세요.

· 파일: src/main/kotlin/com/dh/baro/look/domain/LookProduct.kt
· 라인: 20–21 (@Column(name = "product_id", nullable = false, updatable = false) val productId: Long,)

🤖 Prompt for AI Agents
In src/main/kotlin/com/dh/baro/look/domain/LookProduct.kt around lines 20–21,
the entity currently stores productId: Long instead of using a JPA association;
either document this intentional decoupling or convert it to a ManyToOne
relation. If the decoupling is intentional, add a clear comment above the
productId field and update the module README stating this is a bounded-context
decision to avoid JPA loading and why productId is stored as a primitive. If you
prefer to use JPA associations, replace productId with a @ManyToOne val product:
Product mapped to column "product_id" (configure fetch type, optional=false,
updatable=false as needed), remove the primitive id field, and adjust
constructors/equals/hashCode/serialization accordingly so the relation is
persisted and loaded by JPA.

Copy link
Member Author

Choose a reason for hiding this comment

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

도메인을 분리된 Bounded Context로 유지하기 위한 설계입니다

Copy link

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!


@Column(name = "display_order", nullable = false)
val displayOrder: Int,
) {

companion object {
fun of(
look: Look,
productId: Long,
displayOrder: Int
) = LookProduct(
id = IdGenerator.generate(),
look = look,
productId = productId,
displayOrder = displayOrder,
)
}
}
Loading