diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 2dde982..6e43dfa 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -13,6 +13,7 @@ "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^13.5.0", "axios": "^1.15.2", + "lucide-react": "^1.16.0", "react": "^19.2.5", "react-dom": "^19.2.5", "react-icons": "^5.6.0", @@ -11170,6 +11171,15 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.16.0.tgz", + "integrity": "sha512-dYwyPzb4MEKpGUmNYk3WKWPnMrHs3FKM+q94kAnJrcDIqqn1hq2xY8scaS2ovsOCM5D51ey2gaRG3PBb1vgoYQ==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/lz-string": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index cbfb176..d552f49 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -8,6 +8,7 @@ "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^13.5.0", "axios": "^1.15.2", + "lucide-react": "^1.16.0", "react": "^19.2.5", "react-dom": "^19.2.5", "react-icons": "^5.6.0", diff --git a/frontend/src/pages/qna/QnAListPage.js b/frontend/src/pages/qna/QnAListPage.js index e69de29..bacd730 100644 --- a/frontend/src/pages/qna/QnAListPage.js +++ b/frontend/src/pages/qna/QnAListPage.js @@ -0,0 +1,463 @@ +import { useState } from 'react'; +import styles from './QnAListPage.module.css'; +import { FiChevronLeft, FiChevronRight } from 'react-icons/fi'; + +const CommentImoji = () => ( + + + + +); + +const MeCuriousToo = () => ( + + + + + + + + +); + +const StaffCheck = () => ( + + + + + +); + +const SortBtn = () => ( + +); + +const OBtn = () => ( + + + + + + + + +); + +const XBtn = () => ( + + + + +); + +const CommentCommentArraw = () => ( + +); + +const UNDERSTAND = ['이해했다', '성공했다']; + +// 댓글 최대 표시 개수 (카드에서 항상 노출) +const MAX_VISIBLE_COMMENTS = 3; + +// 질문 목록 데이터 +const MOCK_QUESTIONS = [ + { + id: 1, + text: '벤브 어떻게 활성화 시켜요?', + likes: 7, + iLiked: false, // 내가 좋아요 눌렀는지 + image: null, // 첨부 이미지 없음 + comments: [], // 댓글 없음 + }, + { + id: 2, + text: '오류났어요', + likes: 7, + iLiked: 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, + image: null, + comments: [], + }, + { + id: 4, + text: '벤브 어떻게 활성화 시켜요?', + likes: 7, + iLiked: false, + image: null, + comments: [], + }, + { + id: 5, + text: '벤브 어떻게 활성화 시켜요?', + likes: 7, + iLiked: false, + image: null, + comments: [], + }, +]; + + + + + +function QnAListPage({ + sessionTitle = '1주차 화요일 오전 세션(HTML/CSS)', + sessionId = 1, + onBack, + onCardClick, // 카드 클릭 시 상세 페이지 이동 (questionId를 인자로 받아) +}) { + + + + // 현재 보고 있는 이해도 체크 인덱스 (0 = '이해했다') + const [understandIndex, setUnderstandIndex] = useState(0); + + // 저도 궁금해요 필터 켜져 있는지 + const [filterCurious, setFilterCurious] = useState(false); + + // 정렬 방식 + const [sortOrder, setSortOrder] = useState('정렬'); + + // 정렬 드롭다운 열려 있는지 + const [showSortMenu, setShowSortMenu] = useState(false); + + // 질문 목록 (좋아요 토글 등 변경사항 반영하려고 state로 관리) + const [questions, setQuestions] = useState(MOCK_QUESTIONS); + + // 내가 이해했는지 여부: true = O 누름, false = X 누름, null = 아직 안 누름 + const [myUnderstand, setMyUnderstand] = useState(null); + + // 댓글 입력창이 열려 있는 질문 id + // null이면 아무 질문도 댓글 입력창이 안 열려 있는 상태 + // '댓글달기' 버튼을 누른 질문의 id가 들어옴 + const [commentOpenId, setCommentOpenId] = useState(null); + + // 각 질문별 댓글 입력창 텍스트 (객체로 관리: { 질문id: 입력된텍스트 }) + const [commentInputs, setCommentInputs] = useState({}); + + // 새 질문 입력창 텍스트 + const [newQuestion, setNewQuestion] = useState(''); + + // API 요청 중인지 여부 (true면 버튼 비활성화 → 중복 제출 방지) + const [isSubmitting, setIsSubmitting] = useState(false); + + // 에러 메시지 (null이면 에러 없음) + const [submitError, setSubmitError] = useState(null); + + + // 이해도 + const goPrevUnderstand = () => { + if (understandIndex > 0) setUnderstandIndex(prev => prev - 1); + }; + const goNextUnderstand = () => { + if (understandIndex < UNDERSTAND.length - 1) setUnderstandIndex(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, + } + : q + ) + ); + }; + + const toggleCommentInput = (e, questionId) => { + e.stopPropagation(); + setCommentOpenId(prev => prev === questionId ? null : questionId); + }; + + const handleCommentChange = (questionId, value) => { + setCommentInputs(prev => ({ + ...prev, + [questionId]: value, + })); + }; + + const handleCommentSubmit = (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); + }; + + + + 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); + setSubmitError('질문 등록에 실패했어요.'); + } finally { + setIsSubmitting(false); + } + }; + + + + + const currentUnderstand = UNDERSTAND[understandIndex]; + + // 저도 궁금해요 필터가 켜져 있으면 필터링 + const displayedQuestions = filterCurious + ? questions.filter(q => q.iLiked) + : questions; + + + + + + + return ( + // 상단 +
+ +

{sessionTitle}

+ +
+ + +
+ + + {showSortMenu && ( +
    + {['기본', '최신순', '저도궁금해요순'].map(option => ( +
  • { + setSortOrder(option); + setShowSortMenu(false); + }} + > + {option} +
  • + ))} +
+ )} +
+
+
+ + + {/* 이해도 */} +
+ + + + {currentUnderstand} + (13/29) + + + + + +
+ + + {/* ── 질문 목록 ── */} +
+ {displayedQuestions.map(question => ( +
onCardClick?.(question.id)} + > + +
+ Q. + {question.text} + +
+ + +
+
+ {question.image && ( + 첨부 이미지 e.stopPropagation()} + /> + )} + + + {question.comments.length > 0 && ( +
+ {question.comments.slice(0, MAX_VISIBLE_COMMENTS).map(comment => ( +
+ + {comment.author} + {comment.isStaff && ( + + )} + + {/* 댓글 내용 */} +
+
+ {comment.content} +
+
+
+ ))} + + + {question.comments.length > MAX_VISIBLE_COMMENTS && ( + + 외 {question.comments.length - MAX_VISIBLE_COMMENTS}개 댓글 + + )} +
+ )} + {commentOpenId === question.id && ( +
e.stopPropagation()} + > + handleCommentChange(question.id, e.target.value)} + // 엔터 누르면 제출 + onKeyDown={e => { + if (e.key === 'Enter') handleCommentSubmit(e, question.id); + }} + autoFocus + /> + +
+ )} +
+ ))} +
+ + + +
+ + {submitError && ( +

{submitError}

+ )} + +
+ + + setNewQuestion(e.target.value)} + onKeyDown={e => { + if (e.key === 'Enter') handleNewQuestion(); + }} + disabled={isSubmitting} + /> + +
+
+ +
+ ); +} + +export default QnAListPage; \ No newline at end of file diff --git a/frontend/src/pages/qna/QnAListPage.module.css b/frontend/src/pages/qna/QnAListPage.module.css index 7f81336..95a2d54 100644 --- a/frontend/src/pages/qna/QnAListPage.module.css +++ b/frontend/src/pages/qna/QnAListPage.module.css @@ -1,441 +1,475 @@ -/* ── Page ───────────────────────────────────────────────────────── */ .page { - max-width: 880px; - margin: 0 auto; + display: flex; + flex-direction: column; min-height: 100vh; - padding-bottom: 6rem; -} - - -.sessionHeader { - text-align: center; - padding: 10px; + max-width: 780px; + margin: 0 auto; + padding-bottom: 80px; } -.sessionTitle { +/* 상단 */ +.title { font-family: var(--font-main); font-size: 2rem; - font-weight: 700; + text-align: center; + margin: 10px; color: var(--black); - margin-bottom: 30px; - margin-top: 50px; + padding-top: 60px; + font-weight: 700; + line-height: normal; } -.sessionControls { + +.filterRow { display: flex; - justify-content: flex-end; align-items: center; + justify-content: flex-end; gap: 0.75rem; + padding: 0.5rem 1rem; } -.like { +.curiousLabel { display: flex; align-items: center; - gap: 0.375rem; + gap: 0.3rem; font-family: var(--font-main); - font-size: 0.875rem; + font-size: 0.8rem; color: var(--gray600); cursor: pointer; } -.like input[type='checkbox'] { - accent-color: var(--light); - width: 15px; - height: 15px; +.curiousCheckbox { + accent-color: var(--dark); + width: 14px; + height: 14px; + cursor: pointer; } -.sortSelect { - border: 1px solid var(--gray200); - border-radius: 20px; - padding: 0.25rem 0.75rem; +.sortWrapper { + position: relative; +} + +.sortBtn { + background: none; + border: 1px solid var(--gray600); + border-radius: 6px; + padding: 0.25rem 0.6rem; font-family: var(--font-main); - font-size: 0.875rem; + font-size: 0.8rem; color: var(--gray600); + cursor: pointer; + white-space: nowrap; + border-radius: 20px; +} + +.sortMenu { + position: absolute; + right: 0; + top: calc(100% + 4px); background: var(--white); + border: 1px solid var(--gray200); + border-radius: 8px; + box-shadow: 2px 2px 8px rgba(0, 0, 0, 0.12); + list-style: none; + margin: 0; + padding: 0.25rem 0; + z-index: 10; + min-width: 110px; +} + +.sortOption { + padding: 0.5rem 1rem; + font-family: var(--font-main); + font-size: 0.85rem; + color: var(--black); cursor: pointer; - outline: none; + white-space: nowrap; } -/* ── 이해도 체크 네비게이터 ──────────────────────────────────────── */ -.understandingRow { +.divider { + background: var(--gray200); + width: 100%; + height: 1px; + border: none; +} + +/*이해도*/ +.understandBar { + margin-top: 20px; display: flex; align-items: center; justify-content: space-between; + margin-bottom: 20px; + border-radius: 10px; + border: 1px solid var(--dark); background: var(--white); - border-radius: 0.625rem; - box-shadow: 2px 2px 5px 0 rgba(0, 0, 0, 0.25); - padding: 0.75rem 1rem; - margin-bottom: 1rem; - gap: 0.5rem; + box-shadow: 1px 2px 3px 0 rgba(0, 0, 0, 0.25); + width: 768px; + height: 56px; } -.navArrow { +.arrowBtn { background: none; border: none; - font-size: 1.5rem; - color: var(--gray600); cursor: pointer; - padding: 0 0.25rem; + color: var(--gray600); + display: flex; + align-items: center; + padding: 0; line-height: 1; - transition: color 0.15s; - flex-shrink: 0; } -.navArrow:hover:not(:disabled) { - color: var(--black); -} - -.navArrow:disabled { - opacity: 0.25; +.arrowBtn:disabled { + opacity: 0.3; cursor: default; } -.understandingLabel { - display: flex; - align-items: center; - gap: 0.5rem; +.understandName { flex: 1; -} - -.understandingText { + text-align: left; font-family: var(--font-main); - font-size: 1.1rem; - font-weight: 600; + font-size: 24px; + font-weight: 500; color: var(--black); } -.understandingCount { - font-family: var(--font-main); - font-size: 1rem; - font-weight: 400; - color: var(--gray600); -} - -.checkButtons { - display: flex; - gap: 0.5rem; - flex-shrink: 0; +.understandCount { + font-weight: 300; + color: var(--black); + font-size: 18px; } -.checkBtn { - width: 2.25rem; - height: 2.25rem; - border-radius: 50%; - background: var(--white); - font-family: var(--font-main); - font-size: 0.9375rem; +.oxBtn { + border: none; + font-size: 0.85rem; font-weight: 700; cursor: pointer; display: flex; align-items: center; justify-content: center; - transition: all 0.15s ease; - border: 1.5px solid var(--gray200); -} - -.checkO { - border-color: #4caf50; - color: #4caf50; -} - -.checkO:hover { - background: #f0faf0; + transition: background 0.15s, border-color 0.15s; + margin: 0 0.2rem; + width: 36px; + height: 36px; + aspect-ratio: 1/1; + border-radius: 10px; + background: var(--gray200); } -.checkActive { - background: #4caf50; - color: var(--white); +.oxO, +.oxX { + color: var(--gray600); } -.checkX { - border-color: #e53935; - color: #e53935; +.oxO.oxActive, +.oxX.oxActive { + background: var(--light); + color: var(--dark); } -.checkX:hover { - background: #fff5f5; -} -.checkActiveX { - background: #e53935; - color: var(--white); -} -/* ── Question List ──────────────────────────────────────────────── */ +/*질문*/ .questionList { display: flex; flex-direction: column; - gap: 0.75rem; + gap: 20px; + } -/* ── Question Card ──────────────────────────────────────────────── */ -.card { - background: var(--white); - border-radius: 0.625rem; - padding: 1rem; - box-shadow: 2px 2px 5px 0 rgba(0, 0, 0, 0.25); +.questionCard { + padding: 0.85rem 1rem; + cursor: pointer; transition: box-shadow 0.2s; + border-radius: 30px; + background: var(--white); + box-shadow: 1px 2px 3px 0 rgba(0, 0, 0, 0.25); + width: 720px; + min-height: 40px; } -.card:hover { - box-shadow: 4px 4px 10px 0 rgba(0, 0, 0, 0.2); +.questionCard:hover { + box-shadow: 2px 2px 8px rgba(0, 0, 0, 0.15); } -.questionHeader { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 0.5rem; -} -.questionLeft { +.questionHeader { display: flex; - align-items: flex-start; - gap: 0.5rem; - flex: 1; + align-items: center; + gap: 0.4rem; } .qIcon { - font-family: var(--font-main); - font-size: 1rem; - font-weight: 800; - color: #ff9800; flex-shrink: 0; - line-height: 1.5; + padding-top: 1px; + color: var(--main); + font-family: var(--font-main); + font-size: 36px; + font-weight: 900; + line-height: normal; } .questionText { + flex: 1; font-family: var(--font-main); - font-size: 1rem; - font-weight: 500; color: var(--black); - line-height: 1.5; + word-break: keep-all; + font-size: 20px; + font-weight: 500; + line-height: normal; } .questionActions { display: flex; align-items: center; - gap: 0.5rem; + gap: 0.6rem; flex-shrink: 0; + margin-left: 0.5rem; } -.solvedBadge { - background: #e8f5e9; - color: #388e3c; +.likeBtn { + justify-content: center; + display: flex; + align-items: center; + gap: 7px; + border: none; + cursor: pointer; font-family: var(--font-main); - font-size: 0.75rem; - font-weight: 600; - padding: 2px 0.5rem; + font-size: 15px; + color: var(--gray600); + padding: 0 3px 0 0; + transition: color 0.15s; + width: 37.5px; + height: 25px; + background-color: var(--gray50); border-radius: 10px; - border: 1px solid #c8e6c9; + +} + +.likeBtn.liked { + color: var(--dark); + background-color: var(--light); } -.likeBtn, .commentBtn { - background: none; + justify-content: center; + display: flex; + align-items: center; + gap: 2px; border: none; + cursor: pointer; font-family: var(--font-main); - font-size: 0.8125rem; + font-size: 15px; color: var(--gray600); - cursor: pointer; - padding: 0.25rem 0.375rem; - border-radius: 6px; - transition: all 0.15s ease; + padding: 0; white-space: nowrap; -} + border-radius: 10px; + background: var(--gray50); + width: 87px; + height: 25px; -.likeBtn:hover, -.commentBtn:hover { - background: #f5f5f5; - color: var(--black); } -.liked { - color: #ff9800; -} -.questionMeta { +.questionImage { + width: 100%; + border-radius: 8px; + margin-top: 0.65rem; + object-fit: cover; display: block; - font-family: var(--font-main); - font-size: 0.75rem; - color: var(--gray600); - margin-top: 0.375rem; - padding-left: 1.5rem; } -/* ── Comments ───────────────────────────────────────────────────── */ -.commentSection { - margin-top: 0.875rem; + +/* ── 댓글 미리보기 ── */ +.commentPreview { + margin-top: 26px; + display: flex; + flex-direction: column; + gap: 0.4rem; + padding-left: 20px; border-top: 1px solid var(--gray200); - padding-top: 0.75rem; + padding-top: 17px; + +} + +.commentWrapper { display: flex; flex-direction: column; - gap: 0.625rem; + gap: 0.15rem; + } + + .commentItem { display: flex; flex-direction: column; - gap: 0.2rem; + gap: 0.15rem; + max-width: 646px; + border-radius: 5px 20px 20px 20px; + background: var(--gray50); + padding: 0.5rem 0.8rem; + + } .commentAuthor { font-family: var(--font-main); - font-size: 0.8125rem; - font-weight: 600; - color: var(--gray600); + font-size: 0.78rem; + font-weight: 500; + color: var(--black); display: flex; align-items: center; - gap: 0.375rem; + gap: 3px; + line-height: normal; } .staffBadge { - background: #1976d2; - color: var(--white); - font-size: 0.625rem; - padding: 1px 0.375rem; - border-radius: 10px; - font-weight: 600; + display: inline-flex; + align-items: center; } -.commentBody { +.commentContent { + font-family: var(--font-main); + font-size: 0.82rem; + color: var(--black); + font-weight: 400; + line-height: normal; display: flex; align-items: center; - gap: 0.375rem; - padding-left: 0.25rem; + gap: 4px; } -.commentArrow { +.commentMore { + font-family: var(--font-main); + font-size: 0.78rem; color: var(--gray600); - font-size: 0.875rem; + cursor: pointer; + text-decoration: underline; + margin-top: 0.1rem; } -.commentText { - font-family: var(--font-main); - font-size: 0.875rem; +.commentMore:hover { color: var(--black); } -.attachmentBtn { - background: #f0f4ff; - border: 1px solid #c5d0f0; - border-radius: 6px; - padding: 2px 0.5rem; - font-family: var(--font-main); - font-size: 0.75rem; - color: #1976d2; - cursor: pointer; -} +/* 여기부터 시작 */ -/* ── Comment Input ──────────────────────────────────────────────── */ +/* ── 댓글 입력 행 ── */ .commentInputRow { display: flex; align-items: center; - gap: 0.5rem; - margin-top: 0.75rem; - border-top: 1px solid var(--gray200); - padding-top: 0.625rem; + gap: 0.4rem; + margin-top: 0.65rem; } .commentInput { flex: 1; border: 1px solid var(--gray200); border-radius: 20px; - padding: 0.5rem 0.875rem; + padding: 0.4rem 0.8rem; font-family: var(--font-main); - font-size: 0.875rem; - outline: none; + font-size: 0.82rem; color: var(--black); - background: #fafafa; + background: #f5f5f5; + outline: none; + transition: border-color 0.15s; } .commentInput:focus { - border-color: var(--dark); + border-color: #09C410; background: var(--white); } -.commentSubmit { - width: 2rem; - height: 2rem; +.submitBtn { + width: 30px; + height: 30px; border-radius: 50%; - background: var(--dark); - color: var(--white); + background: #09C410; border: none; + color: var(--white); font-size: 1rem; - font-weight: 700; cursor: pointer; display: flex; align-items: center; justify-content: center; flex-shrink: 0; - transition: opacity 0.15s; + transition: background 0.15s; } -.commentSubmit:hover { - opacity: 0.8; +.submitBtn:hover { + background: #07a50e; } -/* ── Floating Bar ───────────────────────────────────────────────── */ -.floatingBar { +/* ── 하단 새 질문 입력바 ── */ +.newQuestionBar { position: fixed; - bottom: 1rem; + bottom: 0; left: 50%; transform: translateX(-50%); - width: calc(100% - 3rem); - max-width: 850px; + width: 100%; + max-width: 480px; background: var(--white); - border-radius: 28px; - box-shadow: 4px 4px 12px 0 rgba(0, 0, 0, 0.15); + border-top: 1px solid var(--gray200); + padding: 0.6rem 1rem; + z-index: 100; +} + +.errorMsg { + font-family: var(--font-main); + font-size: 0.78rem; + color: #e53935; + margin: 0 0 0.3rem 0.5rem; +} + +.newQuestionInputRow { display: flex; align-items: center; - padding: 0.5rem 0.5rem 0.5rem 0.875rem; gap: 0.5rem; } -.floatingPlus { - font-size: 1.125rem; +.newQuestionPlus { + font-size: 1.2rem; color: var(--gray600); flex-shrink: 0; - cursor: pointer; - font-weight: 300; + line-height: 1; } -.floatingInput { +.newQuestionInput { flex: 1; border: none; - outline: none; + background: none; font-family: var(--font-main); - font-size: 0.9375rem; + font-size: 0.88rem; color: var(--black); - background: transparent; + outline: none; } -.floatingInput::placeholder { +.newQuestionInput::placeholder { color: var(--gray600); } -.floatingSubmit { - width: 2.25rem; - height: 2.25rem; +.newQuestionSubmit { + width: 32px; + height: 32px; border-radius: 50%; - background: var(--dark); - color: var(--white); + background: #09C410; border: none; + color: var(--white); font-size: 1rem; - font-weight: 700; cursor: pointer; display: flex; align-items: center; justify-content: center; flex-shrink: 0; - transition: opacity 0.15s; + transition: background 0.15s; } -.floatingSubmit:hover:not(:disabled) { - opacity: 0.8; +.newQuestionSubmit:hover { + background: #07a50e; } -.floatingSubmit:disabled { - opacity: 0.4; +.newQuestionSubmit:disabled { + background: var(--gray200); cursor: default; } \ No newline at end of file