[251122] fix: DetailPaneel->ProductAllSection Focus-7
🕐 커밋 시간: 2025. 11. 22. 17:57:58 📊 변경 통계: • 총 파일: 2개 • 추가: +97줄 • 삭제: -324줄 📝 수정된 파일: ~ com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx ~ com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.module.less 🔧 함수 변경 내용: 📄 com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx (javascript): ✅ Added: SpotlightContainerDecorator() 🔄 Modified: extractProductMeta() Performance: 코드 최적화로 성능 개선 기대
This commit is contained in:
@@ -34,7 +34,6 @@ import TPopUp from '../../../components/TPopUp/TPopUp.jsx';
|
||||
import TVirtualGridList from '../../../components/TVirtualGridList/TVirtualGridList.jsx';
|
||||
import useReviews from '../../../hooks/useReviews/useReviews';
|
||||
import useScrollTo from '../../../hooks/useScrollTo';
|
||||
import useDetailFocus from '../../../hooks/useDetailFocus';
|
||||
import { BUYNOW_CONFIG } from '../../../utils/BuyNowConfig';
|
||||
import { panel_names } from '../../../utils/Config';
|
||||
import * as Config from '../../../utils/Config.js';
|
||||
@@ -134,6 +133,16 @@ const BuyNowContainer = SpotlightContainerDecorator(
|
||||
'div'
|
||||
);
|
||||
|
||||
const CouponContainer = SpotlightContainerDecorator(
|
||||
{
|
||||
spotlightDirection: 'vertical',
|
||||
enterTo: 'last-focused',
|
||||
restrict: 'self-only',
|
||||
defaultElement: 'detail-coupon-button-0',
|
||||
},
|
||||
'div'
|
||||
);
|
||||
|
||||
const ButtonStackContainer = SpotlightContainerDecorator(
|
||||
{
|
||||
spotlightDirection: 'vertical',
|
||||
@@ -189,9 +198,6 @@ export default function ProductAllSection({
|
||||
}) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
// 포커스 이동 보정 Hook (0.25초 타이머)
|
||||
const { enqueueFocus } = useDetailFocus(500);
|
||||
|
||||
// Redux 상태
|
||||
const webOSVersion = useSelector((state) => state.common.appStatus.webOSVersion);
|
||||
const groupInfos = useSelector((state) => state.product.groupInfo);
|
||||
@@ -869,279 +875,35 @@ export default function ProductAllSection({
|
||||
[]
|
||||
);
|
||||
|
||||
// SHOP BY MOBILE 버튼에서 arrow up 시
|
||||
// focusUpMap을 사용해서 위쪽 버튼으로 이동
|
||||
// focusUpMap이 없으면 (첫 행) BackBtn으로 이동
|
||||
const handleSpotlightUpToBackButton = useCallback(
|
||||
(e) => {
|
||||
e.stopPropagation();
|
||||
console.log(`🛵 [FocusDetail] ShopByMobile - Arrow UP 감지`);
|
||||
|
||||
// focusUpMap에서 ShopByMobile의 위쪽 버튼을 찾음
|
||||
const targetId = focusUpMap[shopByMobileId];
|
||||
|
||||
if (targetId) {
|
||||
console.log(`🛵 [FocusDetail] ShopByMobile - Arrow UP 타깃: ${targetId}`);
|
||||
if (targetId === 'detail-buy-now-button') {
|
||||
console.log(`🛵 [FocusDetail] ShopByMobile - Arrow UP: BuyNow 버튼 있음`);
|
||||
} else if (targetId === 'detail-add-to-cart-button') {
|
||||
console.log(`🛵 [FocusDetail] ShopByMobile - Arrow UP: AddToCart 버튼 있음`);
|
||||
} else if (targetId === 'detail-coupon-button') {
|
||||
console.log(`🛵 [FocusDetail] ShopByMobile - Arrow UP: Coupon 버튼 있음`);
|
||||
}
|
||||
// 즉시 포커스 시도
|
||||
if (!Spotlight.focus(targetId)) {
|
||||
enqueueFocus(targetId);
|
||||
}
|
||||
} else {
|
||||
// focusUpMap에 없으면 (첫 행이거나 위쪽이 없음) BackBtn으로
|
||||
console.log(`🛵 [FocusDetail] ShopByMobile - Arrow UP: 위쪽 버튼 없음, BackBtn으로 이동`);
|
||||
if (!Spotlight.focus('spotlightId_backBtn')) {
|
||||
enqueueFocus('spotlightId_backBtn');
|
||||
}
|
||||
}
|
||||
},
|
||||
[focusUpMap, shopByMobileId, enqueueFocus]
|
||||
);
|
||||
|
||||
// BUY NOW, ADD TO CART 버튼에서 arrow up 시
|
||||
// Coupon 버튼이 있으면 Coupon으로, 없으면 Back으로 이동
|
||||
// 항상 Hook을 통해 포커스를 이동하도록 변경
|
||||
const handleSpotlightUpFromBuyButtons = useCallback(
|
||||
(e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
let targetId;
|
||||
if (promotions && promotions.length > 0) {
|
||||
// 쿠폰이 여러 개면 첫 번째 쿠폰 버튼으로 포커스
|
||||
targetId = `detail-coupon-button-0`;
|
||||
console.log(`💳 [FocusDetail] BuyNow/AddToCart - Arrow UP 감지 (Coupon 있음)`);
|
||||
console.log(`💳 [FocusDetail] BuyNow/AddToCart - Arrow UP 타깃 결정: ${targetId}`);
|
||||
} else {
|
||||
targetId = 'spotlightId_backBtn';
|
||||
console.log(`💳 [FocusDetail] BuyNow/AddToCart - Arrow UP 감지 (Coupon 없음)`);
|
||||
console.log(`💳 [FocusDetail] BuyNow/AddToCart - Arrow UP 타깃 결정: Back Btn`);
|
||||
}
|
||||
|
||||
// 즉시 포커스 시도, 실패 시 보정 큐
|
||||
if (!Spotlight.focus(targetId)) {
|
||||
enqueueFocus(targetId);
|
||||
}
|
||||
},
|
||||
[promotions, enqueueFocus]
|
||||
);
|
||||
|
||||
// Coupon 버튼에서 arrow up 시: Container의 leaveFor 설정으로 처리
|
||||
// stopPropagation을 하지 않아서 Container가 up 방향으로 포커스를 이동하도록 함
|
||||
const handleSpotlightUpFromCouponButtons = useCallback(
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
console.log(`🎫 [FocusDetail] Coupon - Arrow UP 감지`);
|
||||
console.log(`🎫 [FocusDetail] Coupon - Arrow UP 처리: Container의 leaveFor(up) 설정 사용`);
|
||||
if (!Spotlight.focus('spotlightId_backBtn')) {
|
||||
enqueueFocus('spotlightId_backBtn');
|
||||
}
|
||||
},
|
||||
[enqueueFocus]
|
||||
);
|
||||
|
||||
// SHOP BY MOBILE ↑ 타깃 계산
|
||||
const shopByMobileUpTarget = useMemo(() => {
|
||||
if (isBillingProductVisible) return 'detail-buy-now-button';
|
||||
if (userNumber && promotions && promotions.length > 0) return 'detail-coupon-button';
|
||||
return 'spotlightId_backBtn';
|
||||
}, [isBillingProductVisible, promotions, userNumber]);
|
||||
|
||||
const shopByMobileId = useMemo(
|
||||
() => SpotlightIds?.DETAIL_SHOPBYMOBILE || 'detail_shop_by_mobile',
|
||||
[]
|
||||
);
|
||||
|
||||
const stackDefaultElement = useMemo(
|
||||
() => shopByMobileId || firstStackElementId,
|
||||
[shopByMobileId, firstStackElementId]
|
||||
);
|
||||
|
||||
// 버튼 스택(위→아래) 구성: 실제 렌더링 순서에 맞춰 행(row) 단위로 설정
|
||||
const { focusDownMap, focusUpMap, focusOrder, focusRows } = useMemo(() => {
|
||||
const rows = [];
|
||||
|
||||
const stackOrder = useMemo(() => {
|
||||
const ids = [];
|
||||
if (promotions && promotions.length > 0) {
|
||||
// 쿠폰이 여러 개일 수 있으므로 각각 고유 ID로 추가
|
||||
const couponButtonIds = promotions.map((_, idx) => `detail-coupon-button-${idx}`);
|
||||
rows.push(couponButtonIds);
|
||||
promotions.forEach((_, idx) => ids.push(`detail-coupon-button-${idx}`));
|
||||
}
|
||||
|
||||
if (isBillingProductVisible) {
|
||||
rows.push(['detail-buy-now-button', 'detail-add-to-cart-button']);
|
||||
ids.push('detail-buy-now-button', 'detail-add-to-cart-button');
|
||||
}
|
||||
|
||||
const shopRow = [shopByMobileId];
|
||||
ids.push(shopByMobileId);
|
||||
if (panelInfo) {
|
||||
shopRow.push('favoriteBtn');
|
||||
ids.push('favoriteBtn');
|
||||
}
|
||||
rows.push(shopRow);
|
||||
return ids;
|
||||
}, [promotions, isBillingProductVisible, shopByMobileId, panelInfo]);
|
||||
|
||||
rows.push(['product-details-button']);
|
||||
const stackDefaultElement = useMemo(() => {
|
||||
if (promotions && promotions.length > 0) return 'detail-coupon-button-0';
|
||||
if (isBillingProductVisible) return 'detail-buy-now-button';
|
||||
return shopByMobileId || stackOrder[0];
|
||||
}, [promotions, isBillingProductVisible, shopByMobileId, stackOrder]);
|
||||
|
||||
if (isReviewDataComplete) {
|
||||
rows.push(['user-reviews-button']);
|
||||
}
|
||||
|
||||
// 아래 방향: 각 행의 모든 요소가 다음 행의 첫 번째 요소를 바라보도록 매핑
|
||||
const downMap = rows.reduce((acc, row, idx) => {
|
||||
const nextRowFirst = rows[idx + 1]?.[0];
|
||||
row.forEach((id) => {
|
||||
acc[id] = nextRowFirst;
|
||||
});
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// 위 방향: 각 행의 모든 요소가 이전 행의 마지막 요소를 바라보도록 매핑
|
||||
const upMap = rows.reduce((acc, row, idx) => {
|
||||
if (idx > 0) {
|
||||
// 이전 행이 있으면 그 행의 마지막 요소로 매핑
|
||||
const prevRowLast = rows[idx - 1][rows[idx - 1].length - 1];
|
||||
row.forEach((id) => {
|
||||
acc[id] = prevRowLast;
|
||||
});
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// order는 행을 평탄화한 순서
|
||||
const order = rows.flat();
|
||||
|
||||
return { focusDownMap: downMap, focusUpMap: upMap, focusOrder: order, focusRows: rows };
|
||||
}, [isBillingProductVisible, panelInfo, isReviewDataComplete, promotions, shopByMobileId]);
|
||||
|
||||
// 공통 ↓ 이동 핸들러
|
||||
const buildSpotlightDownHandler = useCallback(
|
||||
(currentId) => (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
const nextId = focusDownMap[currentId];
|
||||
const moved = nextId ? Spotlight.focus(nextId) : false;
|
||||
|
||||
// 이동 실패 시, 스택 내 다른 항목으로라도 보정
|
||||
if (!moved) {
|
||||
const fallback = focusOrder.find((id) => id !== currentId);
|
||||
if (fallback) {
|
||||
// Hook을 통한 포커스 이동 보정 (0.5초 타이머로 재시도)
|
||||
enqueueFocus(fallback);
|
||||
}
|
||||
}
|
||||
},
|
||||
[focusDownMap, focusOrder, enqueueFocus]
|
||||
);
|
||||
|
||||
const handleSpotlightDown = useCallback((e) => {
|
||||
e.stopPropagation();
|
||||
}, []);
|
||||
|
||||
// COUPON에서 아래로 이동 시
|
||||
// 항상 Hook을 통해 포커스를 이동하도록 변경
|
||||
const handleSpotlightDownFromCoupon = useCallback(
|
||||
(e, currentCouponId) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
console.log(`🎫 [FocusDetail] Coupon - Arrow DOWN 감지 (ID: ${currentCouponId})`);
|
||||
|
||||
const nextId = focusDownMap[currentCouponId];
|
||||
console.log(`🎫 [FocusDetail] Coupon - Arrow DOWN 다음 타깃: ${nextId}`);
|
||||
|
||||
if (nextId) {
|
||||
// 항상 Hook을 통해 포커스를 이동
|
||||
enqueueFocus(nextId);
|
||||
} else {
|
||||
// 다음 행이 없으면 focusOrder에서 현재 쿠폰 버튼이 아닌 다른 버튼을 찾음
|
||||
const fallback = focusOrder.find((id) => !id.startsWith('detail-coupon-button'));
|
||||
console.log(`🎫 [FocusDetail] Coupon - Arrow DOWN focusDownMap에 없음, 폴백: ${fallback}`);
|
||||
if (fallback) {
|
||||
enqueueFocus(fallback);
|
||||
}
|
||||
}
|
||||
},
|
||||
[focusDownMap, focusOrder, enqueueFocus]
|
||||
);
|
||||
// SHOP BY MOBILE에서 아래로 이동 시
|
||||
// 항상 Hook을 통해 포커스를 이동하도록 변경
|
||||
const handleSpotlightDownFromShopByMobile = useCallback(
|
||||
(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
console.log(`🛵 [FocusDetail] ShopByMobile - Arrow DOWN 감지`);
|
||||
|
||||
const nextId = focusDownMap[shopByMobileId];
|
||||
console.log(`🛵 [FocusDetail] ShopByMobile - Arrow DOWN 다음 타깃: ${nextId}`);
|
||||
|
||||
if (nextId) {
|
||||
// 항상 Hook을 통해 포커스를 이동
|
||||
enqueueFocus(nextId);
|
||||
} else {
|
||||
const fallback = focusOrder.find((id) => id !== shopByMobileId);
|
||||
console.log(
|
||||
`🛵 [FocusDetail] ShopByMobile - Arrow DOWN focusDownMap에 없음, 폴백: ${fallback}`
|
||||
);
|
||||
if (fallback) {
|
||||
enqueueFocus(fallback);
|
||||
}
|
||||
}
|
||||
},
|
||||
[shopByMobileId, focusDownMap, focusOrder, enqueueFocus]
|
||||
);
|
||||
|
||||
// BUY NOW에서 아래로 이동 시
|
||||
// 항상 Hook을 통해 포커스를 이동하도록 변경
|
||||
const handleSpotlightDownFromBuyNow = useCallback(
|
||||
(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
console.log(`💳 [FocusDetail] BuyNow - Arrow DOWN 감지`);
|
||||
|
||||
const nextId = focusDownMap['detail-buy-now-button'];
|
||||
console.log(`💳 [FocusDetail] BuyNow - Arrow DOWN 다음 타깃: ${nextId}`);
|
||||
|
||||
if (nextId) {
|
||||
// 항상 Hook을 통해 포커스를 이동
|
||||
enqueueFocus(nextId);
|
||||
} else {
|
||||
const fallback = focusOrder.find((id) => id !== 'detail-buy-now-button');
|
||||
console.log(`💳 [FocusDetail] BuyNow - Arrow DOWN focusDownMap에 없음, 폴백: ${fallback}`);
|
||||
if (fallback) {
|
||||
enqueueFocus(fallback);
|
||||
}
|
||||
}
|
||||
},
|
||||
[focusDownMap, focusOrder, enqueueFocus]
|
||||
);
|
||||
|
||||
// ADD TO CART에서 아래로 이동 시
|
||||
// 항상 Hook을 통해 포커스를 이동하도록 변경
|
||||
const handleSpotlightDownFromAddToCart = useCallback(
|
||||
(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
console.log(`💳 [FocusDetail] AddToCart - Arrow DOWN 감지`);
|
||||
|
||||
const nextId = focusDownMap['detail-add-to-cart-button'];
|
||||
console.log(`💳 [FocusDetail] AddToCart - Arrow DOWN 다음 타깃: ${nextId}`);
|
||||
|
||||
if (nextId) {
|
||||
// 항상 Hook을 통해 포커스를 이동
|
||||
enqueueFocus(nextId);
|
||||
} else {
|
||||
const fallback = focusOrder.find((id) => id !== 'detail-add-to-cart-button');
|
||||
console.log(
|
||||
`💳 [FocusDetail] AddToCart - Arrow DOWN focusDownMap에 없음, 폴백: ${fallback}`
|
||||
);
|
||||
if (fallback) {
|
||||
enqueueFocus(fallback);
|
||||
}
|
||||
}
|
||||
},
|
||||
[focusDownMap, focusOrder, enqueueFocus]
|
||||
const firstCouponId = useMemo(
|
||||
() => (promotions && promotions.length > 0 ? 'detail-coupon-button-0' : null),
|
||||
[promotions]
|
||||
);
|
||||
|
||||
const onFavoriteFlagChanged = useCallback(
|
||||
@@ -1171,7 +933,7 @@ export default function ProductAllSection({
|
||||
}, [scrollToSection, dispatch]);
|
||||
// 헤더 Back 아이콘에서 아래로 내려올 때 첫 번째 버튼을 바라보도록 설정
|
||||
useEffect(() => {
|
||||
const firstId = focusRows[0]?.[0];
|
||||
const firstId = stackOrder[0];
|
||||
if (firstId && Spotlight && Spotlight.set) {
|
||||
Spotlight.set('spotlightId_backBtn', {
|
||||
next: {
|
||||
@@ -1179,8 +941,7 @@ export default function ProductAllSection({
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [focusRows]);
|
||||
const firstStackElementId = focusRows[0]?.[0];
|
||||
}, [stackOrder]);
|
||||
const scrollPositionRef = useRef(0);
|
||||
const prevScrollPositionRef = useRef(0); // 이전 스크롤 위치 추적
|
||||
const prevScrollTopRef = useRef(0); // HomePanel 스타일 스크롤 위치 추적
|
||||
@@ -1510,35 +1271,45 @@ export default function ProductAllSection({
|
||||
className={css.buttonStackContainer}
|
||||
spotlightId="detail-button-stack"
|
||||
defaultElement={stackDefaultElement}
|
||||
leaveFor={{
|
||||
up: 'spotlightId_backBtn',
|
||||
}}
|
||||
>
|
||||
{userNumber &&
|
||||
promotions.map((promotion, idx) => {
|
||||
const couponButtonId = `detail-coupon-button-${idx}`;
|
||||
return (
|
||||
<div className={css.couponContainer} key={idx}>
|
||||
<div className={css.couponTitleText}>
|
||||
<div className={css.firstTitle}>SPECIAL PROMOTION</div>
|
||||
<div className={css.secondTitle}>
|
||||
Coupon only applicable to this product!
|
||||
{userNumber && promotions.length > 0 && (
|
||||
<CouponContainer
|
||||
className={css.couponStackContainer}
|
||||
spotlightId="detail-coupon-container"
|
||||
defaultElement={firstCouponId}
|
||||
leaveFor={{
|
||||
up: 'spotlightId_backBtn',
|
||||
}}
|
||||
>
|
||||
{promotions.map((promotion, idx) => {
|
||||
const couponButtonId = `detail-coupon-button-${idx}`;
|
||||
return (
|
||||
<div className={css.couponContainer} key={idx}>
|
||||
<div className={css.couponTitleText}>
|
||||
<div className={css.firstTitle}>SPECIAL PROMOTION</div>
|
||||
<div className={css.secondTitle}>
|
||||
Coupon only applicable to this product!
|
||||
</div>
|
||||
</div>
|
||||
<TButton
|
||||
spotlightId={couponButtonId}
|
||||
className={css.couponButton}
|
||||
onClick={() => {
|
||||
handleCouponClick(idx, promotion);
|
||||
}}
|
||||
size="detail_very_small"
|
||||
>
|
||||
<div className={css.couponText}>COUPON</div>
|
||||
<img className={css.buttonImg} src={couponImg} />
|
||||
</TButton>
|
||||
</div>
|
||||
<TButton
|
||||
spotlightId={couponButtonId}
|
||||
className={css.couponButton}
|
||||
onClick={() => {
|
||||
handleCouponClick(idx, promotion);
|
||||
}}
|
||||
onSpotlightUp={handleSpotlightUpFromCouponButtons}
|
||||
onSpotlightDown={(e) => handleSpotlightDownFromCoupon(e, couponButtonId)}
|
||||
data-spotlight-next-down={focusDownMap[couponButtonId]}
|
||||
size="detail_very_small"
|
||||
>
|
||||
<div className={css.couponText}>COUPON</div>
|
||||
<img className={css.buttonImg} src={couponImg} />
|
||||
</TButton>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
);
|
||||
})}
|
||||
</CouponContainer>
|
||||
)}
|
||||
{isBillingProductVisible && (
|
||||
<BuyNowContainer
|
||||
className={css.buyNowCartContainer}
|
||||
@@ -1549,9 +1320,6 @@ export default function ProductAllSection({
|
||||
spotlightId="detail-buy-now-button"
|
||||
className={css.buyNowButton}
|
||||
onClick={handleBuyNowClick}
|
||||
onSpotlightUp={handleSpotlightUpFromBuyButtons}
|
||||
onSpotlightDown={handleSpotlightDownFromBuyNow}
|
||||
data-spotlight-next-down={focusDownMap['detail-buy-now-button']}
|
||||
type="detail_small"
|
||||
>
|
||||
<div className={css.buyNowText}>{$L('BUY NOW')}</div>
|
||||
@@ -1561,9 +1329,6 @@ export default function ProductAllSection({
|
||||
className={css.addToCartButton}
|
||||
// onClick={handleAddToCartClick}
|
||||
onClick={handleBuyNowClick}
|
||||
onSpotlightUp={handleSpotlightUpFromBuyButtons}
|
||||
onSpotlightDown={handleSpotlightDownFromAddToCart}
|
||||
data-spotlight-next-down={focusDownMap['detail-add-to-cart-button']}
|
||||
type="detail_small"
|
||||
>
|
||||
<div className={css.addToCartText}>{$L('ADD TO CART')}</div>
|
||||
@@ -1584,10 +1349,6 @@ export default function ProductAllSection({
|
||||
spotlightId={SpotlightIds.DETAIL_SHOPBYMOBILE}
|
||||
className={css.shopByMobileButton}
|
||||
onClick={handleShopByMobileOpen}
|
||||
onSpotlightUp={handleSpotlightUpToBackButton}
|
||||
onSpotlightDown={handleSpotlightDownFromShopByMobile}
|
||||
data-spotlight-next-down={focusDownMap[shopByMobileId]}
|
||||
data-spotlight-next-up={shopByMobileUpTarget}
|
||||
>
|
||||
<div className={css.shopByMobileText}>{$L('SHOP BY MOBILE')}</div>
|
||||
</TButton>
|
||||
@@ -1600,7 +1361,6 @@ export default function ProductAllSection({
|
||||
favoriteFlag={favoriteFlag}
|
||||
onFavoriteFlagChanged={onFavoriteFlagChanged}
|
||||
kind={'item_detail'}
|
||||
nextDownId={focusDownMap['favoriteBtn']}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -618,6 +618,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
.couponStackContainer {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
> * {
|
||||
margin-bottom: 5px;
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.favoriteBtnWrapper {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
|
||||
Reference in New Issue
Block a user