🕐 커밋 시간: 2025. 12. 09. 18:18:51 📊 변경 통계: • 총 파일: 2개 • 추가: +28줄 • 삭제: -4줄 📝 수정된 파일: ~ com.twin.app.shoptime/src/actions/homeActions.js ~ com.twin.app.shoptime/src/api/TAxios.js 🔧 주요 변경 내용: • 핵심 비즈니스 로직 개선 • API 서비스 레이어 개선 • 소규모 기능 개선
599 lines
17 KiB
JavaScript
599 lines
17 KiB
JavaScript
import { URLS } from '../api/apiConfig';
|
|
import { TAxios, TAxiosPromise } from '../api/TAxios';
|
|
import { types } from './actionTypes';
|
|
import { changeAppStatus, getTermsAgreeYn } from './commonActions';
|
|
import { collectBannerPositions } from '../utils/domUtils';
|
|
import { createDebugHelpers } from '../utils/debug';
|
|
import { setHidePopup, setShowPopup } from './commonActions';
|
|
import { ACTIVE_POPUP } from '../utils/Config';
|
|
|
|
// 디버그 헬퍼 설정
|
|
const DEBUG_MODE = false;
|
|
const { dlog, dwarn, derror } = createDebugHelpers(DEBUG_MODE);
|
|
|
|
// 약관 정보 조회 IF-LGSP-005
|
|
export const getHomeTerms = (props) => (dispatch, getState) => {
|
|
const { trmsTpCdList, mbrNo } = props;
|
|
|
|
const onSuccess = (response) => {
|
|
dlog('getHomeTerms onSuccess ', response.data);
|
|
|
|
if (response.data.retCode === 0) {
|
|
dispatch({
|
|
type: types.GET_HOME_TERMS,
|
|
payload: response.data,
|
|
});
|
|
|
|
// 약관 ID 매핑을 별도로 생성하여 저장
|
|
// Chromium68 호환성을 위해 Optional Chaining 제거
|
|
if (response.data && response.data.data && response.data.data.terms) {
|
|
const termsIdMap = {};
|
|
let hasOptionalTerms = false; // MST00405 존재 여부 확인
|
|
|
|
response.data.data.terms.forEach((term) => {
|
|
if (term.trmsTpCd && term.trmsId) {
|
|
termsIdMap[term.trmsTpCd] = term.trmsId;
|
|
}
|
|
|
|
// MST00405 선택약관 존재 여부 확인
|
|
if (term.trmsTpCd === 'MST00405') {
|
|
hasOptionalTerms = true;
|
|
}
|
|
});
|
|
|
|
dispatch({
|
|
type: types.SET_TERMS_ID_MAP,
|
|
payload: termsIdMap,
|
|
});
|
|
|
|
// 선택약관 존재 여부 상태 설정
|
|
// TODO: 테스트용 - 임시로 false 강제 설정
|
|
const forceDisableOptionalTerms = false; // 테스트 완료 후 false로 변경
|
|
const finalOptionalTermsValue = forceDisableOptionalTerms ? false : hasOptionalTerms;
|
|
|
|
dispatch({
|
|
type: types.SET_OPTIONAL_TERMS_AVAILABILITY,
|
|
payload: finalOptionalTermsValue,
|
|
});
|
|
|
|
dlog(
|
|
'[optionalTermsAvailable] 실제값:',
|
|
hasOptionalTerms,
|
|
'강제설정값:',
|
|
finalOptionalTermsValue
|
|
);
|
|
|
|
if (process.env.NODE_ENV === 'development') {
|
|
dlog('약관 ID 매핑 생성:', termsIdMap);
|
|
dlog('선택약관 존재 여부:', hasOptionalTerms);
|
|
}
|
|
}
|
|
|
|
setTimeout(() => {
|
|
dispatch(getTermsAgreeYn());
|
|
}, 0);
|
|
}
|
|
};
|
|
|
|
const onFail = (error) => {
|
|
derror('getHomeTerms onFail ', error);
|
|
|
|
// TODO: 임시 디버그용 팝업 (재현 후 제거하세요)
|
|
const retCode = error?.data?.retCode ?? error?.retCode ?? 'unknown';
|
|
dispatch(
|
|
setShowPopup(ACTIVE_POPUP.toast, {
|
|
button1Text: `getHomeTerms onFail retCode=${retCode}`,
|
|
button2Text: 'OK',
|
|
})
|
|
);
|
|
setTimeout(() => dispatch(setHidePopup()), 1500);
|
|
|
|
// 약관 미동의(retCode 501)로 GET_HOME_TERMS가 실패하면
|
|
// introTermsAgree를 명시적으로 false로 내려 앱이 IntroPanel을 띄우도록 한다.
|
|
if (retCode === 501) {
|
|
dispatch({
|
|
type: types.GET_TERMS_AGREE_YN_SUCCESS,
|
|
payload: {
|
|
privacyTerms: 'N',
|
|
serviceTerms: 'N',
|
|
purchaseTerms: 'N',
|
|
paymentTerms: 'N',
|
|
optionalTerms: 'N',
|
|
},
|
|
});
|
|
}
|
|
|
|
// 실패 시 로딩 패널을 반드시 내려 백화 상태를 방지
|
|
dispatch(
|
|
changeAppStatus({
|
|
showLoadingPanel: { show: false },
|
|
})
|
|
);
|
|
};
|
|
|
|
TAxios(
|
|
dispatch,
|
|
getState,
|
|
'get',
|
|
URLS.GET_HOME_TERMS,
|
|
{ trmsTpCdList, mbrNo },
|
|
{},
|
|
onSuccess,
|
|
onFail
|
|
);
|
|
};
|
|
|
|
// 현재 로그인 사용자 기준으로 약관 정보 조회 (인자 없이 호출 가능)
|
|
export const fetchCurrentUserHomeTerms = () => (dispatch, getState) => {
|
|
const loginUserData = getState().common.appStatus.loginUserData;
|
|
|
|
if (!loginUserData || !loginUserData.userNumber) {
|
|
derror(
|
|
'fetchCurrentUserHomeTerms: userNumber (mbrNo) is not available. User might not be logged in.'
|
|
);
|
|
dispatch({ type: types.GET_TERMS_AGREE_YN_FAILURE });
|
|
return;
|
|
}
|
|
|
|
const mbrNo = loginUserData.userNumber;
|
|
const trmsTpCdList = 'MST00401, MST00402, MST00405'; // 기본 약관 코드 리스트
|
|
|
|
const onSuccess = (response) => {
|
|
dlog('fetchCurrentUserHomeTerms onSuccess ', response.data);
|
|
|
|
if (response.data.retCode === 0) {
|
|
dispatch({
|
|
type: types.GET_HOME_TERMS, // 기존 GET_HOME_TERMS 타입을 재사용
|
|
payload: response.data,
|
|
});
|
|
|
|
// 약관 ID 매핑을 별도로 생성하여 저장
|
|
// Chromium68 호환성을 위해 Optional Chaining 제거
|
|
if (response.data && response.data.data && response.data.data.terms) {
|
|
const termsIdMap = {};
|
|
let hasOptionalTerms = false; // MST00405 존재 여부 확인
|
|
|
|
response.data.data.terms.forEach((term) => {
|
|
if (term.trmsTpCd && term.trmsId) {
|
|
termsIdMap[term.trmsTpCd] = term.trmsId;
|
|
}
|
|
|
|
// MST00405 선택약관 존재 여부 확인
|
|
if (term.trmsTpCd === 'MST00405') {
|
|
hasOptionalTerms = true;
|
|
}
|
|
});
|
|
|
|
dispatch({
|
|
type: types.SET_TERMS_ID_MAP,
|
|
payload: termsIdMap,
|
|
});
|
|
|
|
// 선택약관 존재 여부 상태 설정
|
|
// TODO: 테스트용 - 임시로 false 강제 설정
|
|
const forceDisableOptionalTerms = false; // 테스트 완료 후 false로 변경
|
|
const finalOptionalTermsValue = forceDisableOptionalTerms ? false : hasOptionalTerms;
|
|
|
|
dispatch({
|
|
type: types.SET_OPTIONAL_TERMS_AVAILABILITY,
|
|
payload: finalOptionalTermsValue,
|
|
});
|
|
dlog(
|
|
'[optionalTermsAvailable] 실제값:',
|
|
hasOptionalTerms,
|
|
'강제설정값:',
|
|
finalOptionalTermsValue
|
|
);
|
|
|
|
if (process.env.NODE_ENV === 'development') {
|
|
dlog('약관 ID 매핑 생성:', termsIdMap);
|
|
dlog('선택약관 존재 여부:', hasOptionalTerms);
|
|
}
|
|
}
|
|
|
|
// getHomeTerms와 동일하게 getTermsAgreeYn 후속 처리
|
|
setTimeout(() => {
|
|
dispatch(getTermsAgreeYn());
|
|
}, 0);
|
|
} else {
|
|
// retCode가 0이 아닌 경우 실패로 처리
|
|
dispatch({ type: types.GET_TERMS_AGREE_YN_FAILURE });
|
|
}
|
|
};
|
|
|
|
const onFail = (error) => {
|
|
derror('fetchCurrentUserHomeTerms onFail ', error);
|
|
dispatch({ type: types.GET_TERMS_AGREE_YN_FAILURE });
|
|
};
|
|
|
|
TAxios(
|
|
dispatch,
|
|
getState,
|
|
'get',
|
|
URLS.GET_HOME_TERMS, // 동일한 API 엔드포인트 사용
|
|
{ trmsTpCdList, mbrNo },
|
|
{},
|
|
onSuccess,
|
|
onFail
|
|
);
|
|
};
|
|
|
|
// 기존 TAxios 패턴과 일치하는 안전한 Redux Action
|
|
export const fetchCurrentUserHomeTermsSafe = () => async (dispatch, getState) => {
|
|
const loginUserData = getState().common.appStatus.loginUserData;
|
|
|
|
if (!loginUserData || !loginUserData.userNumber) {
|
|
derror('fetchCurrentUserHomeTerms: userNumber is not available');
|
|
dispatch({ type: types.GET_TERMS_AGREE_YN_FAILURE });
|
|
return { success: false, message: '사용자 정보가 없습니다.' };
|
|
}
|
|
|
|
const mbrNo = loginUserData.userNumber;
|
|
const trmsTpCdList = 'MST00401, MST00402, MST00405';
|
|
|
|
dlog('Fetching home terms for user:', mbrNo);
|
|
|
|
// 안전한 API 호출 (기존 TAxios 패턴과 동일)
|
|
const result = await TAxiosPromise(dispatch, getState, 'get', URLS.GET_HOME_TERMS, {
|
|
trmsTpCdList,
|
|
mbrNo,
|
|
});
|
|
|
|
// 네트워크 에러인 경우
|
|
if (!result.success) {
|
|
derror('fetchCurrentUserHomeTerms network error:', result.error);
|
|
dispatch({ type: types.GET_TERMS_AGREE_YN_FAILURE });
|
|
return { success: false, message: '네트워크 오류가 발생했습니다.' };
|
|
}
|
|
|
|
// 기존 TAxios처럼 특별한 retCode들은 TAxios 내부에서 이미 처리됨
|
|
// (401, 402, 501, 602, 603, 604 등은 TAxios에서 알아서 처리하고 onSuccess가 호출되지 않음)
|
|
|
|
dlog('fetchCurrentUserHomeTerms response:', result.data);
|
|
|
|
// 정상적으로 onSuccess가 호출된 경우에만 여기까지 옴
|
|
if (result.data && result.data.retCode === 0) {
|
|
dispatch({
|
|
type: types.GET_HOME_TERMS,
|
|
payload: result.data,
|
|
});
|
|
|
|
// 약관 ID 매핑을 별도로 생성하여 저장
|
|
// Chromium68 호환성을 위해 Optional Chaining 제거
|
|
if (result.data && result.data.data && result.data.data.terms) {
|
|
const termsIdMap = {};
|
|
let hasOptionalTerms = false; // MST00405 존재 여부 확인
|
|
|
|
result.data.data.terms.forEach((term) => {
|
|
if (term.trmsTpCd && term.trmsId) {
|
|
termsIdMap[term.trmsTpCd] = term.trmsId;
|
|
}
|
|
|
|
// MST00405 선택약관 존재 여부 확인
|
|
if (term.trmsTpCd === 'MST00405') {
|
|
hasOptionalTerms = true;
|
|
}
|
|
});
|
|
|
|
dispatch({
|
|
type: types.SET_TERMS_ID_MAP,
|
|
payload: termsIdMap,
|
|
});
|
|
|
|
// 선택약관 존재 여부 상태 설정 2025-07-03
|
|
// TODO: 테스트용 - 임시로 false 강제 설정
|
|
const forceDisableOptionalTerms = false; // 테스트 완료 후 false로 변경
|
|
const finalOptionalTermsValue = forceDisableOptionalTerms ? false : hasOptionalTerms;
|
|
|
|
dispatch({
|
|
type: types.SET_OPTIONAL_TERMS_AVAILABILITY,
|
|
payload: finalOptionalTermsValue,
|
|
});
|
|
|
|
if (process.env.NODE_ENV === 'development') {
|
|
dlog('약관 ID 매핑 생성:', termsIdMap);
|
|
dlog(
|
|
'선택약관 존재 여부 - 실제값:',
|
|
hasOptionalTerms,
|
|
'강제설정값:',
|
|
finalOptionalTermsValue
|
|
);
|
|
}
|
|
}
|
|
|
|
// 후속 액션 호출 (기존과 동일)
|
|
setTimeout(() => {
|
|
dispatch(getTermsAgreeYn());
|
|
}, 0);
|
|
|
|
return { success: true, data: result.data };
|
|
} else {
|
|
// retCode가 0이 아닌 일반적인 API 에러
|
|
// Chromium68 호환성을 위해 Optional Chaining 제거
|
|
derror('API returned non-zero retCode:', result.data && result.data.retCode);
|
|
dispatch({ type: types.GET_TERMS_AGREE_YN_FAILURE });
|
|
return {
|
|
success: false,
|
|
message: (result.data && result.data.retMsg) || '서버 오류가 발생했습니다.',
|
|
};
|
|
}
|
|
};
|
|
|
|
// 메뉴 목록 조회 IF-LGSP-044
|
|
export const getHomeMenu = () => (dispatch, getState) => {
|
|
const onSuccess = (response) => {
|
|
// dlog("getHomeMenu onSuccess ", response.data);
|
|
|
|
dispatch({
|
|
type: types.GET_HOME_MENU,
|
|
payload: response.data,
|
|
});
|
|
};
|
|
|
|
const onFail = (error) => {
|
|
derror('getHomeMenu onFail ', error);
|
|
};
|
|
|
|
TAxios(dispatch, getState, 'get', URLS.GET_HOME_MENU, {}, {}, onSuccess, onFail);
|
|
};
|
|
|
|
// 테마 전시 정보 상세 조회 IF-LGSP-060
|
|
export const getThemeCurationDetailInfo = (params) => (dispatch, getState) => {
|
|
const { patnrId, curationId, bgImgNo } = params;
|
|
|
|
dispatch(changeAppStatus({ showLoadingPanel: { show: true, type: 'wait' } }));
|
|
|
|
const onSuccess = (response) => {
|
|
dlog('getThemeCurationDetailInfo onSuccess', response.data);
|
|
|
|
dispatch({
|
|
type: types.GET_THEME_CURATION_DETAIL_INFO,
|
|
payload: response.data.data,
|
|
});
|
|
|
|
dispatch(changeAppStatus({ showLoadingPanel: { show: false } }));
|
|
};
|
|
|
|
const onFail = (error) => {
|
|
derror('getThemeCurationDetailInfo onFail', error);
|
|
dispatch(changeAppStatus({ showLoadingPanel: { show: false } }));
|
|
};
|
|
|
|
TAxios(
|
|
dispatch,
|
|
getState,
|
|
'get',
|
|
URLS.GET_THEME_CURATION_DETAIL_INFO,
|
|
{ patnrId, curationId, bgImgNo },
|
|
{},
|
|
onSuccess,
|
|
onFail
|
|
);
|
|
};
|
|
// 테마호텔 정보 상세 조회 IF-LGSP-085
|
|
export const getThemeHotelDetailInfo = (params) => (dispatch, getState) => {
|
|
const { patnrId, curationId } = params;
|
|
|
|
dispatch(changeAppStatus({ showLoadingPanel: { show: true, type: 'wait' } }));
|
|
|
|
const onSuccess = (response) => {
|
|
dlog('getThemeHotelDetailInfo onSuccess', response.data);
|
|
|
|
dispatch({
|
|
type: types.GET_THEME_HOTEL_DETAIL_INFO,
|
|
payload: response.data.data,
|
|
});
|
|
|
|
dispatch(changeAppStatus({ showLoadingPanel: { show: false } }));
|
|
};
|
|
|
|
const onFail = (error) => {
|
|
derror('getThemeHotelDetailInfo onFail', error);
|
|
dispatch(changeAppStatus({ showLoadingPanel: { show: false } }));
|
|
};
|
|
|
|
TAxios(
|
|
dispatch,
|
|
getState,
|
|
'get',
|
|
URLS.GET_THEME_HOTEL_DETAIL_INFO,
|
|
{ patnrId, curationId },
|
|
{},
|
|
onSuccess,
|
|
onFail
|
|
);
|
|
};
|
|
// HOME LAYOUT 정보 조회 IF-LGSP-300
|
|
export const getHomeLayout = () => (dispatch, getState) => {
|
|
const onSuccess = (response) => {
|
|
dlog('getHomeLayout onSuccess', response.data);
|
|
|
|
dispatch({
|
|
type: types.GET_HOME_LAYOUT,
|
|
payload: response.data.data,
|
|
});
|
|
dispatch(changeAppStatus({ showLoadingPanel: { show: false } }));
|
|
};
|
|
|
|
const onFail = (error) => {
|
|
derror('getHomeLayout onFail', error);
|
|
dispatch(changeAppStatus({ showLoadingPanel: { show: false } }));
|
|
};
|
|
|
|
TAxios(dispatch, getState, 'get', URLS.GET_HOME_LAYOUT, {}, {}, onSuccess, onFail);
|
|
};
|
|
|
|
// HOME Main Contents Banner 정보 조회 IF-LGSP-301
|
|
export const getHomeMainContents = () => (dispatch, getState) => {
|
|
const onSuccess = (response) => {
|
|
dlog('getHomeMainContents onSuccess', response.data);
|
|
|
|
dispatch({
|
|
type: types.GET_HOME_MAIN_CONTENTS,
|
|
payload: response.data.data,
|
|
status: 'fulfilled',
|
|
});
|
|
|
|
dispatch(changeAppStatus({ showLoadingPanel: { show: false } }));
|
|
};
|
|
|
|
const onFail = (error) => {
|
|
derror('getHomeMainContents onFail', error);
|
|
dispatch(changeAppStatus({ showLoadingPanel: { show: false } }));
|
|
};
|
|
|
|
TAxios(dispatch, getState, 'get', URLS.GET_HOME_MAIN_CONTENTS, {}, {}, onSuccess, onFail);
|
|
};
|
|
|
|
// Theme 전시 정보 조회 : IF-LGSP-045
|
|
export const getThemeCurationInfo = () => (dispatch, getState) => {
|
|
const onSuccess = (response) => {
|
|
dlog('getThemeCurationInfo onSuccess', response.data);
|
|
|
|
dispatch({
|
|
type: types.GET_THEME_CURATION_INFO,
|
|
payload: response.data.data,
|
|
retCode: response.data.retCode,
|
|
});
|
|
dispatch(changeAppStatus({ showLoadingPanel: { show: false } }));
|
|
};
|
|
|
|
const onFail = (error) => {
|
|
derror('getThemeCurationInfo onFail', error);
|
|
dispatch(changeAppStatus({ showLoadingPanel: { show: false } }));
|
|
};
|
|
|
|
TAxios(dispatch, getState, 'get', URLS.GET_THEME_CURATION_INFO, {}, {}, onSuccess, onFail);
|
|
};
|
|
|
|
// 테마 메뉴(=테마 페이지) 선반 조회 : IF-LGSP-095
|
|
export const getThemeMenuShelfInfo = (props) => (dispatch, getState) => {
|
|
const { curationId } = props;
|
|
|
|
dispatch(changeAppStatus({ showLoadingPanel: { show: true, type: 'wait' } }));
|
|
|
|
const onSuccess = (response) => {
|
|
dlog('getThemeMenuShelfInfo onSuccess', response.data);
|
|
dispatch(changeAppStatus({ showLoadingPanel: { show: false } }));
|
|
dispatch({
|
|
type: types.GET_THEME_MENU_SHELF_INFO,
|
|
payload: response.data.data,
|
|
});
|
|
};
|
|
|
|
const onFail = (error) => {
|
|
derror('getThemeMenuShelfInfo onFail', error);
|
|
dispatch(changeAppStatus({ showLoadingPanel: { show: false } }));
|
|
};
|
|
|
|
TAxios(
|
|
dispatch,
|
|
getState,
|
|
'get',
|
|
URLS.GET_THEME_MENU_SHELF_INFO,
|
|
{ curationId },
|
|
{},
|
|
onSuccess,
|
|
onFail
|
|
);
|
|
};
|
|
|
|
export const clearThemeMenuShelfInfo = () => ({
|
|
type: types.CLEAR_THEME_MENU_SHELF_INFO,
|
|
});
|
|
|
|
export const resetHomeInfo = () => ({
|
|
type: types.RESET_HOME_INFO,
|
|
});
|
|
|
|
export const updateHomeInfo = (homeInfo) => {
|
|
return {
|
|
type: types.UPDATE_HOME_INFO,
|
|
payload: homeInfo,
|
|
};
|
|
};
|
|
|
|
export const clearThemeDetail = () => ({
|
|
type: types.CLEAR_THEME_DETAIL,
|
|
});
|
|
|
|
export const setDefaultFocus = (focus) => ({
|
|
type: types.SET_DEFAULT_FOCUS,
|
|
payload: focus,
|
|
});
|
|
|
|
export const setVideoTransitionLock = (isLocked) => ({
|
|
type: types.SET_VIDEO_TRANSITION_LOCK,
|
|
payload: Boolean(isLocked),
|
|
});
|
|
|
|
export const checkEnterThroughGNB = (boolean) => ({
|
|
type: types.CHECK_ENTER_THROUGH_GNB,
|
|
payload: boolean,
|
|
});
|
|
|
|
export const setBannerIndex = (bannerId, index) => {
|
|
if (!bannerId) {
|
|
dwarn('setBannerIndex called with undefined bannerId');
|
|
return { type: 'NO_OP' };
|
|
}
|
|
return {
|
|
type: types.SET_BANNER_INDEX,
|
|
payload: { bannerId, index },
|
|
};
|
|
};
|
|
|
|
// 🔽 [추가] 새로운 배너 위치 및 영구재생 관련 액션들
|
|
|
|
/**
|
|
* 모든 배너의 위치 정보를 설정합니다.
|
|
* @param {Object} positions - 배너별 위치 정보 맵
|
|
*/
|
|
export const setBannerPositions = (positions) => ({
|
|
type: types.SET_BANNER_POSITIONS,
|
|
payload: positions,
|
|
});
|
|
|
|
/**
|
|
* 특정 배너의 위치 정보를 업데이트합니다.
|
|
* @param {string} bannerId - 배너 ID
|
|
* @param {Object} position - 위치 정보
|
|
*/
|
|
export const updateBannerPosition = (bannerId, position) => ({
|
|
type: types.UPDATE_BANNER_POSITION,
|
|
payload: { bannerId, position },
|
|
});
|
|
|
|
/**
|
|
* 영구재생 비디오 정보를 설정합니다.
|
|
* @param {Object} videoInfo - 비디오 정보
|
|
*/
|
|
export const setPersistentVideoInfo = (videoInfo) => ({
|
|
type: types.SET_PERSISTENT_VIDEO_INFO,
|
|
payload: videoInfo,
|
|
});
|
|
|
|
/**
|
|
* 영구재생 비디오 정보를 클리어합니다.
|
|
*/
|
|
export const clearPersistentVideoInfo = () => ({
|
|
type: types.CLEAR_PERSISTENT_VIDEO_INFO,
|
|
});
|
|
|
|
/**
|
|
* 모든 배너의 현재 DOM 위치를 수집하여 Redux 스토어에 저장합니다.
|
|
* @param {Array<string>} bannerIds - 수집할 배너 ID 배열
|
|
*/
|
|
export const collectAndSaveBannerPositions = (bannerIds) => async (dispatch) => {
|
|
try {
|
|
const positions = await collectBannerPositions(bannerIds);
|
|
dispatch(setBannerPositions(positions));
|
|
|
|
if (process.env.NODE_ENV === 'development') {
|
|
dlog('[homeActions] 배너 위치 수집 완료:', positions);
|
|
}
|
|
} catch (error) {
|
|
derror('[homeActions] 배너 위치 수집 실패:', error);
|
|
}
|
|
};
|