From c4112aef987bd5e0d2bb1f27f7ef15a78a58b985 Mon Sep 17 00:00:00 2001 From: kdhye1119 Date: Fri, 29 May 2026 16:34:58 +0900 Subject: [PATCH 1/3] =?UTF-8?q?[feat]=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=20api=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/qna/QnADetailPage.js | 604 ++++++++---------- frontend/src/pages/qna/QnAListPage.js | 129 +++- frontend/src/pages/qna/QnAListPage.module.css | 30 + 3 files changed, 378 insertions(+), 385 deletions(-) diff --git a/frontend/src/pages/qna/QnADetailPage.js b/frontend/src/pages/qna/QnADetailPage.js index a947bd9..e2a8596 100644 --- a/frontend/src/pages/qna/QnADetailPage.js +++ b/frontend/src/pages/qna/QnADetailPage.js @@ -1,166 +1,160 @@ -import { useState, useEffect, useCallback } from 'react'; -import { useParams, useNavigate, useLocation } from 'react-router-dom'; -import styles from './QnAListPage.module.css'; -import { FiChevronLeft, FiChevronRight } from 'react-icons/fi'; -import { authFetch } from '../../utils/Api'; +import '../../assets/styles/global.css'; +import { useState, useEffect } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; +import styles from './QnADetailPage.module.css'; +import { FiMoreVertical, FiCornerDownRight } from 'react-icons/fi'; import { - CommentImoji, MeCuriousToo, SortBtn, - OBtn, XBtn, CommentCommentArraw, SumitBtn, + CommentImoji, + MeCuriousToo, + StaffCheck, + SumitBtn, } from '../../components/qna_svg'; +import profileImg from '../../assets/images/profile.png'; +import { authFetch } from '../../utils/Api'; -const MAX_VISIBLE_COMMENTS = 3; - -const DAY_PART_KO = { AM: '오전', PM: '오후' }; -const DAY_OF_WEEK_KO = { - MONDAY: '월', TUESDAY: '화', WEDNESDAY: '수', - THURSDAY: '목', FRIDAY: '금', SATURDAY: '토', SUNDAY: '일', -}; - -function QnAListPage() { - const { sessionId } = useParams(); +function QnADetailPage() { + const { questionId } = useParams(); const navigate = useNavigate(); - const location = useLocation(); - const isPast = location.state?.status === 'AFTER_SESSION'; const isStaff = localStorage.getItem('role') === 'ADMIN'; - const [sessionTitle, setSessionTitle] = useState(''); - const [understanding, setUnderstanding] = useState(null); - const [understandingIndex, setUnderstandingIndex] = useState(0); - const [myChoices, setMyChoices] = useState({}); - - const [popularQuestions, setPopularQuestions] = useState([]); - const [unresolvedQuestions, setUnresolvedQuestions] = useState([]); - const [resolvedQuestions, setResolvedQuestions] = useState([]); - - const [filterCurious, setFilterCurious] = useState(false); - const [filterUnsolved, setFilterUnsolved] = useState(false); - const [sortOrder, setSortOrder] = useState('정렬'); - const [showSortMenu, setShowSortMenu] = useState(false); - - const [commentOpenId, setCommentOpenId] = useState(null); - const [commentInputs, setCommentInputs] = useState({}); - const [newQuestion, setNewQuestion] = useState(''); + const [question, setQuestion] = useState(null); + const [commentText, setCommentText] = useState(''); const [isSubmitting, setIsSubmitting] = useState(false); - const [submitError, setSubmitError] = useState(null); - - const fetchQuestions = useCallback(async (index) => { - try { - const res = await authFetch(`/api/sessions/${sessionId}/questions?understandingIndex=${index}`); - if (!res.ok) throw new Error(`서버 오류: ${res.status}`); - const json = await res.json(); - if (!json.isSuccess) throw new Error(json.message); - - const { session, understanding, questions } = json.result; - - setSessionTitle(`${session.week}주차 ${DAY_OF_WEEK_KO[session.dayOfWeek]}요일 ${DAY_PART_KO[session.dayPart]} (${session.title})`); - setUnderstanding(understanding); - - const allQ = [ - ...(questions.popularQuestions ?? []), - ...(questions.unresolvedQuestions ?? []), - ...(questions.resolvedQuestions ?? []), - ]; + const [loading, setLoading] = useState(true); + const [showMenu, setShowMenu] = useState(false); + const [isEditing, setIsEditing] = useState(false); + const [editText, setEditText] = 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 withLiked = await Promise.all( - allQ.map(async (q) => { + useEffect(() => { + const fetchQuestion = async () => { + try { + setLoading(true); + const res = await authFetch(`/api/questions/${questionId}`); + if (!res.ok) throw new Error(`서버 오류: ${res.status}`); + const json = await res.json(); + if (!json.isSuccess) throw new Error(json.message); + + const result = json.result; + + // 질문 이미지 blob 변환 + if (result.imageUrl) { try { - const r = await authFetch(`/api/questions/${q.questionId}`); - const j = await r.json(); - return { ...q, iLiked: j.result?.isLiked ?? false }; + const imgRes = await authFetch(result.imageUrl); + const blob = await imgRes.blob(); + result.imageUrl = URL.createObjectURL(blob); } catch { - return { ...q, iLiked: false }; + result.imageUrl = null; } - }) - ); - - const idSet = (list) => new Set(list.map(q => q.questionId)); - const popularIds = idSet(questions.popularQuestions ?? []); - const unresolvedIds = idSet(questions.unresolvedQuestions ?? []); - const resolvedIds = idSet(questions.resolvedQuestions ?? []); - - setPopularQuestions(withLiked.filter(q => popularIds.has(q.questionId))); - setUnresolvedQuestions(withLiked.filter(q => unresolvedIds.has(q.questionId))); - setResolvedQuestions(withLiked.filter(q => resolvedIds.has(q.questionId))); - - } catch (err) { - console.error('질문 불러오기 실패:', err); - } - }, [sessionId]); + } - useEffect(() => { - if (sessionId) fetchQuestions(understandingIndex); - }, [sessionId, understandingIndex, fetchQuestions]); + // 댓글 이미지 blob 변환 + if (result.comments) { + result.comments = await Promise.all( + result.comments.map(async (comment) => { + if (comment.imageUrl) { + try { + const imgRes = await authFetch(comment.imageUrl); + const blob = await imgRes.blob(); + return { ...comment, imageUrl: URL.createObjectURL(blob) }; + } catch { + return { ...comment, imageUrl: null }; + } + } + return comment; + }) + ); + } - const goPrevUnderstand = () => { - if (understanding?.hasOlder) setUnderstandingIndex(prev => prev + 1); - }; - const goNextUnderstand = () => { - if (understanding?.hasNewer) setUnderstandingIndex(prev => prev - 1); - }; + console.log(result.displayName); + setQuestion(result); + } catch (err) { + console.error('질문 불러오기 실패:', err); + } finally { + setLoading(false); + } + }; + if (questionId) fetchQuestion(); + }, [questionId]); - const handleUnderstandChoice = async (choice) => { - if (!understanding?.current?.checkId) return; - const checkId = understanding.current.checkId; - const newChoice = myChoices[checkId] === choice ? null : choice; - setMyChoices(prev => ({ ...prev, [checkId]: newChoice })); - if (!newChoice) return; + const toggleLike = async () => { try { - const res = await authFetch( - `/api/sessions/${sessionId}/understanding-checks/${checkId}/responses`, - { method: 'POST', body: JSON.stringify({ choice: newChoice }) } - ); + const res = await authFetch(`/api/questions/${questionId}/like`, { method: 'POST' }); if (!res.ok) throw new Error(); const json = await res.json(); if (json.isSuccess) { - setUnderstanding(prev => ({ + setQuestion(prev => ({ ...prev, - current: { - ...prev.current, - understoodCount: json.result.understoodCount, - notUnderstoodCount: json.result.notUnderstoodCount, - } + likeCount: json.result.likeCount, + isLiked: json.result.isLiked, })); } } catch (err) { - console.error('이해도 응답 실패:', err); + console.error('좋아요 실패:', err); } }; - const toggleLike = async (e, questionId) => { - e.stopPropagation(); + const handleEditStart = () => { + setEditText(question.content); + setIsEditing(true); + setShowMenu(false); + }; + + const handleEditSubmit = async () => { + const text = editText.trim(); + if (!text) return; try { - const res = await authFetch(`/api/questions/${questionId}/like`, { method: 'POST' }); + const res = await authFetch(`/api/questions/${questionId}/modify`, { + method: 'PATCH', + body: JSON.stringify({ content: text }), + }); if (!res.ok) throw new Error(); const json = await res.json(); if (json.isSuccess) { - const update = (list) => list.map(q => - q.questionId === questionId - ? { ...q, likeCount: json.result.likeCount, iLiked: json.result.isLiked } - : q - ); - setPopularQuestions(update); - setUnresolvedQuestions(update); - setResolvedQuestions(update); + setQuestion(prev => ({ ...prev, content: text })); + setIsEditing(false); } } catch (err) { - console.error('좋아요 실패:', err); + console.error('수정 실패:', err); } }; - const toggleCommentInput = (e, questionId) => { - e.stopPropagation(); - if (isPast) return; - setCommentOpenId(prev => prev === questionId ? null : questionId); + const handleDelete = async () => { + if (!window.confirm('질문을 삭제할까요?')) return; + try { + const res = await authFetch(`/api/questions/${questionId}`, { method: 'DELETE' }); + if (!res.ok) throw new Error(); + navigate(-1); + } catch (err) { + console.error('삭제 실패:', err); + } + setShowMenu(false); }; - const handleCommentChange = (questionId, value) => { - setCommentInputs(prev => ({ ...prev, [questionId]: value })); + const handleResolve = async () => { + try { + const res = await authFetch(`/api/questions/${questionId}/status`, { method: 'PATCH' }); + if (!res.ok) throw new Error(); + setQuestion(prev => ({ ...prev, isResolved: true })); + } catch (err) { + console.error('해결됨 처리 실패:', err); + } + setShowMenu(false); }; - const handleCommentSubmit = async (e, questionId) => { - e.stopPropagation(); - const text = (commentInputs[questionId] || '').trim(); + const handleCommentSubmit = async () => { + const text = commentText.trim(); if (!text) return; + setIsSubmitting(true); try { const res = await authFetch(`/api/questions/${questionId}/comments`, { method: 'POST', @@ -171,279 +165,179 @@ function QnAListPage() { if (json.isSuccess) { if (isStaff) { await authFetch(`/api/questions/${questionId}/status`, { method: 'PATCH' }); + setQuestion(prev => ({ ...prev, isResolved: true })); } const newComment = { commentId: json.result.commentId, displayName: json.result.displayName, content: json.result.content, + createdAt: json.result.createdAt, + imageUrl: null, }; - const update = (list) => list.map(q => - q.questionId === questionId - ? { - ...q, - isResolved: isStaff ? true : q.isResolved, - previewComments: [...(q.previewComments ?? []), newComment], - commentCount: (q.commentCount ?? 0) + 1 - } - : q - ); - setPopularQuestions(update); - setUnresolvedQuestions(update); - setResolvedQuestions(update); - setCommentInputs(prev => ({ ...prev, [questionId]: '' })); - setCommentOpenId(null); + setQuestion(prev => ({ + ...prev, + comments: [...(prev.comments ?? []), newComment], + })); + setCommentText(''); } } catch (err) { console.error('댓글 등록 실패:', err); - } - }; - - const handleNewQuestion = async () => { - const text = newQuestion.trim(); - if (!text) return; - setIsSubmitting(true); - setSubmitError(null); - try { - const res = await authFetch(`/api/sessions/${sessionId}/questions`, { - method: 'POST', - body: JSON.stringify({ content: text }), - }); - if (!res.ok) throw new Error(); - const json = await res.json(); - if (json.isSuccess) { - setNewQuestion(''); - fetchQuestions(understandingIndex); - } - } catch (err) { - console.error('질문 등록 실패:', err); - setSubmitError('질문 등록에 실패했어요.'); } finally { setIsSubmitting(false); } }; - const handleNewUnderstandCheck = async () => { - const text = newQuestion.trim(); - if (!text) return; - setIsSubmitting(true); - setSubmitError(null); - try { - const res = await authFetch(`/api/sessions/${sessionId}/understanding-checks`, { - method: 'POST', - body: JSON.stringify({ content: text }), - }); - if (!res.ok) throw new Error(); - const json = await res.json(); - if (json.isSuccess) { - setNewQuestion(''); - setUnderstandingIndex(0); - fetchQuestions(0); - } - } catch (err) { - console.error('이해도 등록 실패:', err); - setSubmitError('이해도 등록에 실패했어요.'); - } finally { - setIsSubmitting(false); - } - }; - - const allQuestions = [ - ...popularQuestions, - ...unresolvedQuestions.filter(q => !popularQuestions.some(p => p.questionId === q.questionId)), - ...resolvedQuestions.filter(q => !popularQuestions.some(p => p.questionId === q.questionId)), - ]; + if (loading) return
불러오는 중...
; + if (!question) return
질문을 찾을 수 없어요
; - const displayedQuestions = (() => { - let list = allQuestions; - if (isStaff && filterUnsolved) list = unresolvedQuestions; - if (!isStaff && filterCurious) list = allQuestions.filter(q => q.iLiked); + const isMyQuestion = question.displayName === '작성자'; - if (sortOrder === '최신순') { - list = [...list].sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); - } else if (sortOrder === '저도궁금해요순') { - list = [...list].sort((a, b) => b.likeCount - a.likeCount); - } + return ( +
+ {/* 상단 바: 해결 여부 */} +
+ {question.isResolved ? ( + 해결 질문 + ) : ( + 미해결 질문 + )} +
- return list; - })(); + {/* 작성자 행 */} +
+
+ {question.displayName} +
+
+ 익명 + {formatDate(question.createdAt)} +
+ {(isMyQuestion || isStaff) && ( +
+ + {showMenu && ( +
+ {isMyQuestion && ( + <> + + + + )} + {isStaff && !isMyQuestion && ( + <> + + {!question.isResolved && ( + + )} + + )} +
+ )} +
+ )} +
- const currentChoice = myChoices[understanding?.current?.checkId]; - return ( -
-

{sessionTitle}

-
- {isStaff ? ( - + {/* 질문 내용 */} +
+ Q. + {isEditing ? ( +
+