ChatReceiveModal.tsx 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. 'use client';
  2. import { useState, useEffect } from 'react';
  3. import { createPortal } from 'react-dom';
  4. import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
  5. import { faXmark } from '@fortawesome/free-solid-svg-icons';
  6. import type { ChatSettings } from '@/types/chat';
  7. import { fetchChatSettings, updateChatSettings } from '@/lib/api/chatSettings';
  8. type Props = {
  9. onClose: () => void;
  10. };
  11. const TOGGLES: { key: keyof ChatSettings; label: string; desc: string }[] = [
  12. { key: 'allowInvite', label: '초대 수신', desc: '다른 사용자의 대화 초대를 받습니다.' },
  13. { key: 'allowWhisper', label: '귓속말 수신', desc: '다른 사용자의 귓속말을 받습니다.' },
  14. { key: 'showFollowingOnline', label: '팔로잉 접속 표시', desc: '내가 팔로우한 사용자에게 접속 상태를 표시합니다.' }
  15. ];
  16. // 수신설정 modal — 초대/귓속말/팔로잉 접속 표시 토글 (백엔드 회원설정 persist). 도크 overflow 회피 위해 portal.
  17. export default function ChatReceiveModal({ onClose }: Props)
  18. {
  19. const [settings, setSettings] = useState<ChatSettings>({ allowInvite: true, allowWhisper: true, showFollowingOnline: true });
  20. const [loading, setLoading] = useState(true);
  21. const [saving, setSaving] = useState(false);
  22. const [error, setError] = useState('');
  23. // 진입 시 현재 설정 로드
  24. useEffect(() => {
  25. let alive = true;
  26. fetchChatSettings().then((res) => {
  27. if (alive && res.data) {
  28. setSettings(res.data);
  29. }
  30. }).catch((err) => {
  31. if (alive) {
  32. setError(err instanceof Error ? err.message : '설정을 불러오지 못했습니다.');
  33. }
  34. }).finally(() => {
  35. if (alive) {
  36. setLoading(false);
  37. }
  38. });
  39. return () => {
  40. alive = false;
  41. };
  42. }, []);
  43. // ESC 로 닫기
  44. useEffect(() => {
  45. const onKey = (e: KeyboardEvent) => {
  46. if (e.key === 'Escape') {
  47. onClose();
  48. }
  49. };
  50. document.addEventListener('keydown', onKey);
  51. return () => {
  52. document.removeEventListener('keydown', onKey);
  53. };
  54. }, [onClose]);
  55. const toggle = (key: keyof ChatSettings) => {
  56. setSettings((prev) => ({ ...prev, [key]: !prev[key] }));
  57. };
  58. const handleSave = async () => {
  59. setSaving(true);
  60. setError('');
  61. try {
  62. await updateChatSettings(settings);
  63. onClose();
  64. } catch (err) {
  65. setError(err instanceof Error ? err.message : '저장에 실패했습니다.');
  66. } finally {
  67. setSaving(false);
  68. }
  69. };
  70. if (typeof document === 'undefined') {
  71. return null;
  72. }
  73. return createPortal(
  74. <div className='chat-modal' role='presentation' onMouseDown={onClose}>
  75. <div className='chat-modal__panel' role='dialog' aria-modal='true' aria-label='채팅 수신 설정' onMouseDown={(e) => e.stopPropagation()}>
  76. <div className='chat-modal__head'>
  77. <h2 className='chat-modal__title'>수신 설정</h2>
  78. <button type='button' className='chat-modal__close' onClick={onClose} aria-label='닫기' title='닫기'>
  79. <FontAwesomeIcon icon={faXmark} />
  80. </button>
  81. </div>
  82. <div className='chat-modal__body'>
  83. {loading ? (
  84. <div className='chat-modal__loading'>불러오는 중…</div>
  85. ) : (
  86. TOGGLES.map((t) => (
  87. <label key={t.key} className='chat-toggle'>
  88. <span className='chat-toggle__text'>
  89. <span className='chat-toggle__label'>{t.label}</span>
  90. <span className='chat-toggle__desc'>{t.desc}</span>
  91. </span>
  92. <span className='chat-toggle__switch'>
  93. <input
  94. type='checkbox'
  95. role='switch'
  96. checked={settings[t.key]}
  97. onChange={() => toggle(t.key)}
  98. disabled={saving}
  99. />
  100. <span className='chat-toggle__slider' aria-hidden='true' />
  101. </span>
  102. </label>
  103. ))
  104. )}
  105. {error && <p className='chat-modal__error' role='alert'>{error}</p>}
  106. </div>
  107. <div className='chat-modal__foot'>
  108. <button type='button' className='chat-modal__btn chat-modal__btn--ghost' onClick={onClose} disabled={saving}>취소</button>
  109. <button type='button' className='chat-modal__btn chat-modal__btn--primary' onClick={handleSave} disabled={loading || saving}>
  110. {saving ? '저장 중…' : '저장'}
  111. </button>
  112. </div>
  113. </div>
  114. </div>,
  115. document.body
  116. );
  117. }