diff --git a/frontend/src/pages/qna/QnADetailPage.js b/frontend/src/pages/qna/QnADetailPage.js index c25d29f..a23ba91 100644 --- a/frontend/src/pages/qna/QnADetailPage.js +++ b/frontend/src/pages/qna/QnADetailPage.js @@ -8,86 +8,49 @@ import { MeCuriousToo, StaffCheck, SumitBtn, -} from '../../components/qna_svg'; + uploadImage, +} from '../../utils/qnaUtils'; import profileImg from '../../assets/images/profile.png'; import { authFetch } from '../../utils/Api'; +// 시간만 표시하는 포맷 함수 (HH:MM) +const formatTime = (dateStr) => { + if (!dateStr) return ''; + const d = new Date(dateStr); + return d.toLocaleTimeString('ko-KR', { + hour: '2-digit', + minute: '2-digit', + hour12: false, + }); +}; + function QnADetailPage() { const { questionId } = useParams(); const navigate = useNavigate(); const isStaff = localStorage.getItem('role') === 'ADMIN'; + // ── 질문 / 로딩 상태 ───────────────────────────── const [question, setQuestion] = useState(null); - const [commentText, setCommentText] = useState(''); - const [isSubmitting, setIsSubmitting] = useState(false); const [loading, setLoading] = useState(true); + + // ── 질문 수정 상태 ─────────────────────────────── const [showMenu, setShowMenu] = useState(false); const [isEditing, setIsEditing] = useState(false); const [editText, setEditText] = useState(''); + + // ── 댓글 입력 상태 ─────────────────────────────── + const [commentText, setCommentText] = useState(''); + const [isSubmitting, setIsSubmitting] = useState(false); const [selectedImage, setSelectedImage] = useState(null); const [imagePreview, setImagePreview] = useState(null); const fileInputRef = useRef(null); + // ── 댓글 수정 상태 ─────────────────────────────── const [commentMenuId, setCommentMenuId] = useState(null); const [editingCommentId, setEditingCommentId] = useState(null); const [editCommentText, setEditCommentText] = useState(''); - const formatDate = (dateStr) => { - if (!dateStr) return ''; - const d = new Date(dateStr); - return d.toLocaleTimeString('ko-KR', { - hour: '2-digit', - minute: '2-digit', - hour12: false, - }); - }; - - const handleCommentDelete = async (commentId) => { - if (!window.confirm('댓글을 삭제할까요?')) return; - try { - const res = await authFetch(`/api/comments/${commentId}`, { method: 'DELETE' }); - if (!res.ok) throw new Error(); - setQuestion(prev => ({ - ...prev, - comments: prev.comments.filter(c => c.commentId !== commentId), - })); - } catch (err) { - console.error('댓글 삭제 실패:', err); - } - setCommentMenuId(null); - }; - - const handleCommentEditStart = (comment) => { - setEditingCommentId(comment.commentId); - setEditCommentText(comment.content); - setCommentMenuId(null); - }; - - const handleCommentEditSubmit = async (commentId) => { - const text = editCommentText.trim(); - if (!text) return; - try { - const res = await authFetch(`/api/comments/${commentId}`, { - method: 'PATCH', - body: JSON.stringify({ content: text }), - }); - if (!res.ok) throw new Error(); - const json = await res.json(); - if (json.isSuccess) { - setQuestion(prev => ({ - ...prev, - comments: prev.comments.map(c => - c.commentId === commentId ? { ...c, content: text } : c - ), - })); - setEditingCommentId(null); - } - } catch (err) { - console.error('댓글 수정 실패:', err); - } - }; - - + // ── 질문 불러오기 ──────────────────────────────── useEffect(() => { const fetchQuestion = async () => { try { @@ -99,6 +62,7 @@ function QnADetailPage() { const result = json.result; + // 질문 이미지 blob 변환 if (result.imageUrl) { try { const imgRes = await authFetch(result.imageUrl); @@ -109,6 +73,7 @@ function QnADetailPage() { } } + // 댓글 이미지 blob 변환 if (result.comments) { result.comments = await Promise.all( result.comments.map(async (comment) => { @@ -135,15 +100,17 @@ function QnADetailPage() { if (questionId) fetchQuestion(); }, [questionId]); + // ── 메뉴 외부 클릭 시 닫기 ────────────────────── useEffect(() => { const handleClickOutside = () => { setShowMenu(false); - setCommentMenuId(null); // ← 추가 + setCommentMenuId(null); }; if (showMenu || commentMenuId) document.addEventListener('click', handleClickOutside); return () => document.removeEventListener('click', handleClickOutside); }, [showMenu, commentMenuId]); + // ── 좋아요 토글 ────────────────────────────────── const toggleLike = async () => { try { const res = await authFetch(`/api/questions/${questionId}/like`, { method: 'POST' }); @@ -161,6 +128,7 @@ function QnADetailPage() { } }; + // ── 질문 수정 ──────────────────────────────────── const handleEditStart = () => { setEditText(question.content); setIsEditing(true); @@ -186,6 +154,7 @@ function QnADetailPage() { } }; + // ── 질문 삭제 ──────────────────────────────────── const handleDelete = async () => { if (!window.confirm('질문을 삭제할까요?')) return; try { @@ -198,6 +167,7 @@ function QnADetailPage() { setShowMenu(false); }; + // ── 질문 해결됨 처리 (운영진 전용) ────────────── const handleResolve = async () => { try { const res = await authFetch(`/api/questions/${questionId}/status`, { method: 'PATCH' }); @@ -209,6 +179,7 @@ function QnADetailPage() { setShowMenu(false); }; + // ── 댓글 이미지 선택 / 붙여넣기 ───────────────── const handleImageSelect = (e) => { const file = e.target.files[0]; if (!file) return; @@ -216,19 +187,6 @@ function QnADetailPage() { setImagePreview(URL.createObjectURL(file)); }; - const uploadImage = async (file) => { - const formData = new FormData(); - formData.append('file', file); - const token = localStorage.getItem('token'); - const res = await fetch('/api/images', { - method: 'POST', - headers: { 'Authorization': `Bearer ${token}` }, - body: formData, - }); - const json = await res.json(); - return json.imageUrl; - }; - const handlePaste = (e) => { const items = e.clipboardData?.items; if (!items) return; @@ -244,6 +202,7 @@ function QnADetailPage() { } }; + // ── 댓글 등록 ──────────────────────────────────── const handleCommentSubmit = async () => { const text = commentText.trim(); if (!text) return; @@ -260,13 +219,8 @@ function QnADetailPage() { if (!res.ok) throw new Error(); const json = await res.json(); if (json.isSuccess) { - if (isStaff) { - await authFetch(`/api/questions/${questionId}/status`, { method: 'PATCH' }); - setQuestion(prev => ({ ...prev, isResolved: true })); - } else if (question.isResolved) { - await authFetch(`/api/questions/${questionId}/status`, { method: 'PATCH' }); - setQuestion(prev => ({ ...prev, isResolved: false })); - } + // 댓글 등록 응답값으로 해결 상태 반영 + setQuestion(prev => ({ ...prev, isResolved: json.result.isResolved })); const newComment = { commentId: json.result.commentId, @@ -274,6 +228,7 @@ function QnADetailPage() { content: json.result.content, createdAt: json.result.createdAt, imageUrl: imagePreview, + isMine: true, }; setQuestion(prev => ({ ...prev, @@ -290,6 +245,53 @@ function QnADetailPage() { } }; + // ── 댓글 삭제 ──────────────────────────────────── + const handleCommentDelete = async (commentId) => { + if (!window.confirm('댓글을 삭제할까요?')) return; + try { + const res = await authFetch(`/api/comments/${commentId}`, { method: 'DELETE' }); + if (!res.ok) throw new Error(); + setQuestion(prev => ({ + ...prev, + comments: prev.comments.filter(c => c.commentId !== commentId), + })); + } catch (err) { + console.error('댓글 삭제 실패:', err); + } + setCommentMenuId(null); + }; + + // ── 댓글 수정 ──────────────────────────────────── + const handleCommentEditStart = (comment) => { + setEditingCommentId(comment.commentId); + setEditCommentText(comment.content); + setCommentMenuId(null); + }; + + const handleCommentEditSubmit = async (commentId) => { + const text = editCommentText.trim(); + if (!text) return; + try { + const res = await authFetch(`/api/comments/${commentId}`, { + method: 'PATCH', + body: JSON.stringify({ content: text }), + }); + if (!res.ok) throw new Error(); + const json = await res.json(); + if (json.isSuccess) { + setQuestion(prev => ({ + ...prev, + comments: prev.comments.map(c => + c.commentId === commentId ? { ...c, content: text } : c + ), + })); + setEditingCommentId(null); + } + } catch (err) { + console.error('댓글 수정 실패:', err); + } + }; + if (loading) return
불러오는 중...
; if (!question) return
질문을 찾을 수 없어요
; @@ -298,18 +300,19 @@ function QnADetailPage() { return (
- {/* 작성자 행 */} + {/* ── 작성자 행 ── */}
{question.displayName}
익명 - {formatDate(question.createdAt)} + {formatTime(question.createdAt)}
{(isMyQuestion || isStaff) && ( -
- {showMenu && ( @@ -334,7 +337,7 @@ function QnADetailPage() { )}
- {/* 상단 바: 해결 여부 */} + {/* ── 해결 여부 뱃지 ── */}
{question.isResolved ? ( 해결 질문 @@ -343,21 +346,18 @@ function QnADetailPage() { )}
- {/* 질문 내용 */} + {/* ── 질문 본문 ── */}
- Q. + Q. {isEditing ? ( -
+