'use client'; import { useCallback, useEffect, useRef, useState } from 'react'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { fetchApi } from '@/lib/utils/client'; import type { RecipientItem, SearchRecipientResponse } from '@/types/response/note/searchRecipient'; import { Search, X } from 'lucide-react'; type Props = { open: boolean; onClose: () => void; initialReceiver?: RecipientItem|null; initialTitle?: string; onSent?: () => void; }; const PAGE_SIZE = 20; const SEARCH_DEBOUNCE_MS = 300; export default function NoteComposeModal({ open, onClose, initialReceiver, initialTitle, onSent }: Props) { const [receiver, setReceiver] = useState(null); const [searchQuery, setSearchQuery] = useState(''); const [searchResults, setSearchResults] = useState([]); const [searchHasMore, setSearchHasMore] = useState(false); const [searchOffset, setSearchOffset] = useState(0); const [searchLoading, setSearchLoading] = useState(false); const searchTimerRef = useRef|null>(null); const [title, setTitle] = useState(''); const [content, setContent] = useState(''); const [sendToRandom, setSendToRandom] = useState(false); const [sending, setSending] = useState(false); const [error, setError] = useState(null); // 모달 열릴 때 상태 초기화 useEffect(() => { if (open) { setReceiver(initialReceiver ?? null); setTitle(initialTitle ?? ''); setContent(''); setSearchQuery(''); setSearchResults([]); setSearchHasMore(false); setSearchOffset(0); setError(null); setSendToRandom(false); } }, [open, initialReceiver, initialTitle]); const runSearch = useCallback(async (q: string, offset: number, append: boolean) => { const trimmed = q.trim(); if (trimmed.length < 2) { setSearchResults([]); setSearchHasMore(false); setSearchOffset(0); return; } setSearchLoading(true); try { const params = new URLSearchParams({ q: trimmed, offset: String(offset), limit: String(PAGE_SIZE) }); const res = await fetchApi(`/api/note/search-recipient?${params.toString()}`, { silent: true }); if (res.data) { setSearchResults(prev => append ? [...prev, ...res.data!.list] : res.data!.list); setSearchHasMore(res.data.hasMore); setSearchOffset(offset + res.data.list.length); } } catch { // ignore — silent } finally { setSearchLoading(false); } }, []); // 검색 query 변경 시 debounce useEffect(() => { if (receiver) { return; } if (searchTimerRef.current) { clearTimeout(searchTimerRef.current); } searchTimerRef.current = setTimeout(() => { runSearch(searchQuery, 0, false); }, SEARCH_DEBOUNCE_MS); return () => { if (searchTimerRef.current) { clearTimeout(searchTimerRef.current); } }; }, [searchQuery, receiver, runSearch]); const handleShowMore = () => { runSearch(searchQuery, searchOffset, true); }; const handleSelect = (item: RecipientItem) => { setReceiver(item); setSearchQuery(''); setSearchResults([]); setSearchHasMore(false); }; const handleClearReceiver = () => { setReceiver(null); }; const handleSend = async () => { setError(null); if (!sendToRandom && !receiver) { setError('받는 사람을 선택해주세요.'); return; } if (!title.trim() || !content.trim()) { setError('제목과 내용을 입력해주세요.'); return; } setSending(true); try { const res = await fetchApi('/api/note/send', { method: 'POST', body: sendToRandom ? { title: title.trim(), content: content.trim(), sendToRandom: true } : { receiverMemberID: receiver!.memberID, title: title.trim(), content: content.trim() } }); if (res.success) { onSent?.(); onClose(); return; } setError(res.message || '전송에 실패했습니다.'); } catch (e: unknown) { const message = e instanceof Error ? e.message : '전송에 실패했습니다.'; setError(message); } finally { setSending(false); } }; return ( { if (!o) { onClose(); } }}> {initialTitle?.startsWith('RE:') ? '답장' : '쪽지 보내기'}
{/* 무작위 발송 (쪽지권) */} {sendToRandom && (
받는 사람은 무작위로 선택되며, 쪽지권 1개가 사용됩니다.
)} {/* 받는 사람 */}
{receiver ? (
{receiver.thumbnailUrl ? ( // eslint-disable-next-line @next/next/no-img-element {receiver.displayName} ) : (
{receiver.displayName[0]?.toUpperCase() ?? '?'}
)}
{receiver.displayName}
{receiver.displaySubtitle && (
{receiver.displaySubtitle}
)}
) : ( <>
setSearchQuery(e.target.value)} placeholder="팔로우한 회원/채널 검색" className="w-full border rounded-lg pl-9 pr-3 py-2 text-sm" />
{searchQuery.trim().length >= 2 && (
{searchLoading && searchResults.length === 0 ? (
검색 중…
) : searchResults.length === 0 ? (
검색 결과가 없습니다. 팔로우한 회원만 검색됩니다.
) : ( <> {searchResults.map(item => ( ))} {searchHasMore && ( )} )}
)} )}
{/* 제목 */}
setTitle(e.target.value)} maxLength={200} placeholder="제목을 입력하세요" className="w-full border rounded-lg px-3 py-2 text-sm" />
{/* 내용 */}