Files
Groupware/Project/frontend/src/pages/BoardList.tsx
backuppc c1c615fe1b feat: 게시판 댓글/답글 시스템 및 대시보드 개선
주요 변경사항:
- 게시판 계층형 댓글/답글 시스템 구현
  - DB: root_idx, depth, thread_path, is_comment, reply_count 컬럼 추가
  - 트리거: 댓글 개수 자동 업데이트
  - 답글(is_comment=false)은 목록에 표시, 댓글(is_comment=true)은 뷰어에만 표시
  - ESC 키로 모달 닫기 기능

- 업무일지 개선
  - 프로젝트 선택 시 최종 설정 자동 불러오기
  - 복사 시 jobgrp, tag 포함
  - 완료(보고) 상태 프로젝트도 검색 가능하도록 수정

- 대시보드 개선
  - 할일 목록 페이징 추가 (6개씩)
  - 할일에 요청자 정보 표시 (제목 좌측에 괄호로)
2025-12-03 10:10:29 +09:00

752 lines
30 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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={() => 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>
);
}