@toktokhan-dev/react-web
Version:
A utility library for global use in React Web environments.
1,268 lines (1,229 loc) • 65.8 kB
JavaScript
import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
import { useRef, cloneElement, useSyncExternalStore, useCallback, useEffect, useMemo, forwardRef, useState } from 'react';
import { isNotNullish, runIfFn, isNullish } from '@toktokhan-dev/universal';
import { Cookies } from 'react-cookie';
import { useCallbackRef } from '@toktokhan-dev/react-universal';
import isNull from 'lodash/isNull.js';
import isArray from 'lodash/isArray.js';
/******************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
/* global Reflect, Promise, SuppressedError, Symbol, Iterator */
function __rest(s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
}
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
var e = new Error(message);
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
};
/**
* 웹에서 파일 업로드를 트리거 하는 컴포넌트 입니다.
*
* 자식 element 에 by 로 지정한 이벤트를 트리거 하면 display none 처리 되어있는 input[type="file"] 클릭되어 파일 선택 창이 열립니다.
* UploadTrigger 의 props 는 숨겨져 있는 input 의 prop 으로 전달되기때문에,
* UploadTrigger 의 onChange prop 으로 선택된 파일에 접근이 가능합니다.
*
* @category Component
*
* @example
*
* ```tsx
* <UploadTrigger by="onClick" onChange={(e) => console.log(e.target.files?.[0]) }>
* <button>Upload</button>
* </UploadTrigger>
* ```
*
*/
const UploadTrigger = (_a) => {
var { children, by = 'onClick' } = _a, props = __rest(_a, ["children", "by"]);
const inputRef = useRef(null);
const handleClick = () => {
var _a;
(_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.click();
};
return (jsxs(Fragment, { children: [cloneElement(children, { [by]: handleClick }), jsx("input", Object.assign({ style: { display: 'none' }, ref: inputRef, type: "file", value: "" }, props))] }));
};
/**
*
* @category Storage
*
* 알림함수를 관리하고,{@link @toktokhan-dev/react-web#SyncedStorage | `SyncedStorage`} 와 {@link @toktokhan-dev/react-web#useSyncWebStorage | `useSyncWebStorage`} 를 연결 하는 모듈입니다.
*
* @remarks {@link @toktokhan-dev/react-web#useSyncWebStorage | `useSyncWebStorage`}로 부터 리랜더링을 촉발시키는 알림함수 를 받아 관리하고,
* {@link @toktokhan-dev/react-web#SyncedStorage | `SyncedStorage`} 모듈에 알림 함수를 넘겨주어 {@link @toktokhan-dev/react-web#useSyncWebStorage | `useSyncWebStorage`}와 연결시켜주는 역할을 합니다.
*
* @example
* ```ts
* const textStorage = new SyncedStorage<string>("text", localStorage)
* const textStorageConnector = new ReactSyncConnector(textStorage)
*
* // Some Action
* textStorage.set("Hello, World!")
*
* // Some component
* const text = useSyncWebStorage(textStorageConnector) // Wrapping Hook with useSyncWebStorage
* console.log(text) // "Hello, World!"
* ```
*/
class ReactSyncConnector {
/**
* ReactSyncConnector 인스턴스를 생성합니다.
* Storage 모듈에 emitChange 함수를 연결합니다.
*
* @param synced - ReactSynced 인터페이스를 구현한 객체입니다.
* @param serverSynced - 사용자가 제공하는 서버 초기값입니다.
*/
constructor(synced, serverSynced) {
var _a;
/**
* 알림함수를 저장하는 배열입니다.
*/
this.listeners = [];
/**
* {@link https://react.dev/reference/react/useSyncExternalStore | `useSyncExternalStore`}에서 알림함수를 받고, 저장해둡니다.
*
* @param listener - 변경 사항을 처리할 콜백 함수입니다.
* @returns 정리 함수를 반환합니다.
*/
this.subscribe = (listener) => {
if (this.synced)
this.listeners = [...this.listeners, listener];
return () => {
this.listeners = this.listeners.filter((l) => l !== listener);
};
};
/**
* 알림함수가 실행되어, 리랜더링 될 시 조회할 데이터를 넘겨줍니다.
*
* @returns 동기화된 데이터 또는 null을 반환합니다.
*/
this.getSnapshot = () => {
var _a, _b;
return (_b = (_a = this.synced) === null || _a === void 0 ? void 0 : _a.data) !== null && _b !== void 0 ? _b : null;
};
/**
* 서버 데이터의 스냅샷을 반환합니다.
*
* @returns 서버 초깃값 또는 null을 반환합니다.
*/
this.getServerSnapShot = () => {
var _a;
return (_a = this.serverSynced) !== null && _a !== void 0 ? _a : null;
};
/**
* 알림함수를 실행시켜 구독 모듈에 알림이 전달해 리랜더링을 촉발시킵니다.
*/
this.emitChange = () => {
for (const listener of this.listeners) {
listener();
}
};
this.synced = synced;
this.serverSynced = serverSynced;
(_a = this.synced) === null || _a === void 0 ? void 0 : _a.connect(this.emitChange);
}
}
/**
* @category Storage
*
* useSyncExternalStore 의 wrapper 입니다.
* {@link @toktokhan-dev/react-web#ReactSyncConnector | `ReactSyncConnector`}를 통해 외부 스토리지와 동기화를 합니다.
*
* @example
* ```ts
* const textStorage = new SyncedStorage<string>("text", localStorage)
* const textConnector = new ReactSyncConnector(textStorage)
*
* textStorage.set("Hello, World!")
* textStorage.set((prev) => prev + "!")
*
* textStorage.get() // "Hello, World!!"
*
* const text = useSyncWebStorage(textConnector)
*
* console.log(text) // "Hello, World!!"
* ```
*/
const useSyncWebStorage = (connector) => {
return useSyncExternalStore(connector.subscribe, connector.getSnapshot, connector.getServerSnapShot);
};
/**
* @category Storage
*
* 데이터를 동기화하는 ReactSynced 클래스입니다. 데이터가 업데이트될 때 리스너 함수를 호출합니다.
*/
class ReactSynced {
constructor() {
/**
* 동기화된 데이터입니다.
*/
this._data = null;
/**
* 리스너 함수입니다.
*/
this.listener = null;
/**
* 데이터가 업데이트될 때 호출될 리스너 함수를 연결합니다.
* @param listener - 호출될 리스너 함수입니다.
*/
this.connect = (listener) => {
this.listener = listener;
};
/**
* 리스너 함수를 연결 해제합니다.
*/
this.unConnect = () => {
this.listener = null;
};
}
/**
* 동기화된 데이터를 가져옵니다.
* @returns T 타입의 동기화된 데이터 또는 데이터가 설정되지 않은 경우 null을 반환합니다.
*/
get data() {
return this._data;
}
/**
* 동기화된 데이터를 설정하고 리스너를 트리거합니다.
* @param data - 동기화할 데이터입니다.
*/
set data(data) {
var _a;
this._data = data;
(_a = this.listener) === null || _a === void 0 ? void 0 : _a.call(this);
}
}
/**
* @category Storage
*
* 데이터를 쿠키에 동기화하는 SyncedCookie 클래스입니다. 데이터가 업데이트될 때 리스너 함수를 호출합니다.
* {@link @toktokhan-dev/react-web#ReactSyncConnector | `ReactSyncConnector`}와 연결하여 사용합니다.
*
* @example
* ```ts
* const cookieStorage = new SyncedCookie<string>("cookie-key", { path: '/' })
* const cookieConnector = new ReactSyncConnector(cookieStorage)
*
* cookieStorage.set("Hello, Cookie!")
* cookieStorage.set((prev) => prev + "!")
*
* cookieStorage.get() // "Hello, Cookie!!"
* cookieStorage.remove()
*
* cookieStorage.get() // null
* ```
*
*/
class SyncedCookie extends ReactSynced {
/**
* SyncedCookie 인스턴스를 생성합니다.
* 데이터를 저장할 키와 쿠키 옵션을 받습니다.
*
* @param key - 데이터를 저장할 키입니다.
* @param options - 쿠키 옵션입니다. (default: { secure: true, sameSite: 'strict', path: '/' })
*/
constructor(key, options) {
super();
this.key = key;
this.storage = new Cookies();
this.defaultOptions = {
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
path: '/',
};
/**
* 쿠키에서 데이터를 가져옵니다.
* 저장된 json 데이터를 parse 한 후 가져옵니다.
*/
this.get = () => {
const item = this.storage.get(this.key);
if (!isNotNullish(item))
return null;
return item;
};
/**
* 쿠키에 데이터를 저장합니다.
* 저장할 데이터 혹은 함수를 받아서 데이터를 저장합니다.
*
* @param data - 저장할 데이터 혹은 데이터를 반환하는 함수입니다.
* @param options - 쿠키 옵션입니다.
*/
this.set = (data, options) => {
this.data = runIfFn(data, this.data);
const finalOptions = Object.assign({}, this.defaultOptions, options);
this.storage.set(this.key, this.data, finalOptions);
};
/**
* 쿠키에 저장된 데이터를 삭제합니다.
*
* @param options - 쿠키 옵션입니다.
*/
this.remove = (options) => {
this.data = null;
const finalOptions = Object.assign({}, this.defaultOptions, options);
this.storage.remove(this.key, finalOptions);
};
if (options) {
this.defaultOptions = Object.assign({}, this.defaultOptions, options);
}
this.key = key;
this.data = this.get();
}
}
/**
* @category Storage
*
* 데이터를 동기화하는 SyncedStorage 클래스입니다. 데이터가 업데이트될 때 리스너 함수를 호출합니다.
* {@link @toktokhan-dev/react-web#ReactSyncConnector | `ReactSyncConnector`} 와 연결하여 사용합니다.
*
* @example
* ```ts
* const textStorage = new SyncedStorage<string>("text", localStorage)
* const textConnector = new ReactSyncConnector(textStorage)
*
* textStorage.set("Hello, World!")
* textStorage.set((prev) => prev + "!")
*
* textStorage.get() // "Hello, World!!"
* textStorage.remove()
*
* textStorage.get() // null
* ```
*
*/
class SyncedStorage extends ReactSynced {
/**
* SyncedStorage 인스턴스를 생성합니다.
* 데이터를 저장할 키와 Storage 객체를 받습니다.
*
* 생성될때, storage 이벤트가 등록되며 다른 브라우저에서의 change event를 감지하여, 최신값을 가져옵니다.
*
* @param key - 데이터를 저장할 키입니다.
* @param storage - 데이터를 저장할 Storage 객체입니다.
*/
constructor(key, storage) {
super();
this.key = key;
this.storage = storage;
/**
* Storage에 저장된 json 데이터를 parse 한 후 가져옵니다.
*/
this.get = () => {
const item = this.storage.getItem(this.key);
if (item === null)
return null;
return JSON.parse(item);
};
/**
* Storage에 데이터를 저장합니다.
* 저장할 데이터 혹은 함수를 받아서 데이터를 저장합니다.
*/
this.set = (data) => {
this.data = runIfFn(data, this.data);
this.storage.setItem(this.key, JSON.stringify(this.data));
};
/**
* Storage에 저장된 데이터를 삭제합니다.
*/
this.remove = () => {
this.data = null;
this.storage.removeItem(this.key);
};
this.key = key;
this.storage = storage;
this.data = this.get();
typeof window !== 'undefined' &&
window.addEventListener('storage', (data) => {
if (this.key === data.key) {
this.data = this.get();
}
});
}
}
var _a;
/**
* @category Storage
*
* 동기화된 스토리지를 생성하는 팩토리 역할을 합니다.
* 해당 클래스의 각 method 는 {@link @toktokhan-dev/react-web#ReactSyncConnector | `ReactSyncConnector`}와 {@link @toktokhan-dev/react-web#SyncedStorage | `SyncedStorage`}를
* 동시에 생성해줍니다.
*
* @example
* ```ts
* type TokenType = {
* access: string
* refresh: string
* }
* const { storage, connector } = SyncedStorageFactory.createLocal<TokenType>('token')
*
* storage.set({ access: 'access', refresh: 'refresh' })
*
* const token = useWebStorage(connector)
* ```
*/
class SyncedStorageFactory {
}
_a = SyncedStorageFactory;
/**
* 로컬 스토리지를 생성합니다.
* @param key 스토리지 키
* @returns 생성된 스토리지와 커넥터 객체
*/
SyncedStorageFactory.createLocal = (key) => {
const store = typeof window === 'undefined' ? null : localStorage;
return _a.create(key, store); //
};
/**
* 세션 스토리지를 생성합니다.
* @param key 스토리지 키
* @returns 생성된 스토리지와 커넥터 객체
*/
SyncedStorageFactory.createSession = (key) => {
const store = typeof window === 'undefined' ? null : sessionStorage;
return _a.create(key, store);
};
/**
* 쿠키를 생성합니다.
* @param key 쿠키 키
* @param store 쿠키 객체
* @returns 생성된 쿠키와 커넥터 객체
*/
SyncedStorageFactory.createCookie = (key, options) => {
const storage = new SyncedCookie(key, options);
const connector = new ReactSyncConnector(storage);
return { storage, connector };
};
/**
* 스토리지를 생성합니다.
* @param key 스토리지 키
* @param store 스토리지 객체
* @returns 생성된 스토리지와 커넥터 객체
*/
SyncedStorageFactory.create = (key, store) => {
const storage = store ? new SyncedStorage(key, store) : null;
const connector = new ReactSyncConnector(storage);
return { storage, connector };
};
/**
* 반환한 targetRef를 사용하여 대상 컴포넌트에 intersectionObserver 이벤틀르 주기 위한 hooks입니다.
*
* hooks 선언시 props 설정이 가능하며, 화면에 표출되는 조건에 따라 onVisible, onHidden 함수가 실행됩니다.
*
* @category Hooks
*
* @typeParam T - 배열 요소의 타입
* @typeParam K - Map의 키 타입
*
* @param onVisible - targetRef 요소가 보여질 때 실행할 함수
* @param onHidden - targetRef 요소가 보이지 않을 때 실행할 함수
* @param options - targetRef에 설정할 intersection observer 옵션
*
* @returns intersection Observer 이벤트가 할당된 Element useRef
*
* @example
*
* ```tsx
*
* const { targetRef } = useIntersectionObserver(
* {
* onVisible: () => onVisibleLast(),
* onHidden: () => onHiddenLast(),
* options: {
* threshold: 0.1,
* },
* },
* [],
* );
* ...
*
* return (
* <LastItem ref={targetRef} w="100%" />
* )
*
* ```
*
*/
const useIntersectionObserver = ({ onVisible, onHidden, options = { threshold: 0.5 }, }, deps) => {
const targetRef = useRef(null);
const onVisibleRef = useCallbackRef(onVisible || (() => { }));
const onHiddenRef = useCallbackRef(onHidden || (() => { }));
const isPassedVisibleCallback = !!onVisible;
const isPassedHiddenCallback = !!onHidden;
const handleIntersection = useCallback(([entry], observer) => {
if (entry.isIntersecting && isPassedVisibleCallback) {
onVisibleRef(entry, observer);
}
if (!entry.isIntersecting && isPassedHiddenCallback) {
onHiddenRef(entry, observer);
}
}, [
onHiddenRef,
onVisibleRef,
isPassedVisibleCallback,
isPassedHiddenCallback,
]);
useEffect(() => {
const observer = new IntersectionObserver(handleIntersection, {
root: options.root,
rootMargin: options.rootMargin,
threshold: options.threshold,
});
const target = targetRef.current;
if (target) {
observer.observe(target);
}
return () => {
if (target) {
observer.unobserve(target);
}
};
}, [
handleIntersection, //
options.root,
options.rootMargin,
options.threshold,
...deps,
]);
return { targetRef };
};
// https://developers.naver.com/docs/login/bi/bi.md
const SOCIALS = {
kakao: {
icon: '../public/kakao.svg',
ko: '카카오 로그인',
en: 'Login with Kakao',
style: {
light: {
bg: '#FEE500',
border: 'transparent',
label: 'rgba(0, 0, 0, 0.85)',
icon: '#000000',
},
dark: {
bg: '#FEE500',
border: 'transparent',
label: 'rgba(0, 0, 0, 0.85)',
icon: '#000000',
},
},
bg: '#FEE500',
label: 'rgba(0, 0, 0, 0.85)',
},
naver: {
icon: '../public/apple.svg',
ko: '네이버 로그인',
en: 'Login with Naver',
style: {
light: {
bg: '#03C75A',
border: 'transparent',
label: '#ffffff',
icon: '#ffffff',
},
dark: {
bg: '#47474A',
border: '#585858',
label: '#A4A4A7',
icon: '#03C75A',
},
},
bg: '#03C75A',
label: '#ffffff',
},
facebook: {
icon: '../public/facebook.svg',
ko: '페이스북 로그인',
en: 'Login with Facebook',
style: {
light: {
bg: '#1877F2',
border: 'transparent',
label: '#ffffff',
icon: '#ffffff',
},
dark: {
bg: '#1877F2',
border: 'transparent',
label: '#ffffff',
icon: '#ffffff',
},
},
bg: '#1877F2',
label: '#ffffff',
},
google: {
icon: '../public/google.svg',
ko: '구글 로그인',
en: 'Login with Google',
bg: '#ffffff',
label: '#000000',
style: {
light: {
bg: '#ffffff',
border: 'transparent',
label: '#000000',
icon: '',
},
dark: {
bg: '#ffffff',
border: 'transparent',
label: '#000000',
icon: '',
},
},
},
apple: {
icon: '../public/apple.svg',
ko: '애플 로그인',
en: 'Login with Apple',
bg: '#000000',
label: '#ffffff',
style: {
light: {
bg: '#000000',
border: 'transparent',
label: '#ffffff',
icon: '#ffffff',
},
dark: {
bg: '#ffffff',
border: 'transparent',
label: '#000000',
icon: '#000000',
},
},
},
};
/**
* 소셜 로그인 아이콘 버튼을 생성합니다.
*
* @param props 컴포넌트가 받는 속성들
* @returns 렌더링된 아이콘 버튼
*/
const IconButton = (_a) => {
var { colorMode = 'dark', socialType, lang = 'ko', variant = 'full', icon, label, style, iconStyle, labelStyle, onClick } = _a, props = __rest(_a, ["colorMode", "socialType", "lang", "variant", "icon", "label", "style", "iconStyle", "labelStyle", "onClick"]);
const handleClick = useCallback((e) => {
e.preventDefault();
return onClick === null || onClick === void 0 ? void 0 : onClick(e);
}, [onClick]);
const variantStyle = useMemo(() => {
switch (variant) {
case 'full':
return {
icon: {
borderRadius: '9999px',
},
label: {
flex: 1,
},
};
case 'rounded':
return {
icon: {
borderRadius: '6px',
},
label: {},
};
case 'square':
return {
icon: {
borderRadius: '0px',
},
label: {},
};
default:
return { icon: {}, label: {} };
}
}, [variant]);
const clonedIcon = useMemo(() => {
var _a;
return cloneElement(icon, Object.assign(Object.assign({ color: (_a = SOCIALS[socialType].style[colorMode]) === null || _a === void 0 ? void 0 : _a.icon, width: '20px', height: '20px' }, icon.props), iconStyle));
}, [icon, socialType, colorMode, iconStyle]);
return (jsxs("a", Object.assign({ onClick: handleClick, href: "#javascript", id: `${socialType}-login-button`, "aria-label": `${socialType}-login-button`, style: Object.assign({ padding: 0, cursor: 'pointer', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: '4px', width: '51px' }, style) }, props, { children: [jsx("div", { "aria-label": "icon-container", style: Object.assign(Object.assign(Object.assign({ display: 'flex', alignItems: 'center', justifyContent: 'center', width: '100%', aspectRatio: '1 / 1', border: 'none', borderRadius: '6px', backgroundColor: SOCIALS[socialType].style[colorMode].bg, color: SOCIALS[socialType].style[colorMode].label }, variantStyle.icon), iconStyle), { boxShadow: '0px 0px 12px 0px rgba(0, 0, 0, 0.10)' }), children: clonedIcon }), !isNull(label) && (jsx("label", { style: Object.assign(Object.assign({ fontSize: '10px', lineHeight: '1.5', fontWeight: 700, whiteSpace: 'nowrap' }, variantStyle.label), labelStyle), children: label || SOCIALS[socialType][lang] }))] })));
};
const AppleIcon = forwardRef(function AppleIcon(props, ref) {
return (jsx("svg", Object.assign({ ref: ref, width: "21", height: "24", viewBox: "0 0 21 24", fill: "none", xmlns: "http://www.w3.org/2000/svg" }, props, { children: jsx("path", { d: "M19.7371 18.8717C19.3893 19.6752 18.9776 20.4149 18.5005 21.0949C17.8503 22.022 17.3179 22.6637 16.9076 23.0201C16.2716 23.605 15.5901 23.9045 14.8604 23.9216C14.3365 23.9216 13.7048 23.7725 12.9693 23.4701C12.2315 23.1691 11.5535 23.0201 10.9335 23.0201C10.2833 23.0201 9.58589 23.1691 8.83997 23.4701C8.09292 23.7725 7.49111 23.9301 7.03098 23.9457C6.3312 23.9755 5.6337 23.6674 4.93748 23.0201C4.49311 22.6325 3.93729 21.968 3.27145 21.0268C2.55705 20.0216 1.96971 18.856 1.50959 17.5272C1.0168 16.0919 0.769775 14.702 0.769775 13.3564C0.769775 11.815 1.10284 10.4856 1.76996 9.37154C2.29426 8.4767 2.99176 7.77082 3.86474 7.25262C4.73772 6.73443 5.68098 6.47036 6.69678 6.45347C7.2526 6.45347 7.98148 6.6254 8.88725 6.96329C9.79047 7.30231 10.3704 7.47424 10.6247 7.47424C10.8148 7.47424 11.4591 7.27321 12.5512 6.87243C13.5841 6.50075 14.4558 6.34685 15.1699 6.40747C17.105 6.56364 18.5587 7.32645 19.5256 8.70073C17.7949 9.74932 16.9389 11.218 16.9559 13.1021C16.9715 14.5697 17.5039 15.7909 18.5502 16.7606C19.0244 17.2106 19.554 17.5584 20.1431 17.8055C20.0154 18.176 19.8805 18.5309 19.7371 18.8717ZM15.2991 1.40782C15.2991 2.55809 14.8789 3.63208 14.0412 4.62616C13.0304 5.80793 11.8077 6.49081 10.4819 6.38305C10.465 6.24506 10.4552 6.09982 10.4552 5.9472C10.4552 4.84295 10.9359 3.66119 11.7896 2.69493C12.2158 2.2057 12.7578 1.79891 13.4151 1.47441C14.071 1.15475 14.6915 0.977966 15.275 0.947693C15.292 1.10146 15.2991 1.25525 15.2991 1.40781V1.40782Z", fill: "currentColor" }) })));
});
const FacebookIcon = forwardRef(function FacebookIcon(props, ref) {
return (jsx("svg", Object.assign({ ref: ref, width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", xmlns: "http://www.w3.org/2000/svg" }, props, { children: jsx("path", { d: "M23.9564 12.3069C23.9564 5.9556 18.8077 0.806879 12.4564 0.806879C6.10514 0.806879 0.956421 5.9556 0.956421 12.3069C0.956421 18.0469 5.1618 22.8044 10.6595 23.6672V15.6311H7.73962V12.3069H10.6595V9.77329C10.6595 6.8911 12.3764 5.29907 15.0033 5.29907C16.2615 5.29907 17.5775 5.52368 17.5775 5.52368V8.35375H16.1274C14.6988 8.35375 14.2533 9.24022 14.2533 10.1497V12.3069H17.4427L16.9329 15.6311H14.2533V23.6672C19.751 22.8044 23.9564 18.0469 23.9564 12.3069Z", fill: "white" }) })));
});
forwardRef(function GithubIcon(props, ref) {
return (jsx("svg", Object.assign({ ref: ref, width: "25", height: "24", viewBox: "0 0 25 24", fill: "none", xmlns: "http://www.w3.org/2000/svg" }, props, { children: jsx("path", { d: "M12.5001 2.24683C6.97791 2.24683 2.5 6.72391 2.5 12.2469C2.5 16.6652 5.36533 20.4137 9.33867 21.736C9.83844 21.8285 10.0219 21.519 10.0219 21.2549C10.0219 21.0164 10.0126 20.2287 10.0083 19.3931C7.22631 19.998 6.63927 18.2132 6.63927 18.2132C6.18437 17.0573 5.52894 16.75 5.52894 16.75C4.62163 16.1293 5.59733 16.1421 5.59733 16.1421C6.60151 16.2126 7.13026 17.1726 7.13026 17.1726C8.02217 18.7014 9.46965 18.2594 10.0403 18.0039C10.1301 17.3576 10.3892 16.9164 10.6752 16.6667C8.45405 16.4138 6.11913 15.5564 6.11913 11.7246C6.11913 10.6328 6.50977 9.74076 7.14947 9.04045C7.04564 8.78858 6.70335 7.77148 7.24635 6.39404C7.24635 6.39404 8.08609 6.12528 9.99708 7.41909C10.7948 7.19752 11.6502 7.0864 12.5001 7.08259C13.3499 7.0864 14.2061 7.19752 15.0052 7.41909C16.9139 6.12528 17.7525 6.39404 17.7525 6.39404C18.2968 7.77148 17.9544 8.78858 17.8505 9.04045C18.4917 9.74076 18.8797 10.6328 18.8797 11.7246C18.8797 15.5655 16.5403 16.4112 14.3135 16.6587C14.6722 16.9691 14.9918 17.5776 14.9918 18.5106C14.9918 19.8486 14.9802 20.9255 14.9802 21.2549C14.9802 21.521 15.1602 21.8328 15.6671 21.7346C19.6383 20.4109 22.5 16.6637 22.5 12.2469C22.5 6.72391 18.0227 2.24683 12.5001 2.24683ZM6.24536 16.4922C6.22333 16.5418 6.14517 16.5567 6.07396 16.5226C6.00143 16.49 5.96069 16.4223 5.98421 16.3724C6.00574 16.3213 6.08406 16.307 6.15643 16.3413C6.22913 16.3739 6.27053 16.4423 6.24536 16.4922ZM6.73725 16.9311C6.68956 16.9753 6.59633 16.9547 6.53307 16.8849C6.46766 16.8151 6.4554 16.7219 6.50376 16.677C6.55294 16.6328 6.64336 16.6535 6.70893 16.7232C6.77434 16.7938 6.78709 16.8864 6.73725 16.9311ZM7.07471 17.4926C7.01344 17.5352 6.91325 17.4953 6.85132 17.4064C6.79004 17.3174 6.79004 17.2108 6.85264 17.1681C6.91474 17.1253 7.01344 17.1638 7.0762 17.252C7.1373 17.3424 7.1373 17.4491 7.07471 17.4926ZM7.64542 18.143C7.59061 18.2035 7.47386 18.1872 7.38841 18.1048C7.30098 18.0241 7.27663 17.9097 7.33161 17.8492C7.38709 17.7886 7.5045 17.8057 7.59061 17.8875C7.67738 17.968 7.70387 18.0832 7.64542 18.143ZM8.38301 18.3626C8.35883 18.4409 8.24639 18.4765 8.13312 18.4432C8.02002 18.4089 7.946 18.3172 7.96885 18.238C7.99237 18.1592 8.1053 18.1221 8.2194 18.1577C8.33234 18.1918 8.40652 18.2829 8.38301 18.3626ZM9.22242 18.4557C9.22523 18.5382 9.12919 18.6066 9.01029 18.6081C8.89073 18.6107 8.79402 18.544 8.79269 18.4628C8.79269 18.3795 8.88659 18.3118 9.00615 18.3098C9.12505 18.3075 9.22242 18.3737 9.22242 18.4557ZM10.047 18.4241C10.0613 18.5046 9.97865 18.5872 9.86058 18.6092C9.7445 18.6304 9.63702 18.5807 9.62229 18.5009C9.60788 18.4185 9.692 18.3358 9.80792 18.3145C9.92616 18.2939 10.032 18.3423 10.047 18.4241Z", fill: "#161614" }) })));
});
const GoogleIcon = forwardRef(function GoogleIcon(props, ref) {
return (jsxs("svg", Object.assign({ ref: ref, width: "25", height: "25", viewBox: "0 0 25 25", fill: "none", xmlns: "http://www.w3.org/2000/svg" }, props, { children: [jsx("rect", { width: "24", height: "24", transform: "translate(0.456421 0.342346)", fill: "white" }), jsx("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M23.4964 12.6037C23.4964 11.7883 23.4232 11.0042 23.2873 10.2515H12.4564V14.6999H18.6455C18.3789 16.1374 17.5687 17.3553 16.3507 18.1708V21.0562H20.0673C22.2419 19.0542 23.4964 16.106 23.4964 12.6037Z", fill: "#4285F4" }), jsx("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M12.4564 23.8422C15.5614 23.8422 18.1646 22.8124 20.0673 21.056L16.3507 18.1706C15.3209 18.8606 14.0037 19.2683 12.4564 19.2683C9.46116 19.2683 6.92593 17.2453 6.02161 14.5272H2.17957V17.5067C4.07184 21.2651 7.96093 23.8422 12.4564 23.8422Z", fill: "#34A853" }), jsx("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M6.02165 14.5274C5.79165 13.8374 5.66097 13.1004 5.66097 12.3424C5.66097 11.5844 5.79165 10.8474 6.02165 10.1574V7.17786H2.1796C1.40074 8.73036 0.956421 10.4867 0.956421 12.3424C0.956421 14.1981 1.40074 15.9544 2.1796 17.5069L6.02165 14.5274Z", fill: "#FBBC05" }), jsx("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M12.4564 5.41621C14.1448 5.41621 15.6607 5.99644 16.8525 7.13598L20.1509 3.83757C18.1593 1.98189 15.5562 0.842346 12.4564 0.842346C7.96093 0.842346 4.07184 3.41939 2.17957 7.1778L6.02161 10.1573C6.92593 7.43916 9.46116 5.41621 12.4564 5.41621Z", fill: "#EA4335" })] })));
});
const KakaoIcon = forwardRef(function KakaoIcon(props, ref) {
return (jsxs("svg", Object.assign({ ref: ref, width: "36", height: "36", viewBox: "0 0 36 36", fill: "none", xmlns: "http://www.w3.org/2000/svg" }, props, { children: [jsx("g", { clipPath: "url(#clip0_1_126)", children: jsx("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M17.9999 1.2C8.05823 1.2 -0.00012207 7.42591 -0.00012207 15.1046C-0.00012207 19.88 3.11669 24.0899 7.86293 26.5939L5.86593 33.889C5.6895 34.5336 6.42671 35.0474 6.99281 34.6738L15.7466 28.8964C16.4853 28.9677 17.236 29.0093 17.9999 29.0093C27.9408 29.0093 35.9997 22.7836 35.9997 15.1046C35.9997 7.42591 27.9408 1.2 17.9999 1.2Z", fill: "black" }) }), jsx("defs", { children: jsx("clipPath", { id: "clip0_1_126", children: jsx("rect", { width: "35.9999", height: "36", fill: "white" }) }) })] })));
});
const NaverIcon = forwardRef(function NaverIcon(props, ref) {
return (jsxs("svg", Object.assign({ ref: ref, width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", xmlns: "http://www.w3.org/2000/svg" }, props, { children: [jsx("g", { clipPath: "url(#clip0_122_131)", children: jsx("path", { d: "M13.5614 10.7033L6.14609 0H0V20H6.43861V9.295L13.8539 20H20V0H13.5614V10.7033Z", fill: "currentColor" }) }), jsx("defs", { children: jsx("clipPath", { id: "clip0_122_131", children: jsx("rect", { width: "20", height: "20", fill: "currentColor" }) }) })] })));
});
/**
* @category Socials/Google
*
* 구글 아이콘 버튼 UI 컴포넌트입니다.
* {@link @toktokhan-dev/react-web#IconButton | `IconButton`} 컴포넌트를 기반으로 하며, 구글 아이콘 및 버튼 스타일링이 가능합니다.
*
* @param props - IconButtonProps에서 'socialType'과 'icon'을 제외한 속성들을 상속받습니다.
* @returns 구글 아이콘 버튼 컴포넌트를 반환합니다.
*/
const GoogleIconButton = (_a) => {
var { style, iconStyle } = _a, props = __rest(_a, ["style", "iconStyle"]);
return (jsx(IconButton, Object.assign({ socialType: "google", icon: jsx(GoogleIcon, {}), style: Object.assign({}, style) }, props)));
};
/**
* 소셜 로그인 버튼을 생성합니다.
*
* @param props 컴포넌트가 받는 속성들
* @returns 렌더링된 버튼
*/
const FullButton = (_a) => {
var { colorMode = 'dark', socialType, align = 'center', variant = 'square', lang = 'ko', label, style, icon, iconStyle, labelStyle, onClick } = _a, props = __rest(_a, ["colorMode", "socialType", "align", "variant", "lang", "label", "style", "icon", "iconStyle", "labelStyle", "onClick"]);
const handleClick = useCallback((e) => {
e.preventDefault();
return onClick === null || onClick === void 0 ? void 0 : onClick(e);
}, [onClick]);
const variantStyle = useMemo(() => {
switch (variant) {
case 'rounded':
return {
borderRadius: '6px',
};
case 'square':
return {};
default:
return {};
}
}, [variant]);
const alignStyle = useMemo(() => {
switch (align) {
case 'left':
return {
container: {
justifyContent: 'space-between',
},
label: {
flex: 1,
textAlign: 'center',
},
};
case 'center':
return {
container: {
justifyContent: 'center',
gap: '8px',
},
label: {},
};
default:
return { container: {}, label: {} };
}
}, [align]);
const clonedIcon = useMemo(() => {
var _a;
return cloneElement(icon, Object.assign(Object.assign({ width: '18px', height: '18px', color: (_a = SOCIALS[socialType].style[colorMode]) === null || _a === void 0 ? void 0 : _a.icon }, icon.props), iconStyle));
}, [icon, socialType, colorMode, iconStyle]);
return (jsxs("a", Object.assign({ href: "#javascript", onClick: handleClick, id: `${socialType}-login-button`, "aria-label": `${socialType}-login-button`, style: Object.assign(Object.assign(Object.assign({ width: '100%', height: '100%', display: 'flex', flex: 1, alignItems: 'center', border: 'none', fontSize: '15px', lineHeight: '1.5', fontWeight: 700, padding: '11px 14px', backgroundColor: SOCIALS[socialType].style[colorMode].bg, color: SOCIALS[socialType].style[colorMode].label }, variantStyle), alignStyle.container), style) }, props, { children: [clonedIcon, !isNull(label) && (jsx("label", { style: Object.assign(Object.assign({ whiteSpace: 'nowrap' }, alignStyle.label), labelStyle), children: label || SOCIALS[socialType][lang] }))] })));
};
/**
* @category Socials/Google
*
* 구글 버튼 UI 컴포넌트입니다.
* {@link @toktokhan-dev/react-web#FullButton | `FullButton`} 컴포넌트를 기반으로 하여, 구글 아이콘과 스타일, 레이블을 포함합니다.
*
* @param props -FullButtonProps에서 'socialType'과 'icon'을 제외한 속성들을 상속받습니다.
* @returns 구글 버튼 컴포넌트를 반환합니다.
*/
const GoogleButton = (_a) => {
var { style } = _a, props = __rest(_a, ["style"]);
return (jsx(FullButton, Object.assign({ socialType: "google", icon: jsx(GoogleIcon, {}), style: Object.assign({ boxShadow: '0px 0px 12px 0px rgba(0, 0, 0, 0.10)' }, style) }, props)));
};
const openPopup = (url) => {
window.open(url, '_blank', 'width=500,height=600');
};
const pushToUrl = (url) => {
window.location.href = url;
};
const serialize = (params) => {
const query = Object.entries(params)
.filter(([, value]) => value !== undefined)
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`)
.join('&');
return query;
};
/**
* SocialOauthInit 클래스는 소셜 로그인 초기화를 담당합니다.
*/
class SocialOauthInit {
/**
* 생성자 함수에서는 클라이언트 ID를 받아 초기화합니다.
* @param clientID - 소셜 로그인을 위한 클라이언트 ID
*/
constructor(clientID) {
this.clientID = clientID;
this.oAuthBaseUrl = '';
this.clientID = clientID;
}
/**
* createOauthUrl 메서드는 OAuth 인증 URL을 생성합니다.
* @param params - OAuth 인증 요청에 필요한 파라미터
* @returns 생성된 OAuth 인증 URL
*/
createOauthUrl(params) {
return `${this.oAuthBaseUrl}?${serialize(params)}`;
}
}
/**
* encodeOAuthState 메서드는 OAuth 상태를 인코딩합니다.
* @param type - 소셜 로그인 타입
* @param returnUrl - 로그인 후 리다이렉트 될 URL
* @returns 인코딩된 OAuth 상태
*/
SocialOauthInit.encodeOAuthState = (state) => {
return Buffer.from(JSON.stringify(state), 'utf8').toString('base64');
};
/**
* decodeOAuthState 메서드는 인코딩된 OAuth 상태를 디코딩합니다.
* @param state - 인코딩된 OAuth 상태
* @returns 디코딩된 OAuth 상태. 디코딩에 실패하면 null을 반환합니다.
*/
SocialOauthInit.decodeOAuthState = (state) => {
try {
const parsed = JSON.parse(Buffer.from(state, 'base64').toString('utf8'));
return parsed;
}
catch (e) {
console.error('Failed to decode OAuth state', e);
return null;
}
};
const GOOGLE_AUTH_SCOPE = {
email: 'https://www.googleapis.com/auth/userinfo.email',
profile: 'https://www.googleapis.com/auth/userinfo.profile',
};
/**
* @category Socials/Google
*
* Google OAuth 인증을 처리하는 클래스입니다.
* SocialOauthInit 클래스를 상속받아 구현되었습니다.
*/
class Google extends SocialOauthInit {
/**
* Google 클래스의 생성자입니다.
* @param clientID - Google OAuth 클라이언트 ID
*/
constructor(clientID) {
if (!clientID) {
console.warn('Google clientID is required');
}
super(clientID || '');
this.oAuthBaseUrl = 'https://accounts.google.com/o/oauth2/auth/oauthchooseaccount';
/**
* OAuth 인증 URL을 생성합니다.
* @param params - OAuth 인증 요청에 필요한 파라미터
* @param params.state - 트랜잭션 동안 유지할 상태
* @param params.scope - 요청할 OAuth 스코프
* @returns 생성된 OAuth 인증 URL
*/
this.createOauthUrl = (_a) => {
var { state, scope } = _a, params = __rest(_a, ["state", "scope"]);
const encoded = isNotNullish(state) ? SocialOauthInit.encodeOAuthState(state) : undefined;
return super.createOauthUrl(Object.assign({ client_id: this.clientID, response_type: 'code', state: encoded, scope: isArray(scope) ? scope === null || scope === void 0 ? void 0 : scope.join(' ') : scope }, params));
};
/**
* OAuth 인증 링크로 리다이렉트합니다.
* @param params - OAuth 인증 요청에 필요한 파라미터
* @param params.state - 트랜잭션 동안 유지할 상태
* @param params.scope - 요청할 OAuth 스코프
*/
this.loginToLink = (params) => {
const authLink = this.createOauthUrl(Object.assign({}, params));
pushToUrl(authLink);
};
/**
* OAuth 인증 팝업을 엽니다.
* @param params - OAuth 인증 요청에 필요한 파라미터
* @param params.state - 트랜잭션 동안 유지할 상태
* @param params.scope - 요청할 OAuth 스코프 (이메일, 프로필 등)
*/
this.loginToPopup = (params) => {
const authLink = this.createOauthUrl(Object.assign({}, params));
openPopup(authLink);
};
}
}
/**
* @category Socials/Kakao
*
* 카카오 버튼 UI 컴포넌트입니다.
* {@link @toktokhan-dev/react-web#FullButton | `FullButton`} 컴포넌트를 기반으로 하여, 카카오 아이콘과 스타일, 레이블을 포함합니다.
*
* @param props -FullButtonProps에서 'socialType'과 'icon'을 제외한 속성들을 상속받습니다.
* @returns 카카오 버튼 컴포넌트를 반환합니다.
*/
const KakaoButton = (props) => {
return jsx(FullButton, Object.assign({ socialType: "kakao", icon: jsx(KakaoIcon, {}) }, props));
};
/**
* @category Socials/Kakao
*
* Kakao OAuth 인증을 처리하는 클래스입니다.
* SocialOauthInit 클래스를 상속받아 구현되었습니다.
*/
class Kakao extends SocialOauthInit {
/**
* Kakao 클래스의 생성자입니다.
* @param clientID - Kakao OAuth 클라이언트 ID
*/
constructor(clientID) {
if (!clientID) {
console.warn('Kakao clientID is required');
}
super(clientID || '');
this.oAuthBaseUrl = 'https://kauth.kakao.com/oauth/authorize';
/**
* OAuth 인증 URL을 생성합니다.
* @param params - OAuth 인증 요청에 필요한 파라미터
* @param params.state - 트랜잭션 동안 유지할 상태
* @returns 생성된 OAuth 인증 URL
*/
this.createOauthUrl = (_a) => {
var { state, scope } = _a, params = __rest(_a, ["state", "scope"]);
const encoded = isNotNullish(state) ? SocialOauthInit.encodeOAuthState(state) : undefined;
return super.createOauthUrl(Object.assign({ scope: isArray(scope) ? scope === null || scope === void 0 ? void 0 : scope.join(' ') : scope, client_id: this.clientID, response_type: 'code', state: encoded }, params));
};
/**
* OAuth 인증 링크로 리다이렉트합니다.
* @param params - OAuth 인증 요청에 필요한 파라미터
* @param params.state - 트랜잭션 동안 유지할 상태
*/
this.loginToLink = (params) => {
const authLink = this.createOauthUrl(Object.assign({}, params));
pushToUrl(authLink);
};
/**
* OAuth 인증 팝업을 엽니다.
* @param params - OAuth 인증 요청에 필요한 파라미터
* @param params.state - 트랜잭션 동안 유지할 상태
*/
this.loginToPopup = (params) => {
const authLink = this.createOauthUrl(Object.assign({}, params));
openPopup(authLink);
};
}
}
/**
* @category Socials/Kakao
*
* 카카오 아이콘 버튼 UI 컴포넌트입니다.
* {@link @toktokhan-dev/react-web#IconButton | `IconButton`} 컴포넌트를 기반으로 하며, 카카오 아이콘 및 버튼 스타일링이 가능합니다.
*
* @param props - IconButtonProps에서 'socialType'과 'icon'을 제외한 속성들을 상속받습니다.
* @returns 카카오 아이콘 버튼 컴포넌트를 반환합니다.
*/
const KakaoIconButton = (props) => {
return jsx(IconButton, Object.assign({ socialType: "kakao", icon: jsx(KakaoIcon, {}) }, props));
};
/**
* @category Socials/Naver
*
* 네이버 아이콘 버튼 UI 컴포넌트입니다.
* {@link @toktokhan-dev/react-web#IconButton | `IconButton`} 컴포넌트를 기반으로 하며, 네이버 아이콘 및 버튼 스타일링이 가능합니다.
*
* @param props - IconButtonProps에서 'socialType'과 'icon'을 제외한 속성들을 상속받습니다.
* @returns 네이버 아이콘 버튼 컴포넌트를 반환합니다.
*/
const NaverIconButton = (props) => {
return jsx(IconButton, Object.assign({ socialType: "naver", icon: jsx(NaverIcon, {}) }, props));
};
/**
* @category Socials/Naver
*
* 네이버 버튼 UI 컴포넌트입니다.
* {@link @toktokhan-dev/react-web#FullButton | `FullButton`} 컴포넌트를 기반으로 하여, 네이버 아이콘과 스타일, 레이블을 포함합니다.
*
* @param props -FullButtonProps에서 'socialType'과 'icon'을 제외한 속성들을 상속받습니다.
* @returns 네이버 버튼 컴포넌트를 반환합니다.
*/
const NaverButton = (props) => {
return (jsx(FullButton, Object.assign({ socialType: "naver", icon: jsx(NaverIcon, { width: '14px', height: '14px', style: { marginBottom: '2px' } }) }, props)));
};
/**
* @category Socials/Naver
*
* 네이버 OAuth 인증을 처리하는 클래스입니다.
* SocialOauthInit 클래스를 상속받아 구현되었습니다.
*/
class Naver extends SocialOauthInit {
/**
* Naver 클래스의 생성자입니다.
* @param clientID - Naver OAuth 클라이언트 ID
*/
constructor(clientID) {
if (!clientID) {
console.warn('Naver clientID is required');
}
super(clientID || '');
this.oAuthBaseUrl = 'https://nid.naver.com/oauth2.0/authorize';
/**
* OAuth 인증 URL을 생성합니다.
* @param params - OAuth 인증 요청에 필요한 파라미터
* @param params.scope - 요청할 OAuth 스코프
* @param params.state - 트랜잭션 동안 유지할 상태
* @returns 생성된 OAuth 인증 URL
*/
this.createOauthUrl = (_a) => {
var { scope, state } = _a, params = __rest(_a, ["scope", "state"]);
const encoded = isNotNullish(state) ? SocialOauthInit.encodeOAuthState(state) : undefined;
return super.createOauthUrl(Object.assign({ scope: isArray(scope) ? scope === null || scope === void 0 ? void 0 : scope.join(' ') : scope, client_id: this.clientID, response_type: 'code', state: encoded }, params));
};
/**
* 로그인을 위한 OAuth 인증 링크로 리다이렉트합니다.
* @param params - OAuth 인증 요청에 필요한 파라미터
* @param params.scope - 요청할 OAuth 스코프
* @param params.state - 트랜잭션 동안 유지할 상태
*/
this.loginToLink = (params) => {
const authLink = this.createOauthUrl(Object.assign({}, params));
pushToUrl(authLink);
};
/**
* 로그인을 위한 OAuth 인증 팝업을 엽니다.
* @param params - OAuth 인증 요청에 필요한 파라미터
* @param params.scope - 요청할 OAuth 스코프
* @param params.state - 트랜잭션 동안 유지할 상태
*/
this.loginToPopup = (params) => {
const authLink = this.createOauthUrl(Object.assign({}, params));
openPopup(authLink);
};
}
}
/**
* @category Socials/Apple
*
* 애플 아이콘 버튼 UI 컴포넌트입니다.
* {@link @toktokhan-dev/react-web#IconButton | `IconButton`} 컴포넌트를 기반으로 하며, 애플 아이콘 및 버튼 스타일링이 가능합니다.
*
* @param props - IconButtonProps에서 'socialType'과 'icon'을 제외한 속성들을 상속받습니다.
* @returns 애플 아이콘 버튼 컴포넌트를 반환합니다.
*/
const AppleIconButton = (props) => {
return (jsx(IconButton, Object.assign({ socialType: "apple", icon: jsx(AppleIcon, { style: { marginBottom: '3px' } }) }, props)));
};
/**
* @category Socials/Apple
*
* 애플 버튼 UI 컴포넌트입니다.
* {@link @toktokhan-dev/react-web#FullButton | `FullButton`} 컴포넌트를 기반으로 하여, 애플 아이콘과 스타일, 레이블을 포함합니다.
*
* @param props -FullButtonProps에서 'socialType'과 'icon'을 제외한 속성들을 상속받습니다.
* @returns 애플 버튼 컴포넌트를 반환합니다.
*/
const AppleButton = (props) => {
return (jsx(FullButton, Object.assign({ socialType: "apple", icon: jsx(AppleIcon, { width: '18px', height: '18px', style: { marginBottom: '3px' } }) }, props)));
};
/**
* @category Socials/Apple
*
* Apple OAuth 인증을 처리하는 클래스입니다.
* SocialOauthInit 클래스를 상속받아 구현되었습니다.
*/
class Apple extends SocialOauthInit {
/**
* Apple 클래스의 생성자입니다.
* @param clientID - Apple OAuth 클라이언트 ID
*/
constructor(clientID) {
if (!clientID) {
console.warn('Apple clientID is required');
}
super(clientID || '');
this.oAuthBaseUrl = 'https://appleid.apple.com/auth/authorize';
/**
* OAuth 인증 URL을 생성합니다.
* @param params - OAuth 인증 요청에 필요한 파라미터
* @param params.scope - 요청할 OAuth 스코프
* @param params.state - 트랜잭션 동안 유지할 상태
* @returns 생성된 OAuth 인증 URL
*/
this.createOauthUrl = (_a) => {
var { scope, state } = _a, params = __rest(_a, ["scope", "state"]);
const encoded = isNotNullish(state) ? SocialOauthInit.encodeOAuthState(state) : undefined;
return super.createOauthUrl(Object.assign({ scope: isArray(scope) ? scope === null || scope === void 0 ? void 0 : scope.join(' ') : scope, client_id: this.clientID, response_type: 'code', state: encoded }, params));
};
/**
* 로그인을 위한 OAuth 인증 링크로 리다이렉트합니다.
* @param params - OAuth 인증 요청에 필요한 파라미터
* @param params.scope - 요청할 OAuth 스코프
* @param params.state - 트랜잭션 동안 유지할 상태
*/
this.loginToLink = (params) => {
const authLink = this.createOauthUrl(params);
pushToUrl(authLink);
};
/**
* 로그인을 위한 OAuth 인증 팝업을 엽니다.
* @param params - OAuth 인증 요청에 필요한 파라미터
* @param params.scope - 요청할 OAuth 스코프
* @param params.state - 트랜잭션 동안 유지할 상태
*/
this.loginToPopup = (params) => {
const authLink = this.createOauthUrl(Object.assign({}, params));
openPopup(authLink);
};
}
}
/**
* @category Socials/Facebook
*
* 페이스북 아이콘 버튼 UI 컴포넌트입니다.
* {@link @toktokhan-dev/react-web#IconButton | `IconButton`} 컴포넌트를 기반으로 하며, 페이스북 아이콘 및 버튼 스타일링이 가능합니다.
*
* @param props - IconButtonProps에서 'socialType'과 'icon'을 제외한 속성들을 상속받습니다.
* @returns 페이스북 아이콘 버튼 컴포넌트를 반환합니다.
*/
const FacebookIconButton = (props) => {
return (jsx(IconButton, Object.assign({ socialType: "facebook", icon: jsx(FacebookIcon, { width: '22px', height: '22px', style: { marginBottom: '2px' } }) }, props)));
};
/**
* @category Socials/Facebook
*
* 페이스북 버튼 UI 컴포넌트입니다.
* {@link @toktokhan-dev/react-web#FullButton | `FullButton`} 컴포넌트를 기반으로 하여, 페이스북 아이콘과 스타일, 레이블을 포함합니다.
*
* @param props -FullButtonProps에서 'socialType'과 'icon'을 제외한 속성들을 상속받습니다.
* @returns 페이스북 버튼 컴포넌트를 반환합니다.
*/
const FacebookButton = (props) => {
return (jsx(FullButton, Object.assign({ socialType: "facebook", icon: jsx(FacebookIcon, { style: { marginBottom: '2px' } }) }, props)));
};
/**
* @category Socials/FaceBook
*
* Facebook OAuth 인증을 처리하는 클래스입니다.
* SocialOauthInit 클래스를 상속받아 구현되었습니다.
*/
class Facebook extends SocialOauthInit {
/**
* Facebook 클래스의 생성자입니다.
* @param clientID - Facebook OAuth 클라이언트 ID
*/
constructor(clientID) {
if (!clientID) {
console.warn('Facebook clientID is required');
}
super(clientID || '');
this.oAuthBaseUrl = 'https://www.facebook.com/v9.0/dialog/oauth';
/**
* OAuth 인증 URL을 생성합니다.
* @param params - OAuth 인증 요청에 필요한 파라미터
* @param params.scope - 요청할 OAuth 스코프
* @param params.state - 트랜잭션 동안 유지할 상태
* @returns 생성된 OAuth 인증 URL
*/
this.createOauthUrl = (_a) => {
var { scope, state } = _a, params = __rest(_a, ["scope", "state"]);
const encoded = isNotNullish(state) ? SocialOauthInit.encodeOAuthState(state) : undefined;
return super.createOauthUrl(Object.assign({ client_id: this.clientID, response_type: 'code', state: encoded, scope: isArray(scope) ? scope === null || scope === void 0 ? void 0 : scope.join(' ') : scope }, params));
};
/**
* 로그인을 위한 OAuth 인증 링크로 리다이렉트합니다.
* @param params - OAuth 인증 요청에 필요한 파라미터
* @param params.scope - 요청할 OAuth 스코프
* @param params.state - 트랜잭션 동안 유지할 상태
*/
this.loginToLink = (params) => {
const authLink = this.createOauthUrl(params);
pushToUrl(authLink);
};
/**
* 로그인을 위한 OAuth 인증 팝업을 엽니다.
* @param params - OAuth 인증 요청에 필요한 파라미터
* @param params.scope - 요청할 OAuth 스코프
* @param params.return_url - 인증 후 리다이렉션될 URL
*/
this.loginToPopup = (params) => {
const authLink = this.createOauthUrl(params);
openPopup(authLink);
};
}
}
const extractOAuthParams = (search) => {
const urlParams = new URLSearchParams(search);
return {
access_token: urlParams.get('access_token') || null, // for google
code: urlParams.get('code') || null,
state: urlParams.get('state') || null,
error: urlParams.get('error') || null,
errorDescription: urlParams.get('error_description') || null,
};
};
/**
* @category Socials
*
* OAuth 링크 콜백을 처리하는 React Hook입니다.
* 이 Hook은 OAuth 인증 후 리다이렉트된 페이지에서 사용됩니다.
*
* @param params 콜백 함수 파라미터. `onSuccess`와 `onFail` 콜백 함수를 포함할 수 있습니다.
* @returns {LinkReturnType} OAuth 응답 데이터와 로딩 상태를 반환합니다.
*
* @example
*
* ```tsx
* // pages/login.tsx
*
* const kakao = new Kakao(ENV.CLIENT_ID)
* const Login = () =>
* <KakaoButton
* onClick={() =>
* kakao.loginToLink({
* redirect_uri: `${window.origin}/social/callback`,
* state: {
* returnUrl: returnUrl || '/login',
* type: 'kakao',
* },
* })
* }
* />
* }
*
*
* // pages/social/callback.tsx
*
* const { data, isLoading } = useOauthLinkCallback<{type: string; returnUrl:string}>({
* onSuccess: (response) => {
* console.log(response.state.returnUrl)
* },
* })
* ```
*/
const useOauthLinkCallback = (params) => {
const [oAuthResponse, setOauthResponse] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const isCalled = useRef(false);
const onSuccess = useCallbackRef((params === null || params === void 0 ? void 0 : params.onSuccess) || (() => { }));
const onFail = useCallbackRef((params === null || params === void 0 ? void 0 : params.onFail) || (() => { }));
useEffect(() => {
if (isCalled.current)
return;
const { access_token, code, state, error, errorDescription } = extractOAuthParams(window.location.search);
const decoded =