import { useState, useEffect } from 'react'; import { FileText, Search, RefreshCw, Calendar, Edit3, User, Plus } from 'lucide-react'; import { comms } from '@/communication'; import { BoardItem } from '@/types'; interface BoardListProps { bidx: number; title: string; icon?: React.ReactNode; defaultCategory?: string; categories?: { value: string; label: string; color: string }[]; } export function BoardList({ bidx, title, icon = , defaultCategory = 'PATCH', categories = [ { value: 'PATCH', label: 'PATCH', color: 'red' }, { value: 'UPDATE', label: 'UPDATE', color: 'lime' } ] }: BoardListProps) { const [boardList, setBoardList] = useState([]); const [loading, setLoading] = useState(false); const [searchKey, setSearchKey] = useState(''); const [selectedItem, setSelectedItem] = useState(null); const [showModal, setShowModal] = useState(false); const [showEditModal, setShowEditModal] = useState(false); const [showReplyModal, setShowReplyModal] = useState(false); const [editFormData, setEditFormData] = useState(null); const [userLevel, setUserLevel] = useState(0); const [userId, setUserId] = useState(''); const [replies, setReplies] = useState([]); // 댓글 목록 (is_comment=true) const [replyPosts, setReplyPosts] = useState([]); // 답글 목록 (is_comment=false) const [commentText, setCommentText] = useState(''); const [replyFormData, setReplyFormData] = useState<{ title: string; contents: string }>({ title: '', contents: '' }); useEffect(() => { loadUserInfo(); loadData(); }, []); // ESC 키 처리 useEffect(() => { const handleEscKey = (e: KeyboardEvent) => { if (e.key === 'Escape') { if (showReplyModal) { setShowReplyModal(false); } else if (showEditModal) { setShowEditModal(false); } else if (showModal) { setShowModal(false); } } }; document.addEventListener('keydown', handleEscKey); return () => document.removeEventListener('keydown', handleEscKey); }, [showModal, showEditModal, showReplyModal]); const loadUserInfo = async () => { try { const response = await comms.checkLoginStatus(); if (response.Success && response.User) { setUserLevel(response.User.Level); setUserId(response.User.Id); } } catch (error) { console.error('사용자 정보 로드 오류:', error); } }; const loadData = async () => { setLoading(true); try { console.log('게시판 조회:', { bidx, searchKey }); const response = await comms.getBoardList(bidx, searchKey); console.log('게시판 응답:', response); if (response.Success && response.Data) { setBoardList(response.Data); } else { console.warn('게시판 없음:', response.Message); setBoardList([]); } } catch (error) { console.error('게시판 로드 오류:', error); alert('데이터를 불러오는 중 오류가 발생했습니다.'); } finally { setLoading(false); } }; const handleSearch = () => { loadData(); }; const loadReplies = async (rootIdx: number) => { try { const response = await comms.getBoardReplies(rootIdx); if (response.Success && response.Data) { setReplies(response.Data); } else { setReplies([]); } } catch (error) { console.error('댓글 로드 오류:', error); setReplies([]); } }; const loadReplyPosts = async (rootIdx: number) => { try { // 목록에서 해당 root_idx의 답글들만 필터링 const replyList = boardList.filter(item => item.root_idx === rootIdx && !item.is_comment); setReplyPosts(replyList); } catch (error) { console.error('답글 로드 오류:', error); setReplyPosts([]); } }; const handleAddComment = async () => { if (!selectedItem || !commentText.trim()) return; try { const response = await comms.addBoardReply(selectedItem.idx, selectedItem.idx, '', commentText, true); if (response.Success) { setCommentText(''); loadReplies(selectedItem.idx); // reply_count 업데이트를 위해 목록 새로고침 loadData(); } else { alert(response.Message || '댓글 등록에 실패했습니다.'); } } catch (error) { console.error('댓글 등록 오류:', error); alert('댓글 등록 중 오류가 발생했습니다.'); } }; const handleAddReply = async () => { if (!selectedItem || !replyFormData.title.trim() || !replyFormData.contents.trim()) { alert('제목과 내용을 입력해주세요.'); return; } try { // 답글은 is_comment=false, title 포함 const rootIdx = selectedItem.root_idx || selectedItem.idx; const response = await comms.addBoardReply(rootIdx, selectedItem.idx, replyFormData.title, replyFormData.contents, false); if (response.Success) { setShowReplyModal(false); setReplyFormData({ title: '', contents: '' }); await loadData(); // 목록 새로고침 loadReplyPosts(rootIdx); // 답글 목록 새로고침 } else { alert(response.Message || '답글 등록에 실패했습니다.'); } } catch (error) { console.error('답글 등록 오류:', error); alert('답글 등록 중 오류가 발생했습니다.'); } }; const handleRowClick = async (item: BoardItem) => { try { const response = await comms.getBoardDetail(item.idx); if (response.Success && response.Data) { setSelectedItem(response.Data); const rootIdx = response.Data.root_idx || response.Data.idx; loadReplies(rootIdx); // 댓글 로드 loadReplyPosts(rootIdx); // 답글 로드 setShowModal(true); // 모두 뷰어로 보기 } } catch (error) { console.error('상세 조회 오류:', error); alert('데이터를 불러오는 중 오류가 발생했습니다.'); } }; const handleEditClick = () => { if (selectedItem) { setEditFormData(selectedItem); setShowModal(false); setShowEditModal(true); } }; const handleEditSave = async () => { if (!editFormData) return; try { const isNew = editFormData.idx === 0; if (isNew) { // 신규 등록 const response = await comms.addBoard( bidx, editFormData.header || '', editFormData.cate || '', editFormData.title || '', editFormData.contents || '' ); if (response.Success) { setShowEditModal(false); setEditFormData(null); loadData(); } else { alert(response.Message || '등록에 실패했습니다.'); } } else { // 수정 const response = await comms.editBoard( editFormData.idx, editFormData.header || '', editFormData.cate || '', editFormData.title || '', editFormData.contents || '' ); if (response.Success) { setShowEditModal(false); setEditFormData(null); loadData(); } else { alert(response.Message || '수정에 실패했습니다.'); } } } catch (error) { console.error('저장 오류:', error); alert('저장 중 오류가 발생했습니다.'); } }; const handleDelete = async () => { if (!editFormData || editFormData.idx === 0) return; if (!confirm('정말 삭제하시겠습니까?')) return; try { const response = await comms.deleteBoard(editFormData.idx); if (response.Success) { setShowEditModal(false); setEditFormData(null); loadData(); } else { alert(response.Message || '삭제에 실패했습니다.'); } } catch (error) { console.error('삭제 오류:', error); alert('삭제 중 오류가 발생했습니다.'); } }; const formatDate = (dateStr: string | null) => { if (!dateStr) return '-'; try { const date = new Date(dateStr); const yy = String(date.getFullYear()).slice(-2); const mm = String(date.getMonth() + 1).padStart(2, '0'); const dd = String(date.getDate()).padStart(2, '0'); return `${yy}.${mm}.${dd}`; } catch { return dateStr; } }; const isNew = (dateStr: string | null) => { if (!dateStr) return false; try { const date = new Date(dateStr); const now = new Date(); const diffTime = now.getTime() - date.getTime(); const diffDays = diffTime / (1000 * 60 * 60 * 24); return diffDays <= 3; } catch { return false; } }; const getCategoryColor = (cate: string) => { const category = categories.find(c => c.value.toUpperCase() === cate.toUpperCase()); if (!category) return 'bg-gray-500/20 text-gray-400'; switch (category.color) { case 'lime': return 'bg-lime-500/20 text-lime-400'; case 'red': return 'bg-red-500/20 text-red-400'; case 'blue': return 'bg-blue-500/20 text-blue-400'; case 'yellow': return 'bg-yellow-500/20 text-yellow-400'; default: return 'bg-gray-500/20 text-gray-400'; } }; return ( {/* 검색 필터 */} 검색어 setSearchKey(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && handleSearch()} placeholder="제목, 내용, 작성자 등" className="flex-1 h-10 bg-white/20 border border-white/30 rounded-lg px-3 text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-primary-400" /> {loading ? ( ) : ( )} 조회 { setEditFormData({ idx: 0, bidx: bidx, gcode: '', header: '', cate: defaultCategory, title: '', contents: '', file: '', guid: '', url: '', wuid: '', wuid_name: '', wdate: null, project: '', pidx: 0, close: false, remark: '' }); setShowEditModal(true); }} disabled={!(userLevel >= 9 || userId === '395552')} className="h-10 bg-green-500 hover:bg-green-600 text-white px-6 rounded-lg transition-colors flex items-center justify-center disabled:opacity-30 disabled:cursor-not-allowed" > 추가 {/* 게시판 목록 */} {icon} {title} {boardList.length}건 {loading ? ( 데이터를 불러오는 중... ) : boardList.length === 0 ? ( 조회된 데이터가 없습니다. ) : ( boardList.map((item) => ( handleRowClick(item)} style={{ paddingLeft: `${24 + (item.depth || 0) * 24}px` }} > {item.depth && item.depth > 0 && ( ↳ )} {item.cate && ( {item.cate} )} {item.header && ( {item.header} )} {formatDate(item.wdate)} {item.title || '(댓글)'} {isNew(item.wdate) && ( NEW )} {(item.reply_count ?? 0) > 0 && ( 💬 {item.reply_count} )} {item.wuid_name || item.wuid} )) )} {/* 상세 모달 */} {showModal && selectedItem && ( {selectedItem.header && ( {selectedItem.header} )} {selectedItem.cate && ( {selectedItem.cate} )} {selectedItem.title} setShowModal(false)} className="text-white/50 hover:text-white transition-colors" > × {selectedItem.wuid_name || selectedItem.wuid} {formatDate(selectedItem.wdate)} {selectedItem.contents && selectedItem.contents.trim() && ( {selectedItem.contents} )} {/* 답글 목록 */} {replyPosts.length > 0 && ( 답글 {replyPosts.length}개 {replyPosts.map((replyPost) => ( handleRowClick(replyPost)} > {replyPost.depth && replyPost.depth > 0 && ( ↳ )} {replyPost.title} {isNew(replyPost.wdate) && ( NEW )} {replyPost.wuid_name || replyPost.wuid} {formatDate(replyPost.wdate)} {replyPost.contents && ( {replyPost.contents} )} ))} )} {/* 댓글 목록 */} 0 ? "border-t border-white/10 pt-6" : ""}> 댓글 {replies.length}개 {replies.map((reply) => ( {reply.wuid_name || reply.wuid} {formatDate(reply.wdate)} {reply.contents} ))} {/* 댓글 입력 */} setCommentText(e.target.value)} placeholder="댓글을 입력하세요..." rows={3} className="w-full bg-white/10 border border-white/30 rounded-lg px-3 py-2 text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-primary-400 resize-none" /> 댓글 등록 setShowReplyModal(true)} className="px-4 py-2 rounded-lg bg-green-500 hover:bg-green-600 text-white transition-colors" > 답글 달기 {(userLevel >= 9 || userId === '395552') && ( 편집 )} setShowModal(false)} className="px-4 py-2 rounded-lg bg-white/10 hover:bg-white/20 text-white transition-colors" > 닫기 )} {/* 편집 모달 */} {showEditModal && editFormData && ( {editFormData.idx === 0 ? `${title} 등록` : `${title} 편집`} setShowEditModal(false)} className="text-white/50 hover:text-white transition-colors" > × 카테고리 setEditFormData({ ...editFormData, cate: e.target.value })} className="w-full h-9 bg-white/10 border border-white/30 rounded-lg px-2 text-white text-sm focus:outline-none focus:ring-2 focus:ring-primary-400" > {categories.map((cat) => ( {cat.label} ))} 제목 setEditFormData({ ...editFormData, title: e.target.value })} className="w-full h-9 bg-white/10 border border-white/30 rounded-lg px-3 text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-primary-400" placeholder="제목" /> 내용 setEditFormData({ ...editFormData, contents: e.target.value })} rows={15} className="w-full bg-white/10 border border-white/30 rounded-lg px-3 py-2 text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-primary-400 resize-none" placeholder="내용을 입력하세요..." /> {editFormData && editFormData.idx > 0 && ( 삭제 )} setShowEditModal(false)} className="px-4 py-2 rounded-lg bg-white/10 hover:bg-white/20 text-white transition-colors" > 취소 저장 )} {/* 답글 달기 모달 */} {showReplyModal && selectedItem && ( 답글 작성 setShowReplyModal(false)} className="text-white/50 hover:text-white transition-colors" > × 원글 {selectedItem.title} 답글 제목 setReplyFormData({ ...replyFormData, title: e.target.value })} className="w-full h-10 bg-white/10 border border-white/30 rounded-lg px-3 text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-primary-400" placeholder="답글 제목을 입력하세요" /> 답글 내용 setReplyFormData({ ...replyFormData, contents: e.target.value })} rows={15} className="w-full bg-white/10 border border-white/30 rounded-lg px-3 py-2 text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-primary-400 resize-none" placeholder="답글 내용을 입력하세요..." /> setShowReplyModal(false)} className="px-4 py-2 rounded-lg bg-white/10 hover:bg-white/20 text-white transition-colors" > 취소 답글 등록 )} ); }
조회된 데이터가 없습니다.