Profile.tsx 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. 'use client';
  2. import '../styles/profile.scss';
  3. import { useEffect, useState } from 'react';
  4. import Link from 'next/link';
  5. import { GoogleOAuthProvider, GoogleLogin, useGoogleOneTapLogin } from '@react-oauth/google';
  6. import useAuth from '@/hooks/useAuth';
  7. import { useConfigContext } from '@/contexts/configProvider';
  8. import { fetchApi } from '@/lib/utils/client';
  9. import { DropdownData } from '@/types/response/mypage/dropdown';
  10. import { LoginResponse } from '@/types/response/auth';
  11. import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar';
  12. import SignalSteamIcon from '@/public/icons/user/signal-steam.svg';
  13. import {
  14. DropdownMenu,
  15. DropdownMenuContent,
  16. DropdownMenuItem,
  17. DropdownMenuLabel,
  18. DropdownMenuSeparator,
  19. DropdownMenuTrigger,
  20. } from '@/components/ui/dropdown-menu';
  21. import {
  22. Package,
  23. UserRoundCog,
  24. LogOut
  25. } from 'lucide-react';
  26. export default function Profile()
  27. {
  28. const config = useConfigContext();
  29. const clientId = config?.external?.googleClientId ?? '';
  30. return (
  31. <GoogleOAuthProvider clientId={clientId} nonce="" locale="ko">
  32. <ProfileInner />
  33. </GoogleOAuthProvider>
  34. );
  35. }
  36. function ProfileInner() {
  37. const { member, login, logout } = useAuth();
  38. const [open, setOpen] = useState(false);
  39. const [data, setData] = useState<DropdownData|null>(null);
  40. // 구글 로그인 핸들러
  41. const handleGoogleLogin = async (credentialResponse: { credential?: string }) => {
  42. try {
  43. const res = await fetchApi<LoginResponse>('/api/auth/google-login', {
  44. method: 'POST',
  45. body: {
  46. credential: credentialResponse.credential
  47. }
  48. });
  49. login(true);
  50. } catch {
  51. // silent
  52. }
  53. };
  54. // Google One Tap — 비로그인 상태에서만 활성화
  55. useGoogleOneTapLogin({
  56. onSuccess: handleGoogleLogin,
  57. onError: () => {},
  58. disabled: !!member
  59. });
  60. // 드롭다운 열 때 잔액 조회
  61. const loadData = async () => {
  62. const res = await fetchApi<DropdownData>('/api/mypage/dropdown');
  63. if (res.data) {
  64. setData(res.data);
  65. }
  66. };
  67. useEffect(() => {
  68. if (!open) {
  69. return;
  70. }
  71. loadData();
  72. }, [open]);
  73. // 충전 완료 시 팝업에서 postMessage 수신 → 잔액 갱신
  74. useEffect(() => {
  75. const handleMessage = (e: MessageEvent) => {
  76. if (e.data?.type === 'CHARGE_COMPLETE') {
  77. loadData();
  78. }
  79. };
  80. window.addEventListener('message', handleMessage);
  81. return () => window.removeEventListener('message', handleMessage);
  82. }, []);
  83. // ── 비로그인 ──────────────────────────────────
  84. if (!member) {
  85. return (
  86. <DropdownMenu>
  87. <DropdownMenuTrigger asChild>
  88. <label className="profile-dropdown__trigger--guest">
  89. 로그인
  90. </label>
  91. </DropdownMenuTrigger>
  92. <DropdownMenuContent className="profile-dropdown__login-content" align="end">
  93. <div className="profile-dropdown__login-panel">
  94. <GoogleLogin
  95. onSuccess={handleGoogleLogin}
  96. onError={() => {}}
  97. size="large"
  98. shape="rectangular"
  99. logo_alignment="center"
  100. />
  101. </div>
  102. </DropdownMenuContent>
  103. </DropdownMenu>
  104. );
  105. }
  106. // ── 로그인 ────────────────────────────────────
  107. const displayName = member.name || member.sid;
  108. const initial = (member.name || member.sid || '?').charAt(0).toUpperCase();
  109. return (
  110. <DropdownMenu open={open} onOpenChange={setOpen}>
  111. <DropdownMenuTrigger asChild>
  112. <button type="button" className="profile-dropdown__trigger" title={displayName}>
  113. <Avatar className="h-8 w-8">
  114. <AvatarImage src={member.thumb || undefined} alt={displayName} />
  115. <AvatarFallback className="profile-dropdown__fallback">{initial}</AvatarFallback>
  116. </Avatar>
  117. </button>
  118. </DropdownMenuTrigger>
  119. <DropdownMenuContent className="profile-dropdown__content" align="end">
  120. {/* 프로필 헤더 */}
  121. <DropdownMenuLabel className="profile-dropdown__header">
  122. <Avatar>
  123. <AvatarImage src={member.thumb || undefined} alt={displayName} />
  124. <AvatarFallback className="profile-dropdown__fallback">{initial}</AvatarFallback>
  125. </Avatar>
  126. <div className="profile-dropdown__user-info">
  127. <div className="profile-dropdown__name">{displayName}</div>
  128. {data && (
  129. <>
  130. <div className="profile-dropdown__balance">
  131. <span className="profile-dropdown__balance-icon">P</span>
  132. <span>{data.spendableBalance.toLocaleString()}</span>
  133. </div>
  134. {data.isCreator && data.withdrawableBalance !== null && (
  135. <div className="profile-dropdown__withdraw">
  136. <span className="profile-dropdown__withdraw-icon">M</span>
  137. <span>{data.withdrawableBalance.toLocaleString()}</span>
  138. </div>
  139. )}
  140. </>
  141. )}
  142. </div>
  143. </DropdownMenuLabel>
  144. <DropdownMenuSeparator />
  145. {member.isCreator && (
  146. <DropdownMenuItem asChild>
  147. <Link href="/studio">
  148. <span className="profile-dropdown__menu-icon">
  149. <SignalSteamIcon width={20} height={20} aria-label='채널 관리' />
  150. </span>
  151. 채널 관리
  152. </Link>
  153. </DropdownMenuItem>
  154. )}
  155. <DropdownMenuItem asChild>
  156. <Link href="/profile">
  157. <span className="profile-dropdown__menu-icon">
  158. <UserRoundCog width={20} height={20} aria-label='내 정보' />
  159. </span>
  160. 내 정보
  161. </Link>
  162. </DropdownMenuItem>
  163. <DropdownMenuItem asChild>
  164. <Link href="/storage">
  165. <span className="profile-dropdown__menu-icon">
  166. <Package width={20} height={20} aria-label='보관함' />
  167. </span>
  168. 보관함
  169. </Link>
  170. </DropdownMenuItem>
  171. <DropdownMenuSeparator />
  172. <DropdownMenuItem className="profile-dropdown__danger" onSelect={logout}>
  173. <span className="profile-dropdown__menu-icon">
  174. <LogOut width={20} height={20} aria-label='로그아웃' />
  175. </span>
  176. 로그아웃
  177. </DropdownMenuItem>
  178. </DropdownMenuContent>
  179. </DropdownMenu>
  180. );
  181. }