- 메뉴: 즐겨찾기를 할일 좌측으로 이동 - 게시판: 답글 있는 게시글 삭제 방지 (댓글은 허용) - 즐겨찾기: ESC 키로 다이얼로그 닫기 지원 - 프로젝트: 기본 필터를 검토/진행/완료로 변경 (보류 해제)
775 lines
31 KiB
TypeScript
775 lines
31 KiB
TypeScript
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 = <FileText className="w-5 h-5" />,
|
||
defaultCategory = 'PATCH',
|
||
categories = [
|
||
{ value: 'PATCH', label: 'PATCH', color: 'red' },
|
||
{ value: 'UPDATE', label: 'UPDATE', color: 'lime' }
|
||
]
|
||
}: BoardListProps) {
|
||
const [boardList, setBoardList] = useState<BoardItem[]>([]);
|
||
const [loading, setLoading] = useState(false);
|
||
const [searchKey, setSearchKey] = useState('');
|
||
const [selectedItem, setSelectedItem] = useState<BoardItem | null>(null);
|
||
const [showModal, setShowModal] = useState(false);
|
||
const [showEditModal, setShowEditModal] = useState(false);
|
||
const [showReplyModal, setShowReplyModal] = useState(false);
|
||
const [editFormData, setEditFormData] = useState<BoardItem | null>(null);
|
||
const [userLevel, setUserLevel] = useState(0);
|
||
const [userId, setUserId] = useState('');
|
||
const [replies, setReplies] = useState<BoardItem[]>([]); // 댓글 목록 (is_comment=true)
|
||
const [replyPosts, setReplyPosts] = useState<BoardItem[]>([]); // 답글 목록 (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 (
|
||
<div className="space-y-6 animate-fade-in">
|
||
{/* 검색 필터 */}
|
||
<div className="glass-effect rounded-2xl p-6">
|
||
<div className="flex items-center gap-4">
|
||
<div className="flex items-center gap-2 flex-1">
|
||
<label className="text-white/70 text-sm font-medium whitespace-nowrap">검색어</label>
|
||
<input
|
||
type="text"
|
||
value={searchKey}
|
||
onChange={(e) => 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"
|
||
/>
|
||
</div>
|
||
|
||
<button
|
||
onClick={handleSearch}
|
||
disabled={loading}
|
||
className="h-10 bg-primary-500 hover:bg-primary-600 text-white px-6 rounded-lg transition-colors flex items-center justify-center disabled:opacity-50"
|
||
>
|
||
{loading ? (
|
||
<RefreshCw className="w-4 h-4 mr-2 animate-spin" />
|
||
) : (
|
||
<Search className="w-4 h-4 mr-2" />
|
||
)}
|
||
조회
|
||
</button>
|
||
|
||
<button
|
||
onClick={() => {
|
||
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"
|
||
>
|
||
<Plus className="w-4 h-4 mr-2" />
|
||
추가
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 게시판 목록 */}
|
||
<div className="glass-effect rounded-2xl overflow-hidden">
|
||
<div className="px-6 py-4 border-b border-white/10 flex items-center justify-between">
|
||
<h3 className="text-lg font-semibold text-white flex items-center">
|
||
{icon}
|
||
<span className="ml-2">{title}</span>
|
||
</h3>
|
||
<span className="text-white/60 text-sm">{boardList.length}건</span>
|
||
</div>
|
||
|
||
<div className="divide-y divide-white/10 max-h-[calc(100vh-300px)] overflow-y-auto">
|
||
{loading ? (
|
||
<div className="px-6 py-8 text-center">
|
||
<div className="flex items-center justify-center">
|
||
<RefreshCw className="w-5 h-5 mr-2 animate-spin text-white/50" />
|
||
<span className="text-white/50">데이터를 불러오는 중...</span>
|
||
</div>
|
||
</div>
|
||
) : boardList.length === 0 ? (
|
||
<div className="px-6 py-8 text-center">
|
||
<FileText className="w-12 h-12 mx-auto mb-3 text-white/30" />
|
||
<p className="text-white/50">조회된 데이터가 없습니다.</p>
|
||
</div>
|
||
) : (
|
||
boardList.map((item) => (
|
||
<div
|
||
key={item.idx}
|
||
className="px-6 py-3 hover:bg-white/5 transition-colors cursor-pointer"
|
||
onClick={() => handleRowClick(item)}
|
||
style={{ paddingLeft: `${24 + (item.depth || 0) * 24}px` }}
|
||
>
|
||
<div className="flex items-center gap-3">
|
||
<div className="flex items-center gap-2 flex-shrink-0">
|
||
{item.depth && item.depth > 0 && (
|
||
<span className="text-white/40 text-xs mr-1">↳</span>
|
||
)}
|
||
{item.cate && (
|
||
<span className={`px-2 py-0.5 text-xs rounded whitespace-nowrap ${getCategoryColor(item.cate)}`}>
|
||
{item.cate}
|
||
</span>
|
||
)}
|
||
{item.header && (
|
||
<span className="px-2 py-0.5 bg-primary-500/20 text-primary-400 text-xs rounded whitespace-nowrap">
|
||
{item.header}
|
||
</span>
|
||
)}
|
||
</div>
|
||
<div className="flex items-center text-white/60 text-xs flex-shrink-0 mr-3">
|
||
<Calendar className="w-3 h-3 mr-1" />
|
||
{formatDate(item.wdate)}
|
||
</div>
|
||
<h4 className="text-white font-medium flex-1 min-w-0 flex items-center gap-2">
|
||
<span className="truncate">{item.title || '(댓글)'}</span>
|
||
{isNew(item.wdate) && (
|
||
<span className="px-1.5 py-0.5 bg-yellow-500 text-white text-[10px] rounded font-bold animate-pulse flex-shrink-0">
|
||
NEW
|
||
</span>
|
||
)}
|
||
{(item.reply_count ?? 0) > 0 && (
|
||
<span className="px-1.5 py-0.5 bg-blue-500/20 text-blue-400 text-[10px] rounded flex-shrink-0">
|
||
💬 {item.reply_count}
|
||
</span>
|
||
)}
|
||
</h4>
|
||
<div className="flex items-center text-white/60 text-xs flex-shrink-0">
|
||
<User className="w-3 h-3 mr-1" />
|
||
{item.wuid_name || item.wuid}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* 상세 모달 */}
|
||
{showModal && selectedItem && (
|
||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm">
|
||
<div className="bg-gray-900 rounded-2xl shadow-2xl w-full max-w-4xl max-h-[90vh] overflow-hidden border border-white/10">
|
||
<div className="flex items-center justify-between px-6 py-4 border-b border-white/10">
|
||
<div className="flex items-center gap-2">
|
||
{selectedItem.header && (
|
||
<span className="px-2 py-1 bg-primary-500/20 text-primary-400 text-sm rounded">
|
||
{selectedItem.header}
|
||
</span>
|
||
)}
|
||
{selectedItem.cate && (
|
||
<span className={`px-2 py-1 text-sm rounded ${getCategoryColor(selectedItem.cate)}`}>
|
||
{selectedItem.cate}
|
||
</span>
|
||
)}
|
||
<h2 className="text-xl font-bold text-white ml-2">{selectedItem.title}</h2>
|
||
</div>
|
||
<button
|
||
onClick={() => setShowModal(false)}
|
||
className="text-white/50 hover:text-white transition-colors"
|
||
>
|
||
<span className="text-2xl">×</span>
|
||
</button>
|
||
</div>
|
||
|
||
<div className="px-6 py-4 border-b border-white/10 flex items-center gap-4 text-sm text-white/60">
|
||
<div className="flex items-center">
|
||
<User className="w-4 h-4 mr-1" />
|
||
{selectedItem.wuid_name || selectedItem.wuid}
|
||
</div>
|
||
<div className="flex items-center">
|
||
<Calendar className="w-4 h-4 mr-1" />
|
||
{formatDate(selectedItem.wdate)}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="overflow-y-auto max-h-[calc(90vh-400px)] p-6">
|
||
{selectedItem.contents && selectedItem.contents.trim() && (
|
||
<div className="prose prose-invert max-w-none mb-8">
|
||
<div className="text-white whitespace-pre-wrap">{selectedItem.contents}</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 답글 목록 */}
|
||
{replyPosts.length > 0 && (
|
||
<div className={selectedItem.contents && selectedItem.contents.trim() ? "border-t border-white/10 pt-6 mb-6" : "mb-6"}>
|
||
<h3 className="text-lg font-semibold text-white mb-4">
|
||
답글 {replyPosts.length}개
|
||
</h3>
|
||
|
||
<div className="space-y-3">
|
||
{replyPosts.map((replyPost) => (
|
||
<div
|
||
key={replyPost.idx}
|
||
className="bg-white/5 hover:bg-white/10 rounded-lg p-4 cursor-pointer transition-colors"
|
||
onClick={() => handleRowClick(replyPost)}
|
||
>
|
||
<div className="flex items-center justify-between mb-2">
|
||
<h4 className="text-white font-medium flex items-center gap-2">
|
||
{replyPost.depth && replyPost.depth > 0 && (
|
||
<span className="text-white/40 text-sm">↳</span>
|
||
)}
|
||
<span>{replyPost.title}</span>
|
||
{isNew(replyPost.wdate) && (
|
||
<span className="px-1.5 py-0.5 bg-yellow-500 text-white text-[10px] rounded font-bold">
|
||
NEW
|
||
</span>
|
||
)}
|
||
</h4>
|
||
<div className="flex items-center gap-3 text-xs text-white/60">
|
||
<div className="flex items-center">
|
||
<User className="w-3 h-3 mr-1" />
|
||
{replyPost.wuid_name || replyPost.wuid}
|
||
</div>
|
||
<div className="flex items-center">
|
||
<Calendar className="w-3 h-3 mr-1" />
|
||
{formatDate(replyPost.wdate)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{replyPost.contents && (
|
||
<div className="text-white/60 text-sm line-clamp-2">
|
||
{replyPost.contents}
|
||
</div>
|
||
)}
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 댓글 목록 */}
|
||
<div className={(selectedItem.contents && selectedItem.contents.trim()) || replyPosts.length > 0 ? "border-t border-white/10 pt-6" : ""}>
|
||
<h3 className="text-lg font-semibold text-white mb-4">
|
||
댓글 {replies.length}개
|
||
</h3>
|
||
|
||
<div className="space-y-4 mb-6">
|
||
{replies.map((reply) => (
|
||
<div key={reply.idx} className="bg-white/5 rounded-lg p-4">
|
||
<div className="flex items-center gap-2 mb-2">
|
||
<User className="w-4 h-4 text-white/60" />
|
||
<span className="text-sm text-white/80">{reply.wuid_name || reply.wuid}</span>
|
||
<span className="text-xs text-white/50">{formatDate(reply.wdate)}</span>
|
||
</div>
|
||
<div className="text-white/70 text-sm whitespace-pre-wrap">{reply.contents}</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
{/* 댓글 입력 */}
|
||
<div className="bg-white/5 rounded-lg p-4">
|
||
<textarea
|
||
value={commentText}
|
||
onChange={(e) => 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"
|
||
/>
|
||
<div className="flex justify-end mt-2">
|
||
<button
|
||
onClick={handleAddComment}
|
||
disabled={!commentText.trim()}
|
||
className="px-4 py-2 bg-primary-500 hover:bg-primary-600 disabled:opacity-50 disabled:cursor-not-allowed text-white rounded-lg transition-colors"
|
||
>
|
||
댓글 등록
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex items-center justify-between px-6 py-4 border-t border-white/10 bg-white/5">
|
||
<div className="flex items-center gap-2">
|
||
<button
|
||
onClick={() => setShowReplyModal(true)}
|
||
className="px-4 py-2 rounded-lg bg-green-500 hover:bg-green-600 text-white transition-colors"
|
||
>
|
||
답글 달기
|
||
</button>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
{(userLevel >= 9 || userId === '395552') && (
|
||
<>
|
||
<button
|
||
onClick={handleEditClick}
|
||
className="px-4 py-2 rounded-lg bg-primary-500 hover:bg-primary-600 text-white transition-colors flex items-center"
|
||
>
|
||
<Edit3 className="w-4 h-4 mr-2" />
|
||
편집
|
||
</button>
|
||
<button
|
||
onClick={async () => {
|
||
if (!selectedItem) return;
|
||
if (!confirm('정말 삭제하시겠습니까?')) return;
|
||
try {
|
||
const response = await comms.deleteBoard(selectedItem.idx);
|
||
if (response.Success) {
|
||
setShowModal(false);
|
||
loadData();
|
||
} else {
|
||
alert(response.Message || '삭제에 실패했습니다.');
|
||
}
|
||
} catch (error) {
|
||
console.error('삭제 오류:', error);
|
||
alert('삭제 중 오류가 발생했습니다.');
|
||
}
|
||
}}
|
||
className="px-4 py-2 rounded-lg bg-red-500 hover:bg-red-600 text-white transition-colors"
|
||
>
|
||
삭제
|
||
</button>
|
||
</>
|
||
)}
|
||
<button
|
||
onClick={() => setShowModal(false)}
|
||
className="px-4 py-2 rounded-lg bg-white/10 hover:bg-white/20 text-white transition-colors"
|
||
>
|
||
닫기
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 편집 모달 */}
|
||
{showEditModal && editFormData && (
|
||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm">
|
||
<div className="bg-gray-900 rounded-2xl shadow-2xl w-full max-w-4xl max-h-[90vh] overflow-hidden border border-white/10">
|
||
<div className="flex items-center justify-between px-6 py-4 border-b border-white/10">
|
||
<h2 className="text-xl font-bold text-white flex items-center">
|
||
<Edit3 className="w-5 h-5 mr-2" />
|
||
{editFormData.idx === 0 ? `${title} 등록` : `${title} 편집`}
|
||
</h2>
|
||
<button
|
||
onClick={() => setShowEditModal(false)}
|
||
className="text-white/50 hover:text-white transition-colors"
|
||
>
|
||
<span className="text-2xl">×</span>
|
||
</button>
|
||
</div>
|
||
|
||
<div className="overflow-y-auto max-h-[calc(90vh-180px)] p-6 space-y-4">
|
||
<div className="flex items-center gap-3">
|
||
<div className="w-32">
|
||
<label className="block text-white/70 text-xs font-medium mb-1">카테고리</label>
|
||
<select
|
||
value={editFormData.cate || defaultCategory}
|
||
onChange={(e) => 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) => (
|
||
<option key={cat.value} value={cat.value} className="bg-gray-800">
|
||
{cat.label}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
|
||
<div className="flex-1">
|
||
<label className="block text-white/70 text-xs font-medium mb-1">제목</label>
|
||
<input
|
||
type="text"
|
||
value={editFormData.title || ''}
|
||
onChange={(e) => 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="제목"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-white/70 text-sm font-medium mb-2">내용</label>
|
||
<textarea
|
||
value={editFormData.contents || ''}
|
||
onChange={(e) => 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="내용을 입력하세요..."
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex items-center justify-between px-6 py-4 border-t border-white/10 bg-white/5">
|
||
<div>
|
||
{editFormData && editFormData.idx > 0 && (
|
||
<button
|
||
onClick={handleDelete}
|
||
className="px-4 py-2 rounded-lg bg-red-500 hover:bg-red-600 text-white transition-colors"
|
||
>
|
||
삭제
|
||
</button>
|
||
)}
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<button
|
||
onClick={() => setShowEditModal(false)}
|
||
className="px-4 py-2 rounded-lg bg-white/10 hover:bg-white/20 text-white transition-colors"
|
||
>
|
||
취소
|
||
</button>
|
||
<button
|
||
onClick={handleEditSave}
|
||
className="px-4 py-2 rounded-lg bg-primary-500 hover:bg-primary-600 text-white transition-colors"
|
||
>
|
||
저장
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 답글 달기 모달 */}
|
||
{showReplyModal && selectedItem && (
|
||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm">
|
||
<div className="bg-gray-900 rounded-2xl shadow-2xl w-full max-w-4xl max-h-[90vh] overflow-hidden border border-white/10">
|
||
<div className="flex items-center justify-between px-6 py-4 border-b border-white/10">
|
||
<h2 className="text-xl font-bold text-white flex items-center">
|
||
<Edit3 className="w-5 h-5 mr-2" />
|
||
답글 작성
|
||
</h2>
|
||
<button
|
||
onClick={() => setShowReplyModal(false)}
|
||
className="text-white/50 hover:text-white transition-colors"
|
||
>
|
||
<span className="text-2xl">×</span>
|
||
</button>
|
||
</div>
|
||
|
||
<div className="overflow-y-auto max-h-[calc(90vh-180px)] p-6 space-y-4">
|
||
<div className="bg-white/5 rounded-lg p-4 mb-4">
|
||
<div className="text-sm text-white/60 mb-2">원글</div>
|
||
<div className="text-white font-medium">{selectedItem.title}</div>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-white/70 text-sm font-medium mb-2">답글 제목</label>
|
||
<input
|
||
type="text"
|
||
value={replyFormData.title}
|
||
onChange={(e) => 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="답글 제목을 입력하세요"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-white/70 text-sm font-medium mb-2">답글 내용</label>
|
||
<textarea
|
||
value={replyFormData.contents}
|
||
onChange={(e) => 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="답글 내용을 입력하세요..."
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex items-center justify-end gap-2 px-6 py-4 border-t border-white/10 bg-white/5">
|
||
<button
|
||
onClick={() => setShowReplyModal(false)}
|
||
className="px-4 py-2 rounded-lg bg-white/10 hover:bg-white/20 text-white transition-colors"
|
||
>
|
||
취소
|
||
</button>
|
||
<button
|
||
onClick={handleAddReply}
|
||
className="px-4 py-2 rounded-lg bg-primary-500 hover:bg-primary-600 text-white transition-colors"
|
||
>
|
||
답글 등록
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|