diff --git a/frontend/src/App.js b/frontend/src/App.js index a6b7474..7c37e68 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -4,7 +4,7 @@ import LoginPage from './pages/login/LoginPage'; import OnboardingPage from './pages/OnboardingPage'; import QnAMainPage from './pages/qna/QnAMainPage'; import QnAListPage from './pages/qna/QnAListPage'; -import QnADetailePage from './pages/qna/QnADetailePage'; +import QnADetailPage from './pages/qna/QnADetailPage'; import CurriculumPage from './pages/curriculum/CurriculumPage'; import PiroCheckMain from './pages/pirocheck/PIroCheckMain'; import Attendance from './pages/pirocheck/attendance/Attendance' @@ -23,14 +23,14 @@ function App() { }> } /> } /> - } /> + } /> } /> - + {/* 다크 헤더 페이지 */} }> - }/> - }/> + } /> + } /> diff --git a/frontend/src/assets/images/profile.png b/frontend/src/assets/images/profile.png new file mode 100644 index 0000000..0ac7d4c Binary files /dev/null and b/frontend/src/assets/images/profile.png differ diff --git a/frontend/src/assets/styles/global.css b/frontend/src/assets/styles/global.css index 24d9dbe..667173f 100644 --- a/frontend/src/assets/styles/global.css +++ b/frontend/src/assets/styles/global.css @@ -24,9 +24,4 @@ --black: #111111; --font-title: 'GemunuLibre', sans-serif; --font-main: 'Pretendard', sans-serif; -} - -/* body 기본 설정 */ -body { - background-color: var(--gray20); } \ No newline at end of file diff --git a/frontend/src/components/Layout.js b/frontend/src/components/Layout.js index e0a4bee..7961f7a 100644 --- a/frontend/src/components/Layout.js +++ b/frontend/src/components/Layout.js @@ -3,7 +3,7 @@ import { Outlet } from 'react-router-dom'; function Layout({ headerType }) { return ( -
+
diff --git a/frontend/src/pages/qna/QnADetailPage.js b/frontend/src/pages/qna/QnADetailPage.js new file mode 100644 index 0000000..f617a2b --- /dev/null +++ b/frontend/src/pages/qna/QnADetailPage.js @@ -0,0 +1,245 @@ +import '../../assets/styles/global.css'; +import { useState } from 'react'; +import styles from './QnADetailPage.module.css'; +import { FiChevronLeft, FiMoreVertical, FiCornerDownRight } from 'react-icons/fi'; +import { + CommentImoji, + MeCuriousToo, + StaffCheck, + 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, + }, + ], +}; + + +// ── 메인 컴포넌트 ──────────────────────────────────────── +function QnADetailPage({ + question: initialQuestion = MOCK_QUESTION, + isStaff = false, + onBack, +}) { + const [question, setQuestion] = useState(initialQuestion); + const [commentText, setCommentText] = useState(''); + const [isSubmitting, setIsSubmitting] = useState(false); + + // 좋아요 토글 + const toggleLike = () => { + setQuestion(prev => ({ + ...prev, + iLiked: !prev.iLiked, + likes: prev.iLiked ? prev.likes - 1 : prev.likes + 1, + })); + }; + + // 댓글 제출 + const handleCommentSubmit = async () => { + const text = commentText.trim(); + if (!text) return; + + setIsSubmitting(true); + 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(''); + } catch (err) { + console.error('댓글 등록 실패:', err); + } finally { + setIsSubmitting(false); + } + }; + + return ( +
+ + {/* ── 상단 바:해결 여부 ── */} +
+ {question.isSolved ? ( + 해결 질문 + ) : ( + 미해결 질문 + )} +
+ + {/* ── 작성자 행 ── */} +
+
+ {question.author} +
+
+ + {question.author} + {question.isStaff && ( + + )} + + {question.date} +
+ +
+ + {/* ── 질문 제목 ── */} +
+ Q. + {question.text} +
+ + {/* ── 첨부 이미지 ── */} + {question.image && ( + 첨부 이미지 + )} + + {/* ── 액션 버튼 (저도 궁금해요 / 댓글달기) ── */} +
+ + +
+ +
+ + {/* ── 댓글 목록 ── */} +
+ {question.comments.map(comment => ( +
+ + {/* 댓글 작성자 */} +
+
+ {comment.author} +
+ + {comment.author} + {comment.isStaff && ( + + )} + +
+ + {/* 댓글 말풍선 */} +
+
+ + {comment.content} +
+ {comment.image && ( + 댓글 첨부 이미지 + )} +
+ + {/* 타임스탬프 */} +

{comment.date}

+
+ ))} +
+ + {/* ── 하단 그라디언트 커버 ── */} +
+ + {/* ── 댓글 입력 바 (하단 고정) ── */} +
+ setCommentText(e.target.value)} + onKeyDown={e => { + if (e.key === 'Enter') handleCommentSubmit(); + }} + disabled={isSubmitting} + /> + +
+ +
+ ); +} + +export default QnADetailPage; \ No newline at end of file diff --git a/frontend/src/pages/qna/QnADetailPage.module.css b/frontend/src/pages/qna/QnADetailPage.module.css new file mode 100644 index 0000000..1f24ed2 --- /dev/null +++ b/frontend/src/pages/qna/QnADetailPage.module.css @@ -0,0 +1,356 @@ +/* ── 전체 페이지 ── */ +.page { + display: flex; + flex-direction: column; + min-height: 100vh; + max-width: 900px; + margin: 70px auto 0; + padding: 52px 90px 120px; + box-sizing: border-box; + background: var(--white); + border-radius: 40px 40px 0 0; + box-shadow: 2px 2px 5px 0 rgba(0, 0, 0, 0.25); +} + +/* ── 상단 헤더 (해결 여부 뱃지) ── */ +.topBar { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px 0 10px; +} + +.solvedBadge { + font-family: var(--font-main); + font-size: 0.78rem; + font-weight: 600; + color: var(--black); + background: var(--gray20); + border-radius: 20px; + padding: 3px 10px; + letter-spacing: 0.01em; +} + +.unsolvedBadge { + font-family: var(--font-main); + font-size: 0.78rem; + font-weight: 600; + color: var(--black); + background: var(--light); + border-radius: 20px; + padding: 3px 10px; +} + +/* ── 작성자 행 ── */ +.authorRow { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 16px; +} + +.avatar { + width: 35px; + height: 35px; + background: none; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + overflow: hidden; +} + +.avatarImg { + width: 100%; + height: 100%; + object-fit: cover; +} + +.authorInfo { + display: flex; + flex-direction: column; + gap: 2px; + flex: 1; +} + +.authorName { + font-family: var(--font-main); + font-size: 0.95rem; + font-weight: 600; + color: var(--black); + display: flex; + align-items: center; + gap: 4px; +} + +.authorDate { + font-family: var(--font-main); + font-size: 0.75rem; + color: var(--gray200); +} + +.menuBtn { + background: none; + border: none; + cursor: pointer; + color: var(--gray600); + padding: 4px; + display: flex; + align-items: center; +} + +/* ── 질문 본문 ── */ +.questionTitle { + display: flex; + align-items: flex-start; + gap: 6px; + margin-bottom: 18px; +} + +.qIcon { + font-family: var(--font-main); + font-size: 2rem; + font-weight: 900; + color: var(--main); + line-height: 1.1; + flex-shrink: 0; +} + +.questionText { + font-family: var(--font-main); + font-size: 24px; + font-weight: 600; + color: var(--black); + line-height: 1.3; + word-break: keep-all; + padding-top: 4px; +} + +/* ── 첨부 이미지 ── */ +.questionImage { + width: 100%; + border-radius: 12px; + object-fit: cover; + display: block; + margin-bottom: 18px; +} + +/* ── 액션 버튼 행 ── */ +.actionRow { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 10px; + margin-bottom: 10px; +} + +.likeBtn { + display: flex; + align-items: center; + gap: 6px; + border: none; + cursor: pointer; + font-family: var(--font-main); + font-size: 0.85rem; + color: var(--gray600); + padding: 4px 10px 4px 8px; + background: var(--gray50); + border-radius: 10px; + transition: color 0.15s, background 0.15s; + height: 28px; +} + +.likeBtn.liked { + color: var(--dark); + background: var(--light); +} + +.commentBtn { + display: flex; + align-items: center; + gap: 4px; + border: none; + cursor: pointer; + font-family: var(--font-main); + font-size: 0.85rem; + color: var(--gray600); + padding: 4px 10px; + background: var(--gray50); + border-radius: 10px; + height: 28px; + white-space: nowrap; +} + +/* ── 구분선 ── */ +.divider { + background: var(--gray200); + width: 100%; + height: 1px; + border: none; + margin: 4px 0 0; +} + +/* ── 댓글 목록 ── */ +.commentList { + 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 { + width: 35px; + height: 35px; + background: none; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + overflow: hidden; +} + +.commentAvatarImg { + width: 100%; + height: 100%; + object-fit: cover; +} + +.commentAuthorName { + font-family: var(--font-main); + font-size: 0.9rem; + font-weight: 600; + color: var(--black); + display: flex; + align-items: center; + gap: 4px; +} + +/* ── 댓글 말풍선 ── */ +.commentBubble { + margin-left: 42px; + background: var(--gray50); + border-radius: 5px 20px 20px 20px; + padding: 10px 14px; + display: inline-flex; + flex-direction: column; + gap: 10px; + max-width: calc(100% - 42px); + box-sizing: border-box; +} + +.commentContent { + font-family: var(--font-main); + font-size: 0.9rem; + color: var(--black); + font-weight: 400; + line-height: 1.5; + display: flex; + align-items: flex-start; + gap: 6px; +} + +.commentArrow { + flex-shrink: 0; + color: var(--gray600); + margin-top: 2px; +} + +.commentImage { + width: 100%; + max-width: 380px; + border-radius: 8px; + object-fit: cover; + display: block; +} + +/* 댓글 타임스탬프 */ +.commentDate { + margin-left: 42px; + font-family: var(--font-main); + font-size: 0.72rem; + color: var(--gray200); + margin-top: 6px; +} + +/* ── 댓글 입력 바 (하단 고정) ── */ +.bottomCover { + position: fixed; + bottom: 0; + left: 0; + right: 0; + height: 80px; + background: linear-gradient(to bottom, transparent, var(--white) 60%); + z-index: 99; + pointer-events: none; +} + +.commentInputBar { + position: fixed; + bottom: 20px; + left: 50%; + transform: translateX(-50%); + width: min(740px, 100vw - 32px); + height: 56px; + border-radius: 30px; + background: var(--white); + box-shadow: 1px 2px 10px 0 rgba(0, 0, 0, 0.25); + display: flex; + align-items: center; + padding: 0 12px; + box-sizing: border-box; + z-index: 100; + gap: 8px; +} + +.commentInput { + flex: 1; + border: none; + background: none; + font-family: var(--font-main); + font-size: 1rem; + color: var(--black); + outline: none; + height: 100%; +} + +.commentInput::placeholder { + color: var(--gray200); +} + +.submitBtn { + width: 36px; + height: 36px; + border-radius: 50%; + background: #09C410; + border: none; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + padding: 0; + transition: opacity 0.15s; +} + +.submitBtn:disabled { + background: var(--gray200); + cursor: default; +} + +/* ── 스태프 뱃지 ── */ +.staffBadge { + display: inline-flex; + align-items: center; +} \ No newline at end of file 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/QnAMainPage.js b/frontend/src/pages/qna/QnAMainPage.js index f875ffc..d01df87 100644 --- a/frontend/src/pages/qna/QnAMainPage.js +++ b/frontend/src/pages/qna/QnAMainPage.js @@ -15,6 +15,7 @@ const pastSessions = [ // css 보려고 걍 적어둠 ]; function QNAMainPage() { + return (