Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/dev-ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ name: 키키하이 dev CI-CD 파이프라인

on:
push:
branches: [ "develop"]
branches: [ "develop","feat/product/KIKI-61-ElasticSearch"]

jobs:
#1. 개발 서버 CI, Build 용
Expand Down
24 changes: 2 additions & 22 deletions docker/dev-docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ services:
- '3306:3306'
volumes:
- ./mysql/data:/var/lib/mysql
- ./mysql/my.cnf:/etc/my.cnf
environment:
- MYSQL_DATABASE=${MYSQL_DATABASE}
- MYSQL_USER=${MYSQL_USER}
- MYSQL_PASSWORD=${MYSQL_PASSWORD}
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
- TZ=Asia/Seoul

# Redis
redis:
Expand All @@ -37,7 +39,6 @@ services:
- xpack.license.self_generated.type=${LICENSE}
ports:
- ${ES_PORT}:9200
mem_limit: ${ES_MEM_LIMIT}
ulimits:
memlock:
soft: -1
Expand All @@ -56,29 +57,8 @@ services:
- ELASTICSEARCH_HOSTS=http://es01:9200
ports:
- ${KIBANA_PORT}:5601
mem_limit: ${KB_MEM_LIMIT}
healthcheck:
test: [ "CMD-SHELL", "curl -s -I http://localhost:5601 | grep -q 'HTTP/1.1 302 Found'" ]
interval: 10s
timeout: 10s
retries: 120

logstash01:
image: docker.elastic.co/logstash/logstash:${STACK_VERSION}
container_name: kkh-logstash01
user: root
volumes:
- "./logstash/logstash_ingest_data/:/usr/share/logstash/ingest_data/"
- "./logstash/pipeline/logstash.conf:/usr/share/logstash/pipeline/logstash.conf:ro"
environment:
- xpack.monitoring.enabled=false
- ELASTIC_HOSTS=http://es01:9200

prometheus:
image: prom/prometheus
container_name: prometheus
volumes:
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
ports:
- 9090:9090
restart: always
6 changes: 4 additions & 2 deletions docker/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,13 @@ services:
- '3306:3306'
volumes:
- ./mysql/data:/var/lib/mysql
- ./mysql/my.cnf:/etc/my.cnf
environment:
- MYSQL_DATABASE=${MYSQL_DATABASE}
- MYSQL_USER=${MYSQL_USER}
- MYSQL_PASSWORD=${MYSQL_PASSWORD}
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
- TZ=Asia/Seoul
networks:
- monstache-network

Expand Down Expand Up @@ -173,7 +175,7 @@ services:
- xpack.security.transport.ssl.certificate_authorities=certs/ca/ca.crt
- xpack.security.transport.ssl.verification_mode=certificate
- xpack.license.self_generated.type=${LICENSE}
mem_limit: 1g
mem_limit: ${ES_MEM_LIMIT}
networks:
- monstache-network
ulimits:
Expand Down Expand Up @@ -222,7 +224,7 @@ services:
- XPACK_SECURITY_ENCRYPTIONKEY=${ENCRYPTION_KEY}
- XPACK_ENCRYPTEDSAVEDOBJECTS_ENCRYPTIONKEY=${ENCRYPTION_KEY}
- XPACK_REPORTING_ENCRYPTIONKEY=${ENCRYPTION_KEY}
mem_limit: 1g
mem_limit: ${KB_MEM_LIMIT}
networks:
- monstache-network
healthcheck:
Expand Down
20 changes: 20 additions & 0 deletions docker/mysql/my.cnf
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[client]
default-character-set = utf8mb4

[mysql]
default-character-set = utf8mb4

[mysqld]
# Character set & collation
character-set-server = utf8mb4
collation-server = utf8mb4_general_ci

# Timezone
default-time-zone = 'Asia/Seoul'

# Skip host name resolve (성능 향상)
skip-host-cache
skip-name-resolve

# Other configs
max_connections = 200
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;

import java.util.List;
import java.util.*;

import static site.kikihi.custom.platform.adapter.in.web.dto.response.util.ProductPriceUtil.getPrice;

/**
* 상품 상세 응답 DTO
Expand All @@ -14,12 +16,8 @@
* @param manufacturerName 제조사명
* @param category 카테고리
* @param productName 제품명
* @param discountRate 할인율 (예: 0.15는 15%)
* @param originalPrice 최저가
* @param discountedPrice 할인가
* @param likedByMe 나의 북마크 여부
* @param deliveryInfo 배송 정보 (배송비, 배송 종류, 예상 도착일 등)
* @param recommendedItems 어울리는 상품 목록
* @param cautions 상품 유의사항
* @param imageUrl 상세 정보 이미지 URL 목록
*/
Expand All @@ -45,27 +43,18 @@ public record ProductDetailResponse(
@Schema(description = "제품명", example = "독거미 Aula F99")
String productName,

@Schema(description = "할인율 (예: 0.15=15%)", example = "0.15")
double discountRate,

@Schema(description = "정상가(원)", example = "599000.0")
double originalPrice,

@Schema(description = "할인가(원)", example = "509000.0")
double discountedPrice,
@Schema(description = "최저가 가격부터", example = "599000 ~")
String originalPrice,

@Schema(description = "북마크(좋아요)한 상품 여부", example = "true")
boolean likedByMe,

@Schema(description = "배송 정보 응답 DTO", implementation = DeliveryInfoResponse.class)
DeliveryInfoResponse deliveryInfo,

@Schema(description = "어울리는 추천 상품 목록", implementation = RecommendedItemResponse.class)
List<RecommendedItemResponse> recommendedItems,

@Schema(description = "상품 유의사항", example = "도착일은 배송지나 배송사 사정으로 변경 또는 지연될 수 있습니다.")
String cautions,

@Schema(description = "상품 옵션")
List<ProductOptions> options,

@Schema(description = "상세 정보 이미지 URL 목록", example = "[\"https://example.com/img1.jpg\", \"https://example.com/img2.jpg\"]")
List<String> imageUrl
) {
Expand All @@ -78,12 +67,9 @@ public static ProductDetailResponse from(Product product) {
.manufacturerName(product.getManufacturer())
.category(product.getCategory())
.productName(product.getName())
.discountRate(0)
.originalPrice(product.getPrice())
.discountedPrice(product.getPrice())
.originalPrice(getPrice(product))
.likedByMe(false)
.deliveryInfo(DeliveryInfoResponse.of(3000, "일반배송", "3일 이내 발송 예정"))
.recommendedItems(null)
.options(ProductOptions.from(product))
.cautions("도착일은 배송지나 배송사 사정으로 변경 또는 지연될 수 있습니다.")
.imageUrl(product.getAllDetailImages())
.build();
Expand All @@ -93,19 +79,70 @@ public static ProductDetailResponse from(Product product) {
public static ProductDetailResponse from(Product product, boolean likedByMe) {
return ProductDetailResponse.builder()
.id(product.getId())
.thumbnail(product.getThumbnail())
.manufacturerName(product.getManufacturer())
.category(product.getCategory())
.productName(product.getName())
.discountRate(0)
.originalPrice(product.getPrice())
.discountedPrice(product.getPrice())
.originalPrice(getPrice(product))
.likedByMe(likedByMe)
.deliveryInfo(DeliveryInfoResponse.of(3000, "일반배송", "3일 이내 발송 예정"))
.recommendedItems(null)
.options(ProductOptions.from(product))
.cautions("도착일은 배송지나 배송사 사정으로 변경 또는 지연될 수 있습니다.")
.imageUrl(product.getAllDetailImages())
.build();
}

/// 내부에서만 사용되는 옵션
@Builder
private record ProductOptions(
String optionName,
Double price,
String site,
String siteUrl
) {

/// 정적 팩토리 메서드
private static ProductOptions of(String optionName, Double price, String site, String siteUrl) {
return ProductOptions.builder()
.optionName(optionName)
.price(price)
.site(site)
.siteUrl(siteUrl)
.build();
}

/// 정적 팩토리 메서드
public static List<ProductOptions> from(Product product) {
List<Map<String, Object>> productOptions = Optional.ofNullable(product.getOptions())
.orElse(Collections.emptyList());

return productOptions.stream()
/// option_name 없는 건 필터링
.filter(option -> option.get("option_name") != null)
.map(option -> {

/// 옵션 명 가져오기
String optionName = (String) option.get("option_name");
Double price = null;

/// 가격이 있다면
if (option.get("main_price") != null) {
price = Double.valueOf(option.get("main_price").toString());
}

String site = null;
String siteUrl = null;
Object vendorsObj = option.get("vendors");
if (vendorsObj instanceof Map<?, ?> vendors) {
site = (String) vendors.get("shop");
siteUrl = (String) vendors.get("url");
}

return of(optionName, price, site, siteUrl);
})
.toList();
}
}


}

Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import java.util.List;
import java.util.Set;

import static site.kikihi.custom.platform.adapter.in.web.dto.response.util.ProductPriceUtil.getPrice;

/**
* 상품 목록 응답 DTO
*
Expand All @@ -15,9 +17,8 @@
* @param category 카테고리
* @param manufacturerName 제조사명
* @param productName 제품명
* @param discountRate 할인율 (예: 0.2 = 20%)
* @param discountedPrice 할인가 (정상가에서 할인 적용된 가격)
* @param likedByMe 나의 좋아요 여부 (true/false)
* @param discountedPrice 가격 (기존 DTO 컬럼과 동일하게 유지)
*/

@Builder
Expand All @@ -39,11 +40,8 @@ public record ProductListResponse(
@Schema(description = "제품명", example = "맥북 키보드")
String productName,

@Schema(description = "할인율 (0.2 = 20%)", example = "0.15")
double discountRate,

@Schema(description = "할인가 (정상가에서 할인 적용된 가격)", example = "339000.0")
double discountedPrice,
@Schema(description = "최저가 가격부터", example = "599000원 ~")
String discountedPrice,

@Schema(description = "내가 좋아요(북마크)한 상품 여부", example = "true")
boolean likedByMe
Expand All @@ -57,8 +55,7 @@ public static ProductListResponse from(Product product) {
.category(product.getCategory())
.manufacturerName(product.getManufacturer())
.productName(product.getName())
.discountRate(0)
.discountedPrice(product.getPrice())
.discountedPrice(getPrice(product))
.likedByMe(false)
.build();
}
Expand Down Expand Up @@ -87,8 +84,7 @@ public static ProductListResponse from(Product product, boolean likedByMe) {
.category(product.getCategory())
.manufacturerName(product.getManufacturer())
.productName(product.getName())
.discountRate(0)
.discountedPrice(product.getPrice())
.discountedPrice(getPrice(product))
.likedByMe(likedByMe)
.build();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package site.kikihi.custom.platform.adapter.in.web.dto.response.util;

import org.springframework.stereotype.Component;
import site.kikihi.custom.platform.domain.product.Product;

import java.util.*;

@Component
public class ProductPriceUtil {

/// 최저가 가격을 위한 설정
public static String getPrice(Product product) {

// 옵션 Optional 처리
List<Map<String, Object>> productOptions = Optional.ofNullable(product.getOptions())
.orElse(Collections.emptyList());

// 옵션이 없다면 기본 가격 제공
if (productOptions.isEmpty()) {
return (int) product.getPrice() + "원";
}

// 옵션 중 최저가 찾기
OptionalDouble minPrice = productOptions.stream()
.map(option -> option.get("main_price"))
.filter(Objects::nonNull)
.mapToDouble(price -> Double.parseDouble(price.toString()))
.min();

// 최저가 있으면 옵션 가격으로, 없으면 기본 가격으로
if (minPrice.isPresent()) {
return (int) minPrice.getAsDouble() + "원 ~";
} else {
return (int) product.getPrice() + "원";
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ ApiResponse<String> deleteBookmark(

String SUCCESS_PAYLOAD = """
{
"productId": "686bd26dff7b820865d6e005"
"productId": "6896ed675198cf586e933d6c"
}
""";
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public interface ProductControllerSpec {
description = "ID를 바탕으로 상품 상세정보를 조회할 수 있습니다."
)
ApiResponse<ProductDetailResponse> getProduct(
@Parameter(description = "상품 ID", example = "686bd26dff7b820865d6e005")
@Parameter(description = "상품 ID", example = "6896ed675198cf586e933d6c")
@RequestParam String id,

@Parameter(hidden = true)
Expand Down
Loading
Loading