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
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,8 @@ class ProductFacade(
fun getProductInfo(productId: Long): ProductResult.ProductInfo {
val product = productService.getProduct(productId)
val brand = brandService.getBrand(product.brandId)
val likeCount = likeService.countLikesByProductId(productId)

return ProductResult.ProductInfo.of(product, brand, likeCount)
val productInfo = ProductResult.ProductInfo.of(product, brand, product.likeCount)
return productInfo
}

@Transactional(readOnly = true)
Expand All @@ -35,7 +34,6 @@ class ProductFacade(
fun createProduct(name: String, price: BigDecimal, brandId: Long): ProductResult.ProductInfo {
val brand = brandService.getBrand(brandId)
val product = productService.createProduct(name, price, brandId)

return ProductResult.ProductInfo.of(product, brand)
}

Expand All @@ -50,9 +48,8 @@ class ProductFacade(

val product = productService.updateProduct(productId, name, price, brandId)
val brand = brandService.getBrand(product.brandId)
val likeCount = likeService.countLikesByProductId(productId)

return ProductResult.ProductInfo.of(product, brand, likeCount)
val productInfo = ProductResult.ProductInfo.of(product, brand, product.likeCount)
return productInfo
}

@Transactional
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ data class PageCommand(
val pageNumber: Long,
val pageSize: Long,
val sort: List<SortCondition> = emptyList(),
val brandId: Long? = null,
) {
init {
require(pageNumber >= 0) { "페이지 번호는 0 이상이어야 합니다." }
Expand All @@ -13,6 +14,10 @@ data class PageCommand(

val offset: Long = pageNumber * pageSize

fun cacheKey(): String {
return "${brandId}_${pageNumber}_${pageSize}_$sort"
}
Comment on lines +17 to +19

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

캐시 키 생성 로직에 안정성 문제가 있습니다.

cacheKey() 메서드에 다음 문제가 있습니다:

  1. sort 리스트의 toString() 사용: 리스트의 기본 toString()은 예측 가능하고 안정적인 캐시 키 형식을 보장하지 않습니다.
  2. 정렬 조건 순서 민감성: 동일한 정렬 조건이 다른 순서로 제공되면 의미상 동일한 쿼리임에도 다른 캐시 키가 생성되어 캐시 미스가 발생합니다.

이는 캐시 효율성을 저하시키고 불필요한 캐시 미스를 유발합니다.

다음 diff를 적용하여 안정적인 캐시 키를 생성하세요:

 fun cacheKey(): String {
-    return "${brandId}_${pageNumber}_${pageSize}_$sort"
+    val sortKey = sort
+        .sortedWith(compareBy({ it.field }, { it.direction }))
+        .joinToString(",") { "${it.field}:${it.direction}" }
+    return "${brandId}_${pageNumber}_${pageSize}_${sortKey}"
 }
📝 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
fun cacheKey(): String {
return "${brandId}_${pageNumber}_${pageSize}_$sort"
}
fun cacheKey(): String {
val sortKey = sort
.sortedWith(compareBy({ it.field }, { it.direction }))
.joinToString(",") { "${it.field}:${it.direction}" }
return "${brandId}_${pageNumber}_${pageSize}_${sortKey}"
}
🤖 Prompt for AI Agents
In apps/commerce-api/src/main/kotlin/com/loopers/domain/common/PageCommand.kt
around lines 17 to 19, the cacheKey() uses the sort list's default toString()
which is order-sensitive and non-deterministic; change it to build a
deterministic, order-insensitive string by normalizing and serializing the sort
criteria: null-safe handle brandId/pageNumber/pageSize, sort each sort-entry by
a stable key (e.g., property name + direction), map each entry to a consistent
"field:direction" token, sort those tokens alphabetically, join with a delimiter
(e.g., comma) and use that joined string in the cache key so identical logical
sorts produce the same key regardless of input order.


fun hasNext(totalCount: Long): Boolean {
return (this.pageNumber + 1) * this.pageSize < totalCount
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.loopers.domain.like
import com.loopers.domain.BaseEntity
import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.Index
import jakarta.persistence.Table
import jakarta.persistence.UniqueConstraint

Expand All @@ -15,6 +16,9 @@ import jakarta.persistence.UniqueConstraint
columnNames = ["user_id", "product_id"],
),
],
indexes = [
Index(name = "idx_like_product_id", columnList = "product_id"),
],
)
class Like(
@Column(nullable = false)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package com.loopers.domain.like

import com.loopers.domain.product.ProductService
import org.springframework.stereotype.Component
import org.springframework.transaction.annotation.Transactional

@Component
class LikeService(
private val likeRepository: LikeRepository,
private val productService: ProductService,
) {
Comment on lines 1 to 11

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Get the full LikeService.kt file to understand the changes
find . -name "LikeService.kt" -type f

Repository: Loopers-dev-lab/loopers-spring-kotlin-template

Length of output: 72


🏁 Script executed:

#!/bin/bash
# Find Product entity and like_count field references
find . -name "Product.kt" -o -name "Product.java" -type f

Repository: Loopers-dev-lab/loopers-spring-kotlin-template

Length of output: 72


🏁 Script executed:

#!/bin/bash
# Search for migration files and SQL schemas
find . -name "*.sql" -o -name "*migration*" -o -name "*schema*" | head -20

Repository: Loopers-dev-lab/loopers-spring-kotlin-template

Length of output: 72


🏁 Script executed:

#!/bin/bash
# Look for data initialization or test data files
find . -name "data.sql" -o -name "test-data.sql" -o -name "*init*.sql" -type f

Repository: Loopers-dev-lab/loopers-spring-kotlin-template

Length of output: 72


🏁 Script executed:

#!/bin/bash
# List repository structure to understand layout
ls -la

Repository: Loopers-dev-lab/loopers-spring-kotlin-template

Length of output: 1563


🏁 Script executed:

#!/bin/bash
# Check git status and recent changes to understand PR scope
git log --oneline -10

Repository: Loopers-dev-lab/loopers-spring-kotlin-template

Length of output: 110


🏁 Script executed:

#!/bin/bash
# Search for all .kt files in the repository
find . -name "*.kt" -type f | head -30

Repository: Loopers-dev-lab/loopers-spring-kotlin-template

Length of output: 2542


🏁 Script executed:

#!/bin/bash
# Search for SQL and database related files
find . -name "*.sql" -o -name "*.xml" | grep -E "(sql|database|schema|migration)" | head -20

Repository: Loopers-dev-lab/loopers-spring-kotlin-template

Length of output: 72


🏁 Script executed:

#!/bin/bash
# Check all git branches
git branch -a

Repository: Loopers-dev-lab/loopers-spring-kotlin-template

Length of output: 203


🏁 Script executed:

#!/bin/bash
# Check current branch and status
git status

Repository: Loopers-dev-lab/loopers-spring-kotlin-template

Length of output: 297


🏁 Script executed:

#!/bin/bash
# Look for any like-related files in the commerce-api
find ./apps/commerce-api -name "*ike*" -o -name "*product*" -o -name "*like*" 2>/dev/null

Repository: Loopers-dev-lab/loopers-spring-kotlin-template

Length of output: 72


🏁 Script executed:

#!/bin/bash
# Check git diff to see what's being changed in this PR
git diff --name-only HEAD

Repository: Loopers-dev-lab/loopers-spring-kotlin-template

Length of output: 72


🏁 Script executed:

#!/bin/bash
# Check for untracked files
git ls-files --others --exclude-standard

Repository: Loopers-dev-lab/loopers-spring-kotlin-template

Length of output: 108


🏁 Script executed:

#!/bin/bash
# Check out coderabbit_46 branch and look for LikeService.kt
git checkout coderabbit_46 2>&1
find . -name "LikeService.kt" -o -name "Product.kt" 2>/dev/null | head -20

Repository: Loopers-dev-lab/loopers-spring-kotlin-template

Length of output: 321


🏁 Script executed:

#!/bin/bash
# Check git log to see commits in the PR
git log --oneline -20

Repository: Loopers-dev-lab/loopers-spring-kotlin-template

Length of output: 110


🏁 Script executed:

#!/bin/bash
# Show git diff with stats
git diff main... --stat | head -30

Repository: Loopers-dev-lab/loopers-spring-kotlin-template

Length of output: 72


🏁 Script executed:

#!/bin/bash
# Look for all changes related to like or product
git diff main... --name-only | grep -E "(like|product|Like|Product)" -i

Repository: Loopers-dev-lab/loopers-spring-kotlin-template

Length of output: 72


🏁 Script executed:

#!/antml/bash
# Show files that differ between main and coderabbit_46
git log --oneline main..coderabbit_46

Repository: Loopers-dev-lab/loopers-spring-kotlin-template

Length of output: 1362


🏁 Script executed:

#!/bin/bash
# Read the LikeService.kt file
cat -n ./apps/commerce-api/src/main/kotlin/com/loopers/domain/like/LikeService.kt

Repository: Loopers-dev-lab/loopers-spring-kotlin-template

Length of output: 1471


🏁 Script executed:

#!/bin/bash
# Read the Product.kt file
cat -n ./apps/commerce-api/src/main/kotlin/com/loopers/domain/product/Product.kt

Repository: Loopers-dev-lab/loopers-spring-kotlin-template

Length of output: 2763


🏁 Script executed:

#!/bin/bash
# Search for migration files related to like or product
find . -path "*/db/migration/*" -o -path "*/resources/db/*" -o -name "*Like*" -o -name "*Product*" | grep -E "\.(sql|xml)" | head -20

Repository: Loopers-dev-lab/loopers-spring-kotlin-template

Length of output: 72


🏁 Script executed:

#!/bin/bash
# Look for JPA entity files
find ./apps/commerce-api -name "*Entity.kt" -o -name "*Jpa*.kt" | grep -E "(like|product)" -i

Repository: Loopers-dev-lab/loopers-spring-kotlin-template

Length of output: 273


🏁 Script executed:

#!/bin/bash
# Look for Flyway or Liquibase migration files
find . -path "*/db/migration*" -type d

Repository: Loopers-dev-lab/loopers-spring-kotlin-template

Length of output: 72


🏁 Script executed:

#!/bin/bash
# Search for all SQL files in the project
find . -name "*.sql" -type f 2>/dev/null | grep -v node_modules

Repository: Loopers-dev-lab/loopers-spring-kotlin-template

Length of output: 72


🏁 Script executed:

#!/bin/bash
# Check the resources directory structure
find ./apps/commerce-api/src/main/resources -type f 2>/dev/null | head -30

Repository: Loopers-dev-lab/loopers-spring-kotlin-template

Length of output: 144


🏁 Script executed:

#!/antml/bash
# Look for schema or initialization files
ls -la ./apps/commerce-api/src/main/resources/ 2>/dev/null || echo "resources dir not found"

Repository: Loopers-dev-lab/loopers-spring-kotlin-template

Length of output: 265


🏁 Script executed:

#!/bin/bash
# Read application.yml to understand JPA configuration
cat ./apps/commerce-api/src/main/resources/application.yml

Repository: Loopers-dev-lab/loopers-spring-kotlin-template

Length of output: 1049


🏁 Script executed:

#!/bin/bash
# Search for CommandLineRunner or ApplicationRunner beans that might handle initialization
rg -n "CommandLineRunner|ApplicationRunner|@PostConstruct" --type=kt

Repository: Loopers-dev-lab/loopers-spring-kotlin-template

Length of output: 116


🏁 Script executed:

#!/bin/bash
# Look for any test data or initialization classes
find ./apps/commerce-api -name "*Init*" -o -name "*Fixture*" -o -name "*Seed*" 2>/dev/null

Repository: Loopers-dev-lab/loopers-spring-kotlin-template

Length of output: 328


🏁 Script executed:

#!/bin/bash
# Check the commit that added the likeCount denormalization to see if migration was included
git show 89cedec --stat

Repository: Loopers-dev-lab/loopers-spring-kotlin-template

Length of output: 683


🏁 Script executed:

#!/bin/bash
# Check the commit for like feature implementation
git show bbe4e98 --stat

Repository: Loopers-dev-lab/loopers-spring-kotlin-template

Length of output: 1095


🏁 Script executed:

#!/bin/bash
# Find and read jpa.yml
find . -name "jpa.yml" -type f

Repository: Loopers-dev-lab/loopers-spring-kotlin-template

Length of output: 130


🏁 Script executed:

#!/bin/bash
# Search for any file that contains jpa configuration
rg -l "hibernate.ddl-auto|spring.jpa" --type=yaml

Repository: Loopers-dev-lab/loopers-spring-kotlin-template

Length of output: 72


🏁 Script executed:

#!/bin/bash
# Look at the modules structure for shared JPA config
ls -la ./modules/jpa/src/main/resources/ 2>/dev/null || echo "no resources"

Repository: Loopers-dev-lab/loopers-spring-kotlin-template

Length of output: 257


🏁 Script executed:

#!/bin/bash
# Check if there are any schema initialization files
find ./modules -name "*.sql" -o -name "*schema*" -o -name "*init*" 2>/dev/null

Repository: Loopers-dev-lab/loopers-spring-kotlin-template

Length of output: 72


🏁 Script executed:

#!/bin/bash
# Look at the full Product entity to understand initialization
git show 89cedec:apps/commerce-api/src/main/kotlin/com/loopers/domain/product/Product.kt | head -40

Repository: Loopers-dev-lab/loopers-spring-kotlin-template

Length of output: 1062


🏁 Script executed:

#!/bin/bash
# Read jpa.yml configuration
cat ./modules/jpa/src/main/resources/jpa.yml

Repository: Loopers-dev-lab/loopers-spring-kotlin-template

Length of output: 2236


🏁 Script executed:

#!/bin/bash
# Check the full git history of Product.kt to see when likeCount was added
git log --oneline --all -- "**/Product.kt" | head -10

Repository: Loopers-dev-lab/loopers-spring-kotlin-template

Length of output: 253


🏁 Script executed:

#!/bin/bash
# Look at the LikeService test to understand the expected behavior
find ./apps/commerce-api -name "*LikeServiceTest*" -o -name "*LikeService*Test*" 2>/dev/null

Repository: Loopers-dev-lab/loopers-spring-kotlin-template

Length of output: 168


🏁 Script executed:

#!/bin/bash
# Check when the Like entity was first introduced
git log --oneline --all -- "**/Like.kt" | head -10

Repository: Loopers-dev-lab/loopers-spring-kotlin-template

Length of output: 230


🏁 Script executed:

#!/bin/bash
# Read the LikeServiceTest to understand test coverage
cat -n ./apps/commerce-api/src/test/kotlin/com/loopers/domain/like/LikeServiceTest.kt

Repository: Loopers-dev-lab/loopers-spring-kotlin-template

Length of output: 3313


🏁 Script executed:

#!/bin/bash
# Check the detailed changes in commit 89cedec to see if migration was mentioned
git show 89cedec

Repository: Loopers-dev-lab/loopers-spring-kotlin-template

Length of output: 8050


기존 데이터의 일관성 확보가 필수입니다. 이 변경사항을 배포하기 전에 데이터 마이그레이션이 반드시 필요합니다.

이 커밋 (89cedec)은 상품 좋아요 수를 Product.likeCount 필드로 비정규화하는 변경입니다. 그러나 배포 환경 (dev/qa/prd)에서는 hibernate.ddl-auto: none으로 설정되어 있어 수동 마이그레이션이 필수입니다.

문제점:

  • 변경 후 신규 좋아요는 Product.likeCount가 올바르게 증가/감소됩니다
  • 하지만 기존 데이터의 경우, 새로운 like_count 컬럼이 기본값 0으로 초기화되어 데이터 불일치 발생
  • 이미 존재하는 Like 레코드들이 Product.likeCount에 반영되지 않음
  • 좋아요 수로 정렬하는 기능 (idx_product_like_count 인덱스 사용)이 부정확한 결과 반환

필요한 마이그레이션:

UPDATE loopers_product p
SET like_count = (
    SELECT COUNT(*) 
    FROM loopers_like l 
    WHERE l.product_id = p.id
);
🤖 Prompt for AI Agents
In apps/commerce-api/src/main/kotlin/com/loopers/domain/like/LikeService.kt
around lines 1–11: the commit denormalizes like counts into Product.likeCount
but does not populate existing rows; before deploying you must add and run an
idempotent DB migration that sets product.like_count = count of existing
loopers_like rows per product, ensure the migration is managed by the project's
migration tool (Flyway/Liquibase) and added to the release, run it in dev/qa/prd
(back up DB first), verify counts and the idx_product_like_count-driven sorting
are correct after migration, and only then deploy the code that relies on
Product.likeCount.


@Transactional(readOnly = true)
Expand All @@ -20,11 +22,17 @@ class LikeService(
}

likeRepository.save(Like.of(userId, productId))

val product = productService.getProduct(productId)
product.incrementLikeCount()
}

@Transactional
fun removeLike(userId: Long, productId: Long) {
val like = likeRepository.findByUserIdAndProductId(userId, productId) ?: return
likeRepository.delete(like)

val product = productService.getProduct(productId)
product.decrementLikeCount()
}
Comment on lines 30 to 37

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

동일한 동시성 문제가 좋아요 취소에도 존재합니다.

removeLike에서도 addLike와 동일한 Lost Update 문제가 발생할 수 있습니다. 동일한 해결 방안을 적용해야 합니다.

 @Transactional
 fun removeLike(userId: Long, productId: Long) {
     val like = likeRepository.findByUserIdAndProductId(userId, productId) ?: return
     likeRepository.delete(like)
-
-    val product = productService.getProduct(productId)
+    val product = productService.getProductWithLock(productId)
     product.decrementLikeCount()
 }

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,18 @@ import com.loopers.support.error.CoreException
import com.loopers.support.error.ErrorType
import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.Index
import jakarta.persistence.Table
import java.math.BigDecimal

@Entity
@Table(name = "loopers_product")
@Table(
name = "loopers_product",
indexes = [
Index(name = "idx_product_brand_id", columnList = "brand_id"),
Index(name = "idx_product_like_count", columnList = "like_count"),
],
)
class Product(
@Column(nullable = false, length = 200)
var name: String,
Expand All @@ -19,13 +26,26 @@ class Product(

@Column(nullable = false)
var brandId: Long,

@Column(nullable = false)
var likeCount: Long = 0,
) : BaseEntity() {

init {
validateName(name)
validatePrice(price)
}

fun incrementLikeCount() {
this.likeCount++
}

fun decrementLikeCount() {
if (this.likeCount > 0) {
this.likeCount--
}
}
Comment on lines +39 to +47

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

증가/감소 메서드 구현이 적절합니다.

incrementLikeCountdecrementLikeCount 메서드가 명확하게 구현되었습니다. 특히 decrementLikeCount의 음수 방지 로직은 데이터 무결성을 보호합니다.

다만, 동시성 제어를 위해 Product 엔티티에 낙관적 잠금을 추가하는 것을 권장합니다.

+@Version
+var version: Long = 0
+
 @Column(nullable = false)
 var likeCount: Long = 0,

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apps/commerce-api/src/main/kotlin/com/loopers/domain/product/Product.kt
around lines 39 to 47, the increment/decrement methods are fine but the entity
lacks optimistic locking; add a version field annotated with JPA's @Version
(e.g., a Long) to the Product entity, import the annotation, ensure the field is
persisted and initialized appropriately, update the database schema/migration to
include the version column, and keep the existing methods as-is so concurrent
updates will throw OptimisticLockException for callers to handle.


fun update(name: String?, price: BigDecimal?, brandId: Long?) {
name?.let {
validateName(it)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import com.loopers.domain.common.PageCommand
import com.loopers.domain.common.PageResult
import com.loopers.support.error.CoreException
import com.loopers.support.error.ErrorType
import org.springframework.cache.annotation.CacheEvict
import org.springframework.cache.annotation.Cacheable
import org.springframework.cache.annotation.Caching
import org.springframework.stereotype.Component
import org.springframework.transaction.annotation.Transactional
import java.math.BigDecimal
Expand All @@ -13,6 +16,10 @@ class ProductService(
private val productRepository: ProductRepository,
) {

@Cacheable(
cacheNames = ["Product"],
key = "#id",
)
@Transactional(readOnly = true)
fun getProduct(id: Long): Product {
return productRepository.findById(id)
Expand All @@ -24,11 +31,20 @@ class ProductService(
return productRepository.findByIdIn(ids).associateBy { it.id }
}

@Cacheable(
cacheNames = ["Products"],
key = "#pageCommand.cacheKey()",
)
@Transactional(readOnly = true)
fun getProductById(pageCommand: PageCommand): PageResult<ProductResult.ProductInfo> {
return productRepository.getProducts(pageCommand)
}

@Caching(
evict = [
CacheEvict(cacheNames = ["Products"], allEntries = true),
],
)
@Transactional
fun createProduct(name: String, price: BigDecimal, brandId: Long): Product {
val product = Product.of(
Expand All @@ -39,6 +55,12 @@ class ProductService(
return productRepository.save(product)
}

@Caching(
evict = [
CacheEvict(cacheNames = ["Products"], allEntries = true),
CacheEvict(cacheNames = ["Product"], key = "#id"),
],
)
@Transactional
fun updateProduct(id: Long, name: String?, price: BigDecimal?, brandId: Long?): Product {
val product = getProduct(id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package com.loopers.infrastructure.product
import com.loopers.domain.brand.QBrand.brand
import com.loopers.domain.common.PageCommand
import com.loopers.domain.common.PageResult
import com.loopers.domain.like.QLike.like
import com.loopers.domain.product.QProduct.product
import com.querydsl.core.types.OrderSpecifier
import com.querydsl.jpa.impl.JPAQueryFactory
Expand All @@ -18,7 +17,10 @@ class ProductQuerydslRepository(
val totalCount = queryFactory
.select(product.count())
.from(product)
.where(product.deletedAt.isNull)
.where(
pageCommand.brandId?.let { product.brandId.eq(it) },
product.deletedAt.isNull,
)
.fetchOne() ?: 0L

val items = queryFactory
Expand All @@ -29,7 +31,7 @@ class ProductQuerydslRepository(
product.price,
brand.id,
brand.name,
like.count(),
product.likeCount,
product.createdAt,
product.updatedAt,
),
Expand All @@ -39,20 +41,9 @@ class ProductQuerydslRepository(
.on(
product.brandId.eq(brand.id),
)
.leftJoin(like)
.on(
product.id.eq(like.productId),
like.deletedAt.isNull,
)
.where(product.deletedAt.isNull)
.groupBy(
product.id,
product.name,
product.price,
brand.id,
brand.name,
product.createdAt,
product.updatedAt,
.where(
product.deletedAt.isNull,
pageCommand.brandId?.let { product.brandId.eq(it) },
)
.orderBy(*createOrderSpecifier(pageCommand.sort))
.offset(pageCommand.offset)
Expand All @@ -74,7 +65,7 @@ class ProductQuerydslRepository(
val path = when (field) {
"createdAt" -> product.createdAt
"price" -> product.price
"likeCount" -> like.count()
"likeCount" -> product.likeCount
"name" -> product.name
else -> product.createdAt
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ data class PageRequestDto(
example = "[{\"field\": \"createdAt\", \"direction\": \"DESC\"}]",
)
val sort: List<SortCondition> = emptyList(),

@get:Schema(description = "브랜드 ID 필터", example = "1")
val brandId: Long? = null,
) {
init {
require(pageNumber >= 0) { "페이지 번호는 0 이상이어야 합니다." }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ class ProductV1Controller(
},
)
},
brandId = request.brandId,
)

val productInfoPageResult = productFacade.getProducts(command)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
package com.loopers.domain.like

import com.loopers.domain.product.Product
import com.loopers.domain.product.ProductService
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.runs
import io.mockk.verify
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import java.math.BigDecimal

class LikeServiceTest {

private val likeRepository: LikeRepository = mockk()
private val likeService = LikeService(likeRepository)
private val productService: ProductService = mockk()
private val likeService = LikeService(likeRepository, productService)

@DisplayName("좋아요가 정상적으로 추가된다.")
@Test
Expand All @@ -20,16 +24,19 @@ class LikeServiceTest {
val userId = 1L
val productId = 100L
val like = Like.of(userId, productId)
val product = Product.of("Test Product", BigDecimal("10000"), 1L)

every { likeRepository.existsByUserIdAndProductId(userId, productId) } returns false
every { likeRepository.save(any()) } returns like
every { productService.getProduct(productId) } returns product

// act
likeService.addLike(userId, productId)

// assert
verify(exactly = 1) { likeRepository.existsByUserIdAndProductId(userId, productId) }
verify(exactly = 1) { likeRepository.save(any()) }
verify(exactly = 1) { productService.getProduct(productId) }
}

@DisplayName("이미 존재하는 좋아요는 중복 처리되지 않는다.")
Expand All @@ -56,15 +63,18 @@ class LikeServiceTest {
val userId = 1L
val productId = 100L
val like = Like.of(userId, productId)
val product = Product.of("Test Product", BigDecimal("10000"), 1L)

every { likeRepository.findByUserIdAndProductId(userId, productId) } returns like
every { likeRepository.delete(like) } just runs
every { productService.getProduct(productId) } returns product

// act
likeService.removeLike(userId, productId)

// assert
verify(exactly = 1) { likeRepository.findByUserIdAndProductId(userId, productId) }
verify(exactly = 1) { likeRepository.delete(like) }
verify(exactly = 1) { productService.getProduct(productId) }
}
}