| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248 |
- 'use client';
- import { useState, useRef, useEffect, useCallback, type KeyboardEvent } from 'react';
- import useAuth from '@/hooks/useAuth';
- import useChat from '@/hooks/useChat';
- import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
- import { faUsers, faEllipsisVertical, faPaperPlane, faTrashCan, faMagnifyingGlassPlus, faMagnifyingGlassMinus, faRotateRight, faClock } from '@fortawesome/free-solid-svg-icons';
- import './chat-sidebar.scss';
- const MIN_FONT_SIZE = 11;
- const MAX_FONT_SIZE = 19;
- const DEFAULT_FONT_SIZE = 13;
- export default function ChatSidebar() {
- const { isAuthenticated } = useAuth();
- const { messages, systemMessages, participantCount, participants, sendMessage, clearMessages, refreshChat, requestParticipants, chatConnected } = useChat();
- const [inputValue, setInputValue] = useState('');
- const [fontSize, setFontSize] = useState(DEFAULT_FONT_SIZE);
- const [showMenu, setShowMenu] = useState(false);
- const [showTime, setShowTime] = useState(false);
- const messagesRef = useRef<HTMLDivElement>(null);
- const isAutoScrollRef = useRef(true);
- const [showParticipants, setShowParticipants] = useState(false);
- const menuRef = useRef<HTMLDivElement>(null);
- // 메시지 + 시스템 메시지를 시간순 병합
- const mergedMessages = (() => {
- const items: Array<
- | { type: 'chat'; data: typeof messages[number] }
- | { type: 'system'; data: typeof systemMessages[number] }
- > = [];
- messages.forEach((m) => items.push({ type: 'chat', data: m }));
- systemMessages.forEach((m) => items.push({ type: 'system', data: m }));
- items.sort((a, b) => {
- const timeA = a.type === 'chat' ? a.data.sentAt : a.data.receivedAt;
- const timeB = b.type === 'chat' ? b.data.sentAt : b.data.receivedAt;
- return new Date(timeA).getTime() - new Date(timeB).getTime();
- });
- return items;
- })();
- // 자동 스크롤
- useEffect(() => {
- const el = messagesRef.current;
- if (!el || !isAutoScrollRef.current) return;
- el.scrollTop = el.scrollHeight;
- }, [mergedMessages.length]);
- // 스크롤 이벤트로 자동 스크롤 제어
- const handleScroll = useCallback(() => {
- const el = messagesRef.current;
- if (!el) return;
- const threshold = 50;
- isAutoScrollRef.current = el.scrollTop + el.clientHeight >= el.scrollHeight - threshold;
- }, []);
- // 메뉴 외부 클릭 닫기
- useEffect(() => {
- if (!showMenu) return;
- const handleClick = (e: MouseEvent) => {
- if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
- setShowMenu(false);
- }
- };
- document.addEventListener('mousedown', handleClick);
- return () => document.removeEventListener('mousedown', handleClick);
- }, [showMenu]);
- const handleSend = useCallback(() => {
- if (!inputValue.trim()) return;
- sendMessage(inputValue);
- setInputValue('');
- }, [inputValue, sendMessage]);
- const handleKeyDown = useCallback((e: KeyboardEvent<HTMLInputElement>) => {
- if (e.key === 'Enter' && !e.nativeEvent.isComposing) {
- e.preventDefault();
- handleSend();
- }
- }, [handleSend]);
- const handleClear = useCallback(() => {
- clearMessages();
- setShowMenu(false);
- }, [clearMessages]);
- const handleFontIncrease = useCallback(() => {
- setFontSize((prev) => Math.min(prev + 2, MAX_FONT_SIZE));
- setShowMenu(false);
- }, []);
- const handleFontDecrease = useCallback(() => {
- setFontSize((prev) => Math.max(prev - 2, MIN_FONT_SIZE));
- setShowMenu(false);
- }, []);
- const handleRefresh = useCallback(() => {
- setShowMenu(false);
- refreshChat();
- }, [refreshChat]);
- const handleToggleTime = useCallback(() => {
- setShowTime((prev) => !prev);
- setShowMenu(false);
- }, []);
- const handleShowParticipants = useCallback(() => {
- requestParticipants();
- setShowParticipants(true);
- }, [requestParticipants]);
- const handleCloseParticipants = useCallback(() => {
- setShowParticipants(false);
- }, []);
- const formatTime = (dateStr: string) => {
- const date = new Date(dateStr);
- return `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
- };
- return (
- <div className='chat-sidebar'>
- {/* 헤더 */}
- <div className='chat-header'>
- <span className='chat-header-title'>실시간 채팅</span>
- <div className='chat-header-actions'>
- <button type='button' className='chat-participant-count' title='참여자 보기' onClick={handleShowParticipants}>
- <FontAwesomeIcon icon={faUsers} />
- <span>{participantCount}명</span>
- </button>
- <div className='chat-menu-wrapper' ref={menuRef}>
- <button type='button' title='메뉴' onClick={() => setShowMenu((prev) => !prev)}>
- <FontAwesomeIcon icon={faEllipsisVertical} />
- </button>
- {showMenu && (
- <div className='chat-menu'>
- <button type='button' onClick={handleClear}>
- <FontAwesomeIcon icon={faTrashCan} />
- <span>채팅 지우기</span>
- </button>
- <button type='button' onClick={handleToggleTime}>
- <FontAwesomeIcon icon={faClock} />
- <span>시간 {showTime ? '숨기기' : '보기'}</span>
- </button>
- <button type='button' onClick={handleFontIncrease} disabled={fontSize >= MAX_FONT_SIZE}>
- <FontAwesomeIcon icon={faMagnifyingGlassPlus} />
- <span>글자 크게</span>
- </button>
- <button type='button' onClick={handleFontDecrease} disabled={fontSize <= MIN_FONT_SIZE}>
- <FontAwesomeIcon icon={faMagnifyingGlassMinus} />
- <span>글자 작게</span>
- </button>
- <button type='button' onClick={handleRefresh}>
- <FontAwesomeIcon icon={faRotateRight} />
- <span>새로고침</span>
- </button>
- </div>
- )}
- </div>
- </div>
- </div>
- {/* 메시지 영역 */}
- <div className='chat-messages' ref={messagesRef} onScroll={handleScroll} style={{ fontSize: `${fontSize}px` }}>
- {!chatConnected && (
- <div className='chat-system'>채팅 서버에 연결 중...</div>
- )}
- {mergedMessages.map((item, index) => {
- if (item.type === 'system') {
- return (
- <div key={`sys-${item.data.id}`} className='chat-system'>
- {item.data.content}
- </div>
- );
- }
- const msg = item.data;
- return (
- <div key={`msg-${index}`} className='chat-message'>
- {showTime && <span className='chat-message-time'>{formatTime(msg.sentAt)}</span>}
- <span className='chat-message-name'>{msg.memberName || msg.memberSID}</span>
- <span className='chat-message-content'>{msg.content}</span>
- </div>
- );
- })}
- </div>
- {/* 입력 영역 */}
- <div className='chat-input-area'>
- {isAuthenticated ? (
- <div className='chat-input-row'>
- <input
- type='text'
- placeholder='메시지를 입력하세요'
- value={inputValue}
- onChange={(e) => setInputValue(e.target.value)}
- onKeyDown={handleKeyDown}
- maxLength={500}
- disabled={!chatConnected}
- />
- <button type='button' title='전송' onClick={handleSend} disabled={!chatConnected || !inputValue.trim()}>
- <FontAwesomeIcon icon={faPaperPlane} />
- </button>
- </div>
- ) : (
- <div className='chat-login-notice'>
- <a href='/login'>로그인</a> 후 채팅에 참여하세요
- </div>
- )}
- </div>
- {/* 참여자 목록 패널 */}
- {showParticipants && (
- <>
- <div className='chat-participants-overlay' onClick={handleCloseParticipants} />
- <div className='chat-participants-dialog'>
- <div className='chat-participants-header'>
- <h3>참여자 ({participantCount}명)</h3>
- <button type='button' onClick={handleCloseParticipants} aria-label='닫기'>×</button>
- </div>
- <ul className='chat-participants-list'>
- {participants.length === 0 && participantCount === 0 ? (
- <li className='chat-participants-empty'>참여자가 없습니다</li>
- ) : (
- <>
- {participants.map((p) => (
- <li key={p.memberName}>
- <FontAwesomeIcon icon={faUsers} className='chat-participant-icon' />
- <span>{p.memberName}</span>
- </li>
- ))}
- {participantCount - participants.length > 0 && (
- <li className='chat-participants-guest'>
- <span>비회원 {participantCount - participants.length}명</span>
- </li>
- )}
- </>
- )}
- </ul>
- </div>
- </>
- )}
- </div>
- );
- }
|