저장소가 총 건수를 상한까지만 세게 되면서 마지막 페이지 번호가 없는 목록이 생겼는데, 화면은 그 값을 1 로 접거나 페이저 노출 조건으로만 쓰고 있었다. 앞은 "1페이지뿐" 으로 잘못 말하고 뒤는 페이저를 통째로 지워 뒤쪽 페이지로 갈 방법 자체를 없앤다. 둘 다 예외도 404 도 내지 않아 화면은 정상으로 보이고 기능만 없다. Pagination 컴포넌트가 세 형태를 모두 이해하도록 넓혔다. 마지막 페이지를 알면 페이지 산술이 우선이고, 모를 때만 다음 페이지 존재 여부로 판정한다. 처음·마지막 버튼을 따로 켜고 끌 수 있게 나눠, 마지막 페이지를 모르는 목록에서는 그 버튼만 감춰지고 나머지 이동은 그대로 열려 있다. DataGrid 는 페이저를 그리는 두 경로 모두에 그 값을 전달한다 — 한쪽만 전달하면 그 화면만 첫 페이지에 갇힌다. 마이페이지 주문·마일리지·위시리스트·알림함, 게시판 목록, 상품 리뷰·문의, 내가 쓴 글, 관리자 설정 탭과 스케줄 이력의 소비 지점을 전건 옮겼다. 검색 화면은 총 건수를 정확히 세지 못한 경우 "N건 이상" 으로 표기하고 검색어를 좁히면 정확한 건수를 볼 수 있다고 안내한다. 탭 배지도 세지 못한 값을 정확한 숫자처럼 보여주지 않는다. 최신순은 첫 페이지부터 커서로 이동하며, 주소로 특정 페이지를 지목한 딥링크는 종전 동작을 유지한다. 쇼핑 첫 화면과 분류 화면을 단일 요청으로 재배선하고, 섹션 partial 이 부모가 선언한 이름을 읽도록 통일했다. 데이터소스 이름에 직결돼 있으면 다른 화면이 재사용할 때 폴백에 걸려 예외도 로그도 없이 섹션이 사라진다. 상품 상세의 "최근 본 상품" 이 계속 비어 있던 것은 존재하지 않는 라우트 파라미터를 읽고 있어서였다. 게시판 목록 세 스킨의 블라인드 아이콘 대비와 이미지 위 공통 색의 다크 모드 표기를 정리했다.
528 lines
26 KiB
JSON
528 lines
26 KiB
JSON
{
|
|
"version": "1.0.0",
|
|
"layout_name": "shop/show",
|
|
"permissions": [],
|
|
"extends": "_user_base",
|
|
"computed": {
|
|
"popularItems": "{{popularProducts?.data ?? []}}",
|
|
"optionChoices": "{{(product.data?.option_groups ?? []).map((g, gi) => (g.values_localized ?? []).map(v => { const combos = (product.data?.options ?? []).filter(o => o.option_values_localized?.[g.name_localized] === v && (product.data?.option_groups ?? []).slice(0, gi).every(ug => !_local.currentSelection?.[ug.name_localized] || o.option_values_localized?.[ug.name_localized] === _local.currentSelection[ug.name_localized])); const soldOut = combos.length > 0 && combos.every(o => o.is_sold_out || (o.stock_quantity ?? 0) <= 0 || !o.is_active); return { value: v, label: soldOut ? v + ' ' + $t('shop.product.sold_out_suffix') : v, disabled: soldOut }; }))}}"
|
|
},
|
|
"meta": {
|
|
"seo": {
|
|
"enabled": true,
|
|
"extensions": [
|
|
{ "type": "module", "id": "sirsoft-ecommerce" }
|
|
],
|
|
"data_sources": ["product", "popularProducts", "reviews", "qna"],
|
|
"page_type": "product",
|
|
"toggle_setting": "$module_settings:sirsoft-ecommerce:seo.seo_product_detail",
|
|
"vars": {
|
|
"product_name": "{{$localized(product.data.meta_title) ?? $localized(product.data.name) ?? ''}}",
|
|
"product_description": "{{$localized(product.data.meta_description) ?? $localized(product.data.short_description) ?? $localized(product.data.description) ?? ''}}",
|
|
"site_name": "$core_settings:general.site_name",
|
|
"shopBase": "$module_settings:sirsoft-ecommerce:basic_info.route_path"
|
|
},
|
|
"priority": 0.8,
|
|
"changefreq": "daily",
|
|
"og": {
|
|
"type": "product",
|
|
"title": "{{_seo.product.title ?? $localized(product.data.meta_title) ?? $localized(product.data.name) ?? ''}}",
|
|
"description": "{{_seo.product.description ?? $localized(product.data.meta_description) ?? $localized(product.data.description) ?? ''}}"
|
|
}
|
|
}
|
|
},
|
|
"init_actions": [
|
|
{
|
|
"handler": "loadFromLocalStorage",
|
|
"params": {
|
|
"key": "recentProductIds",
|
|
"defaultValue": []
|
|
},
|
|
"resultTo": {
|
|
"target": "_global",
|
|
"key": "recentProductIds"
|
|
}
|
|
},
|
|
{
|
|
"comment": "이전 페이지에서 열린 모달 닫기",
|
|
"handler": "closeModal"
|
|
},
|
|
{
|
|
"comment": "장바구니 이동 로딩 상태 초기화",
|
|
"handler": "setState",
|
|
"params": {
|
|
"target": "global",
|
|
"isNavigatingToCart": false
|
|
}
|
|
},
|
|
{
|
|
"comment": "옵션 선택 상태 초기화",
|
|
"handler": "setState",
|
|
"params": {
|
|
"target": "local",
|
|
"selectedOptionItems": [],
|
|
"currentSelection": {},
|
|
"noOptionQuantity": 1,
|
|
"additionalOptions": {},
|
|
"wishlistLoading": false,
|
|
"isOrdering": false,
|
|
"downloadableCoupons": null,
|
|
"downloadableCouponsLoading": false,
|
|
"downloadingMore": false,
|
|
"downloadingCouponId": null,
|
|
"selectedCouponForDownload": null,
|
|
"reviewsPage": 1,
|
|
"reviewSortBy": "created_at_desc",
|
|
"reviewPhotoOnly": false,
|
|
"reviewRating": "",
|
|
"reviewOptionFilters": {},
|
|
"activeTab": "{{query.tab ?? 'info'}}",
|
|
"starNums": [5, 4, 3, 2, 1],
|
|
"starList": [1, 2, 3, 4, 5]
|
|
}
|
|
}
|
|
],
|
|
"errorHandling": {
|
|
"401": {
|
|
"comment": "비로그인 시 current_user 401은 정상 — 무시 (상품 상세는 비로그인 접근 허용)",
|
|
"handler": "setState",
|
|
"params": {
|
|
"target": "local",
|
|
"productAccessDenied": false
|
|
}
|
|
},
|
|
"403": {
|
|
"comment": "인증된 사용자 상품 조회 권한 없음 — 에러 페이지 표시",
|
|
"handler": "showErrorPage",
|
|
"params": {
|
|
"errorCode": 403,
|
|
"target": "content"
|
|
}
|
|
}
|
|
},
|
|
"data_sources": [
|
|
{
|
|
"id": "product",
|
|
"label_key": "$t:editor.data_source.product",
|
|
"type": "api",
|
|
"endpoint": "/api/modules/sirsoft-ecommerce/products/{{route.product_code}}",
|
|
"method": "GET",
|
|
"auto_fetch": true,
|
|
"auth_mode": "optional",
|
|
"loading_strategy": "progressive",
|
|
"fallback": {
|
|
"data": null
|
|
},
|
|
"onSuccess": [
|
|
{
|
|
"comment": "최근 본 상품 기록 — 상품이 실제로 조회된 뒤에 남긴다. 라우트 파라미터는 상품 코드(문자열)라 서버가 요구하는 숫자 ID 가 아니므로, 응답에서 받은 id 를 쓴다",
|
|
"handler": "saveToLocalStorage",
|
|
"params": {
|
|
"key": "recentProductIds",
|
|
"value": "{{(response?.data?.data?.id ?? response?.data?.id) ? [Number(response?.data?.data?.id ?? response?.data?.id), ...(_global.recentProductIds ?? []).filter(id => Number(id) !== Number(response?.data?.data?.id ?? response?.data?.id))].slice(0, 20) : (_global.recentProductIds ?? [])}}"
|
|
}
|
|
},
|
|
{
|
|
"comment": "같은 화면에서 이어지는 참조를 위해 전역 상태도 맞춰 둔다",
|
|
"handler": "setState",
|
|
"params": {
|
|
"target": "global",
|
|
"recentProductIds": "{{(response?.data?.data?.id ?? response?.data?.id) ? [Number(response?.data?.data?.id ?? response?.data?.id), ...(_global.recentProductIds ?? []).filter(id => Number(id) !== Number(response?.data?.data?.id ?? response?.data?.id))].slice(0, 20) : (_global.recentProductIds ?? [])}}"
|
|
}
|
|
}
|
|
],
|
|
"errorHandling": {
|
|
"403": {
|
|
"handler": "showErrorPage",
|
|
"params": { "errorCode": 403, "target": "content" }
|
|
},
|
|
"404": {
|
|
"handler": "showErrorPage",
|
|
"params": { "errorCode": 404, "target": "content" }
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"id": "reviews",
|
|
"label_key": "$t:editor.data_source.reviews",
|
|
"type": "api",
|
|
"endpoint": "/api/modules/sirsoft-ecommerce/products/{{route.product_code}}/reviews",
|
|
"method": "GET",
|
|
"auto_fetch": true,
|
|
"auth_mode": "optional",
|
|
"params": {
|
|
"page": "{{_local.reviewsPage ?? 1}}",
|
|
"sort": "{{_local.reviewSortBy ?? 'created_at_desc'}}",
|
|
"photo_only": "{{_local.reviewPhotoOnly ?? false}}",
|
|
"rating": "{{_local.reviewRating ?? ''}}",
|
|
"option_filters": "{{JSON.stringify(_local.reviewOptionFilters ?? null)}}"
|
|
},
|
|
"loading_strategy": "progressive",
|
|
"fallback": {
|
|
"reviews": {
|
|
"data": [],
|
|
"meta": {
|
|
"current_page": 1,
|
|
"last_page": 1,
|
|
"per_page": 10,
|
|
"total": 0
|
|
}
|
|
},
|
|
"rating_stats": {
|
|
"avg": 0,
|
|
"1": { "count": 0, "percent": 0 },
|
|
"2": { "count": 0, "percent": 0 },
|
|
"3": { "count": 0, "percent": 0 },
|
|
"4": { "count": 0, "percent": 0 },
|
|
"5": { "count": 0, "percent": 0 }
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"id": "qna",
|
|
"label_key": "$t:editor.data_source.qna",
|
|
"type": "api",
|
|
"endpoint": "/api/modules/sirsoft-ecommerce/products/{{route.product_code}}/inquiries",
|
|
"method": "GET",
|
|
"auto_fetch": true,
|
|
"auth_mode": "optional",
|
|
"loading_strategy": "progressive",
|
|
"params": {
|
|
"page": "{{_local.qnaPage ?? 1}}",
|
|
"per_page": 10,
|
|
"exclude_secret": "{{_local.qnaExcludeSecret ?? false}}"
|
|
},
|
|
"fallback": {
|
|
"items": [],
|
|
"meta": {
|
|
"inquiry_available": false,
|
|
"board_settings": {
|
|
"secret_mode": "disabled",
|
|
"categories": [],
|
|
"use_file_upload": false,
|
|
"max_file_count": 5,
|
|
"max_file_size": 10485760,
|
|
"allowed_extensions": [],
|
|
"min_title_length": 2,
|
|
"max_title_length": 200,
|
|
"min_content_length": 10,
|
|
"max_content_length": 10000
|
|
},
|
|
"total": 0,
|
|
"current_page": 1,
|
|
"per_page": 10,
|
|
"last_page": 1
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"id": "productDownloadableCoupons",
|
|
"label_key": "$t:editor.data_source.productDownloadableCoupons",
|
|
"type": "api",
|
|
"endpoint": "/api/modules/sirsoft-ecommerce/products/{{route.product_code}}/downloadable-coupons",
|
|
"method": "GET",
|
|
"auto_fetch": true,
|
|
"auth_mode": "optional",
|
|
"loading_strategy": "progressive",
|
|
"fallback": {
|
|
"data": []
|
|
}
|
|
},
|
|
{
|
|
"id": "popularProducts",
|
|
"label_key": "$t:editor.data_source.popularProducts",
|
|
"type": "api",
|
|
"endpoint": "/api/modules/sirsoft-ecommerce/products/popular?limit=8",
|
|
"method": "GET",
|
|
"auto_fetch": true,
|
|
"auth_mode": "optional",
|
|
"loading_strategy": "progressive",
|
|
"fallback": {
|
|
"data": []
|
|
}
|
|
}
|
|
],
|
|
"modals": [
|
|
{
|
|
"partial": "partials/shop/detail/_modal_cart_added.json"
|
|
},
|
|
{ "partial": "partials/shop/_modal_coupon_download.json" },
|
|
{ "partial": "partials/shop/detail/_modal_login_required.json" },
|
|
{ "partial": "partials/shop/detail/_modal_coupon_download_confirm.json" },
|
|
{ "partial": "partials/shop/detail/_modal_review_image.json" },
|
|
{ "partial": "partials/shop/detail/_modal_qna_write.json" },
|
|
{ "partial": "partials/shop/detail/_modal_qna_reply.json" },
|
|
{ "partial": "partials/shop/detail/_modal_inquiry_delete.json" }
|
|
],
|
|
"slots": {
|
|
"content": [
|
|
{
|
|
"type": "layout",
|
|
"name": "Container",
|
|
"props": {
|
|
"className": "py-4"
|
|
},
|
|
"responsive": {
|
|
"desktop": {
|
|
"props": {
|
|
"className": "py-8"
|
|
}
|
|
}
|
|
},
|
|
"children": [
|
|
{
|
|
"comment": "뒤로가기 버튼",
|
|
"type": "basic",
|
|
"name": "Button",
|
|
"props": {
|
|
"className": "inline-flex items-center gap-2 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white mb-4 cursor-pointer"
|
|
},
|
|
"responsive": {
|
|
"desktop": {
|
|
"props": {
|
|
"className": "inline-flex items-center gap-2 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white mb-6 cursor-pointer"
|
|
}
|
|
}
|
|
},
|
|
"children": [
|
|
{
|
|
"type": "basic",
|
|
"name": "Icon",
|
|
"props": {
|
|
"name": "chevron-left",
|
|
"size": "sm"
|
|
}
|
|
},
|
|
{
|
|
"type": "basic",
|
|
"name": "Span",
|
|
"text": "$t:shop.back"
|
|
}
|
|
],
|
|
"actions": [
|
|
{
|
|
"type": "click",
|
|
"handler": "navigate",
|
|
"params": {
|
|
"path": "{{_global.shopBase}}/products",
|
|
"mergeQuery": true,
|
|
"query": {}
|
|
}
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"comment": "상품 정보 영역 - PC: 2열, 모바일: 1열",
|
|
"type": "basic",
|
|
"name": "Div",
|
|
"blur_until_loaded": {
|
|
"enabled": true,
|
|
"data_sources": "product"
|
|
},
|
|
"props": {
|
|
"className": "flex flex-col mb-8"
|
|
},
|
|
"responsive": {
|
|
"desktop": {
|
|
"props": {
|
|
"className": "flex flex-row gap-8 mb-8"
|
|
}
|
|
}
|
|
},
|
|
"children": [
|
|
{
|
|
"comment": "이미지 갤러리 영역",
|
|
"type": "basic",
|
|
"name": "Div",
|
|
"props": {
|
|
"className": "w-full mb-4 transition-opacity duration-200 {{_local.isOrdering ? 'pointer-events-none opacity-50' : ''}}"
|
|
},
|
|
"responsive": {
|
|
"desktop": {
|
|
"props": {
|
|
"className": "w-1/2 mb-0 transition-opacity duration-200 {{_local.isOrdering ? 'pointer-events-none opacity-50' : ''}}"
|
|
}
|
|
}
|
|
},
|
|
"children": [
|
|
{
|
|
"type": "composite",
|
|
"name": "ProductImageViewer",
|
|
"props": {
|
|
"images": "{{product.data?.images ?? []}}"
|
|
}
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"comment": "상품 정보 + 구매 카드 영역 - responsive로 mobile/desktop 분기",
|
|
"type": "basic",
|
|
"name": "Div",
|
|
"props": {
|
|
"className": "w-full space-y-4"
|
|
},
|
|
"responsive": {
|
|
"desktop": {
|
|
"props": {
|
|
"className": "w-1/2 space-y-6"
|
|
}
|
|
}
|
|
},
|
|
"children": [
|
|
{
|
|
"comment": "상품 정보 영역 (주문 중 블러 처리)",
|
|
"type": "basic",
|
|
"name": "Div",
|
|
"props": {
|
|
"className": "space-y-4 transition-opacity duration-200 {{_local.isOrdering ? 'pointer-events-none opacity-50' : ''}}"
|
|
},
|
|
"responsive": {
|
|
"desktop": {
|
|
"props": {
|
|
"className": "space-y-6 transition-opacity duration-200 {{_local.isOrdering ? 'pointer-events-none opacity-50' : ''}}"
|
|
}
|
|
}
|
|
},
|
|
"children": [
|
|
{
|
|
"comment": "상품 헤더 (카테고리, 상품명, 찜)",
|
|
"partial": "partials/shop/detail/_header.json"
|
|
},
|
|
{
|
|
"comment": "모바일 전용: 가격 표시",
|
|
"type": "basic",
|
|
"name": "Div",
|
|
"if": "{{true}}",
|
|
"responsive": {
|
|
"desktop": {
|
|
"if": "{{false}}"
|
|
}
|
|
},
|
|
"children": [
|
|
{
|
|
"partial": "partials/shop/detail/_price_mobile.json"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"comment": "상품 정보 요약",
|
|
"partial": "partials/shop/detail/_info_summary.json"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"comment": "구매 카드",
|
|
"partial": "partials/shop/detail/_purchase_card.json"
|
|
},
|
|
{
|
|
"comment": "판매상태 안내 (품절/판매중지/출시예정)",
|
|
"type": "basic",
|
|
"name": "Div",
|
|
"if": "{{product.data?.sales_status !== 'on_sale'}}",
|
|
"props": {
|
|
"className": "flex items-center gap-3 px-4 py-3 rounded-lg border {{product.data?.sales_status === 'sold_out' ? 'bg-red-50 border-red-200 dark:bg-red-900/20 dark:border-red-800' : product.data?.sales_status === 'suspended' ? 'bg-gray-100 border-gray-300 dark:bg-gray-700/50 dark:border-gray-600' : 'bg-blue-50 border-blue-200 dark:bg-blue-900/20 dark:border-blue-800'}}"
|
|
},
|
|
"children": [
|
|
{
|
|
"type": "basic",
|
|
"name": "Icon",
|
|
"props": {
|
|
"name": "{{product.data?.sales_status === 'sold_out' ? 'box-open' : product.data?.sales_status === 'suspended' ? 'ban' : 'clock'}}",
|
|
"size": "lg",
|
|
"className": "{{product.data?.sales_status === 'sold_out' ? 'text-red-500 dark:text-red-400' : product.data?.sales_status === 'suspended' ? 'text-gray-500 dark:text-gray-400' : 'text-blue-500 dark:text-blue-400'}} shrink-0"
|
|
}
|
|
},
|
|
{
|
|
"type": "basic",
|
|
"name": "Span",
|
|
"props": { "className": "text-sm font-medium {{product.data?.sales_status === 'sold_out' ? 'text-red-700 dark:text-red-300' : product.data?.sales_status === 'suspended' ? 'text-gray-700 dark:text-gray-300' : 'text-blue-700 dark:text-blue-300'}}" },
|
|
"text": "{{product.data?.sales_status === 'sold_out' ? $t('shop.product.status_message_sold_out') : product.data?.sales_status === 'suspended' ? $t('shop.product.status_message_suspended') : $t('shop.product.status_message_coming_soon')}}"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"comment": "탭 네비게이션",
|
|
"type": "composite",
|
|
"name": "TabNavigation",
|
|
"props": {
|
|
"tabs": [
|
|
{
|
|
"id": "info",
|
|
"label": "$t:shop.tabs.detail_info",
|
|
"iconName": "file-text"
|
|
},
|
|
{
|
|
"id": "reviews",
|
|
"label": "$t:shop.tabs.reviews",
|
|
"badge": "{{reviews.data?.total_count ?? 0}}",
|
|
"iconName": "star"
|
|
},
|
|
{
|
|
"id": "qna",
|
|
"label": "$t:shop.tabs.qna",
|
|
"badge": "{{qna.data.meta.total ?? 0}}",
|
|
"iconName": "comment"
|
|
}
|
|
],
|
|
"hiddenTabIds": "{{_global.modules?.['sirsoft-ecommerce']?.inquiry?.board_slug ? [] : ['qna']}}",
|
|
"activeTabId": "{{_local.activeTab ?? 'info'}}",
|
|
"variant": "underline"
|
|
},
|
|
"actions": [
|
|
{
|
|
"event": "onTabChange",
|
|
"handler": "sequence",
|
|
"actions": [
|
|
{
|
|
"handler": "setState",
|
|
"params": {
|
|
"target": "local",
|
|
"activeTab": "{{$args[0]}}"
|
|
}
|
|
},
|
|
{
|
|
"comment": "탭은 이 화면 안에서만 바뀌는 값이다 — 목록에서 실어 온 page·정렬·검색어·분류는 그대로 두고 tab 만 얹는다. 병합하지 않으면 탭을 한 번 누르는 순간 목록 상태가 사라져 '상품 목록' 버튼이 1페이지로 되돌아간다 (#75).",
|
|
"handler": "replaceUrl",
|
|
"params": {
|
|
"mergeQuery": true,
|
|
"query": {
|
|
"tab": "{{$args[0]}}"
|
|
}
|
|
}
|
|
}
|
|
]
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"partial": "partials/shop/detail/_tab_detail.json"
|
|
},
|
|
{
|
|
"partial": "partials/shop/detail/_tab_reviews.json"
|
|
},
|
|
{
|
|
"partial": "partials/shop/detail/_tab_qna.json"
|
|
},
|
|
{
|
|
"comment": "인기 상품 섹션",
|
|
"type": "basic",
|
|
"name": "Div",
|
|
"props": {
|
|
"className": "mt-12"
|
|
},
|
|
"children": [
|
|
{
|
|
"partial": "partials/shop/list/_popular_products.json"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|