Skip to content

Commit

Permalink
Merge pull request #223 from lotteon2/feat/couponinfo-for-best_new_ho…
Browse files Browse the repository at this point in the history
…me_View

Feat/couponinfo for best new home view
  • Loading branch information
CokeLee777 authored Jan 18, 2024
2 parents 96e86ab + daf5a58 commit 5f9b64d
Show file tree
Hide file tree
Showing 9 changed files with 347 additions and 68 deletions.
10 changes: 10 additions & 0 deletions src/apis/coupon/CouponItemDto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,13 @@ export interface Coupons {
memberCouponInfoReadItemResponse: Coupon[]
totalCounts: number
}

export interface MultipleProductsCouponRequest {
products: ProductCategoryPair[]
}

export interface MultipleProductCouponsResponse {
coupons: {
[productId: number]: CouponInfoItemResponse[]
}
}
32 changes: 30 additions & 2 deletions src/apis/coupon/coupon.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import axios, { AxiosError } from 'axios'
import { authAxiosInstance } from '@/apis/utils/index'
import { authAxiosInstance, defaultAxiosInstance } from '@/apis/utils/index'
import type {
CouponInfoItemWithAvailabilityResponse,
ProductCategoryPair,
CheckoutCouponApplicationResponse,
CouponInfoItemResponse,
MultipleCouponDownloadResponse
MultipleCouponDownloadResponse,
MultipleProductsCouponRequest,
MultipleProductCouponsResponse
} from '@/apis/coupon/CouponItemDto'
import { openInternalServerErrorNotification } from '@/utils/Toast'
import { warningModal } from '@/utils/Modal'
Expand Down Expand Up @@ -147,3 +149,29 @@ export const getMyCoupons = async (page: Number) => {
throw error
}
}

export const getMultipleProductsCoupons = async (
requestPayload: MultipleProductsCouponRequest
): Promise<MultipleProductCouponsResponse> => {
try {
const response = await defaultAxiosInstance.post(
`${PROMOTION_PREFIX_PATH}${COUPON_DOMAIN_PREFIX_PATH}/multiple-products`,
requestPayload
)
return response.data
} catch (error) {
if (error instanceof AxiosError) {
if (error.response) {
if (error.response.status >= 400 && error.response.status < 500) {
await warningModal('알림', error.response.data.message)
console.error(`Client Error=${error.response.data.message}`)
}
if (error.response.status >= 500) {
openInternalServerErrorNotification()
console.error('Internal Server Error')
}
}
}
throw error
}
}
13 changes: 12 additions & 1 deletion src/apis/product/ProductDto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ export interface ReadProductSliceResponse {
productResponses: ReadProductResponse[]
}

export interface ExtendedReadProductResponse extends ReadProductResponse {
categoryId: number
}

export interface ReadProductResponse {
id: number
brandName: string
Expand All @@ -13,7 +17,7 @@ export interface ReadProductResponse {
imgUrl: string
avgRating: number
reviewCount: number
coupons: CouponInfoItemResponse[]
coupons?: CouponInfoItemResponse[]
}

export interface CouponInfoItemResponse {
Expand Down Expand Up @@ -62,9 +66,16 @@ export interface ReadCacheProductListResponse {
responses: ReadCacheProductResponse[]
}

export interface ExtendedReadCacheProductResponse extends ReadCacheProductResponse {
avgRating: number
reviewCount: number
coupons: CouponInfoItemResponse[]
}

export interface ReadCacheProductResponse {
id: number
brandName: string
categoryId: number
categoryName: string
price: number
name: string
Expand Down
58 changes: 36 additions & 22 deletions src/components/product/ProductListPriceDisplay.vue
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
<template>
<div class="product-price">
<template v-if="hasDiscount">
<span class="product-price__original">
<div
class="product-price"
:class="hasDiscount ? 'product-price--has-discount' : 'product-price--no-discount'"
>
<div v-if="hasDiscount" class="product-price__original-and-discount">
<div class="product-price__original">
<del>{{ originalPrice?.toLocaleString() }}원</del>
</span>
<span class="product-price__discount">{{ discountPercentage }}%</span>
</template>
<strong class="product-price__final">{{ finalPrice?.toLocaleString() }}원</strong>
</div>
<div class="product-price__discount-and-final">
<span class="product-price__discount">{{ discountPercentage }}%</span>
<strong class="product-price__final">{{ finalPrice?.toLocaleString() }}원</strong>
</div>
</div>
<strong v-else class="product-price__final">{{ finalPrice?.toLocaleString() }}원</strong>
</div>
</template>

Expand All @@ -27,43 +33,51 @@ const hasDiscount = computed(() => props.discountPercentage > 0)

<style scoped>
.product-price {
width: auto;
height: auto;
/* display: flex;
display: flex;
flex-direction: column;
align-items: center;
text-align: end; */
font-family: 'TheJamsil';
}
.product-price {
.product-price__original-and-discount {
display: flex;
flex-direction: column;
align-items: flex-start;
width: 100%;
}
.product-price__discount-and-final {
display: flex;
justify-content: flex-end;
align-items: center;
font-size: 0; /* inline-block elements간의 의도하지않은 spacing방지용 */
/* margin-left: auto; */
/* margin-left 안넣는게 좋을까요? 😀 */
}
.product-price__original,
.product-price__discount,
.product-price__final {
font-size: 18px; /* 이 컴포넌트의 기본 폰트크기 */
line-height: 24px; /* 가격은 좀 더 크게 뒀음. */
display: inline-block;
font-size: 18px;
line-height: 24px;
}
.product-price__original {
color: #888;
text-decoration: line-through; /* 긋는 효과 */
margin-right: 6px;
font-size: 16px;
color: #888;
text-decoration: line-through;
}
.product-price__discount {
color: #c22727;
margin-right: 7px;
font-size: 18px;
font-weight: bold;
margin-right: 10px; /* Adjust spacing between discount and final price */
}
.product-price__final {
font-weight: 700;
color: #000;
}
.product-price--no-discount .product-price__final {
margin-left: auto;
}
</style>
58 changes: 58 additions & 0 deletions src/utils/UtilFunc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import type { ReadProductResponse, ReadProductSliceResponse } from '@/apis/product/ProductDto'

/**
혜택가 관련 계산입니다.
*/
export const getFloorDiscountPercentage = (product: ReadProductResponse) => {
const { maxDiscountPercentageBeforeFlooring, maxDiscountPercentageAfterFlooring } =
getProductMaxDiscountPercentage(product)
return Math.floor(maxDiscountPercentageAfterFlooring)
}

export const getFinalPrice = (product: ReadProductResponse) => {
const { maxDiscountPercentageBeforeFlooring, maxDiscountPercentageAfterFlooring } =
getProductMaxDiscountPercentage(product)
return Math.floor(product.price - (product.price * maxDiscountPercentageBeforeFlooring) / 100)
}

const getProductMaxDiscountPercentage = (product: ReadProductResponse) => {
let maxDiscountPercentageBeforeFlooring = 0 // 가격을 따로 정확히 보여주기 위한 보존값
let maxDiscountPercentageAfterFlooring = 0

// product.coupons 없을시, for Each 발동안함. 0, 0 return
if (!product.coupons) {
return { maxDiscountPercentageBeforeFlooring, maxDiscountPercentageAfterFlooring }
}
product.coupons.forEach((coupon) => {
if (product.price >= coupon.minPurchaseAmount) {
// Listview에서는 일단 1개 구매를 기준으로 display
// N개 이상 구매했을때를 가정한 최적 할인율과 해당 쿠폰 적용환경의 최소개수도 구할까 고민중임.
let discountPercentage = 0

if (coupon.discountType === 'PERCENTAGE') {
discountPercentage = coupon.discountValue
} else if (coupon.discountType === 'FIXED_AMOUNT') {
discountPercentage = (coupon.discountValue / product.price) * 100
}

// 쿠폰별 maxDiscount cap 적용해서 갱신
if (coupon.maxDiscountAmount !== null) {
const maxDiscountValue = (coupon.maxDiscountAmount / product.price) * 100
discountPercentage = Math.min(discountPercentage, maxDiscountValue)
}

// 직전 iteration과 비교 갱신
maxDiscountPercentageBeforeFlooring = Math.max(
// 가격을 따로 정확히 보여주기 위한 보존값
maxDiscountPercentageBeforeFlooring,
discountPercentage
)
maxDiscountPercentageAfterFlooring = Math.max(
maxDiscountPercentageAfterFlooring,
Math.floor(discountPercentage)
)
}
})

return { maxDiscountPercentageBeforeFlooring, maxDiscountPercentageAfterFlooring }
}
68 changes: 59 additions & 9 deletions src/views/BestProductView.vue
Original file line number Diff line number Diff line change
@@ -1,30 +1,78 @@
<script setup lang="ts">
import { onBeforeMount, ref } from 'vue'
import { getBestProducts } from '@/apis/product/ProductClient'
import type { ReadCacheProductResponse } from '@/apis/product/ProductDto'
import type {
ProductCategoryPair,
MultipleProductsCouponRequest,
CouponInfoItemResponse
} from '@/apis/coupon/CouponItemDto'
import type {
ReadCacheProductResponse,
ExtendedReadCacheProductResponse
} from '@/apis/product/ProductDto'
import { Image } from 'ant-design-vue'
import WhitePageComponent from '@/components/wishcart/WhitePageComponent.vue'
import ProductListPriceDisplay from '@/components/product/ProductListPriceDisplay.vue'
import { getFloorDiscountPercentage, getFinalPrice } from '@/utils/UtilFunc'
import { getMultipleProductsCoupons } from '@/apis/coupon/coupon'
const VITE_STATIC_IMG_URL = ref<string>(import.meta.env.VITE_STATIC_IMG_URL)
const bestProducts = ref<ReadCacheProductResponse[]>([
const bestProducts = ref<ExtendedReadCacheProductResponse[]>([
{
id: 0,
brandName: '',
code: '',
categoryId: 0,
categoryName: '',
imgUrl: '',
name: '',
price: 0
price: 0,
avgRating: 0,
reviewCount: 0,
coupons: []
}
])
const fetchNewProduct = async () => {
const fetchCoupons = async (
products: ReadCacheProductResponse[]
): Promise<ExtendedReadCacheProductResponse[]> => {
// product-service에서 조회해온 products를 이용해서 productCategoryPairs 가공
const productCategoryPairs = products.map<ProductCategoryPair>((product) => ({
productId: product.id,
categoryId: product.categoryId
}))
// request payload 가공
const couponRequest: MultipleProductsCouponRequest = {
products: productCategoryPairs
}
// promotion-service 조회요청
const couponsResponse = await getMultipleProductsCoupons(couponRequest)
const couponsMap = couponsResponse.coupons
// products결과와 coupons 결과 합쳐서 ReadProductResponse 가공
return products.map<ExtendedReadCacheProductResponse>((product: ReadCacheProductResponse) => {
const productCoupons: CouponInfoItemResponse[] = couponsMap[product.id] || []
return {
...product,
coupons: productCoupons,
avgRating: 0, // 상품 캐싱정보에는 avgRating, reviewCount 정보가 없음.
reviewCount: 0
} as ExtendedReadCacheProductResponse
})
}
const fetchBestProductsAndCoupons = async () => {
const bestProductData = await getBestProducts()
bestProducts.value = bestProductData.responses
if (bestProductData.responses) {
const productsWithCoupons = await fetchCoupons(bestProductData.responses)
bestProducts.value = productsWithCoupons
}
}
onBeforeMount(fetchNewProduct)
onBeforeMount(fetchBestProductsAndCoupons)
</script>

<template>
Expand Down Expand Up @@ -55,9 +103,11 @@ onBeforeMount(fetchNewProduct)
</Image>
<h1>{{ product.brandName }}</h1>
<h2>{{ product.name }}</h2>
<div class="product-third-info">
<h3>{{ product.price.toLocaleString() }}원</h3>
</div>
<ProductListPriceDisplay
:original-price="product.price"
:discount-percentage="getFloorDiscountPercentage(product)"
:final-price="getFinalPrice(product)"
/>
</RouterLink>
</div>
<WhitePageComponent v-else message="결과가 없습니다" />
Expand Down
Loading

0 comments on commit 5f9b64d

Please sign in to comment.