(() => {
const { pageHelper, EventManager, utils } = ShopbySkin;
const WITHDRAWAL_REASON = '회원 직접 탈퇴';
const WITHDRAWAL_CONFIRM_MESSAGE = '탈퇴하시면 적립금 및 쿠폰이 모두 사라집니다.
탈퇴하시겠습니까?';
const HOME_URL = '/';
const KEYCLOAK_ID_TOKEN_STORAGE_KEY = 'gongbiz-store-keycloak-id-token';
const KEYCLOAK_ACCESS_TOKEN_STORAGE_KEY = 'gongbiz-store-keycloak-access-token';
const AUTH_INTENT_STORAGE_KEY = 'gongbiz-store-auth-intent';
const WITHDRAWAL_COMPLETED_AT_STORAGE_KEY = 'gongbiz-store-withdrawal-completed-at';
const PROD_HOSTS = ['www.gongbizstore.com', 'gongbizstore.com', 'm.gongbizstore.com'];
const DEV_HOSTS = ['gongbizstore.shopby.co.kr', 'm-gongbizstore.shopby.co.kr'];
const KEYCLOAK_REALM = 'gongbiz';
const KEYCLOAK_CLIENT_ID = 'cafe24';
// 환경 분기는 GongbizSsoEnv 로 일원화. (withdrawal.html 에서 gongbiz-sso-env.js 로드, 미로드 시 hostname 폴백)
const ssoEnv = (typeof window !== 'undefined' && window.GongbizSsoEnv) || null;
const withdrawalHelper = pageHelper.withdrawalHelper();
const containerEl = document.querySelector('[shopby-helper-key="withdrawal"]');
let isInitialized = false;
let isBlocked = false;
const isKakaoOrNaverProvider = (value) => {
const provider = String(value || '').trim().toLowerCase();
return ['kakao', 'naver', '카카오', '네이버'].includes(provider);
};
const useDevEndpoints = DEV_HOSTS.includes(location.hostname) || !PROD_HOSTS.includes(location.hostname);
const getKeycloakBaseUrl = () =>
ssoEnv?.baseUrl || (useDevEndpoints ? 'https://dev-sso.gongbiz.kr' : 'https://sso.gongbiz.kr');
const getApiBaseUrl = () =>
ssoEnv?.apiBaseUrl || (useDevEndpoints ? 'https://crm-dev1.gongbiz.kr' : 'https://gongbiz.kr');
const getKeycloakIdTokenHint = () => {
try {
return localStorage.getItem(KEYCLOAK_ID_TOKEN_STORAGE_KEY);
} catch (e) {
return null;
}
};
const getKeycloakAccessToken = () => {
try {
return localStorage.getItem(KEYCLOAK_ACCESS_TOKEN_STORAGE_KEY);
} catch (e) {
return null;
}
};
const getKeycloakTokenPayload = (token) => {
try {
const payload = token?.split('.')[1];
if (!payload) return {};
const normalizedPayload = payload.replace(/-/g, '+').replace(/_/g, '/');
const paddedPayload = normalizedPayload.padEnd(Math.ceil(normalizedPayload.length / 4) * 4, '=');
const decodedPayload = new TextDecoder().decode(Uint8Array.from(atob(paddedPayload), (char) => char.charCodeAt(0)));
return JSON.parse(decodedPayload);
} catch (e) {
return {};
}
};
const getKeycloakIdTokenPayload = () => getKeycloakTokenPayload(getKeycloakIdTokenHint());
const isExpiredTokenPayload = (payload) => {
if (!payload || typeof payload.exp !== 'number') return true;
return payload.exp * 1000 <= Date.now();
};
const getValidatedKeycloakAccessToken = () => {
const accessToken = getKeycloakAccessToken();
if (!accessToken) {
throw new Error('missing Keycloak access token');
}
const accessPayload = getKeycloakTokenPayload(accessToken);
if (isExpiredTokenPayload(accessPayload)) {
throw new Error('expired Keycloak access token');
}
if (!isKakaoOrNaverProvider(accessPayload.provider)) {
throw new Error('invalid Keycloak access token provider');
}
const idPayload = getKeycloakIdTokenPayload();
if (idPayload.sub && accessPayload.sub && idPayload.sub !== accessPayload.sub) {
throw new Error('mismatched Keycloak token subject');
}
return accessToken;
};
const buildKeycloakLogoutUrl = () => {
const url = new URL(`${getKeycloakBaseUrl()}/realms/${KEYCLOAK_REALM}/protocol/openid-connect/logout`);
const idTokenHint = getKeycloakIdTokenHint();
url.searchParams.set('client_id', KEYCLOAK_CLIENT_ID);
url.searchParams.set('post_logout_redirect_uri', `${location.origin}${HOME_URL}`);
if (idTokenHint) {
url.searchParams.set('id_token_hint', idTokenHint);
}
return url.toString();
};
const clearStoreAuthState = () => {
try {
localStorage.removeItem(KEYCLOAK_ID_TOKEN_STORAGE_KEY);
localStorage.removeItem(KEYCLOAK_ACCESS_TOKEN_STORAGE_KEY);
} catch (e) {
// 저장소 접근 실패는 탈퇴 완료 이동을 막지 않는다.
}
try {
sessionStorage.removeItem(AUTH_INTENT_STORAGE_KEY);
} catch (e) {
// 저장소 접근 실패는 탈퇴 완료 이동을 막지 않는다.
}
};
const withdrawStoreConsumer = async () => {
const accessToken = getValidatedKeycloakAccessToken();
const res = await fetch(`${getApiBaseUrl()}/api/v2/gongbizstore/consumer/withdraw`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({ reason: WITHDRAWAL_REASON }),
});
if (!res.ok) {
throw new Error(`CRM withdrawal failed: ${res.status}`);
}
};
const markWithdrawalCompleted = () => {
try {
localStorage.setItem(WITHDRAWAL_COMPLETED_AT_STORAGE_KEY, String(Date.now()));
} catch (e) {
// 저장소 접근 실패는 탈퇴 완료 이동을 막지 않는다.
}
};
const hasValidSocialKeycloakToken = () => {
const payload = getKeycloakIdTokenPayload();
return !isExpiredTokenPayload(payload) && isKakaoOrNaverProvider(payload.provider);
};
const canAccessWithdrawalPage = (profile) => {
const safeProfile = profile || {};
const providerCandidates = [
safeProfile.openIdProvider,
safeProfile.openIdProviderLabel,
safeProfile.provider,
safeProfile.providerType,
safeProfile.identityProvider,
];
return providerCandidates.some(isKakaoOrNaverProvider)
|| hasValidSocialKeycloakToken();
};
const redirectToHome = () => {
location.replace(HOME_URL);
};
const completeWithdrawal = () => {
const logoutUrl = buildKeycloakLogoutUrl();
clearStoreAuthState();
utils.signOut({
redirect: logoutUrl,
replace: true,
});
};
const WITHDRAWAL_BLOCKED_MESSAGE =
'공비서 회원은 공비서스토어에서 직접 회원 탈퇴가 불가합니다.
자세한 사항은 고객센터로 문의해 주세요.';
const blockWithdrawalPage = () => {
if (isBlocked) return;
isBlocked = true;
containerEl?.setAttribute('hidden', '');
// 탈퇴 불가 회원(공비서 인입 등)이 클릭 시 조용히 홈으로 튕겨 오류처럼 보이던 것을
// 안내 팝업으로 사유를 알린 뒤 홈으로 이동한다.
try {
EventManager.fire('MODAL_ALERT_OPEN', {
noticeType: 'WARNING',
message: `${WITHDRAWAL_BLOCKED_MESSAGE}`,
onClose: () => {
redirectToHome();
},
});
} catch (e) {
redirectToHome();
}
};
const initializeWithdrawalPage = () => {
if (isInitialized) return;
isInitialized = true;
withdrawalHelper.initialize({
helperKey: 'withdrawal',
platform: 'MOBILE',
profileNonMasking: true,
});
containerEl?.removeAttribute('hidden');
};
const authorizeWithdrawalPage = (profile) => {
if (canAccessWithdrawalPage(profile)) {
initializeWithdrawalPage();
return;
}
blockWithdrawalPage();
};
const handleWithdrawal = (helper) => {
EventManager.fire('MODAL_CONFIRM_OPEN', {
noticeType: 'WARNING',
message: `${WITHDRAWAL_CONFIRM_MESSAGE}`,
onConfirm: async () => {
try {
await withdrawStoreConsumer();
markWithdrawalCompleted();
EventManager.fire('MODAL_ALERT_OPEN', {
noticeType: 'SUCCESS',
message: '회원 탈퇴가 완료되었습니다.',
onClose: () => {
completeWithdrawal();
},
});
} catch (e) {
EventManager.fire('MODAL_ALERT_OPEN', {
noticeType: 'WARNING',
message: '회원 탈퇴에 실패했습니다. 다시 로그인 후 시도해 주세요.',
});
}
},
});
};
const clickEventListener = (event) => {
const action = event.target.closest('[shopby-action]')?.getAttribute('shopby-action');
if (action === 'CLICK_WITHDRAWAL' && isInitialized) {
handleWithdrawal(withdrawalHelper);
}
};
containerEl?.setAttribute('hidden', '');
containerEl?.addEventListener('click', clickEventListener);
EventManager.on('PAGE_LOAD_COMPLETED', (event = {}) => {
authorizeWithdrawalPage(event.profile);
});
})();