/*
* © NHN Commerce Corp. All rights reserved.
* NHN Corp. PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
*
* @author hyeyeon-park
* @author Bomee Yoon
* @since 2021.6.24
*/
$(() => {
const $container = $('#viewContainer');
const { getUrlParam, toCurrencyString } = shopby.utils;
const productNo = Number(getUrlParam('productNo'));
const pathVariable = { productNo };
const searchParams = new URLSearchParams(location.search);
const channelType = searchParams.get('channelType');
shopby.product.view = {
async initiate() {
try {
this.addRecentProduct();
const [product, options, optionImages, relatedProducts, coupons] = await this.fetchProduct();
const [
reviewableInfo,
reviewableOptions,
reviews,
reviewsConfigurations,
photoReviews,
inquires,
] = await this.fetchReviewsAndInquiries();
shopby.setGlobalVariableBy('PRODUCT', product);
const { productInquiryConfig, productReviewConfig } = shopby.cache.getBoardsConfig();
await this.summary.initiate(generateData.summary(product, options, coupons));
this.detail.initiate(generateData.detail(product, optionImages, options));
this.basic.initiate(generateData.basic(product));
this.review.initiate(
generateData.review(
reviewableInfo,
reviewableOptions,
reviews,
reviewsConfigurations,
photoReviews,
productReviewConfig,
),
);
this.inquiry.initiate(generateData.inquiry(inquires, productInquiryConfig));
this.renderProductInfo(product);
this.renderTabInfo(reviews, inquires);
this.renderRelatedProducts(relatedProducts);
this.bindEvents();
$('.sub_content').removeClass('invisible').addClass('visible');
} catch (error) {
console.log(error);
this._errorHandler(error);
}
},
bindEvents() {
$(window).on('scroll', this.onScrollTab.bind(this));
},
async addRecentProduct() {
if (shopby.logined()) {
await shopby.api.product.postProfileRecentProducts({ requestBody: { productNo } });
} else {
shopby.localStorage.unshiftItemByOrder('guestRecentProducts', productNo, 60 * 60 * 24 * 1000);
}
},
async fetchProduct() {
const queryString = { includesCartCoupon: true };
const result = await shopby.utils.allSettled([
shopby.api.product.getProductsProductNo({ pathVariable }),
shopby.api.product.getProductsProductNoOptions({ pathVariable }),
shopby.api.product.getProductsProductNoOptionsImages({ pathVariable }),
shopby.api.product.getProductsProductNoRelatedProducts({ pathVariable }),
shopby.api.promotion.getCouponsProductsProductNoIssuableCoupons({ queryString, pathVariable }),
]);
const hasError = result.some(r => r && r.status === 'rejected');
if (hasError) {
throw result;
} else {
return result.map(({ value }) => value.data);
}
},
async fetchReviewsAndInquiries() {
const result = await Promise.all([
this.review.fetchReviewableInfo(),
this.review.fetchProductsProductNoReviewableOptions(),
this.review.fetchReviews(),
this.review.fetchReviewsConfigurations(),
this.review.fetchPhotoReviews(),
this.inquiry.fetchInquires(),
]);
return result.map(({ data }) => data);
},
renderProductInfo({ baseInfo }) {
$container.data('productInfo', {
productNo: baseInfo.productNo,
productName: baseInfo.productName,
imageUrl: (baseInfo && baseInfo.imageUrls && baseInfo.imageUrls[0]) || '',
});
},
renderTabInfo(reviews, inquires) {
$('#tabInfo').render({
reviews,
inquires,
});
},
renderRelatedProducts(relatedProducts) {
displayHelper.toggleRelatedProducts(relatedProducts && relatedProducts.length > 0, relatedProducts);
},
onScrollTab() {
const scrollPosition = $(window).scrollTop();
this.setTabPosition(scrollPosition);
this.setTabMark(scrollPosition);
},
setTabPosition(scrollPosition) {
const tabPosition = $('#tabInfo').offset().top;
const headerHeight = $('#header').height();
const subTopHeight = $('#pageTitleBox').height();
const topContentsHeight = $('#summary').height();
if (scrollPosition < headerHeight + subTopHeight + topContentsHeight) {
$('#tabInfo').removeAttr('style');
} else if (scrollPosition >= tabPosition) {
$('#tabInfo').css({ position: 'fixed', top: 0 });
}
},
setTabMark(scrollPosition) {
const $tabInfo = $('#tabInfo');
$.each($tabInfo.find('a'), (index, el) => {
const hash = $(el).attr('href');
const catchInTheNet = () => {
const offset = $(hash).offset();
const tabHeight = $('#tabInfo').innerHeight();
return (
scrollPosition + tabHeight > offset.top && scrollPosition + tabHeight < offset.top + $(hash).innerHeight()
);
};
if (catchInTheNet()) {
this.summary.activeCurrentTab($tabInfo, hash);
}
});
},
async _errorHandler(error) {
shopby.utils.hideAllContents();
if (error && error.length > 0) {
const [{ reason: productReason }, { reason: optionReason }, { reason: imageReason }] = error;
if (productReason.code === 'E0008') {
await shopby.alert(productReason.message, () => {
shopby.goAdultCertification();
});
return;
}
if (optionReason.code === 'SPEC0002' || imageReason.code === 'SPEC0002' || productReason.code === 'PNPE001') {
optionReason &&
optionReason.message &&
shopby.alert(optionReason.message.replace(/,/g, ''), () => {
history.back();
});
}
}
},
// 상품정보 및 구매
summary: {
$summary: $container.find('#summary'),
naverPay: null,
orderConfigs: null,
option: null,
async initiate(data) {
this.option = shopby.helper.option(data.options, data.reservationData);
this.optionUsed = data.options.type !== 'DEFAULT';
this.render(data);
this.bindEvents(data.remoteDeliveryAreaFees);
this.naverPay = await this.generateNaverPay(data);
this.naverPay &&
this.naverPay.applyNaverPayButton(this.loadNaverPayOrder.bind(this), this.loadNaverWishlist.bind(this));
},
render(data) {
this.renderOptionSelector();
this.renderLiked(data.liked, true);
this.$summary.render(data);
$('.detail_buy').render(data); //하단 고정영역 : 찜/장바구니/구매하기
displayHelper.slickMainImages(data.imageUrls);
displayHelper.isUnablePurchasing(!data.isUnablePurchasing);
},
renderOptionSelector() {
this.$summary.find('#optionSelector').render({
selectType: this.option.option._selectType,
options: this.option.options,
textOptions: this.option.textOptions,
optionUsed: this.optionUsed,
showsTotalPriceWithSelectedOptionsArea: this.option.selectedOptions.length > 0,
});
this.renderSelectedOptions();
},
renderSelectedOptions() {
this.$summary.find('#selectedOptions').render({
selectedOptions: this.option.selectedOptions,
textOptions: this.option.textOptions,
optionUsed: this.optionUsed,
});
this.renderTotalPrice();
},
renderTotalPrice() {
this.$summary.find('#totalPrice').text(toCurrencyString(this.option.totalPrice));
},
renderLiked(liked, init) {
const $wishBtn = this.$summary.find('#wishBtn');
liked ? $wishBtn.addClass('on') : $wishBtn.removeClass('on');
displayHelper.fadeOutLikeMessage(liked, init);
},
activeCurrentTab($wrap, hash) {
$wrap.find('.selected').removeClass('selected');
$(hash + 'Tab').addClass('selected');
},
setLocTop(e) {
e.preventDefault();
const $wrap = $('#tabInfo');
const $currentTab = $(e.target);
const hash = $currentTab.attr('href');
const tabHeight = 60; // tab높이 + 10
$('body, html').animate(
{
scrollTop: $(hash).offset().top - tabHeight + 'px',
},
function () {
this.activeCurrentTab($wrap, hash);
}.bind(this),
);
},
bindEvents(remoteDeliveryAreaFees) {
this.$summary
.on('change', 'select[name="optionSelector"]', this.events.onChangeOptionSelector.bind(this))
.on('change', 'input[name="orderCnt"]', this.events.onChangeOrderCount.bind(this))
.on('blur', 'input', this.events.onBlurTextOptionInput.bind(this))
.on('keyup', 'input', this.events.onKeyUpTextOptionInput.bind(this))
.on('click', '#btnToggleTextOptionMode', this.events.onClickTextOptionMode.bind(this))
.on('click', '#orderCountBtns', this.events.onClickOrderCountBtns.bind(this))
.on('click', '#bottomBuyBox', this.events.openPurchaseLayer.bind(this))
.on('click', '#purchaseBtns', this.events.onClickPurchaseBtns.bind(this))
.on('click', '.detail_apply_del', this.events.onClickDeleteOption.bind(this))
.on('click', '#downloadCoupon', this.events.onClickDownloadCoupon.bind(this))
.on('click', '.share_btn_box', this.events.onClickWishOrShare.bind(this))
.on('click', '.js_lys_close', this.events.closeShareLayer)
.on('click', '.st_buy_close', this.events.closeBuyLayer)
.on('click', '#additionalDeliveryFee', () =>
displayHelper.onClickAdditionalDeliveryFee(remoteDeliveryAreaFees),
);
$('.item_photo_big').on('click', this.events.onClickMainImage.bind(this));
$('#tabInfo').on('click', this.setLocTop.bind(this));
},
events: {
onChangeOptionSelector({ target }) {
const data = $(target).data();
const selectedDepth = Number(data.depth);
const $option = $($('#optionSelector option:selected')[selectedDepth]);
try {
const currentSelectedOptionCount = this.option.selectedOptions.length;
const selectedOptionIndex = Number($option.val());
this.option.changeSelectOption(selectedDepth, selectedOptionIndex);
if (currentSelectedOptionCount !== this.option.selectedOptions.length) {
this.option.resetSelectOption();
}
this.renderOptionSelector();
} catch (error) {
util.alertFailMsg(error);
}
},
onChangeOrderCount({ target }) {
const $target = $(target);
if (!$target.val()) return;
const optionNo = Number($target.closest('.option_selected').data('option-no'));
this.option.changeOrderCount('custom', optionNo, Number($target.val()));
this.renderOptionSelector();
},
onBlurTextOptionInput({ target }) {
const $target = $(target);
const inputMatchingType = $target.data('option-matching-type');
if (!inputMatchingType) return;
const inputNo = Number($target.attr('name'));
const optionNo = Number($target.closest('.option_selected').data('option-no'));
this.option.changeTextOption(inputMatchingType, optionNo, inputNo, $target.val(), $target.data('index'));
},
onKeyUpTextOptionInput({ target }) {
if (target.value.length > 100) {
shopby.alert('텍스트옵션 입력글자수를 초과하였습니다.');
target.value = target.value.substring(0, 100);
}
},
onClickTextOptionMode({ currentTarget }) {
const optionNo = $(currentTarget).siblings('#amountTextOption').data('option-no');
const textInputNo = $(currentTarget).siblings('#amountTextOption').data('text-input-no');
const { orderCnt } = this.option.selectedOptions.find(option => option.optionNo === optionNo);
//수량별일때 : orderCnt 만큼 inputbox 뿌려주기
//옵션별일때 : inputbox 1개만
this.option.drawAmountTextOption(optionNo, textInputNo, orderCnt);
this.renderSelectedOptions();
},
onClickOrderCountBtns({ target }) {
const actionType = util.actionType(target);
const optionNo = Number($(target).closest('.option_selected').data('option-no'));
if (!actionType) return;
this.option.changeOrderCount(actionType, optionNo);
this.renderSelectedOptions();
},
openPurchaseLayer({ target, currentTarget }) {
const actionType = util.actionType(target);
if (!actionType) return;
if (actionType === 'buy' || actionType === 'cart') {
$('.st_buy_content').removeClass('hidden');
$(currentTarget).addClass('hidden');
}
},
closeBuyLayer({ currentTarget }) {
$(currentTarget).closest('.st_buy_content').addClass('hidden');
$('#bottomBuyBox').removeClass('hidden');
},
onClickPurchaseBtns({ target }) {
const actionType = util.actionType(target);
if (!actionType) return;
switch (actionType) {
case 'createCart':
this.createCart();
break;
case 'createOrder':
this.createOrder();
break;
default:
break;
}
},
onClickDeleteOption({ target }) {
const optionNo = Number($(target).closest('.option_selected').data('option-no'));
this.option.deleteSelectedOption(optionNo);
this.renderSelectedOptions();
},
onClickDownloadCoupon(event) {
event.preventDefault();
if (shopby.logined()) {
shopby.popup('download-coupon', { productNo });
} else {
shopby.alert('로그인 후 서비스를 이용하실 수 있습니다.', () => shopby.goLogin(location.href));
}
},
onClickMainImage(event) {
event.preventDefault();
const src = event.target.src;
if (src) {
const imageObjectList = [{ imageUrl: src }];
shopby.popup('slide-images', { imageObjectList });
}
},
onClickWishOrShare({ target }) {
const actionType = util.actionType(target);
if (!actionType) return;
switch (actionType) {
case 'wish':
this.addWish();
break;
case 'share':
this.openShareLayer();
break;
default:
break;
}
},
closeShareLayer() {
$('.ly_share').hide();
},
},
async generateNaverPay({ limitations, isUnablePurchasing }) {
const {
data: { naverPay },
} = await shopby.api.order.getOrderConfigs(); // 네이버페이 버튼은 어드민설정과 동일하게 실시간으로 반영되어야 한다.
// 네이버페이 사용하는 몰이면서, 네이버페이 주문 가능 상품인 경우만 가능
if (naverPay && limitations.naverPayHandling) {
return shopby.helper.naverPay({
EMBED_ID: 'naverPay',
BUTTON_KEY: naverPay.buttonKey,
BUTTON_COUNT: 2,
ENABLE: isUnablePurchasing ? 'N' : 'Y',
});
}
},
loadNaverPayOrder() {
try {
this.option.validate();
const { selectedOptions } = this.option;
const param = selectedOptions.map(opt => ({
channelType,
productNo,
optionNo: opt.optionNo,
additionalProductNo: 0,
orderCnt: opt.orderCnt,
optionInputs: opt.textOptions
.filter(textOption => textOption.value !== '')
.map(({ inputLabel, value }) => ({ inputLabel, inputValue: value })),
}));
this.naverPay.requestNaverPayOrder(param);
} catch (error) {
errorHandler.cartAndOrder(error);
}
},
loadNaverWishlist() {
this.naverPay.requestNaverWishlist(productNo);
},
get requestForCartAndOrder() {
return shopby.helper.option.requestBodyForCartOrOrder(
productNo,
this.option.selectedOptions,
this.option.textOptions,
this.optionUsed,
);
},
get requestForOrder() {
return {
requestBody: {
cartNos: null,
productCoupons: null,
trackingKey: null,
channelType,
products: this.requestForCartAndOrder,
},
};
},
async createCart() {
try {
this.option.validate();
const isSuccess = await shopby.helper.cart.addCart(this.requestForCartAndOrder);
if (!isSuccess) return;
shopby.helper.cart.updateCartCount(true);
const confirmOption = {
message: '상품이 장바구니에 담겼습니다.바로 확인하시겠습니까?',
iconType: 'cart',
};
const afterAdding = ({ state }) => {
if (state === 'ok') {
window.location.href = '/pages/order/cart.html';
}
};
shopby.confirm(confirmOption, afterAdding);
} catch (error) {
errorHandler.cartAndOrder(error);
}
},
async createOrder() {
try {
this.option.validate();
const { data } = await shopby.api.order.postOrderSheets(this.requestForOrder);
const orderPage = `/pages/order/order.html?ordersheetno=${data.orderSheetNo}`;
if (shopby.logined()) {
window.location.replace(orderPage);
} else {
shopby.goLogin(orderPage);
}
} catch (error) {
const unauthenticatedErrorCodes = ['O3336', 'O3338'];
if (unauthenticatedErrorCodes.includes(error.code)) return;
errorHandler.cartAndOrder(error);
}
},
addWish() {
shopby.logined() ? this.toggleLiked() : util.confirmGoToLogin();
},
async toggleLiked() {
const requestBody = { productNos: [productNo] };
const { data } = await shopby.api.product.postProfileLikeProducts({ requestBody });
const isLiked = data[0].result;
this.renderLiked(isLiked);
},
openShareLayer() {
$('.ly_share').show();
},
},
// 상품상세 & 정보제공고시
detail: {
$detail: $container.find('#detail'),
initiate(detailData) {
this.render(detailData);
this.bindEvents();
},
render(detailData) {
//todo showsDetailInfo 옵션 이미지 확인
this.$detail.render(detailData);
displayHelper.slickRelatedProducts();
displayHelper.showMoreDetailBtn();
},
bindEvents() {
$('.sb_optionImageItem').on('click', this.onClickImageArea.bind(this));
$('.btn_more_detail').on('click', this.onClickMoreDetail.bind(this));
},
async onClickImageArea(event) {
event.preventDefault();
const $target = $(event.currentTarget);
const title = '자세히 보기';
const imageObjectList = await this.fetchOptionImages($target);
shopby.popup('slide-images', { imageObjectList, title });
},
async fetchOptionImages($target) {
const optionNo = $target.data('optionNo');
const value = $target.find('.sb_p_optionName').text();
const request = {
pathVariable: {
productNo,
optionNo,
},
};
const { data } = await shopby.api.product.getProductsProductNoOptionsOptionNoImages(request);
return data.map(image => ({
...image,
value,
}));
},
onClickMoreDetail({ currentTarget }) {
$('.detail_explain_box').css('height', '100%');
$(currentTarget).hide();
},
},
//기본정보 ( 기본정보, 배송/교환/반품 정보)
basic: {
$basic: $container.find('#basic'),
initiate(basicData) {
this.render(basicData);
this.bindEvents(basicData.remoteDeliveryAreaFees);
},
render(basicData) {
const { deliveryGuide, exchangeGuide, afterServiceGuide, refundGuide } = basicData;
const hasGuide = deliveryGuide || exchangeGuide || afterServiceGuide || refundGuide ? true : false;
this.$basic.render({ ...basicData, hasGuide });
},
bindEvents(remoteDeliveryAreaFees) {
$('#basic')
.on('click', '.openblock_header', this.onClickBasicInfo.bind(this))
.on('click', '#additionalDeliveryFee', () =>
displayHelper.onClickAdditionalDeliveryFee(remoteDeliveryAreaFees),
);
},
onClickBasicInfo({ currentTarget }) {
if ($(currentTarget).hasClass('arrow_down')) {
$(currentTarget).siblings('.openblock_content').removeClass('hidden');
$(currentTarget).removeClass('arrow_down').addClass('arrow_up');
} else {
$(currentTarget).siblings('.openblock_content').addClass('hidden');
$(currentTarget).removeClass('arrow_up').addClass('arrow_down');
}
},
},
review: {
$review: $container.find('#review'),
reviews: [],
reviewPage: {},
reviewable: false,
reviewableProducts: [],
reviewCommentsInfo: {}, // {reviewNo: { page:1 -> 리뷰더보기 누를 경우 1씩 증가, content: [리뷰 데이터 (페이지 증가 시 데이터 추가)]}}
photoReviews: {},
photoReviewParam: {},
MAX_PHOTO_REVIEW_SIZE: 3,
initiate(data) {
this.reviewPage = new shopby.readMore(this.appendReviews.bind(this), '#btnReviewsMore', 5);
this.reviewable = data.reviewable;
this.reviewableProducts = data.item;
this.render(data);
this.bindEvents();
},
render(data) {
$(this.$review.find('#reviewTit')).text(data.config.name);
this.renderReviews(data);
},
async renderReviews(reviews, onlyPhotos, selectedOrder) {
if (!reviews) {
const results = await this.fetchReviewDefaultData(onlyPhotos, selectedOrder);
reviews = generateData.review(...results, null, onlyPhotos);
this.reviewable = reviews.reviewable;
this.reviewableProducts = reviews.item;
} else {
this.reviewableProducts = reviews.item;
this.$review.find('#writeReview-wrap').render({ reviewable: reviews.reviewable });
}
this.$review.find('#reviewList').render(reviews);
this.reviewPage.render(reviews.totalCount);
this.renderTabCount(reviews);
this.$review.find('#reviewsViewType').render({ onlyPhotos });
if (reviews.photoReviews) {
this.$review.find('#photoReviews').render(reviews.photoReviews);
this.$review.find('#sortReviews').render(reviews.photoReviews);
}
this.reviews = reviews.items;
this.photoReviews = reviews.photoReviews;
this.setReviewCommentsInfo(reviews.items);
},
async renderTabCount(reviews) {
const { data: inquiries } = await shopby.product.view.inquiry.fetchInquires();
shopby.product.view.renderTabInfo(reviews, inquiries);
},
renderComment(commentData, $reviewComment) {
this.$review.find('#moreComments').remove();
$reviewComment.find('#reviewComments').render(commentData);
},
setReviewCommentsInfo(reviews) {
const FIRST_PAGE = 1;
this.reviewCommentsInfo = Object.assign(
{},
...reviews.map(({ reviewNo }) => ({ [reviewNo]: { page: FIRST_PAGE, contents: [] } })),
);
},
async appendReviews() {
const results = await this.fetchReviewDefaultData();
const reviews = generateData.review(...results, null);
if (reviews.totalCount === 0) return;
this.reviewPage.render(reviews.totalCount);
const compiled = Handlebars.compile($('#reviewListTemplate').html());
const appendHtml = $(compiled({ ...reviews })).find('li');
$('#reviewList ul').append(appendHtml);
},
fetchReviews(onlyPhotos, selectedOrder) {
const request = this.getRequestForReview(onlyPhotos, selectedOrder);
return shopby.api.display.getProductsProductNoProductReviews(request);
},
fetchReviewableInfo() {
if (shopby.logined()) {
return shopby.api.display.getProfileOrderOptionsProductReviewable({
queryString: { hasTotalCount: true, productNo },
});
} else {
const guestReviewableInfo = {
data: {
totalCount: 0,
items: [],
},
};
return new Promise(resolve => resolve(guestReviewableInfo));
}
},
fetchReviewsConfigurations() {
return shopby.api.display.getProductReviewsConfigurations();
},
fetchPhotoReviews() {
this.photoReviewParam = {
pathVariable: { productNo },
queryString: { hasTotalCount: true, pageNumber: 1, pageSize: this.MAX_PHOTO_REVIEW_SIZE },
};
return shopby.api.display.getProductsProductNoPhotoReviews(this.photoReviewParam);
},
async fetchReviewDefaultData(onlyPhotos, selectedOrder) {
const fetchData = await Promise.all([
this.fetchReviewableInfo(),
this.fetchProductsProductNoReviewableOptions(),
this.fetchReviews(onlyPhotos, selectedOrder),
this.fetchReviewsConfigurations(),
this.fetchPhotoReviews(),
]);
return fetchData.map(({ data }) => data);
},
async postReviewRecommend(request) {
await shopby.api.display.postProductsProductNoProductReviewsReviewNoRecommend(request);
},
async deleteReviewRecommend(request) {
await shopby.api.display.deleteProductsProductNoProductReviewsReviewNoRecommend(request);
},
getReview(reviewNo) {
return shopby.api.display.getProductsProductNoProductReviewsReviewNo({
pathVariable: { productNo, reviewNo },
});
},
async changeReviewRecommendStatus(reviewNo, isRecommend, $target) {
const request = {
pathVariable: {
productNo,
reviewNo,
},
};
isRecommend ? await this.postReviewRecommend(request) : await this.deleteReviewRecommend(request);
const { data } = await this.getReview(reviewNo);
$target.find('.recommendCnt').text(data.recommendCnt);
},
generateCommentData(contents, totalCount) {
const hasMoreCommentsBtn = totalCount - contents.length > 0;
return {
hasMoreCommentsBtn,
contents: contents.map(content => ({
ydmt: this.removeSecondFromYdmt(content.registerYmdt),
...content,
})),
};
},
async fetchReviewComments(reviewNo) {
const COMMENT_PAGE_SIZE = 20;
const { page, contents } = this.reviewCommentsInfo[reviewNo];
const request = {
pathVariable: {
productNo,
reviewNo,
},
queryString: {
size: COMMENT_PAGE_SIZE,
page,
hasTotalCount: true,
},
};
const { data } = await shopby.api.display.getProductsProductNoProductReviewsReviewNoComments(request);
this.reviewCommentsInfo[reviewNo].contents.push(...data.contents);
return this.generateCommentData(contents, data.totalCount);
},
async fetchProductsProductNoReviewableOptions() {
if (!shopby.logined()) {
const reviewableOptions = {
data: {
reviewable: false,
item: [],
},
};
return new Promise(resolve => resolve(reviewableOptions));
}
const request = {
pathVariable: {
productNo,
},
};
return await shopby.api.display.getProductsProductNoReviewableOptions(request);
},
bindEvents() {
this.$review
.on('click', '#reviewsViewType', this.events.onClickFilterReviewsByViewType.bind(this))
.on('click', '#writeReview', this.events.onClickWriteReview.bind(this))
.on('change', '#sortReviews', this.events.onClickSortReviews.bind(this))
.on('click', '#reviewList', this.events.onClickReviewList.bind(this))
.on('click', '#attachImage', this.events.viewAttahments.bind(this))
.on('click', '#reviewThumbs', this.events.onClickReviewThumbs.bind(this));
},
events: {
onClickFilterReviewsByViewType({ target }) {
$(target).toggleClass('on');
const onlyPhotos = $(target).hasClass('on');
this.reviewPage.pageNumber = 1;
this.renderReviews('', onlyPhotos);
},
async onClickWriteReview() {
const { data } = await this.fetchProductsProductNoReviewableOptions();
if (data.item[0].nonReviewableProduct === true) {
shopby.alert({ message: '선택하신 상품은 후기를 작성하실 수 없습니다.' });
return;
}
const option = helper.review.generateReviewOptionForUpdatingLayer(
this.reviewableProducts[0],
true,
this.reviewableProducts.length > 1,
true,
);
helper.review.openReviewLayer.apply(this, [option]);
},
async onClickSortReviews(event) {
const $target = $(event.target).find('option:selected');
const orderBy = $target.data('order-by');
const orderDirection = $target.data('order-direction');
const selectedOrder = { orderBy, orderDirection };
this.reviewPage.pageNumber = 1;
await this.renderReviews(null, null, selectedOrder);
const sortBtnArr = this.$review.find('#sortReviews').children();
for (let i = 0; i < sortBtnArr.length; i = i + 1) {
if (sortBtnArr[i].dataset.orderBy === orderBy && sortBtnArr[i].dataset.orderDirection === orderDirection) {
sortBtnArr[i].selected = true;
break;
}
}
},
onClickReviewList(event) {
event.stopPropagation();
event.preventDefault();
const $target = $(event.target);
const actionType = $target.data('action-type') || $target.parents().data('action-type');
const reviewNo = $target.closest('li').data('review-no');
const givenAccumulation = $target.closest('li').data('given-accumulation');
switch (actionType) {
case 'viewMoreReviews':
$target.closest('li').toggleClass('on').find($target).toggleClass('on');
$target.toggleClass('on');
break;
case 'modifyReview':
this.modifyReview(reviewNo);
break;
case 'deleteReview':
this.deleteReview(reviewNo, givenAccumulation);
break;
case 'reviewRecommend':
this.recommendReview(reviewNo, event, $target);
break;
case 'reviewComment':
this.showComment(reviewNo, $target);
break;
case 'moreComments':
this.showMoreComments(reviewNo, $target);
break;
default:
break;
}
},
async viewAttahments({ target }) {
const result = await this.fetchReviews();
const { items } = result.data;
const attachImages = items
.find(({ reviewNo }) => reviewNo === $(target).closest('li').data('review-no'))
.fileUrls.map(fileUrl => ({
imageUrl: fileUrl,
originName: '',
}));
shopby.popup('slide-images', {
title: '첨부파일',
imageObjectList: attachImages,
clickedImageIndex: $(target.closest('.uploadFile-item')).data('index'),
});
},
onClickReviewThumbs({ target }) {
const $target = $(target);
const reviewNo = $target.closest('button').data('review-no');
const actionType = $target.data('action-type') || $target.parents().data('action-type');
switch (actionType) {
case 'photoReviews':
$target.closest('.reviews_list_con').toggleClass('on').find('.txt_more').toggle();
break;
case 'photoReviewDetail':
this.openPhotoReviewDetailPopup(reviewNo);
break;
case 'morePhotoReviews':
this.openPhotoReviewsPopup(reviewNo);
break;
default:
break;
}
},
},
resetCommentsPageInfo(reviewNo) {
this.reviewCommentsInfo[reviewNo].page = 1;
this.reviewCommentsInfo[reviewNo].contents = [];
},
removeSecondFromYdmt(ydmt) {
const SECONDS_LENGTH = 3;
return ydmt.substring(0, ydmt.length - SECONDS_LENGTH);
},
modifyReview(selectedReviewNo) {
const review = this.reviews.find(({ reviewNo }) => reviewNo === selectedReviewNo);
const option = helper.review.generateReviewOptionForUpdatingLayer(review, true);
helper.review.openReviewLayer.apply(this, [option]);
},
deleteReview(reviewNo, givenAccumulation) {
const deleteReview = async ({ state }) => {
if (state !== 'ok') return;
const pathVariable = {
productNo,
reviewNo,
};
await shopby.api.display.deleteProductsProductNoProductReviewsReviewNo({ pathVariable });
shopby.alert('삭제되었습니다.', () => this.renderReviews());
};
if (givenAccumulation === 'Y') {
shopby.confirm(
{ message: '삭제 시 재작성이 불가하며, 지급된 적립금이 차감됩니다. 삭제하시겠습니까?' },
deleteReview,
);
} else {
shopby.confirm({ message: '삭제 시 재작성이 불가합니다. 상품후기를 삭제하시겠습니까?' }, deleteReview);
}
},
toggleElement($target) {
$target.toggleClass('on');
},
isCheckedElement($target) {
return $target.hasClass('on');
},
recommendReview(reviewNo, event, $target) {
if (!shopby.logined()) {
event.preventDefault();
shopby.alert({ message: '로그인 후 이용할 수 있습니다.' });
return;
}
const $targetParents = $target.closest('#reviewRecommendParents');
this.toggleElement($targetParents);
const isRecommend = this.isCheckedElement($targetParents);
$targetParents.closest('label').find('input').attr('checked', isRecommend);
this.changeReviewRecommendStatus(reviewNo, isRecommend, $targetParents);
},
async showComment(reviewNo, $target) {
this.toggleElement($target);
const $targetReview = $target.closest('li');
const checked = this.isCheckedElement($target);
$target.closest('label').find('input').attr('checked', checked);
if (checked) {
this.resetCommentsPageInfo(reviewNo);
const commentData = await this.fetchReviewComments(reviewNo);
this.renderComment(commentData, $targetReview);
} else {
$targetReview.find('#reviewComments').hide();
this.$review.find('#moreComments').remove();
}
},
async showMoreComments(reviewNo, $target) {
const $targetReview = $target.closest('li');
this.reviewCommentsInfo[reviewNo].page += 1;
const commentData = await this.fetchReviewComments(reviewNo);
this.renderComment(commentData, $targetReview);
},
openPhotoReviewDetailPopup(reviewNo) {
const popupCallback = () => {
const onlyPhotos = this.$review.find('#filterReviewsByViewType').hasClass('on');
this.renderReviews('', onlyPhotos);
};
this.photoReviewParam.queryString.pageSize = 9999;
shopby.popup(
'photo-review-detail',
{
productNo,
reviewNo,
hasCloseBtn: true,
type: 'photoReviews',
parameter: this.photoReviewParam,
productReviewConfig: { name: '상세 후기' },
onlyDisplayedReviews: true,
totalPage: this.photoReviews.totalCount,
},
popupCallback.bind(this),
);
},
async openPhotoReviewsPopup() {
shopby.popup('photo-reviews', { productNo });
},
getRequestForReview(hasAttachmentFile = false, selectedOrder) {
const orderBy = (selectedOrder && selectedOrder.orderBy) || 'BEST_REVIEW';
const orderDirection = (selectedOrder && selectedOrder.orderDirection) || 'DESC';
return {
pathVariable: {
productNo,
},
queryString: {
productNo,
pageNumber: (this.reviewPage && this.reviewPage.pageNumber) || 1,
pageSize: (this.reviewPage && this.reviewPage.pageSize) || 5,
hasTotalCount: true,
'order.by': orderBy,
'order.direction': orderDirection,
hasAttachmentFile: hasAttachmentFile ? hasAttachmentFile : '',
},
};
},
},
inquiry: {
$inquiry: $container.find('#inquiry'),
inquiryPage: null,
initiate(data) {
this.inquiryPage = new shopby.readMore(this.appendInquiries.bind(this), '#btnInquiriesMore', 5);
this.render(data);
this.bindEvents();
},
fetchInquires() {
return shopby.api.display.getProductsProductNoInquires(this.requestForInquiry);
},
render(data) {
this.inquiryPage.render(data.totalCount);
$(this.$inquiry.find('#inquiryTit')).text(data.config.name);
this.$inquiry.data('guestPostingUsed', data.config.guestPostingUsed);
this.renderInquiries(data);
},
async renderInquiries(inquiries) {
if (!inquiries) {
const { data } = await this.fetchInquires();
inquiries = {
totalCount: data.totalCount,
items: data.items,
};
}
this.renderTabCount(inquiries);
this.inquiryPage.render(inquiries.totalCount);
this.$inquiry.find('.inquiryTotalCount').text(inquiries.totalCount);
this.$inquiry.find('#inquiries').render(inquiries);
},
async renderTabCount(inquiries) {
const { data: reviews } = await shopby.product.view.review.fetchReviews();
shopby.product.view.renderTabInfo(reviews, inquiries);
},
bindEvents() {
this.$inquiry.on('click', '#inquiryContainer', this.events.onClickInquiryTable.bind(this));
},
events: {
onClickInquiryTable(event) {
event.preventDefault();
const $target = $(event.target);
const actionType = $target.data('action-type') || $target.parents().data('action-type');
if (!actionType) return;
const inquiryNo = $target.closest('li').data('inquiry-no');
switch (actionType) {
case 'viewMore':
this.viewMore($target);
break;
case 'writeInquiry':
this.writeInquiry();
break;
case 'modifyInquiry':
this.modifyInquiry($target);
break;
case 'deleteInquiry':
this.deleteInquiry(inquiryNo);
break;
default:
break;
}
},
},
viewMore($target) {
const { secreted, myInquiry } = $target.closest('li').data();
if (secreted && !myInquiry) {
shopby.alert('비밀글 조회 권한이 없습니다');
return;
}
$target.closest('li').toggleClass('on').find($target).toggleClass('on');
$target.closest('li').find('.answer').toggle();
},
afterUpdatingInquiry({ state }) {
state === 'ok' && this.renderInquiries();
},
writeInquiry() {
const guestPostingUsed = this.$inquiry.data('guestPostingUsed');
if (!guestPostingUsed && !shopby.logined()) {
util.confirmGoToLogin();
} else {
const option = helper.inquiry.generateInquiryOptionForUpdatingLayer();
shopby.popup('product-inquiry', option, this.afterUpdatingInquiry.bind(this));
}
},
modifyInquiry($target) {
const openModifyingLayer = ({ state }) => {
const data = $target.closest('li').data();
const option = helper.inquiry.generateInquiryOptionForUpdatingLayer(data);
if (state !== 'ok') return;
shopby.popup('product-inquiry', option, this.afterUpdatingInquiry.bind(this));
};
shopby.confirm({ message: '해당 상품문의를 수정하시겠습니까?' }, openModifyingLayer);
},
deleteInquiry(inquiryNo) {
const deleteInquiry = async ({ state }) => {
if (state !== 'ok') return;
await shopby.api.display.deleteProductsInquiresInquiryNo({ pathVariable: { inquiryNo } });
this.renderInquiries();
};
shopby.confirm({ message: '해당 상품문의를 삭제하시겠습니까?' }, deleteInquiry);
},
async appendInquiries() {
const result = await this.fetchInquires();
const { items, totalCount } = result.data;
if (totalCount === 0) return;
this.inquiryPage.render(totalCount);
const compiled = Handlebars.compile($('#inquiriesTemplate').html());
const appendHtml = $(compiled({ items })).find('li');
$('#inquiries ul').append(appendHtml);
},
get requestForInquiry() {
const START_YMD = '1000-01-01';
const END_YMD = '9999-12-30';
return {
pathVariable: {
productNo,
},
queryString: {
productNo,
pageNumber: (this.inquiryPage && this.inquiryPage.pageNumber) || 1,
pageSize: (this.inquiryPage && this.inquiryPage.pageSize) || 5,
hasTotalCount: true,
isMyInquiries: false,
endYmd: END_YMD,
startYmd: START_YMD,
},
};
},
},
};
const generateData = {
summary(product, options, coupons) {
const { status, price, baseInfo, deliveryFee, reservationData, limitations } = product;
const { contentsIfPausing, additionDiscountAmt, immediateDiscountAmt, salePrice: originPrice } = price;
const totalDiscount = additionDiscountAmt + immediateDiscountAmt;
const salePrice = originPrice - totalDiscount;
const shouldShowContentsIfPausing = status.saleStatusType === 'STOP' && contentsIfPausing !== '';
const { getCouponRateAndPrice, getDeliveryLabels, checkUnablePurchasing } = helper.product;
const { couponRate, priceWithCoupon } = getCouponRateAndPrice(price);
const deliveryLabels = getDeliveryLabels.apply(helper.product, [product.deliveryFee]);
const isUnablePurchasing = checkUnablePurchasing(product, options.flatOptions);
const { naverPayHandling } = product.limitations;
return {
...price,
...baseInfo,
...deliveryFee,
options,
reservationData,
shouldShowContentsIfPausing,
totalDiscount,
couponRate,
priceWithCoupon,
originPrice,
salePrice,
deliveryFeeLabel: (deliveryLabels && deliveryLabels.deliveryFeeLabel) || '',
deliveryLabel: (deliveryLabels && deliveryLabels.deliveryLabel) || '',
conditionLabel: (deliveryLabels && deliveryLabels.conditionLabel) || '',
isUnablePurchasing,
liked: product.liked,
naverPayHandling,
hasCoupon: coupons.length > 0,
limitations,
useLimitations:
limitations.minBuyCnt > 0 ||
limitations.maxBuyTimeCnt > 0 ||
limitations.maxBuyPersonCnt > 0 ||
limitations.maxBuyPeriodCnt > 0,
useLimitationsComma:
limitations.minBuyCnt > 0 &&
(limitations.maxBuyTimeCnt > 0 || limitations.maxBuyPersonCnt > 0 || limitations.maxBuyPeriodCnt > 0),
};
},
detail(product, images, option) {
const { baseInfo } = product;
const dutyInfo = JSON.parse(baseInfo.dutyInfo);
const { showsDetailInfo, showsDutyInfo, optionImages, mapDutyInfoContents } = helper.product;
return {
...baseInfo,
dutyInfoContents: mapDutyInfoContents(dutyInfo.contents),
showsDetailInfo: showsDetailInfo(baseInfo),
showsDutyInfo: showsDutyInfo(dutyInfo),
optionImages: optionImages(images, option.flatOptions),
};
},
basic(product) {
const { baseInfo, deliveryGuide, exchangeGuide, afterServiceGuide, refundGuide, deliveryFee } = product;
const { getDeliveryLabels } = helper.product;
const deliveryLabels = getDeliveryLabels.apply(helper.product, [product.deliveryFee]);
return {
...baseInfo,
deliveryGuide,
exchangeGuide,
afterServiceGuide,
refundGuide,
remoteDeliveryAreaFees: deliveryFee && deliveryFee.remoteDeliveryAreaFees,
deliveryFeeLabel: deliveryLabels && deliveryLabels.deliveryFeeLabel,
deliveryLabel: deliveryLabels && deliveryLabels.deliveryLabel,
conditionLabel: deliveryLabels && deliveryLabels.conditionLabel,
};
},
review(
reviewableInfo,
reviewableOptions,
reviews,
reviewsConfigurations,
photoReviews,
config,
onlyPhotos = false,
) {
photoReviews.contents = photoReviews.contents.map(contents => ({ ...contents, url: contents.urls[0] }));
return {
...reviews,
photoReviews: { ...reviewsConfigurations, ...photoReviews },
reviewable: reviewableOptions.reviewable,
item: reviewableInfo.items,
onlyPhotos,
config,
isLogin: shopby.logined(),
};
},
inquiry({ items, totalCount }, config) {
return {
items,
totalCount,
config,
};
},
};
const helper = {
product: {
getCouponRateAndPrice(priceInfo) {
const { salePrice, couponDiscountAmt, immediateDiscountAmt } = priceInfo;
const coupon = couponDiscountAmt || 0;
const price = salePrice || 0;
const immediateDiscount = immediateDiscountAmt || 0;
const couponRate = !couponDiscountAmt ? 0 : ((coupon / (price - immediateDiscount)) * 100).toFixed(0);
return {
priceWithCoupon: salePrice - (couponDiscountAmt + immediateDiscountAmt),
couponRate,
};
},
getDeliveryLabels(deliveryFee) {
if (!deliveryFee) return null;
return {
deliveryFeeLabel: this.getDeliveryFeeLabel(deliveryFee),
deliveryLabel: this.getDeliveryLabel(deliveryFee),
conditionLabel: this.getDeliveryConditionLabel(deliveryFee),
};
},
getDeliveryFeeLabel({ deliveryConditionType, deliveryAmt }) {
const isFree = deliveryConditionType === 'FREE';
return isFree ? '무료' : `${toCurrencyString(deliveryAmt)}원`;
},
getDeliveryLabel({ deliveryType, deliveryPrePayment }) {
const DELIVERY_TYPE_LABEL = deliveryType === 'PARCEL_DELIVERY' ? '택배/등기/소포' : '직접배송';
const PAYMENT_TYPE_LABEL = deliveryPrePayment ? '선결제' : '착불';
return `${DELIVERY_TYPE_LABEL} / ${PAYMENT_TYPE_LABEL}`;
},
getDeliveryConditionLabel({ perOrderCnt, aboveDeliveryAmt, deliveryConditionType }) {
switch (deliveryConditionType) {
case 'QUANTITY_PROPOSITIONAL_FEE':
return `(${perOrderCnt}개마다 부과)`;
case 'CONDITIONAL':
return `(${toCurrencyString(aboveDeliveryAmt)}원 이상 구매 시 무료)`;
default:
return '';
}
},
checkUnablePurchasing({ status, limitations }, options) {
const { saleStatusType, soldout } = status;
// 판매종료, 판매정지, 판매금지, 판매대기
if (['FINISHED', 'STOP', 'PROHIBITION', 'READY'].includes(saleStatusType)) {
return true;
}
// 품절 (예약판매 재고, 일반 판매재고)
if (soldout) {
return true;
}
if (limitations.memberOnly && !shopby.logined()) {
return true;
}
return !(options && options.length > 0);
},
showsDetailInfo({ optionImageViewable, content, contentHeader, contentFooter }) {
return (
optionImageViewable || [content, contentHeader, contentFooter].some(content => content && content.length > 0)
);
},
showsDutyInfo({ contents }) {
return contents.length > 0;
},
optionImages(images, flatOptions) {
return images.map(image => {
const { value } = flatOptions.find(({ optionNo }) => optionNo === image.optionNo);
return {
...image,
value,
};
});
},
mapDutyInfoContents(contents) {
let isKcCertifications = false;
const mappingDutyInfo = contents.map(content => {
const [key, value] = Object.entries(content).flat();
if (key === 'KC 인증정보') {
isKcCertifications = true;
}
return { key, value };
});
return { mappingDutyInfo, isKcCertifications };
},
},
review: {
forRegistrationMode(productOption) {
const option = shopby.utils.pickObjectByKeys(productOption, [
'orderedOption',
'optionNo',
'orderOptionNo',
'orderStatusType',
'optionUsed',
'optionName',
'optionValue',
]);
return {
options: [{ ...option }],
};
},
forModificationMode(productOption) {
const { orderedOption, ...rest } = productOption;
return {
myProductReview: {
...rest,
},
options: [{ ...orderedOption }],
};
},
generateReviewOptionForUpdatingLayer(
productOption,
notSelectProduct = false,
needsSelectProductBtn = false,
registration = false,
) {
const { imageUrl: imageUrls, productName, productNo } = productOption;
const extraOption = registration
? this.forRegistrationMode(productOption)
: this.forModificationMode(productOption);
return {
productReviewConfig: shopby.cache.getBoardsConfig().productReviewConfig,
product: {
imageUrls,
productName,
productNo,
},
notSelectProduct,
needsSelectProductBtn,
...extraOption,
isProductViewPage: true,
};
},
openReviewLayer(option) {
const callback = ({ state }) => state === 'ok' && this.renderReviews();
shopby.popup('product-review', option, callback.bind(this));
},
},
inquiry: {
generateInquiryOptionForUpdatingLayer(additionalData) {
const mode = additionalData ? 'modification' : 'registration';
return {
mode,
...$container.data('productInfo'),
...additionalData,
};
},
},
};
const util = {
actionType(event) {
const target = event.target || event;
return $(target).data('action-type') || $(target).parents().data('action-type');
},
alertFailMsg(error) {
const message = error.message || error.result.message || error;
shopby.alert({ message, iconType: 'fail' });
},
confirmGoToLogin() {
const confirmOption = { message: '로그인 후 이용할 수 있습니다.' };
const callback = ({ state }) => state === 'ok' && shopby.goLogin(location.href);
shopby.confirm(confirmOption, callback);
},
};
const displayHelper = {
slickMainImages(imageUrls) {
const $main = $container.find('.item_photo_big');
$main.slick({
slidesToShow: 1,
slidesToScroll: 1,
arrows: false,
fade: true,
infinite: true,
dots: true,
});
imageUrls.forEach(url => {
$main.slick('slickAdd', ``);
});
//todo : 이미지 영역 사이즈 리사이즈
},
slickRelatedProducts() {
$container
.find('.item_swipe_type ul.slider-wrap')
.on('init', function () {})
.slick({
arrows: false,
draggable: true,
autoplay: true,
infinite: true,
slidesToShow: 2,
slidesToScroll: 1,
})
.on('beforeChange', function () {});
},
fadeOutLikeMessage(liked, init) {
if (init) return;
const text = liked ? '찜 리스트에 추가' : '찜 리스트에서 제거';
const messageHtml = `
상품이 ${text}되었습니다.
`; const $wrapper = $($container.find('.like-message-wrapper')); $wrapper.fadeIn(); $container.find('.add_wish_box').html(messageHtml).removeClass('hidden'); $wrapper.fadeOut(); }, optionSelector(selectedDepthIndex, selectedOptionIndex) { $container.find('.chosen-select').each(function (selectIdx) { if (selectIdx === selectedDepthIndex) { $('option', this).each(function (optionIdx) { $(this).prop('selected', optionIdx === selectedOptionIndex + 1); }); } }); }, onClickAdditionalDeliveryFee(remoteDeliveryAreaFees) { shopby.popup('additional-delivery-fee', { remoteDeliveryAreaFees }); }, toggleRelatedProducts(viewable, data) { if (viewable) { $container.find('#relatedProducts').render({ relatedProducts: data }); this.slickRelatedProducts(); } else { $container.find('#relatedProductsTit').hide().siblings().hide(); } }, isUnablePurchasing(viewable) { if (viewable) return; $container.find('.item_choice_list, .item_add_option_box').hide(); }, showMoreDetailBtn() { const $detailArea = $('.detail_explain_box'); if ($detailArea.height() > 500) { $detailArea.css('height', '500px'); $('.btn_more_detail').show(); } }, }; //todo 동작하지 않음.. const errorHandler = { cartAndOrder(error) { const ERROR_CODE = { NO_EXHIBITION: 'PPVE0019', NO_MINOR_1: 'PPVE0003', NO_MINOR_2: 'ODSH0002', SOLDOUT_OPTION: 'PPVE0011', }; if (error && error.code === ERROR_CODE.NO_EXHIBITION) { shopby.goHome(); return; } if ((error && error.code === ERROR_CODE.NO_MINOR_1) || (error && error.code === ERROR_CODE.NO_MINOR_2)) { const message = '해당 상품은 성인전용 상품입니다. 성인인증 후 상품을 구매하시겠습니까?'; const callback = ({ state }) => (state === 'ok' ? shopby.goAdultCertification() : shopby.goHome()); shopby.confirm({ message }, callback); return; } if (error.code === ERROR_CODE.SOLDOUT_OPTION) { shopby.alert('재고가 부족합니다. 수량을 조정해주세요.'); throw error; } if (error && error.code) { const message = error.message || error.result.message; shopby.alert({ message }, () => history.go(0)); } else { util.alertFailMsg(error); } }, }; shopby.start.initiate(shopby.product.view.initiate.bind(shopby.product.view)); });