(() => { 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); }); })();