| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134 |
- 'use client';
- import { useState, useEffect } from 'react';
- import { createPortal } from 'react-dom';
- import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
- import { faXmark } from '@fortawesome/free-solid-svg-icons';
- import type { ChatSettings } from '@/types/chat';
- import { fetchChatSettings, updateChatSettings } from '@/lib/api/chatSettings';
- type Props = {
- onClose: () => void;
- };
- const TOGGLES: { key: keyof ChatSettings; label: string; desc: string }[] = [
- { key: 'allowInvite', label: '초대 수신', desc: '다른 사용자의 대화 초대를 받습니다.' },
- { key: 'allowWhisper', label: '귓속말 수신', desc: '다른 사용자의 귓속말을 받습니다.' },
- { key: 'showFollowingOnline', label: '팔로잉 접속 표시', desc: '내가 팔로우한 사용자에게 접속 상태를 표시합니다.' }
- ];
- // 수신설정 modal — 초대/귓속말/팔로잉 접속 표시 토글 (백엔드 회원설정 persist). 도크 overflow 회피 위해 portal.
- export default function ChatReceiveModal({ onClose }: Props)
- {
- const [settings, setSettings] = useState<ChatSettings>({ allowInvite: true, allowWhisper: true, showFollowingOnline: true });
- const [loading, setLoading] = useState(true);
- const [saving, setSaving] = useState(false);
- const [error, setError] = useState('');
- // 진입 시 현재 설정 로드
- useEffect(() => {
- let alive = true;
- fetchChatSettings().then((res) => {
- if (alive && res.data) {
- setSettings(res.data);
- }
- }).catch((err) => {
- if (alive) {
- setError(err instanceof Error ? err.message : '설정을 불러오지 못했습니다.');
- }
- }).finally(() => {
- if (alive) {
- setLoading(false);
- }
- });
- return () => {
- alive = false;
- };
- }, []);
- // ESC 로 닫기
- useEffect(() => {
- const onKey = (e: KeyboardEvent) => {
- if (e.key === 'Escape') {
- onClose();
- }
- };
- document.addEventListener('keydown', onKey);
- return () => {
- document.removeEventListener('keydown', onKey);
- };
- }, [onClose]);
- const toggle = (key: keyof ChatSettings) => {
- setSettings((prev) => ({ ...prev, [key]: !prev[key] }));
- };
- const handleSave = async () => {
- setSaving(true);
- setError('');
- try {
- await updateChatSettings(settings);
- onClose();
- } catch (err) {
- setError(err instanceof Error ? err.message : '저장에 실패했습니다.');
- } finally {
- setSaving(false);
- }
- };
- if (typeof document === 'undefined') {
- return null;
- }
- return createPortal(
- <div className='chat-modal' role='presentation' onMouseDown={onClose}>
- <div className='chat-modal__panel' role='dialog' aria-modal='true' aria-label='채팅 수신 설정' onMouseDown={(e) => e.stopPropagation()}>
- <div className='chat-modal__head'>
- <h2 className='chat-modal__title'>수신 설정</h2>
- <button type='button' className='chat-modal__close' onClick={onClose} aria-label='닫기' title='닫기'>
- <FontAwesomeIcon icon={faXmark} />
- </button>
- </div>
- <div className='chat-modal__body'>
- {loading ? (
- <div className='chat-modal__loading'>불러오는 중…</div>
- ) : (
- TOGGLES.map((t) => (
- <label key={t.key} className='chat-toggle'>
- <span className='chat-toggle__text'>
- <span className='chat-toggle__label'>{t.label}</span>
- <span className='chat-toggle__desc'>{t.desc}</span>
- </span>
- <span className='chat-toggle__switch'>
- <input
- type='checkbox'
- role='switch'
- checked={settings[t.key]}
- onChange={() => toggle(t.key)}
- disabled={saving}
- />
- <span className='chat-toggle__slider' aria-hidden='true' />
- </span>
- </label>
- ))
- )}
- {error && <p className='chat-modal__error' role='alert'>{error}</p>}
- </div>
- <div className='chat-modal__foot'>
- <button type='button' className='chat-modal__btn chat-modal__btn--ghost' onClick={onClose} disabled={saving}>취소</button>
- <button type='button' className='chat-modal__btn chat-modal__btn--primary' onClick={handleSave} disabled={loading || saving}>
- {saving ? '저장 중…' : '저장'}
- </button>
- </div>
- </div>
- </div>,
- document.body
- );
- }
|