diff --git a/frontend/src/App.js b/frontend/src/App.js
index 0e5c892..bc1358b 100644
--- a/frontend/src/App.js
+++ b/frontend/src/App.js
@@ -26,19 +26,19 @@ function App() {
{/* 라이트 헤더 페이지 */}
}>
} />
- } />
- } />
+ } />
+ } />
} />
{/* 다크 헤더 페이지 */}
}>
- }/>
- }/>
- }/>
- }/>
- }/>
- }/>
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
diff --git a/frontend/src/pages/login/LoginPage.js b/frontend/src/pages/login/LoginPage.js
index c025156..91a2eeb 100644
--- a/frontend/src/pages/login/LoginPage.js
+++ b/frontend/src/pages/login/LoginPage.js
@@ -24,17 +24,15 @@ function LoginPage() {
if (response.ok) {
const data = await response.json();
- console.log('로그인 응답:', data);
localStorage.setItem('token', data.token);
localStorage.setItem('role', data.role);
+ localStorage.setItem('name', data.name);
navigate('/sessions'); // 로그인 성공 시 이동할 페이지
} else {
const errData = await response.json();
- console.log('로그인 실패 응답:', errData);
alert('이름 또는 비밀번호가 올바르지 않습니다.');
}
} catch (error) {
- console.log('catch 에러:', error);
alert('서버 오류가 발생했습니다.');
}
};
diff --git a/frontend/src/pages/qna/QnADetailPage.js b/frontend/src/pages/qna/QnADetailPage.js
index f617a2b..a947bd9 100644
--- a/frontend/src/pages/qna/QnADetailPage.js
+++ b/frontend/src/pages/qna/QnADetailPage.js
@@ -1,245 +1,449 @@
-import '../../assets/styles/global.css';
-import { useState } from 'react';
-import styles from './QnADetailPage.module.css';
-import { FiChevronLeft, FiMoreVertical, FiCornerDownRight } from 'react-icons/fi';
+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 {
- CommentImoji,
- MeCuriousToo,
- StaffCheck,
- SumitBtn,
+ CommentImoji, MeCuriousToo, SortBtn,
+ OBtn, XBtn, CommentCommentArraw, SumitBtn,
} from '../../components/qna_svg';
-import profileImg from '../../assets/images/profile.png';
-
-
-
-// ── 목업 데이터 ──────────────────────────────────────────
-const MOCK_QUESTION = {
- id: 2,
- author: '익명',
- isStaff: false,
- avatarUrl: null,
- date: '2025/04/25 13:20',
- text: '오류 났어요',
- image: 'https://dora-guide.com/wp-content/uploads/2019/11/Visual-studio-code-%EC%84%A4%EC%B9%98-%EB%B0%8F-%EC%82%AC%EC%9A%A9%EB%B2%95.png',
- likes: 7,
- iLiked: false,
- isSolved: true,
- comments: [
- {
- id: 1,
- author: '운영진1',
- isStaff: true,
- avatarUrl: null,
- date: '2025/04/25 13:28',
- content: '사진 참고하세요',
- image: 'https://dora-guide.com/wp-content/uploads/2019/11/Visual-studio-code-%EC%84%A4%EC%B9%98-%EB%B0%8F-%EC%82%AC%EC%9A%A9%EB%B2%95.png',
- },
- {
- id: 2,
- author: '작성자',
- isStaff: false,
- avatarUrl: null,
- date: '2025/04/25 13:28',
- content: '감사합니다',
- image: null,
- },
- {
- id: 3,
- author: '익명1',
- isStaff: false,
- avatarUrl: null,
- date: '2025/04/25 13:28',
- content: '감사합니다',
- image: null,
- },
- ],
+
+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();
+ 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({});
-// ── 메인 컴포넌트 ────────────────────────────────────────
-function QnADetailPage({
- question: initialQuestion = MOCK_QUESTION,
- isStaff = false,
- onBack,
-}) {
- const [question, setQuestion] = useState(initialQuestion);
- const [commentText, setCommentText] = 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 [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 withLiked = await Promise.all(
+ allQ.map(async (q) => {
+ try {
+ const r = await authFetch(`/api/questions/${q.questionId}`);
+ const j = await r.json();
+ return { ...q, iLiked: j.result?.isLiked ?? false };
+ } catch {
+ return { ...q, iLiked: false };
+ }
+ })
+ );
+
+ 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 ?? []);
- // 좋아요 토글
- const toggleLike = () => {
- setQuestion(prev => ({
- ...prev,
- iLiked: !prev.iLiked,
- likes: prev.iLiked ? prev.likes - 1 : prev.likes + 1,
- }));
+ 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]);
+
+ const goPrevUnderstand = () => {
+ if (understanding?.hasOlder) setUnderstandingIndex(prev => prev + 1);
+ };
+ const goNextUnderstand = () => {
+ if (understanding?.hasNewer) setUnderstandingIndex(prev => prev - 1);
+ };
+
+ 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;
+ try {
+ const res = await authFetch(
+ `/api/sessions/${sessionId}/understanding-checks/${checkId}/responses`,
+ { method: 'POST', body: JSON.stringify({ choice: newChoice }) }
+ );
+ if (!res.ok) throw new Error();
+ const json = await res.json();
+ if (json.isSuccess) {
+ setUnderstanding(prev => ({
+ ...prev,
+ current: {
+ ...prev.current,
+ understoodCount: json.result.understoodCount,
+ notUnderstoodCount: json.result.notUnderstoodCount,
+ }
+ }));
+ }
+ } catch (err) {
+ console.error('이해도 응답 실패:', err);
+ }
};
- // 댓글 제출
- const handleCommentSubmit = async () => {
- const text = commentText.trim();
+ const toggleLike = async (e, questionId) => {
+ e.stopPropagation();
+ try {
+ const res = await authFetch(`/api/questions/${questionId}/like`, { method: 'POST' });
+ 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);
+ }
+ } catch (err) {
+ console.error('좋아요 실패:', err);
+ }
+ };
+
+ const toggleCommentInput = (e, questionId) => {
+ e.stopPropagation();
+ if (isPast) return;
+ setCommentOpenId(prev => prev === questionId ? null : questionId);
+ };
+
+ const handleCommentChange = (questionId, value) => {
+ setCommentInputs(prev => ({ ...prev, [questionId]: value }));
+ };
+
+ const handleCommentSubmit = async (e, questionId) => {
+ e.stopPropagation();
+ const text = (commentInputs[questionId] || '').trim();
if (!text) return;
+ try {
+ const res = await authFetch(`/api/questions/${questionId}/comments`, {
+ method: 'POST',
+ body: JSON.stringify({ content: text, parentCommentId: null }),
+ });
+ if (!res.ok) throw new Error();
+ const json = await res.json();
+ if (json.isSuccess) {
+ if (isStaff) {
+ await authFetch(`/api/questions/${questionId}/status`, { method: 'PATCH' });
+ }
+ const newComment = {
+ commentId: json.result.commentId,
+ displayName: json.result.displayName,
+ content: json.result.content,
+ };
+ 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);
+ }
+ } catch (err) {
+ console.error('댓글 등록 실패:', err);
+ }
+ };
+ const handleNewQuestion = async () => {
+ const text = newQuestion.trim();
+ if (!text) return;
setIsSubmitting(true);
+ setSubmitError(null);
try {
- const newComment = {
- id: Date.now(),
- author: isStaff ? '운영진' : '나',
- isStaff,
- avatarUrl: null,
- date: new Date().toLocaleDateString('ko-KR', {
- year: 'numeric',
- month: '2-digit',
- day: '2-digit',
- hour: '2-digit',
- minute: '2-digit',
- }).replace(/\. /g, '/').replace('.', ''),
- content: text,
- image: null,
- };
- setQuestion(prev => ({
- ...prev,
- comments: [...prev.comments, newComment],
- }));
- setCommentText('');
+ 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);
+ 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)),
+ ];
+
+ const displayedQuestions = (() => {
+ let list = allQuestions;
+ if (isStaff && filterUnsolved) list = unresolvedQuestions;
+ if (!isStaff && filterCurious) list = allQuestions.filter(q => q.iLiked);
+
+ 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 list;
+ })();
+
+ const currentChoice = myChoices[understanding?.current?.checkId];
+
return (
+
{sessionTitle}
- {/* ── 상단 바:해결 여부 ── */}
-
- {question.isSolved ? (
-
해결 질문
+
+ {isStaff ? (
+
) : (
- 미해결 질문
+
)}
-
-
- {/* ── 작성자 행 ── */}
-
-
-

+
+
+ {showSortMenu && (
+
+ {['기본', '최신순', '저도궁금해요순'].map(option => (
+ - { setSortOrder(option); setShowSortMenu(false); }}>
+ {option}
+
+ ))}
+
+ )}
-
-
- {question.author}
- {question.isStaff && (
-
- )}
-
- {question.date}
-
-
+
- {/* ── 질문 제목 ── */}
-
- Q.
- {question.text}
-
-
- {/* ── 첨부 이미지 ── */}
- {question.image && (
-

- )}
-
- {/* ── 액션 버튼 (저도 궁금해요 / 댓글달기) ── */}
-
+ {/* 이해도 */}
+
+
+
+ {understanding?.current?.content ?? '이해도 없음'}
+
+ ({understanding?.current?.understoodCount ?? 0}/
+ {(understanding?.current?.understoodCount ?? 0) + (understanding?.current?.notUnderstoodCount ?? 0)})
+
+
+
-
-
- {/* ── 댓글 목록 ── */}
-
- {question.comments.map(comment => (
-
-
- {/* 댓글 작성자 */}
-
-
-

-
-
- {comment.author}
- {comment.isStaff && (
-
+ {/* 질문 목록 */}
+
+ {displayedQuestions.map(question => (
+
navigate(`/sessions/${sessionId}/questions/${question.questionId}`)}>
+
+
Q.
+
{question.content}
+
+
+ {!isPast && (
+
)}
-
+
- {/* 댓글 말풍선 */}
-
-
-
- {comment.content}
+ {question.imageUrl && (
+

e.stopPropagation()} />
+ )}
+
+ {question.previewComments?.length > 0 && (
+
+ {question.previewComments.slice(0, MAX_VISIBLE_COMMENTS).map(comment => (
+
+
{comment.displayName}
+
+
+ {comment.content}
+
+
+
+ ))}
+ {question.commentCount > MAX_VISIBLE_COMMENTS && (
+
+ 외 {question.commentCount - MAX_VISIBLE_COMMENTS}개 댓글
+
+ )}
- {comment.image && (
-

- )}
-
+ )}
- {/* 타임스탬프 */}
-
{comment.date}
+ {commentOpenId === question.questionId && (
+
e.stopPropagation()}>
+ handleCommentChange(question.questionId, e.target.value)}
+ onKeyDown={e => { if (e.key === 'Enter') handleCommentSubmit(e, question.questionId); }}
+ autoFocus
+ />
+
+
+ )}
))}
- {/* ── 하단 그라디언트 커버 ── */}
- {/* ── 댓글 입력 바 (하단 고정) ── */}
-
- setCommentText(e.target.value)}
- onKeyDown={e => {
- if (e.key === 'Enter') handleCommentSubmit();
- }}
- disabled={isSubmitting}
- />
-
-
-
+ {!isPast && (
+
+ {submitError &&
{submitError}
}
+
+
+ setNewQuestion(e.target.value)}
+ onKeyDown={e => {
+ if (e.key === 'Enter') isStaff ? handleNewUnderstandCheck() : handleNewQuestion();
+ }}
+ disabled={isSubmitting}
+ />
+
+
+
+ )}
);
}
-export default QnADetailPage;
\ No newline at end of file
+export default QnAListPage;
\ No newline at end of file
diff --git a/frontend/src/pages/qna/QnADetailPage.module.css b/frontend/src/pages/qna/QnADetailPage.module.css
index 1f24ed2..9df34c4 100644
--- a/frontend/src/pages/qna/QnADetailPage.module.css
+++ b/frontend/src/pages/qna/QnADetailPage.module.css
@@ -44,9 +44,10 @@
/* ── 작성자 행 ── */
.authorRow {
display: flex;
- align-items: center;
+ align-items: flex-start;
+
gap: 10px;
- margin-bottom: 16px;
+ margin-bottom: 5px;
}
.avatar {
@@ -75,8 +76,8 @@
.authorName {
font-family: var(--font-main);
- font-size: 0.95rem;
- font-weight: 600;
+ font-size: 16px;
+ font-weight: 500;
color: var(--black);
display: flex;
align-items: center;
@@ -97,6 +98,75 @@
padding: 4px;
display: flex;
align-items: center;
+
+}
+
+.dropdownMenu {
+ position: absolute;
+ right: 0;
+ top: 100%;
+ background: var(--white);
+ border-radius: 10px;
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.15);
+ z-index: 100;
+ min-width: 100px;
+ overflow: hidden;
+}
+
+.dropdownItem {
+ display: block;
+ width: 100%;
+ padding: 10px 16px;
+ border: none;
+ background: none;
+ font-family: var(--font-main);
+ font-size: 0.9rem;
+ color: var(--black);
+ cursor: pointer;
+ text-align: left;
+}
+
+.dropdownItem:hover {
+ background: var(--gray20);
+}
+
+.editInput {
+ flex: 1;
+ font-family: var(--font-main);
+ font-size: 24px;
+ font-weight: 600;
+ color: var(--black);
+ border: 1px solid var(--gray200);
+ border-radius: 8px;
+ padding: 4px 8px;
+ resize: none;
+ outline: none;
+ line-height: 1.3;
+ width: 100%;
+}
+
+.editConfirmBtn {
+ background: var(--main);
+ color: var(--white);
+ border: none;
+ border-radius: 6px;
+ padding: 4px 10px;
+ font-family: var(--font-main);
+ font-size: 0.85rem;
+ cursor: pointer;
+ white-space: nowrap;
+}
+
+.editCancelBtn {
+ background: var(--gray20);
+ color: var(--black);
+ border: none;
+ border-radius: 6px;
+ padding: 4px 10px;
+ font-family: var(--font-main);
+ font-size: 0.85rem;
+ cursor: pointer;
+ white-space: nowrap;
}
/* ── 질문 본문 ── */
@@ -104,12 +174,12 @@
display: flex;
align-items: flex-start;
gap: 6px;
- margin-bottom: 18px;
+ padding-bottom: 5px;
}
.qIcon {
font-family: var(--font-main);
- font-size: 2rem;
+ font-size: 36px;
font-weight: 900;
color: var(--main);
line-height: 1.1;
@@ -188,6 +258,7 @@
height: 1px;
border: none;
margin: 4px 0 0;
+ margin-bottom: 20px;
}
/* ── 댓글 목록 ── */
@@ -195,19 +266,14 @@
display: flex;
flex-direction: column;
gap: 0;
- padding-top: 4px;
}
-.commentBlock {
- padding: 16px 0;
-}
/* 댓글 작성자 행 */
.commentAuthorRow {
display: flex;
align-items: center;
gap: 8px;
- margin-bottom: 10px;
}
.commentAvatar {
diff --git a/frontend/src/pages/qna/QnADetailePage.js b/frontend/src/pages/qna/QnADetailePage.js
deleted file mode 100644
index e69de29..0000000
diff --git a/frontend/src/pages/qna/QnADetailePage.module.css b/frontend/src/pages/qna/QnADetailePage.module.css
deleted file mode 100644
index e69de29..0000000
diff --git a/frontend/src/pages/qna/QnAListPage.js b/frontend/src/pages/qna/QnAListPage.js
index 83e66ca..6666a3f 100644
--- a/frontend/src/pages/qna/QnAListPage.js
+++ b/frontend/src/pages/qna/QnAListPage.js
@@ -1,151 +1,148 @@
-import { useState } from 'react';
+import { useState, useEffect, useCallback } from 'react';
+import { useParams, useNavigate } from 'react-router-dom';
import styles from './QnAListPage.module.css';
import { FiChevronLeft, FiChevronRight } from 'react-icons/fi';
+import { authFetch } from '../../utils/Api';
import {
- CommentImoji,
- MeCuriousToo,
- StaffCheck,
- SortBtn,
- OBtn,
- XBtn,
- CommentCommentArraw,
- SumitBtn,
+ CommentImoji, MeCuriousToo, SortBtn,
+ OBtn, XBtn, CommentCommentArraw, SumitBtn,
} from '../../components/qna_svg';
-const UNDERSTAND = ['이해했다', '성공했다'];
-
-// 댓글 최대 표시 개수 (카드에서 항상 노출)
const MAX_VISIBLE_COMMENTS = 3;
-// 질문 목록 데이터
-const MOCK_QUESTIONS = [
- {
- id: 1,
- text: '벤브 어떻게 활성화 시켜요?',
- likes: 7,
- iLiked: false, // 내가 좋아요 눌렀는지
- isSolved: false, // 운영진이 해결 표시했는지
- image: null, // 첨부 이미지 없음
- comments: [], // 댓글 없음
- },
- {
- id: 2,
- text: '오류났어요',
- likes: 7,
- iLiked: false,
- isSolved: false,
- image: 'https://dora-guide.com/wp-content/uploads/2019/11/Visual-studio-code-%EC%84%A4%EC%B9%98-%EB%B0%8F-%EC%82%AC%EC%9A%A9%EB%B2%95.png',
- comments: [
- { id: 1, author: '운영진1', isStaff: true, content: '사진 참고하세요' },
- { id: 2, author: '작성자', isStaff: false, content: '감사합니다' },
- { id: 3, author: '익명1', isStaff: false, content: '감사합니다' },
- ],
- },
- {
- id: 3,
- text: '벤브 어떻게 활성화 시켜요?',
- likes: 7,
- iLiked: false,
- isSolved: false,
- image: null,
- comments: [],
- },
- {
- id: 4,
- text: '벤브 어떻게 활성화 시켜요?',
- likes: 7,
- iLiked: false,
- isSolved: true,
- image: null,
- comments: [],
- },
- {
- id: 5,
- text: '벤브 어떻게 활성화 시켜요?',
- likes: 7,
- iLiked: false,
- isSolved: true,
- image: null,
- comments: [],
- },
-];
-
-
-
-
-
-function QnAListPage({
- sessionTitle = '1주차 화요일 오전 세션(HTML/CSS)',
- sessionId = 1,
- isStaff = true,
- onBack,
- onCardClick, // 카드 클릭 시 상세 페이지 이동 (questionId를 인자로 받아)
-}) {
-
-
-
- // 현재 보고 있는 이해도 체크 인덱스 (0 = '이해했다')
- const [understandIndex, setUnderstandIndex] = useState(0);
-
- // 저도 궁금해요 필터 켜져 있는지(부원)
- const [filterCurious, setFilterCurious] = useState(false);
-
- // 미해결 질문 필터 켜져 있는지(운영진)
- const [filterUnsolved, setFilterUnsolved] = useState(false);
+const DAY_PART_KO = { AM: '오전', PM: '오후' };
+const DAY_OF_WEEK_KO = {
+ MONDAY: '월', TUESDAY: '화', WEDNESDAY: '수',
+ THURSDAY: '목', FRIDAY: '금', SATURDAY: '토', SUNDAY: '일',
+};
- // 정렬 방식
- const [sortOrder, setSortOrder] = useState('정렬');
+function QnAListPage() {
+ const { sessionId } = useParams();
+ const navigate = useNavigate();
+ const isStaff = localStorage.getItem('role') === 'ADMIN';
- // 정렬 드롭다운 열려 있는지
- const [showSortMenu, setShowSortMenu] = useState(false);
+ const [sessionTitle, setSessionTitle] = useState('');
+ const [understanding, setUnderstanding] = useState(null);
+ const [understandingIndex, setUnderstandingIndex] = useState(0);
+ const [myChoices, setMyChoices] = useState({});
- // 질문 목록 (좋아요 토글 등 변경사항 반영하려고 state로 관리)
- const [questions, setQuestions] = useState(MOCK_QUESTIONS);
+ const [popularQuestions, setPopularQuestions] = useState([]);
+ const [unresolvedQuestions, setUnresolvedQuestions] = useState([]);
+ const [resolvedQuestions, setResolvedQuestions] = useState([]);
- // 내가 이해했는지 여부: true = O 누름, false = X 누름, null = 아직 안 누름
- const [myUnderstand, setMyUnderstand] = useState(null);
+ const [filterCurious, setFilterCurious] = useState(false);
+ const [filterUnsolved, setFilterUnsolved] = useState(false);
+ const [sortOrder, setSortOrder] = useState('정렬');
+ const [showSortMenu, setShowSortMenu] = useState(false);
- // 댓글 입력창이 열려 있는 질문 id
- // null이면 아무 질문도 댓글 입력창이 안 열려 있는 상태
- // '댓글달기' 버튼을 누른 질문의 id가 들어옴
const [commentOpenId, setCommentOpenId] = useState(null);
-
- // 각 질문별 댓글 입력창 텍스트 (객체로 관리: { 질문id: 입력된텍스트 })
const [commentInputs, setCommentInputs] = useState({});
-
- // 새 질문 입력창 텍스트x
const [newQuestion, setNewQuestion] = useState('');
-
- // API 요청 중인지 여부 (true면 버튼 비활성화 → 중복 제출 방지)
const [isSubmitting, setIsSubmitting] = useState(false);
-
- // 에러 메시지 (null이면 에러 없음)
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 withLiked = await Promise.all(
+ allQ.map(async (q) => {
+ try {
+ const r = await authFetch(`/api/questions/${q.questionId}`);
+ const j = await r.json();
+ return { ...q, iLiked: j.result?.isLiked ?? false };
+ } catch {
+ return { ...q, iLiked: false };
+ }
+ })
+ );
+
+ 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]);
- // 이해도
const goPrevUnderstand = () => {
- if (understandIndex > 0) setUnderstandIndex(prev => prev - 1);
+ if (understanding?.hasOlder) setUnderstandingIndex(prev => prev + 1);
};
const goNextUnderstand = () => {
- if (understandIndex < UNDERSTAND.length - 1) setUnderstandIndex(prev => prev + 1);
+ if (understanding?.hasNewer) setUnderstandingIndex(prev => prev - 1);
};
- // 질문
- const toggleLike = (e, id) => {
- e.stopPropagation();
-
- setQuestions(prev =>
- prev.map(q =>
- q.id === id
- ? {
- ...q,
- iLiked: !q.iLiked,
- likes: q.iLiked ? q.likes - 1 : q.likes + 1,
+ 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;
+ try {
+ const res = await authFetch(
+ `/api/sessions/${sessionId}/understanding-checks/${checkId}/responses`,
+ { method: 'POST', body: JSON.stringify({ choice: newChoice }) }
+ );
+ if (!res.ok) throw new Error();
+ const json = await res.json();
+ if (json.isSuccess) {
+ setUnderstanding(prev => ({
+ ...prev,
+ current: {
+ ...prev.current,
+ understoodCount: json.result.understoodCount,
+ notUnderstoodCount: json.result.notUnderstoodCount,
}
- : q
- )
- );
+ }));
+ }
+ } catch (err) {
+ console.error('이해도 응답 실패:', err);
+ }
+ };
+
+ const toggleLike = async (e, questionId) => {
+ e.stopPropagation();
+ try {
+ const res = await authFetch(`/api/questions/${questionId}/like`, { method: 'POST' });
+ 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);
+ }
+ } catch (err) {
+ console.error('좋아요 실패:', err);
+ }
};
const toggleCommentInput = (e, questionId) => {
@@ -154,124 +151,150 @@ function QnAListPage({
};
const handleCommentChange = (questionId, value) => {
- setCommentInputs(prev => ({
- ...prev,
- [questionId]: value,
- }));
+ setCommentInputs(prev => ({ ...prev, [questionId]: value }));
};
- const handleCommentSubmit = (e, questionId) => {
+ const handleCommentSubmit = async (e, questionId) => {
e.stopPropagation();
-
const text = (commentInputs[questionId] || '').trim();
if (!text) return;
-
- setQuestions(prev =>
- prev.map(q =>
- q.id === questionId
- ? {
- ...q,
- comments: [
- ...q.comments,
- { id: Date.now(), author: '나', isStaff: false, content: text },
- ],
- }
- : q
- )
- );
- setCommentInputs(prev => ({ ...prev, [questionId]: '' }));
- setCommentOpenId(null);
+ try {
+ const res = await authFetch(`/api/questions/${questionId}/comments`, {
+ method: 'POST',
+ body: JSON.stringify({ content: text, parentCommentId: null }),
+ });
+ if (!res.ok) throw new Error();
+ const json = await res.json();
+ if (json.isSuccess) {
+ if (isStaff) {
+ await authFetch(`/api/questions/${questionId}/status`, { method: 'PATCH' });
+ }
+ const newComment = {
+ commentId: json.result.commentId,
+ displayName: json.result.displayName,
+ content: json.result.content,
+ };
+ 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);
+ }
+ } catch (err) {
+ console.error('댓글 등록 실패:', err);
+ }
};
-
-
const handleNewQuestion = async () => {
const text = newQuestion.trim();
if (!text) return;
-
setIsSubmitting(true);
setSubmitError(null);
-
try {
- setQuestions(prev => [
- { id: Date.now(), text, likes: 0, iLiked: false, image: null, comments: [] },
- ...prev,
- ]);
- setNewQuestion('');
-
- } catch (error) {
- console.error('질문 등록 실패:', error);
+ 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)),
+ ];
-
- const currentUnderstand = UNDERSTAND[understandIndex];
-
- // 저도 궁금해요 필터가 켜져 있으면 필터링
const displayedQuestions = (() => {
- if (isStaff && filterUnsolved) return questions.filter(q => q.comments.length === 0 && !q.isSolved);
- if (!isStaff && filterCurious) return questions.filter(q => q.iLiked);
- return questions;
- })();
-
-
-
+ let list = allQuestions;
+ if (isStaff && filterUnsolved) list = unresolvedQuestions;
+ if (!isStaff && filterCurious) list = allQuestions.filter(q => q.iLiked);
+
+ 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 list;
+ })();
+ const currentChoice = myChoices[understanding?.current?.checkId];
return (
- // 상단
-
{sessionTitle}
{isStaff ? (
) : (
)}
-
-
-
{/* 이해도 */}
-
-
+
- {currentUnderstand}
- (13/29)
-
+ {understanding?.current?.content ?? '이해도 없음'}
+
+ ({understanding?.current?.understoodCount ?? 0}/
+ {(understanding?.current?.understoodCount ?? 0) + (understanding?.current?.notUnderstoodCount ?? 0)})
+
setMyUnderstand(prev => prev === true ? null : true)}
- title="이해했어요"
+ className={`${styles.oxBtn} ${styles.oxO} ${currentChoice === 'UNDERSTOOD' ? styles.oxActive : ''}`}
+ onClick={() => handleUnderstandChoice('UNDERSTOOD')}
disabled={isStaff}
>
- {isStaff && 7}
+ {isStaff && {understanding?.current?.understoodCount ?? 0}}
setMyUnderstand(prev => prev === false ? null : false)}
- title="모르겠어요"
+ className={`${styles.oxBtn} ${styles.oxX} ${currentChoice === 'NOT_UNDERSTOOD' ? styles.oxActive : ''}`}
+ onClick={() => handleUnderstandChoice('NOT_UNDERSTOOD')}
disabled={isStaff}
>
- {isStaff && 6}
+ {isStaff && {understanding?.current?.notUnderstoodCount ?? 0}}
-
+
-
- {/* ── 질문 목록 ── */}
+ {/* 질문 목록 */}
{displayedQuestions.map(question => (
-
onCardClick?.(question.id)}
- >
-
+
navigate(`/sessions/${sessionId}/questions/${question.questionId}`)}>
-
Q.
-
{question.text}
-
+
Q.
+
{question.content}
toggleLike(e, question.id)}
+ onClick={e => toggleLike(e, question.questionId)}
>
- {question.likes}
+ {question.likeCount}
- toggleCommentInput(e, question.id)}
- >
-
- 댓글달기
+ toggleCommentInput(e, question.questionId)}>
+ 댓글달기
- {question.image && (
-

e.stopPropagation()}
- />
+ onClick={e => e.stopPropagation()} />
)}
-
- {question.comments.length > 0 && (
+ {question.previewComments?.length > 0 && (
- {question.comments.slice(0, MAX_VISIBLE_COMMENTS).map(comment => (
-
-
- {comment.author}
- {comment.isStaff && (
-
- )}
-
- {/* 댓글 내용 */}
+ {question.previewComments.slice(0, MAX_VISIBLE_COMMENTS).map(comment => (
+
+
{comment.displayName}
{comment.content}
@@ -374,35 +382,26 @@ function QnAListPage({
))}
-
-
- {question.comments.length > MAX_VISIBLE_COMMENTS && (
+ {question.commentCount > MAX_VISIBLE_COMMENTS && (
- 외 {question.comments.length - MAX_VISIBLE_COMMENTS}개 댓글
+ 외 {question.commentCount - MAX_VISIBLE_COMMENTS}개 댓글
)}
)}
- {commentOpenId === question.id && (
-
e.stopPropagation()}
- >
+
+ {commentOpenId === question.questionId && (
+
e.stopPropagation()}>
handleCommentChange(question.id, e.target.value)}
- // 엔터 누르면 제출
- onKeyDown={e => {
- if (e.key === 'Enter') handleCommentSubmit(e, question.id);
- }}
+ value={commentInputs[question.questionId] || ''}
+ onChange={e => handleCommentChange(question.questionId, e.target.value)}
+ onKeyDown={e => { if (e.key === 'Enter') handleCommentSubmit(e, question.questionId); }}
autoFocus
/>
- handleCommentSubmit(e, question.id)}
- >
+ handleCommentSubmit(e, question.questionId)}>
@@ -411,36 +410,30 @@ function QnAListPage({
))}
-
-
- {submitError && (
-
{submitError}
- )}
-
+ {submitError &&
{submitError}
}
+
setNewQuestion(e.target.value)}
onKeyDown={e => {
- if (e.key === 'Enter') handleNewQuestion();
+ if (e.key === 'Enter') isStaff ? handleNewUnderstandCheck() : handleNewQuestion();
}}
disabled={isSubmitting}
/>
{isSubmitting ? '⏳' : }
-
);
}
diff --git a/frontend/src/pages/qna/QnAMainPage.js b/frontend/src/pages/qna/QnAMainPage.js
index 4e611ea..c750fb9 100644
--- a/frontend/src/pages/qna/QnAMainPage.js
+++ b/frontend/src/pages/qna/QnAMainPage.js
@@ -1,27 +1,22 @@
import { useState, useEffect } from 'react';
+import { useNavigate } from 'react-router-dom';
import styles from './QnAMainPage.module.css';
import { FiLogIn } from 'react-icons/fi';
+import { authFetch } from '../../utils/Api';
-// ─────────────────────────────────────────────
-// 📌 목업 데이터
-// ─────────────────────────────────────────────
-// const MOCK_DATA = {
-// activeSessions: [
-// { sessionId: 1, week: 1, dayOfWeek: '화요일', dayPart: '오전', sessionDate: '2026.05.23', title: 'HTML/CSS' }
-// ],
-// pastSessions: [
-// { sessionId: 1, week: 1, dayOfWeek: '화요일', dayPart: '오전', sessionDate: '2026.05.23', title: 'HTML/CSS' },
-// { sessionId: 2, week: 1, dayOfWeek: '화요일', dayPart: '오후', sessionDate: '2026.05.23', title: 'Git 기초' },
-// ]
-// };
-// ─────────────────────────────────────────────
+const DAY_PART_KO = { AM: '오전', PM: '오후' };
-const BASE_URL = '';
+const DAY_OF_WEEK_KO = {
+ MONDAY: '월요일', TUESDAY: '화요일', WEDNESDAY: '수요일',
+ THURSDAY: '목요일', FRIDAY: '금요일', SATURDAY: '토요일', SUNDAY: '일요일',
+};
-const getIcon = (dayPart) => dayPart === '오전' ? '☀' : '☾';
-const getTime = (dayPart) => dayPart === '오전' ? '10:00 ~ 13:00' : '14:00 ~ 17:00';
+const formatDate = (dateStr) => dateStr?.replace(/-/g, '.') ?? '';
+const getIcon = (dayPart) => dayPart === 'AM' ? '☀' : '☾';
+const getTime = (dayPart) => dayPart === 'AM' ? '10:00 ~ 13:00' : '14:00 ~ 17:00';
function QNAMainPage() {
+ const navigate = useNavigate();
const [activeSessions, setActiveSessions] = useState([]);
const [pastSessions, setPastSessions] = useState([]);
const [loading, setLoading] = useState(true);
@@ -31,31 +26,16 @@ function QNAMainPage() {
const fetchSessions = async () => {
try {
setLoading(true);
-
- const token = localStorage.getItem('token');
-
- const res = await fetch(`${BASE_URL}/api/sessions`, {
- method: 'GET',
- headers: {
- 'Authorization': `Bearer ${token}`,
- 'Content-Type': 'application/json',
- }
- });
-
+ const res = await authFetch('/api/sessions');
if (!res.ok) throw new Error(`서버 오류: ${res.status}`);
-
const json = await res.json();
-
if (!json.isSuccess) throw new Error(json.message);
- // DB에 데이터 없으면 목업으로 대체
- // setActiveSessions(json.result.activeSessions.length > 0 ? json.result.activeSessions : MOCK_DATA.activeSessions);
- // setPastSessions(json.result.pastSessions.length > 0 ? json.result.pastSessions : MOCK_DATA.pastSessions);
-
+ setActiveSessions(json.result.activeSessions ?? []);
+ setPastSessions(json.result.pastSessions ?? []);
} catch (err) {
console.error('세션 불러오기 실패:', err);
- // setActiveSessions(MOCK_DATA.activeSessions);
- // setPastSessions(MOCK_DATA.pastSessions);
+ setError(err.message);
} finally {
setLoading(false);
}
@@ -70,19 +50,26 @@ function QNAMainPage() {
return (
- {/* 진행 중인 세션 있을 때만 표시 */}
+ {/* 진행 중인 세션 */}
{activeSessions.length > 0 && (
<>
Q&A
{activeSessions.map(session => (
-
+
navigate(`/sessions/${session.sessionId}/questions`, { state: { status: 'IN_SESSION' } })}
+ style={{ cursor: 'pointer' }}
+ >
{getIcon(session.dayPart)}
{session.title}
-
{session.week}주차 {session.dayOfWeek} {session.dayPart}
-
{session.sessionDate}
+
+ {session.week}주차 {DAY_OF_WEEK_KO[session.dayOfWeek]} {DAY_PART_KO[session.dayPart]}
+
+
{formatDate(session.sessionDate)}
{getTime(session.dayPart)}
))}
@@ -92,7 +79,7 @@ function QNAMainPage() {
)}
{/* 지난 세션 */}
- {pastSessions.length > 0 ? (
+ {pastSessions.length > 0 && (
지난 세션
@@ -101,20 +88,27 @@ function QNAMainPage() {
{getIcon(session.dayPart)}
{session.title}
- •{session.week}주차 {session.dayOfWeek} {session.dayPart}
+
+ • {session.week}주차 {DAY_OF_WEEK_KO[session.dayOfWeek]} {DAY_PART_KO[session.dayPart]}
+
-
+ navigate(`/sessions/${session.sessionId}/questions`, { state: { status: 'AFTER_SESSION' } })}
+ >
+
+
))}
- ) : (
- /* 진행 중인 세션도 없고 지난 세션도 없을 때 */
- activeSessions.length === 0 && (
-
- )
+ )}
+
+ {/* 둘 다 없을 때 */}
+ {activeSessions.length === 0 && pastSessions.length === 0 && (
+
)}