Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions frontend/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -23,14 +23,14 @@ function App() {
<Route element={<Layout headerType="light" />}>
<Route path="/sessions" element={<QnAMainPage />} />
<Route path="/sessions/questions" element={<QnAListPage />} />
<Route path="/sessions/questions/:id" element={<QnADetailePage />} />
<Route path="/sessions/questions/:id" element={<QnADetailPage />} />
<Route path="/curriculum" element={<CurriculumPage />} />
</Route>

{/* 다크 헤더 페이지 */}
<Route element={<Layout headerType="dark" />}>
<Route path="/pirocheck" element={<PiroCheckMain />}/>
<Route path="/pirocheck/attendance" element={<Attendance />}/>
<Route path="/pirocheck" element={<PiroCheckMain />} />
<Route path="/pirocheck/attendance" element={<Attendance />} />
</Route>

</Routes>
Expand Down
Binary file added frontend/src/assets/images/profile.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 0 additions & 5 deletions frontend/src/assets/styles/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,4 @@
--black: #111111;
--font-title: 'GemunuLibre', sans-serif;
--font-main: 'Pretendard', sans-serif;
}

/* body 기본 설정 */
body {
background-color: var(--gray20);
}
2 changes: 1 addition & 1 deletion frontend/src/components/Layout.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Outlet } from 'react-router-dom';

function Layout({ headerType }) {
return (
<div style={{ background: headerType === 'dark' ? '#111111' : '#ffffff', minHeight: '100vh' }}>
<div style={{ background: headerType === 'dark' ? '#111111' : 'var(--gray20)', minHeight: '100vh' }}>
<Header type={headerType} />
<Outlet />
</div>
Expand Down
245 changes: 245 additions & 0 deletions frontend/src/pages/qna/QnADetailPage.js
Original file line number Diff line number Diff line change
@@ -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 (
<div className={styles.page}>

{/* ── 상단 바:해결 여부 ── */}
<div className={styles.topBar}>
{question.isSolved ? (
<span className={styles.solvedBadge}>해결 질문</span>
) : (
<span className={styles.unsolvedBadge}>미해결 질문</span>
)}
</div>

{/* ── 작성자 행 ── */}
<div className={styles.authorRow}>
<div className={styles.avatar}>
<img src={profileImg} alt={question.author} className={styles.avatarImg} />
</div>
<div className={styles.authorInfo}>
<span className={styles.authorName}>
{question.author}
{question.isStaff && (
<span className={styles.staffBadge}><StaffCheck /></span>
)}
</span>
<span className={styles.authorDate}>{question.date}</span>
</div>
<button className={styles.menuBtn} aria-label="더보기">
<FiMoreVertical size={20} />
</button>
</div>

{/* ── 질문 제목 ── */}
<div className={styles.questionTitle}>
<span className={styles.qIcon}>Q.</span>
<span className={styles.questionText}>{question.text}</span>
</div>

{/* ── 첨부 이미지 ── */}
{question.image && (
<img
src={question.image}
alt="첨부 이미지"
className={styles.questionImage}
/>
)}

{/* ── 액션 버튼 (저도 궁금해요 / 댓글달기) ── */}
<div className={styles.actionRow}>
<button
className={`${styles.likeBtn} ${question.iLiked ? styles.liked : ''}`}
onClick={toggleLike}
>
<MeCuriousToo /> 저도 궁금해요&nbsp;{question.likes}
</button>
<button
className={styles.commentBtn}
onClick={() => document.getElementById('commentInput')?.focus()}
>
<CommentImoji />&nbsp;댓글달기
</button>
</div>

<hr className={styles.divider} />

{/* ── 댓글 목록 ── */}
<div className={styles.commentList}>
{question.comments.map(comment => (
<div key={comment.id} className={styles.commentBlock}>

{/* 댓글 작성자 */}
<div className={styles.commentAuthorRow}>
<div className={styles.commentAvatar}>
<img src={profileImg} alt={comment.author} className={styles.commentAvatarImg} />
</div>
<span className={styles.commentAuthorName}>
{comment.author}
{comment.isStaff && (
<span className={styles.staffBadge}><StaffCheck /></span>
)}
</span>
</div>

{/* 댓글 말풍선 */}
<div className={styles.commentBubble}>
<div className={styles.commentContent}>
<FiCornerDownRight size={14} className={styles.commentArrow} />
{comment.content}
</div>
{comment.image && (
<img
src={comment.image}
alt="댓글 첨부 이미지"
className={styles.commentImage}
/>
)}
</div>

{/* 타임스탬프 */}
<p className={styles.commentDate}>{comment.date}</p>
</div>
))}
</div>

{/* ── 하단 그라디언트 커버 ── */}
<div className={styles.bottomCover} />

{/* ── 댓글 입력 바 (하단 고정) ── */}
<div className={styles.commentInputBar}>
<input
id="commentInput"
className={styles.commentInput}
placeholder="댓글을 입력해주세요..."
value={commentText}
onChange={e => setCommentText(e.target.value)}
onKeyDown={e => {
if (e.key === 'Enter') handleCommentSubmit();
}}
disabled={isSubmitting}
/>
<button
className={styles.submitBtn}
onClick={handleCommentSubmit}
disabled={!commentText.trim() || isSubmitting}
aria-label="댓글 제출"
>
{isSubmitting ? '⏳' : <SumitBtn />}
</button>
</div>

</div>
);
}

export default QnADetailPage;
Loading
Loading