PageSourceSearch

https://www.campingtalk.me/1790749436508/static/app/module/authManager.js

js campingtalk.me collected 2026-09-30 11:36:14 UTC 18,975 bytes, 573 lines download raw bytes

1/**
2 * authManager.js v0.2
3 *
4 * SNS 로그인 및 인증 관련 공통 기능 제공
5 * v2/memberManager.js에서 마이그레이션된 기능을 포함
6 *
7 * 사용:
8 *   const authManager = new AuthManager({
9 *     authServerHost: 'https://auth.campingtalk.me',
10 *     accessTokenKey: 'ACCESS_TOKEN_KEY',
11 *     refreshTokenKey: 'REFRESH_TOKEN_KEY'
12 *   });
13 */
14
15class AuthManager {
16  constructor(options = {}) {
17    this.authServerHost = options.authServerHost || window.AUTH_SERVER_HOST || 'https://auth.campingtalk.me';
18    this.accessTokenKey = options.accessTokenKey || 'ACCESS_TOKEN_KEY';
19    this.refreshTokenKey = options.refreshTokenKey || 'REFRESH_TOKEN_KEY';
20
21    // 토큰
22    this.accessToken = localStorage.getItem(this.accessTokenKey) || '';
23    this.refreshToken = localStorage.getItem(this.refreshTokenKey) || '';
24
25    // 인증 컨텍스트 (cuid, clientType, serviceName, channelName)
26    this.cuid = options.cuid || '';
27    this.clientType = this._resolveClientType(options.clientType);
28    this.serviceName = options.serviceName || 'booking';
29    this.channelName = this._normalizeChannelName(options.channelName);
30    this.channelId = options.channelId || '';
31    this.channelKey = options.channelKey || '';
32    this.member = {};
33    this.memberName = '';
34    this.isAvailableMemberDetail = false;
35  }
36
37  /**
38   * 토큰 유효시간 확인 (분 단위)
39   */
40  validTime(accessToken) {
41    if (!accessToken) return 0;
42
43    try {
44      const payload = accessToken.split('.')[1];
45      const decodedPayload = atob(payload);
46      const payloadObject = JSON.parse(decodedPayload);
47
48      const exp = new Date(payloadObject.exp * 1000);
49      const now = new Date();
50      const diff = (exp.getTime() - now.getTime()) / (1000 * 60);
51
52      return diff;
53    } catch (e) {
54      return 0;
55    }
56  }
57
58  restoreTokens() {
59    const hasLocalContext = ['_cuid', '_clientType', '_serviceName', '_channelName']
60      .some((key) => !!localStorage.getItem(key));
61    if (hasLocalContext) this.restoreContextFromLocalStorage();
62    else this.restoreContextFromCookie();
63
64    const localAccessToken = localStorage.getItem(this.accessTokenKey);
65    const localRefreshToken = localStorage.getItem(this.refreshTokenKey);
66    if (localAccessToken || localRefreshToken) {
67      this.accessToken = localAccessToken || '';
68      this.refreshToken = localRefreshToken || '';
69      return;
70    }
71    this.accessToken = this._getCookie(this.accessTokenKey);
72    this.refreshToken = this._getCookie(this.refreshTokenKey);
73  }
74
75  reissue(callback) {
76    _get(`${this.authServerHost}/token/reissueAccessToken`, {
77      cuid: this.cuid,
78      refreshToken: this.refreshToken,
79    }, (response) => {
80      if (response.status === RestResponseStatus.OK) {
81        this.saveTokens(response.accessToken, response.refreshToken);
82        if (typeof callback === 'function') callback(response);
83      } else {
84        // 리프레시 토큰마저 만료/무효한 경우 잔존 토큰을 정리한다.
85        this.initAuth();
86      }
87    });
88  }
89
90  scheduleTokenRefresh() {
91    const remainingTime = this.validTime(this.accessToken);
92    if (remainingTime <= 0) return;
93
94    const timeout = 60000 * (remainingTime - (60 * 24 * 7));
95    setTimeout(() => {
96      this.reissue(() => {
97        if (typeof StoreTokenToApp !== 'undefined') {
98          try {
99            StoreTokenToApp.postMessage('e');
100          } catch (e) {}
101        }
102        this.scheduleTokenRefresh();
103      });
104    }, Math.max(0, timeout));
105  }
106
107  /**
108   * 현재 accessToken이 유효한가
109   */
110  isValid() {
111    return this.validTime(this.accessToken) > 0;
112  }
113
114  get isMember() {
115    return this.isValid();
116  }
117
118  async getMemberName() {
119    if (!this.isMember) return;
120    return new Promise((resolve) => {
121      _get('/rest/member/info', {}, async (response) => {
122        const member = await response.data.memberInfoRes;
123        this.member = member;
124        this.memberName = member.memberInfo.memberName;
125        this.isAvailableMemberDetail = member.isAvailableMemberDetail;
126        resolve(member);
127      });
128    });
129  }
130
131  isChannelMember() {
132    return ['benepia', 'etbs'].includes(this.channelName);
133  }
134
135  checkChannelMemberIsAuthorized(options = {}, callback) {
136    const channelId = options.channelId || this.channelId;
137    const channelKey = options.channelKey || this.channelKey;
138    if (!channelId || !channelKey) return;
139    _get(`/rest/domain/mall/channel/${channelId}/channelKey/${channelKey}`, {}, (res) => {
140      const isAuthorized = !!res.data.isAuthorized;
141      if (typeof callback === 'function') callback(isAuthorized, res);
142    });
143  }
144
145
146  goChannelMemberAuthorizationPage(options = {}) {
147    const channelId = options.channelId || this.channelId;
148    const channelKey = options.channelKey || this.channelKey;
149    const serviceName = options.serviceName || `mall_${this.channelName}_promotion`;
150    if (!channelId || !channelKey) return;
151    window.location.href = `${this.authServerHost}/identification/channel/${channelId}/channelKey/${channelKey}/service/${serviceName}/apply`;
152  }
153
154  /**
155   * 인증 초기화 (로그아웃 시 호출)
156   */
157  initAuth() {
158    this.accessToken = '';
159    this.refreshToken = '';
160    localStorage.removeItem(this.accessTokenKey);
161    localStorage.removeItem(this.refreshTokenKey);
162    localStorage.removeItem('_cpt_reservation_token');
163    //localStorage.removeItem('_cuid');
164    //localStorage.removeItem('_clientType');
165    //localStorage.removeItem('_serviceName');
166    //localStorage.removeItem('_channelName');
167    //localStorage.removeItem('_isFirstLogin');
168
169    // 쿠키 삭제
170    this._deleteCookie(this.accessTokenKey);
171    this._deleteCookie(this.refreshTokenKey);
172    this._deleteCookie('user-token');
173    //this._deleteCookie('cuid');
174    //this._deleteCookie('clientType');
175    //this._deleteCookie('serviceName');
176    //this._deleteCookie('channelName');
177
178    // 앱 환경이면 ClearTokenToApp 호출
179    if (typeof ClearTokenToApp !== 'undefined') {
180      try {
181        ClearTokenToApp.postMessage('');
182      } catch (e) {
183        // 무시
184      }
185    }
186  }
187
188  /**
189   * 비회원 인증 초기화 (예약완료 혹은 예약상세 확인 시 호출)
190   */
191  initNonMemberAuth() {
192    localStorage.removeItem('_cpt_reservation_token');
193
194    // 쿠키 삭제
195    this._deleteCookie('user-token');
196  }
197
198  /**
199   * 토큰 저장 (accessToken, refreshToken)
200   */
201  saveTokens(accessToken, refreshToken) {
202    if (accessToken) {
203      this.accessToken = accessToken;
204      localStorage.setItem(this.accessTokenKey, accessToken);
205      this._setCookie(this.accessTokenKey, accessToken, '31104000');
206    }
207    if (refreshToken) {
208      this.refreshToken = refreshToken;
209      localStorage.setItem(this.refreshTokenKey, refreshToken);
210      this._setCookie(this.refreshTokenKey, refreshToken, '31104000');
211    }
212  }
213
214  /**
215   * 인증 컨텍스트 저장/복원
216   */
217  ensureAuthContext(cuid, clientType, serviceName, channelName) {
218    const storedCuid = localStorage.getItem('_cuid');
219    const storedClientType = localStorage.getItem('_clientType');
220    const storedServiceName = localStorage.getItem('_serviceName');
221    const storedChannelName = localStorage.getItem('_channelName');
222    this.cuid = this._isUsableValue(cuid) ? cuid
223      : this._isUsableValue(this.cuid) ? this.cuid
224        : this._isUsableValue(storedCuid) ? storedCuid
225          : this._createCuid();
226    this.clientType = this._resolveClientType(clientType, storedClientType, this.clientType);
227    this.serviceName = this._isUsableValue(serviceName) ? serviceName
228      : this._isUsableValue(storedServiceName) ? storedServiceName
229        : this.serviceName || 'booking';
230    this.channelName = this._normalizeChannelName(
231      this._isUsableValue(channelName) ? channelName
232        : this._isUsableValue(storedChannelName) ? storedChannelName
233          : this.channelName
234    );
235
236    this.saveContextToLocalStorage();
237    this.saveContextToCookie();
238  }
239
240  /**
241   * localStorage에서 인증 컨텍스트 복원
242   */
243  restoreContextFromLocalStorage() {
244    this.cuid = localStorage.getItem('_cuid') || this.cuid || '';
245    this.clientType = localStorage.getItem('_clientType') || this.clientType || '';
246    this.serviceName = localStorage.getItem('_serviceName') || this.serviceName || '';
247    this.channelName = this._normalizeChannelName(localStorage.getItem('_channelName') || this.channelName);
248  }
249
250  /**
251   * 쿠키에서 인증 컨텍스트 복원
252   */
253  restoreContextFromCookie() {
254    this.cuid = this._getCookie('cuid') || this.cuid || '';
255    this.clientType = this._getCookie('clientType') || this.clientType || '';
256    this.serviceName = this._getCookie('serviceName') || this.serviceName || '';
257    this.channelName = this._normalizeChannelName(this._getCookie('channelName') || this.channelName);
258  }
259
260  /**
261   * 인증 컨텍스트를 localStorage에 저장
262   */
263  saveContextToLocalStorage() {
264    if (this.cuid) localStorage.setItem('_cuid', this.cuid);
265    if (this.clientType) localStorage.setItem('_clientType', this.clientType);
266    if (this.serviceName) localStorage.setItem('_serviceName', this.serviceName);
267    if (this.channelName) localStorage.setItem('_channelName', this.channelName);
268  }
269
270  /**
271   * 인증 컨텍스트를 쿠키에 저장
272   */
273  saveContextToCookie() {
274    if (this.cuid) this._setCookie('cuid', this.cuid, 1200);
275    if (this.clientType) this._setCookie('clientType', this.clientType, 1200);
276    if (this.serviceName) this._setCookie('serviceName', this.serviceName, 1200);
277    if (this.channelName) this._setCookie('channelName', this.channelName, 1200);
278  }
279
280  /**
281   * SNS 로그인 URL 구성
282   */
283  buildSnsLoginUrl(type, options = {}) {
284    const params = new URLSearchParams();
285
286    // 기본 파라미터
287    const cuid = options.cuid || this.cuid || '';
288    const clientType = this._resolveClientType(options.clientType, this.clientType);
289    const serviceName = options.serviceName || this.serviceName || 'booking';
290    const channelName = this._normalizeChannelName(options.channelName || this.channelName);
291
292    if (cuid) params.append('cuid', cuid);
293    params.append('clientType', clientType);
294    params.append('serviceName', serviceName);
295    if (channelName) params.append('channelName', channelName);
296    if (type) params.append('type', type);
297    // destinationUri는 pathname + search를 모두 포함하는 완전한 URL
298    if (options.destinationUri) params.append('destinationUri', options.destinationUri);
299
300    return `${this.authServerHost}/auth/snsLogin?${params.toString()}`;
301  }
302
303  /**
304   * SNS 로그인 URL만 생성한다. 호출자가 링크를 표시하거나 반환할 때 사용한다.
305   */
306  getSnsLoginUrl(options = {}) {
307    this.ensureAuthContext(
308      options.cuid,
309      options.clientType,
310      options.serviceName,
311      options.channelName
312    );
313    return this.buildSnsLoginUrl('', options);
314  }
315
316  /**
317   * 로그인 목적지를 채널별 localStorage에 저장한다.
318   * 로그인 과정에서 channelSessionStorage가 유실될 수 있으므로
319   * URL과 query만으로 재진입할 수 있도록 한다.
320   */
321  saveLoginDestination(destinationUrl = null) {
322    if (typeof window === 'undefined') return;
323
324    let urlToSave;
325    try {
326      urlToSave = destinationUrl
327        // destinationUri는 URLSearchParams가 이미 필요한 query 인코딩을 처리하므로
328        // URL 전체에 decodeURIComponent를 적용하지 않는다. query 값의 %26, %3D 등이
329        // 실제 &, =로 변환되면 파라미터 경계가 바뀌어 destination이 손상될 수 있다.
330        ? new URL(destinationUrl, window.location.origin)
331        : new URL(window.location.href);
332    } catch (e) {
333      // 잘못된 URL은 기존 fallback 동작을 유지한다.
334      urlToSave = destinationUrl
335        ? new URL(destinationUrl, window.location.origin)
336        : new URL(window.location.href);
337    }
338
339    const destinationUrlKey = typeof DESTINATION_URL_KEY !== 'undefined'
340      ? DESTINATION_URL_KEY : '_destination_url';
341    const channelName = this._normalizeChannelName(this.channelName || localStorage.getItem('_channelName'));
342    const scope = channelName ? `@${channelName}/` : '';
343
344    // pathname + search를 통합하여 저장 (query 파라미터 포함)
345    const fullUrl = urlToSave.pathname + urlToSave.search;
346    localStorage.setItem(`${scope}${destinationUrlKey}`, fullUrl);
347  }
348
349  /**
350   * SNS 로그인 (location.href 변경)
351   */
352  goSnsLogin(type = '', options = {}) {
353    try {
354      // type 없이 호출하면 SNS provider 선택 화면으로 이동한다.
355      if (typeof type === 'object') {
356        options = type;
357        type = '';
358      }
359
360      this.ensureAuthContext(
361        options.cuid,
362        options.clientType,
363        options.serviceName,
364        options.channelName
365      );
366
367      // destinationUri 유무로 저장소 결정
368      if (options.destinationUri) {
369        this.saveLoginDestination(options.destinationUri);
370      } else {
371        // destinationUri 없으면 currentUrl 저장 (pathname + search 포함)
372        const currentUrlKey = typeof CURRENT_URL_KEY !== 'undefined'
373          ? CURRENT_URL_KEY : '_current_url';
374        const channelName = this._normalizeChannelName(options.channelName || this.channelName);
375        const scope = channelName ? `@${channelName}/` : "";
376        localStorage.setItem(`${scope}${currentUrlKey}`, window.location.pathname + window.location.search);
377      }
378
379      // SNS 로그인 URL로 이동
380      const url = type ? this.buildSnsLoginUrl(type, options) : this.getSnsLoginUrl(options);
381      document.location = url;
382    } catch (e) {
383      console.error('goSnsLogin error:', e);
384    }
385  }
386
387  /**
388   * SNS 로그인 redirect (저장된 context 기반)
389   * 60개 파일 통합용 헬퍼 - 기본값 사용, 필요시 옵션으로 오버라이드
390   */
391  goSnsLoginRedirect(options = {}) {
392    return this.goSnsLogin(options);
393  }
394
395  /**
396   * 본인인증 페이지로 이동
397   */
398  goIdentificationPage(serviceName, options = {}) {
399    try {
400      const params = new URLSearchParams();
401      params.append('cuid', options.cuid || this.cuid);
402      params.append('clientType', options.clientType || this.clientType || 'web');
403      params.append('serviceName', serviceName || 'booking');
404      if (this.accessToken) {
405        params.append('accessToken', this.accessToken);
406      }
407
408      const url = `${this.authServerHost}/identification/member/apply?${params.toString()}`;
409      document.location = url;
410    } catch (e) {
411      console.error('goIdentificationPage error:', e);
412    }
413  }
414
415  /**
416   * 로그아웃
417   */
418  logout(options = {}) {
419    const refreshToken = this.refreshToken;
420
421    if (typeof clientEnv !== 'undefined' && clientEnv.isApp) {
422      // 앱 환경: POST로 로그아웃 처리
423      this._postLogoutToApp(refreshToken, options);
424    } else {
425      // 웹 환경: GET으로 로그아웃 처리
426      this._getLogoutToWeb(refreshToken, options);
427    }
428  }
429
430  /**
431   * 앱 환경 로그아웃
432   */
433  _postLogoutToApp(refreshToken, options) {
434    const url = `${window.location.origin}/rest/member/removePushIdentityAndLogout`;
435    const data = { refreshToken };
436
437    // 기존 앱 호환 계약(_post callback)을 우선 사용한다.
438    if (typeof _post === 'function') {
439      _post('/rest/member/removePushIdentityAndLogout', data, () => {
440        this.initAuth();
441        if (typeof CptLogout !== 'undefined') {
442          try {
443            CptLogout.postMessage(true);
444          } catch (e) {}
445        }
446        setTimeout(() => {
447          window.location.href = options.redirectUrl || '/main';
448        }, 500);
449      });
450      return;
451    }
452
453    if (typeof axios !== 'undefined') {
454      axios.post(url, data)
455        .then(() => {
456          this.initAuth();
457          if (typeof CptLogout !== 'undefined') {
458            try {
459              CptLogout.postMessage(true);
460            } catch (e) {}
461          }
462          setTimeout(() => {
463            window.location.href = options.redirectUrl || '/main';
464          }, 500);
465        })
466        .catch(e => console.error('logout error:', e));
467    }
468  }
469
470  /**
471   * 웹 환경 로그아웃
472   */
473  _getLogoutToWeb(refreshToken, options) {
474    const url = `${this.authServerHost}/token/logout?refreshToken=${refreshToken}`;
475
476    if (typeof fetch !== 'undefined') {
477      fetch(url)
478        .then(() => {
479          this.initAuth();
480          window.location.href = options.redirectUrl || '/main';
481        })
482        .catch(e => console.error('logout error:', e));
483    } else {
484      // fallback
485      this.initAuth();
486      window.location.href = options.redirectUrl || '/main';
487    }
488  }
489
490  /**
491   * 쿠키 설정 헬퍼
492   */
493  _setCookie(name, value, expires) {
494    let cookieString = `${name}=${encodeURIComponent(value)}`;
495
496    if (expires) {
497      if (typeof expires === 'string') {
498        cookieString += `; Max-Age=${expires}`;
499      } else {
500        cookieString += `; Max-Age=${expires}`;
501      }
502    }
503
504    cookieString += '; Path=/';
505    document.cookie = cookieString;
506  }
507
508  /**
509   * 쿠키 가져오기 헬퍼
510   */
511  _getCookie(name) {
512    const nameEQ = `${name}=`;
513    const cookies = document.cookie.split(';');
514
515    for (let i = 0; i < cookies.length; i++) {
516      let cookie = cookies[i].trim();
517      if (cookie.startsWith(nameEQ)) {
518        return decodeURIComponent(cookie.substring(nameEQ.length));
519      }
520    }
521
522    return '';
523  }
524
525  /**
526   * 쿠키 삭제 헬퍼
527   */
528  _deleteCookie(name) {
529    document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
530  }
531
532  _isUsableValue(value) {
533    return value !== undefined && value !== null && value !== '' && value !== 'null' && value !== 'undefined';
534  }
535
536  _resolveClientType(...fallbackValues) {
537    const appType = this._getAppType();
538    if (this._isUsableValue(appType)) return appType;
539
540    for (const value of fallbackValues) {
541      if (this._isUsableValue(value)) return value;
542    }
543    return 'web';
544  }
545
546  _getAppType() {
547    try {
548      const osName = clientEnv.additional.os.name.toLowerCase();
549      if (clientEnv.isApp && osName === 'android') return 'android-app';
550      if (clientEnv.isApp && osName === 'ios') return 'ios-app';
551      return 'web';
552    } catch (e) {
553      return '';
554    }
555  }
556
557  _normalizeChannelName(channelName) {
558    return this._isUsableValue(channelName) ? channelName : 'campingtalk';
559  }
560
561  _createCuid() {
562    const part = (dash) => {
563      const randomPart = (Math.random().toString(16) + '000000000').substring(2, 10);
564      return dash ? `-${randomPart.substring(0, 4)}-${randomPart.substring(4, 8)}` : randomPart;
565    };
566    return part(false) + part(true) + part(true) + part(false);
567  }
568}
569
570// 글로벌 인스턴스 생성 (옵션)
571if (typeof window !== 'undefined' && typeof window.authManager === 'undefined') {
572  // authManager를 전역으로 선언하지 않음 - 각 모듈에서 명시적으로 생성해야 함
573}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.