Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/couponinfo for best new home view #223

Merged
merged 6 commits into from
Jan 18, 2024
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
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