page.tsx 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. 'use client';
  2. import './style.scss';
  3. import Link from 'next/link';
  4. import Image from 'next/image';
  5. import { useCallback, useEffect, useMemo, useState } from 'react';
  6. import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
  7. import { faGoogle } from '@fortawesome/free-brands-svg-icons';
  8. import useAuth from '@/hooks/useAuth';
  9. import { fetchApi, getDateTime, stripHtmlTags } from '@/lib/utils/client';
  10. import { SnsLinkItem, SnsListResponse } from '@/types/sns';
  11. import NavTabs from '../navTabs';
  12. import { FEATURE_CHANNEL } from '@/constants/features';
  13. type ProviderMeta = {
  14. provider: 'Google' | 'Naver' | 'Kakao';
  15. label: string;
  16. enabled: boolean;
  17. };
  18. const PROVIDERS: ProviderMeta[] = [
  19. { provider: 'Google', label: 'Google', enabled: true },
  20. { provider: 'Naver', label: 'Naver', enabled: false },
  21. { provider: 'Kakao', label: 'Kakao', enabled: false }
  22. ];
  23. export default function Profile()
  24. {
  25. const { member } = useAuth();
  26. const [snsItems, setSnsItems] = useState<SnsLinkItem[]>([]);
  27. const loadSnsItems = useCallback(async () => {
  28. try {
  29. const res = await fetchApi<SnsListResponse>('/api/mypage/sns', { silent: true });
  30. if (res.data) {
  31. setSnsItems(res.data.items);
  32. }
  33. } catch {
  34. // silent
  35. }
  36. }, []);
  37. useEffect(() => {
  38. if (!member) {
  39. return;
  40. }
  41. loadSnsItems();
  42. }, [member, loadSnsItems]);
  43. const snsItemMap = useMemo(() => {
  44. const map = new Map<string, SnsLinkItem>();
  45. snsItems.forEach((item) => map.set(item.provider, item));
  46. return map;
  47. }, [snsItems]);
  48. if (!member) {
  49. return null;
  50. }
  51. return (
  52. <>
  53. <NavTabs />
  54. <div id="profile">
  55. <h1>내 정보</h1>
  56. <div className="grid grid-cols-1 xl:grid-cols-2 gap-3">
  57. <table className="table-auto">
  58. <caption>등록 정보</caption>
  59. <colgroup>
  60. <col className="w-[clamp(80px,30%,200px)]"/>
  61. <col/>
  62. </colgroup>
  63. <tbody>
  64. <tr>
  65. <th>사용자 ID</th>
  66. <td colSpan={2}>{member?.sid}</td>
  67. </tr>
  68. <tr>
  69. <th>이메일</th>
  70. <td>{member?.email}</td>
  71. <td>
  72. <Link href="/change-email" className="btn btn-default">이메일 변경</Link>
  73. </td>
  74. </tr>
  75. <tr>
  76. <th>별명</th>
  77. <td>{member?.name || '-'}</td>
  78. <td>
  79. <Link href="/change-name" className="btn btn-default">별명 변경</Link>
  80. </td>
  81. </tr>
  82. <tr>
  83. <th>본인인증</th>
  84. <td>{member?.isAuthCertified ? '인증 완료' : '-'}</td>
  85. <td></td>
  86. </tr>
  87. <tr>
  88. <th>SNS 연동</th>
  89. <td>
  90. <div className="profile__sns-logos" aria-label="SNS 연동 상태">
  91. {PROVIDERS.map((meta) => {
  92. const item = snsItemMap.get(meta.provider);
  93. const connected = item?.connected ?? false;
  94. const stateText = !meta.enabled
  95. ? `${meta.label} 준비중`
  96. : (connected ? `${meta.label} 연동됨` : `${meta.label} 미연동`);
  97. return (
  98. <span
  99. key={meta.provider}
  100. className={`profile__sns-logo profile__sns-logo--${meta.provider.toLowerCase()}${connected ? ' profile__sns-logo--connected' : ' profile__sns-logo--inactive'}${meta.enabled ? '' : ' profile__sns-logo--disabled'}`}
  101. title={stateText}
  102. aria-label={stateText}
  103. >
  104. {meta.provider === 'Google' && <FontAwesomeIcon icon={faGoogle} />}
  105. {meta.provider === 'Naver' && <span className="profile__sns-logo-text">N</span>}
  106. {meta.provider === 'Kakao' && <span className="profile__sns-logo-text">K</span>}
  107. </span>
  108. );
  109. })}
  110. </div>
  111. </td>
  112. <td>
  113. <Link href="/sns-link" className="btn btn-default">SNS 관리</Link>
  114. </td>
  115. </tr>
  116. <tr>
  117. <th>회원가입 일시</th>
  118. <td colSpan={2}>{getDateTime(member.createdAt)}</td>
  119. </tr>
  120. <tr>
  121. <th>마지막 로그인 일시</th>
  122. <td colSpan={2}>{getDateTime(member.lastLoginAt)}</td>
  123. </tr>
  124. <tr>
  125. <th>알림 수신</th>
  126. <td>
  127. <div className="flex items-center gap-4 flex-wrap">
  128. <label className="flex items-center gap-2 text-gray-700">
  129. <input type="checkbox" checked={member.memberApprove.isReceiveSMS} readOnly disabled className="accent-gray-500 cursor-not-allowed" />
  130. SMS
  131. </label>
  132. <label className="flex items-center gap-2 text-gray-700">
  133. <input type="checkbox" checked={member.memberApprove.isReceiveEmail} readOnly disabled className="accent-gray-500 cursor-not-allowed" />
  134. 메일
  135. </label>
  136. <label className="flex items-center gap-2 text-gray-700">
  137. <input type="checkbox" checked={member.memberApprove.isReceiveNote} readOnly disabled className="accent-gray-500 cursor-not-allowed" />
  138. 쪽지
  139. </label>
  140. </div>
  141. </td>
  142. <td>
  143. <Link href="/change-approve" className="btn btn-default">수신 설정</Link>
  144. </td>
  145. </tr>
  146. </tbody>
  147. </table>
  148. <table className="mt-3 xl:mt-0 table-auto">
  149. <caption>활동 정보</caption>
  150. <colgroup>
  151. <col className="w-full min-w-[80px] max-w-[30%]"/>
  152. <col width="*"/>
  153. <col width="*"/>
  154. </colgroup>
  155. <tbody>
  156. <tr>
  157. <th>회원 사진</th>
  158. <td>
  159. {member?.thumb ?
  160. (<Image src={member?.thumb} alt="회원 사진" width={64} height={64} className="object-cover" unoptimized={true} />) : '-'}
  161. </td>
  162. <td>
  163. <Link href="/change-thumb" className="btn btn-default">사진 변경</Link>
  164. </td>
  165. </tr>
  166. <tr>
  167. <th>회원 등급</th>
  168. <td></td>
  169. <td></td>
  170. </tr>
  171. <tr>
  172. <th>한마디</th>
  173. <td colSpan={2}>
  174. <div className="grid grid-cols-[1fr_auto] gap-2 items-center">
  175. <span className="truncate">{member?.summary || '-'}</span>
  176. <Link href="/change-summary" className="btn btn-default">한마디 변경</Link>
  177. </div>
  178. </td>
  179. </tr>
  180. <tr>
  181. <th>자기소개</th>
  182. <td colSpan={2}>
  183. <div className="grid grid-cols-[1fr_auto] gap-2 items-center">
  184. <span className="truncate">{stripHtmlTags(member.intro) || '-'}</span>
  185. <Link href="/change-intro" className="btn btn-default">자기소개 변경</Link>
  186. </div>
  187. </td>
  188. </tr>
  189. <tr>
  190. <th>경험치</th>
  191. <td colSpan={2}>{member?.memberStats.exp}</td>
  192. </tr>
  193. <tr>
  194. <th>작성 게시글</th>
  195. <td colSpan={2}>{member?.memberStats.postCount}</td>
  196. </tr>
  197. <tr>
  198. <th>작성 댓글</th>
  199. <td colSpan={2}>{member?.memberStats.commentCount}</td>
  200. </tr>
  201. <tr>
  202. <th>팔로워</th>
  203. <td colSpan={2}>{member?.memberStats.followerCount ?? 0}</td>
  204. </tr>
  205. <tr>
  206. <th>팔로잉</th>
  207. <td colSpan={2}>{member?.memberStats.followingCount ?? 0}</td>
  208. </tr>
  209. </tbody>
  210. </table>
  211. {/* 채널 기능 OFF 시 스튜디오 진입 링크 미노출 */}
  212. {FEATURE_CHANNEL && (
  213. <Link href='/studio' className='text-sky-600 hover:text-red-600 hover:underline'>채널 관리</Link>
  214. )}
  215. </div>
  216. <br />
  217. <dl>
  218. <dt>회원님의 개인정보는 안전하게 보호됩니다.</dt>
  219. <dd>※ 의심스러운 활동이 감지되면 즉시 비밀번호를 변경하세요.</dd>
  220. <dd>※ 보안을 위해 비밀번호를 주기적으로 변경하는 것이 좋습니다.</dd>
  221. <dd>※ 자동 로그인 기능을 사용하면, 로그인 상태가 유지됩니다.</dd>
  222. <dd>※ 공용 컴퓨터에서는 자동 로그인을 사용하지 않는 것이 좋습니다. </dd>
  223. </dl>
  224. </div>
  225. </>
  226. );
  227. }