Profile.tsx 6.5 KB

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