(() => { const { pageHelper, EventManager, constants, utils } = ShopbySkin; if (utils.isSignedIn()) { if (!document.referrer || location.href === document.referrer) { history.back(); return; } utils.moveTo({ url: document.referrer, }); } const signUpFormHelper = pageHelper.signUpFormHelper(); const containerEl = document.querySelector(`[shopby-helper-key="sign-up-form"]`); const profileExtraInformationEl = document.querySelector('profile-extra-information'); signUpFormHelper.initialize({ helperKey: 'sign-up-form', }); const flattenRequest = (helper) => { const { profileBasicInformation, profileNicknameInformation, profileEmailInformation, profileSmsInformation, profileCertification, profileOptionalInformation, termsInformation, profileExtraInformation, } = helper.getState(); const selectedCustomTerms = termsInformation?.customTerms.filter(({ checked }) => !!checked).map((term) => term.id); return { ...profileBasicInformation, ...profileEmailInformation, ...profileSmsInformation, ...profileCertification, ...profileOptionalInformation, extraInfo: profileExtraInformation?.extraInfoContents, nickname: profileNicknameInformation?.nickname, joinTermsAgreements: termsInformation?.terms, customTermsNos: selectedCustomTerms, }; }; const checkInvalidProfileForm = (request) => { // eslint-disable-next-line complexity const errors = Object.keys(utils.changeProfileFormVerifyMap)?.map((key) => { const value = request?.[key]?.value ?? ''; switch (key) { case 'memberId': return { ...utils.changeProfileFormVerifyMap?.[key]({ value, isDuplicated: request?.memberId?.isDuplicate, isDormantMemberId: !!request?.isRecentlyExitedMember, }), field: key, }; case 'passwordConfirm': return { ...utils.changeProfileFormVerifyMap?.[key]({ value, comparisonValue: request?.password?.value }), field: key, }; case 'memberName': return { ...utils.changeProfileFormVerifyMap?.[key]({ value, isRequired: request?.memberName?.isRequired }), field: key, }; case 'nickname': return { ...utils.changeProfileFormVerifyMap?.[key]({ value, isDuplicated: request?.nickname?.isDuplicate, isRequired: request?.nickname?.isRequired, }), field: key, }; case 'email': return { ...utils.changeProfileFormVerifyMap?.[key]({ value, isDuplicated: request?.email?.isDuplicate, isRequired: request?.email?.isRequired, }), field: key, }; case 'mobileNo': case 'telephoneNo': return { ...utils.changeProfileFormVerifyMap?.[key]({ value, isRequired: request?.[key]?.isRequired }), field: key, }; case 'detailAddress': return { ...utils.changeProfileFormVerifyMap?.[key]({ value, zipCode: request?.zipCd, isRequired: request?.detailAddress?.isRequired, }), field: key, }; case 'birthday': case 'sex': return { ...utils.changeProfileFormVerifyMap?.[key]({ value, isRequired: request?.[key]?.isRequired }), field: key, }; case 'extraInfo': return { ...utils.changeProfileFormVerifyMap?.[key](request.extraInfo), field: key }; case 'joinTermsAgreements': return { ...utils.changeProfileFormVerifyMap?.[key](request.joinTermsAgreements), field: key }; default: return { ...utils.changeProfileFormVerifyMap?.[key]({ value, isRequired: request?.[key]?.isRequired }), field: key, }; } }); return errors.filter((error) => !error.isValid); }; // eslint-disable-next-line complexity const checkCertificatedValidation = (request) => { // ✅ KCP 본인인증 완료 시 SMS/이메일 검증 건너뛰기 if (kcpAuthManagerMobile.authDataStore.isAuthenticated) { // 인증 유효 시간 체크 (30분) const now = Date.now(); const authAge = now - kcpAuthManagerMobile.authDataStore.timestamp; const THIRTY_MINUTES = 30 * 60 * 1000; if (authAge > THIRTY_MINUTES) { EventManager.fire('MODAL_ALERT_OPEN', { noticeType: 'CAUTION', message: '인증이 만료되었습니다. 휴대폰 본인인증을 다시 진행해주세요.', }); return false; } return true; // KCP 인증 완료, 모든 인증 검증 건너뛰기 } const invalidEmail = request?.emailCertificationStatus === 'INITIAL'; const invalidSmsInternalCertification = request?.smsCertificationStatus === 'INITIAL'; const invalidSmsExternalAuthentication = request?.smsCertificationStatus === 'SMS_AUTHENTICATION' && !request?.ci; if (invalidEmail || invalidSmsInternalCertification || invalidSmsExternalAuthentication) { EventManager.fire('MODAL_ALERT_OPEN', { noticeType: 'CAUTION', message: `${invalidEmail ? '이메일 인증을 진행해 주세요.' : '휴대폰 인증을 진행해 주세요.'}`, onClose: () => { utils.handleFocusEvent({ containerEl, fields: invalidEmail ? 'email' : 'mobileNo' }); }, }); return false; } const message = request.certificatedNumber?.length ? '인증을 진행해주세요.' : '인증번호를 입력해주세요.'; if (request.certificated && (!request.certificated.value || !request.certificated.isValid)) { EventManager.fire('MODAL_ALERT_OPEN', { noticeType: 'CAUTION', message: `${request.certificated.message ?? message}`, onClose: () => { utils.handleFocusEvent({ containerEl, fields: 'certificatedNumber' }); }, }); return false; } return true; }; const CLICK_EVENT_HANDLER_MAP = { // eslint-disable-next-line complexity EMAIL_CERTIFICATION: async (helper) => { const { profileEmailInformation } = helper.getState(); const isInvalidEmail = profileEmailInformation?.email.value === '@' || !profileEmailInformation?.email.isValid; if (isInvalidEmail) { EventManager.fire('MODAL_ALERT_OPEN', { noticeType: 'CAUTION', message: `${profileEmailInformation?.email.message ?? '이메일을 입력해주세요.'}`, }); return; } if (profileEmailInformation?.emailCertificationStatus === 'INITIAL') { await helper.sendCertificationCode(profileEmailInformation.email.value, 'EMAIL'); EventManager.fire('MODAL_ALERT_OPEN', { noticeType: 'SUCCESS', message: '인증번호가 발송되었습니다.', }); } else { EventManager.fire('MODAL_CONFIRM_OPEN', { noticeType: 'WARNING', message: '인증번호를 재발송하시겠습니까?', onConfirm: async () => { await helper.sendCertificationCode(profileEmailInformation.email.value, 'EMAIL'); EventManager.fire('MODAL_ALERT_OPEN', { noticeType: 'SUCCESS', message: '인증번호가 발송되었습니다.', }); }, }); } }, // eslint-disable-next-line complexity SMS_CERTIFICATION: async (helper) => { const { profileSmsInformation } = helper.getState(); const isInvalidMobileNo = !profileSmsInformation?.mobileNo.value || !profileSmsInformation?.mobileNo.isValid; if (isInvalidMobileNo) { EventManager.fire('MODAL_ALERT_OPEN', { noticeType: 'CAUTION', message: `${profileSmsInformation?.mobileNo.message ?? '휴대폰 번호를 입력해주세요.'}`, }); return; } if (profileSmsInformation?.smsCertificationStatus === 'INITIAL') { await helper.sendCertificationCode(profileSmsInformation.mobileNo.value, 'SMS'); EventManager.fire('MODAL_ALERT_OPEN', { noticeType: 'SUCCESS', message: '인증번호가 발송되었습니다.', }); } else { EventManager.fire('MODAL_CONFIRM_OPEN', { noticeType: 'WARNING', message: '인증번호를 재발송하시겠습니까?', onConfirm: async () => { await helper.sendCertificationCode(profileSmsInformation.mobileNo.value, 'SMS'); EventManager.fire('MODAL_ALERT_OPEN', { noticeType: 'SUCCESS', message: '인증번호가 발송되었습니다.', }); }, }); } }, AUTHENTICATION_BY_PHONE: () => { EventManager.fire('OPEN_LAYER_MODAL', { name: 'kcp-sms-authentication', data: { type: 'JOIN_TIME' }, onClose: ({ reason, state }) => { if (reason === 'DID_SUBMIT') { ShopbySkin.EventManager.fire('SUCCESS_AUTHENTICATION_SMS', state); } }, }); }, SEARCH_ZIP_CODE: () => { EventManager.fire('OPEN_LAYER_MODAL', { modalAddClass: 'search-zip-code full-modal', name: 'page-zip-code', onClose: ({ reason, state }) => { if (reason === 'DID_SUBMIT') { ShopbySkin.EventManager.fire('SELECT_ZIP_CODE', { moduleKey: 'profile-optional-information', state, }); } }, }); }, SHOW_TERM_DETAIL: ({ elTarget, helper }) => { const { termsInformation } = helper.getState(); if (!termsInformation) { return; } const mergedTerms = [...termsInformation.terms, ...termsInformation.customTerms]; const selectedTerm = mergedTerms.find((term) => elTarget.getAttribute('shopby-term-id') === term.id.toString()); EventManager.fire('OPEN_LAYER_MODAL', { name: 'term-detail', data: selectedTerm, }); }, // eslint-disable-next-line complexity SUBMIT: async (helper) => { await profileExtraInformationEl?.refetchConfigMemberExtraInfo(); const flattedRequest = flattenRequest(helper); const invalidRequest = checkInvalidProfileForm(flattedRequest); if (invalidRequest?.length) { const [error] = invalidRequest; EventManager.fire('MODAL_ALERT_OPEN', { noticeType: 'CAUTION', message: `${error.message}`, onClose: () => { utils.handleFocusEvent({ containerEl, fields: error.field }); EventManager.fire('INVALID_PROFILE_FORM', { data: invalidRequest, }); }, }); return; } if (!checkCertificatedValidation(flattedRequest)) { return; } const { data } = await helper.submit(flattedRequest); EventManager.fire('MODAL_ALERT_OPEN', { noticeType: 'SUCCESS', message: '회원가입이 완료되었습니다.', onClose: () => { if (data.memberStatus === 'PENDING') { utils.moveTo({ url: constants.PAGE.SIGN_UP_PENDING, replace: true, }); } else { utils.moveTo({ url: constants.PAGE.SIGN_UP_COMPLETE, replace: true, }); } }, }); }, HANDLE_DELETE_IMAGE_BTN_CLICK: (_, event) => { event.preventDefault(); EventManager.fire('MODAL_CONFIRM_OPEN', { noticeType: 'WARNING', message: '첨부파일을 삭제하시겠습니까?', onConfirm: () => { const extraInfoNo = event.target.closest('[shopby-extra-info-no]').getAttribute('shopby-extra-info-no'); profileExtraInformationEl.deleteImage(extraInfoNo); }, }); }, }; const clickEventListener = (event) => { const { target } = event; const action = target.getAttribute('shopby-action'); if (action === 'SHOW_TERM_DETAIL') { CLICK_EVENT_HANDLER_MAP[action]?.({ helper: signUpFormHelper, elTarget: target, }); } else { CLICK_EVENT_HANDLER_MAP[action]?.(signUpFormHelper, event); } }; // KCP 인증 및 성인 인증 시스템 (모바일) const kcpAuthManagerMobile = { authDataStore: { timestamp: null, originalData: null, isAuthenticated: false }, initialize: function() { this.showKcpFields(); this.bindEvents(); }, showKcpFields: function() { const kcpFieldsContainer = document.getElementById('kcp-auth-fields-mobile'); if (kcpFieldsContainer) { kcpFieldsContainer.style.display = 'block'; } }, formatPhoneNumber: function(phone) { if (!phone) return ''; const cleaned = phone.replace(/\D/g, ''); if (cleaned.length === 11) { return cleaned.replace(/(\d{3})(\d{4})(\d{4})/, '$1-$2-$3'); } return phone; }, formatBirthday: function(birthday) { if (!birthday) return ''; if (birthday.length === 8) { return `${birthday.substr(0, 4)}-${birthday.substr(4, 2)}-${birthday.substr(6, 2)}`; } return birthday; }, fillKcpAuthData: function(authData) { // 데이터 저장 this.authDataStore.timestamp = Date.now(); this.authDataStore.originalData = { ...authData }; this.authDataStore.isAuthenticated = true; // 데이터 포맷팅 const formattedPhone = this.formatPhoneNumber(authData.phone); const formattedBirthday = this.formatBirthday(authData.birthday); // UI 업데이트 this.updateUI({ name: authData.name, phone: formattedPhone, birthday: formattedBirthday, mobile: formattedPhone }); // 성인 인증 확인 const isAdultUser = adultVerificationManagerMobile.isAdult(formattedBirthday); adultVerificationManagerMobile.updateSubmitButton(isAdultUser); }, updateUI: function(data) { const fields = [ { id: 'kcp-member-name-mobile', value: data.name, desc: `이름: ${data.name}` }, { id: 'kcp-telephone-no-mobile', value: data.phone, desc: `전화번호: ${data.phone}` }, { id: 'kcp-birthday-mobile', value: data.birthday, desc: `생년월일: ${data.birthday}` }, { id: 'kcp-mobile-no-mobile', value: data.mobile, desc: `휴대폰번호: ${data.mobile}` } ]; fields.forEach(field => { const input = document.getElementById(field.id); const container = input?.closest('.kcp-auth-field-custom'); const description = container?.querySelector('.description'); if (input && container && description) { input.value = field.value; description.textContent = field.desc; container.classList.remove('not-authenticated'); container.classList.add('authenticated'); } }); }, copyToShopbyFields: function() { const fieldMappings = [ { source: 'kcp-member-name-mobile', target: 'div[slot="memberName"] input[name="memberName"]' }, { source: 'kcp-telephone-no-mobile', target: 'div[slot="telephoneNo"] input[name="telephoneNo"]' }, { source: 'kcp-mobile-no-mobile', target: 'div[slot="mobileNo"] input[name="telephoneNo"]' } ]; fieldMappings.forEach(mapping => { const sourceInput = document.getElementById(mapping.source); const targetInput = document.querySelector(mapping.target); if (sourceInput && targetInput && sourceInput.value) { targetInput.value = sourceInput.value; // 이벤트 발생으로 ShopBy 프레임워크에 알림 const inputEvent = new Event('input', { bubbles: true }); const changeEvent = new Event('change', { bubbles: true }); targetInput.dispatchEvent(inputEvent); targetInput.dispatchEvent(changeEvent); } }); // 생년월일 특별 처리 this.copyBirthdayToShopby(); }, copyBirthdayToShopby: function() { const birthdayInput = document.getElementById('kcp-birthday-mobile'); if (!birthdayInput || !birthdayInput.value) return; const birthday = birthdayInput.value; // YYYY-MM-DD 형식 const [year, month, day] = birthday.split('-'); // ShopBy 생년월일 필드 찾기 및 설정 const yearSelect = document.querySelector('div[slot="birthday"] select:nth-child(1)'); const monthSelect = document.querySelector('div[slot="birthday"] select:nth-child(2)'); const daySelect = document.querySelector('div[slot="birthday"] select:nth-child(3)'); if (yearSelect) { yearSelect.value = year; yearSelect.dispatchEvent(new Event('change', { bubbles: true })); } if (monthSelect) { monthSelect.value = month; monthSelect.dispatchEvent(new Event('change', { bubbles: true })); } if (daySelect) { daySelect.value = day; daySelect.dispatchEvent(new Event('change', { bubbles: true })); } }, bindEvents: function() { // SUCCESS_AUTHENTICATION_SMS 이벤트 리스너 EventManager.on('SUCCESS_AUTHENTICATION_SMS', (authData) => { this.fillKcpAuthData(authData); }); } }; const adultVerificationManagerMobile = { initialize: function() { this.showInitialNotice(); }, isAdult: function(birthDate) { if (!birthDate) return false; const birth = new Date(birthDate); const birthYear = birth.getFullYear(); const currentYear = new Date().getFullYear(); // 세는 나이 기준: 현재년도 - 출생년도 >= 20 const koreanAge = currentYear - birthYear + 1; const adultBirthYear = currentYear - 19 // 2007년생부터는 미성년자 (2006년생까지만 성인) return birthYear <= adultBirthYear && koreanAge >= 20; }, updateSubmitButton: function(isAdult) { const submitButtons = document.querySelectorAll('button[shopby-action="SUBMIT"]'); submitButtons.forEach(button => { if (isAdult) { button.classList.add('adult-verified'); } else { button.classList.remove('adult-verified'); this.showAgeWarning(); } }); }, showInitialNotice: function() { const noticeDiv = document.createElement('div'); noticeDiv.className = 'adult-verification-notice'; noticeDiv.innerHTML = '성인 인증을 위해 휴대폰 본인인증을 진행해주세요.'; const submitSection = document.querySelector('.sign-up-form__confirm'); if (submitSection) { submitSection.insertBefore(noticeDiv, submitSection.firstChild); } }, showAgeWarning: function() { // 기존 경고 메시지 제거 const existingWarning = document.querySelector('.age-warning'); if (existingWarning) { existingWarning.remove(); } const warningDiv = document.createElement('div'); warningDiv.className = 'age-warning'; warningDiv.textContent = '⚠️ 20세 미만은 회원가입이 불가능합니다.'; const submitSection = document.querySelector('.sign-up-form__confirm'); if (submitSection) { submitSection.insertBefore(warningDiv, submitSection.firstChild); } }, validateForSubmit: function() { if (!kcpAuthManagerMobile.authDataStore.isAuthenticated) { return { isValid: false, message: '휴대폰 본인인증을 통해 성인 인증을 완료해주세요.' }; } // 시간 만료 검사 (30분) const currentTime = Date.now(); const authTime = kcpAuthManagerMobile.authDataStore.timestamp; const thirtyMinutes = 30 * 60 * 1000; if (currentTime - authTime > thirtyMinutes) { return { isValid: false, message: '본인인증이 만료되었습니다. 다시 인증해주세요.' }; } // 나이 재검증 const originalData = kcpAuthManagerMobile.authDataStore.originalData; if (!originalData || !originalData.birthday) { return { isValid: false, message: '생년월일 정보가 올바르지 않습니다.' }; } const formattedBirthday = kcpAuthManagerMobile.formatBirthday(originalData.birthday); if (!this.isAdult(formattedBirthday)) { return { isValid: false, message: '20세 미만은 회원가입이 불가능합니다.' }; } // CSS 우회 공격 방지 const submitButtons = document.querySelectorAll('button[shopby-action="SUBMIT"]'); const hasAdultVerified = Array.from(submitButtons).some(btn => btn.classList.contains('adult-verified') ); if (!hasAdultVerified) { return { isValid: false, message: '성인 인증이 완료되지 않았습니다.' }; } return { isValid: true }; } }; // 기존 SUBMIT 함수를 확장하여 성인 인증 검사 추가 const originalSubmit = CLICK_EVENT_HANDLER_MAP.SUBMIT; CLICK_EVENT_HANDLER_MAP.SUBMIT = async (helper) => { // 성인 인증 검사 (CSS 우회 방지) const adultVerificationResult = adultVerificationManagerMobile.validateForSubmit(); if (!adultVerificationResult.isValid) { EventManager.fire('MODAL_ALERT_OPEN', { noticeType: 'CAUTION', message: `${adultVerificationResult.message}` }); return; } // KCP 데이터를 ShopBy 필드로 복사 kcpAuthManagerMobile.copyToShopbyFields(); await profileExtraInformationEl?.refetchConfigMemberExtraInfo(); const flattedRequest = flattenRequest(helper); // ✅ KCP 인증 완료 시 ci 값 직접 주입 if (kcpAuthManagerMobile.authDataStore.isAuthenticated) { const storedAuthData = kcpAuthManagerMobile.authDataStore.originalData; if (storedAuthData && storedAuthData.ci) { flattedRequest.ci = storedAuthData.ci; } } const invalidRequest = checkInvalidProfileForm(flattedRequest); if (invalidRequest?.length) { const [error] = invalidRequest; EventManager.fire('MODAL_ALERT_OPEN', { noticeType: 'CAUTION', message: `${error.message}`, onClose: () => { utils.handleFocusEvent({ containerEl, fields: error.field }); EventManager.fire('INVALID_PROFILE_FORM', { data: invalidRequest, }); }, }); return; } if (!checkCertificatedValidation(flattedRequest)) { return; } const { data } = await helper.submit(flattedRequest); EventManager.fire('MODAL_ALERT_OPEN', { noticeType: 'SUCCESS', message: '회원가입이 완료되었습니다.', onClose: () => { if (data.memberStatus === 'PENDING') { utils.moveTo({ url: constants.PAGE.SIGN_UP_PENDING, replace: true, }); } else { utils.moveTo({ url: constants.PAGE.SIGN_UP_COMPLETE, replace: true, }); } }, }); }; // 페이지 로드 시 시스템 초기화 document.addEventListener('DOMContentLoaded', function() { kcpAuthManagerMobile.initialize(); adultVerificationManagerMobile.initialize(); }); containerEl.addEventListener('click', clickEventListener); })();