diff --git a/frontend/src/pages/qna/QnADetailPage.js b/frontend/src/pages/qna/QnADetailPage.js
index 52d2e1e..bb93229 100644
--- a/frontend/src/pages/qna/QnADetailPage.js
+++ b/frontend/src/pages/qna/QnADetailPage.js
@@ -2,7 +2,7 @@ import '../../assets/styles/global.css';
import { useState, useEffect, useRef, useCallback } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import styles from './QnADetailPage.module.css';
-import { FiMoreVertical, FiCornerDownRight, FiChevronLeft } from 'react-icons/fi';
+import { FiMoreVertical, FiCornerDownRight, FiChevronLeft, FiChevronRight, FiX } from 'react-icons/fi';
import {
CommentImoji,
MeCuriousToo,
@@ -97,6 +97,60 @@ function QnADetailPage() {
const [editingCommentId, setEditingCommentId] = useState(null);
const [editCommentText, setEditCommentText] = useState('');
+ // ── 이미지 확대보기(라이트박스) 상태 ─────────────
+ // images: 같은 묶음(질문 또는 한 댓글)의 이미지 url 배열, index: 현재 보고 있는 인덱스
+ const [lightbox, setLightbox] = useState(null);
+
+ const openLightbox = (images, index) => setLightbox({ images, index });
+ const closeLightbox = () => setLightbox(null);
+
+ const showPrevImage = useCallback(() => {
+ setLightbox(prev => {
+ if (!prev) return prev;
+ const nextIndex = (prev.index - 1 + prev.images.length) % prev.images.length;
+ return { ...prev, index: nextIndex };
+ });
+ }, []);
+
+ const showNextImage = useCallback(() => {
+ setLightbox(prev => {
+ if (!prev) return prev;
+ const nextIndex = (prev.index + 1) % prev.images.length;
+ return { ...prev, index: nextIndex };
+ });
+ }, []);
+
+ useEffect(() => {
+ if (!lightbox) return undefined;
+
+ const handleKeyDown = (e) => {
+ if (e.key === 'Escape') closeLightbox();
+ if (e.key === 'ArrowLeft') showPrevImage();
+ if (e.key === 'ArrowRight') showNextImage();
+ };
+ document.addEventListener('keydown', handleKeyDown);
+ return () => document.removeEventListener('keydown', handleKeyDown);
+ }, [lightbox, showPrevImage, showNextImage]);
+
+ // 모바일 좌우 스와이프로 이미지 넘기기
+ const touchStartXRef = useRef(null);
+
+ const handleLightboxTouchStart = (e) => {
+ touchStartXRef.current = e.touches[0].clientX;
+ };
+
+ const handleLightboxTouchEnd = (e) => {
+ if (touchStartXRef.current === null) return;
+ const deltaX = e.changedTouches[0].clientX - touchStartXRef.current;
+ const SWIPE_THRESHOLD = 50;
+ if (deltaX > SWIPE_THRESHOLD) {
+ showPrevImage();
+ } else if (deltaX < -SWIPE_THRESHOLD) {
+ showNextImage();
+ }
+ touchStartXRef.current = null;
+ };
+
const fetchQuestion = useCallback(async ({ showLoading = false } = {}) => {
try {
@@ -446,7 +500,13 @@ function QnADetailPage() {
{comment.imageUrls?.length > 0 && (
{comment.imageUrls.map((url, idx) => (
-

+

openLightbox(comment.imageUrls, idx)}
+ />
))}
)}
@@ -544,7 +604,13 @@ function QnADetailPage() {
{question.imageUrls?.length > 0 && (
{question.imageUrls.map((url, idx) => (
-

+

openLightbox(question.imageUrls, idx)}
+ />
))}
)}
@@ -622,8 +688,59 @@ function QnADetailPage() {
+
+ {/* ── 이미지 확대보기 ── */}
+ {lightbox && (
+
+
+
+ {lightbox.images.length > 1 && (
+
+ )}
+
+

e.stopPropagation()}
+ />
+
+ {lightbox.images.length > 1 && (
+
+ )}
+
+ {lightbox.images.length > 1 && (
+
+ {lightbox.index + 1} / {lightbox.images.length}
+
+ )}
+
+ )}
);
}
-export default QnADetailPage;
+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
index c1e4091..83616c3 100644
--- a/frontend/src/pages/qna/QnADetailPage.module.css
+++ b/frontend/src/pages/qna/QnADetailPage.module.css
@@ -244,12 +244,29 @@
}
/* ── 질문 첨부 이미지 ── */
+.questionImages {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px;
+ margin-bottom: 18px;
+}
+
.questionImage {
width: 100%;
border-radius: 12px;
object-fit: cover;
display: block;
- margin-bottom: 18px;
+ cursor: zoom-in;
+ transition: opacity 0.15s, transform 0.15s;
+}
+
+.questionImage:hover {
+ opacity: 0.92;
+}
+
+/* 이미지가 여러 장일 때는 한 줄에 나눠 배치 (간격은 위 gap 속성으로 처리) */
+.questionImages:has(.questionImage:nth-child(2)) .questionImage {
+ width: calc(50% - 5px);
}
/* ── 액션 버튼 행 (좋아요 / 댓글달기) ── */
@@ -419,12 +436,30 @@
margin-top: 2px;
}
+.commentImages {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+}
+
.commentImage {
width: 100%;
max-width: 380px;
border-radius: 8px;
object-fit: cover;
display: block;
+ cursor: zoom-in;
+ transition: opacity 0.15s;
+}
+
+.commentImage:hover {
+ opacity: 0.92;
+}
+
+/* 댓글에 이미지가 여러 장일 때는 한 줄에 나눠 배치 */
+.commentImages:has(.commentImage:nth-child(2)) .commentImage {
+ width: calc(50% - 4px);
+ max-width: calc(50% - 4px);
}
.commentDate {
@@ -550,6 +585,12 @@
}
/* ── 이미지 미리보기 ── */
+.imagePreviewList {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+}
+
.imagePreviewWrapper {
position: relative;
display: inline-block;
@@ -558,6 +599,14 @@
align-self: flex-start;
}
+.imagePreviewList .imagePreviewWrapper {
+ margin: 4px 0 0 0;
+}
+
+.imagePreviewList .imagePreviewWrapper:first-child {
+ margin-left: 12px;
+}
+
.imagePreview {
width: 80px;
height: 80px;
@@ -583,6 +632,109 @@
z-index: 101;
}
+/* ── 이미지 확대보기(라이트박스) ── */
+.lightboxOverlay {
+ position: fixed;
+ inset: 0;
+ background: rgba(0, 0, 0, 0.85);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1000;
+ padding: 40px;
+ box-sizing: border-box;
+ cursor: zoom-out;
+ animation: lightboxFadeIn 0.15s ease-out;
+}
+
+@keyframes lightboxFadeIn {
+ from {
+ opacity: 0;
+ }
+
+ to {
+ opacity: 1;
+ }
+}
+
+.lightboxImage {
+ max-width: 100%;
+ max-height: 100%;
+ object-fit: contain;
+ border-radius: 8px;
+ cursor: default;
+ box-shadow: 0 4px 30px rgba(0, 0, 0, 0.4);
+}
+
+.lightboxCloseBtn {
+ position: fixed;
+ top: 24px;
+ right: 24px;
+ width: 44px;
+ height: 44px;
+ border-radius: 50%;
+ background: rgba(255, 255, 255, 0.15);
+ border: none;
+ color: var(--white);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ z-index: 1001;
+ transition: background 0.15s;
+}
+
+.lightboxCloseBtn:hover {
+ background: rgba(255, 255, 255, 0.3);
+}
+
+.lightboxPrevBtn,
+.lightboxNextBtn {
+ position: fixed;
+ top: 50%;
+ transform: translateY(-50%);
+ width: 48px;
+ height: 48px;
+ border-radius: 50%;
+ background: rgba(255, 255, 255, 0.15);
+ border: none;
+ color: var(--white);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ z-index: 1001;
+ transition: background 0.15s;
+}
+
+.lightboxPrevBtn:hover,
+.lightboxNextBtn:hover {
+ background: rgba(255, 255, 255, 0.3);
+}
+
+.lightboxPrevBtn {
+ left: 24px;
+}
+
+.lightboxNextBtn {
+ right: 24px;
+}
+
+.lightboxCounter {
+ position: fixed;
+ bottom: 28px;
+ left: 50%;
+ transform: translateX(-50%);
+ background: rgba(255, 255, 255, 0.15);
+ color: var(--white);
+ font-family: var(--font-main);
+ font-size: 13px;
+ font-weight: 500;
+ padding: 5px 14px;
+ border-radius: 20px;
+ z-index: 1001;
+}
+
/* ════════════════════════════════════════
반응형 — 태블릿 (768px 이하)
════════════════════════════════════════ */
@@ -682,8 +834,9 @@
font-size: 13px;
}
- .commentImage {
- max-width: 100%;
+ .commentImages:has(.commentImage:nth-child(2)) .commentImage {
+ width: calc(50% - 4px);
+ max-width: calc(50% - 4px);
}
/* ── 댓글 수정 ── */
@@ -701,4 +854,35 @@
bottom: 12px;
padding: 6px 10px;
}
-}
+
+ /* ── 라이트박스 ── */
+ .lightboxOverlay {
+ padding: 16px;
+ }
+
+ .lightboxCloseBtn {
+ top: 12px;
+ right: 12px;
+ width: 38px;
+ height: 38px;
+ }
+
+ .lightboxPrevBtn,
+ .lightboxNextBtn {
+ width: 38px;
+ height: 38px;
+ }
+
+ .lightboxPrevBtn {
+ left: 8px;
+ }
+
+ .lightboxNextBtn {
+ right: 8px;
+ }
+
+ .lightboxCounter {
+ bottom: 16px;
+ font-size: 12px;
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/pages/qna/QnAListPage.js b/frontend/src/pages/qna/QnAListPage.js
index 3f56a65..7f0e696 100644
--- a/frontend/src/pages/qna/QnAListPage.js
+++ b/frontend/src/pages/qna/QnAListPage.js
@@ -610,6 +610,23 @@ function QnAListPage() {
setImagePreviews(next.map(f => URL.createObjectURL(f)));
};
+ const handleNewQuestionPaste = (e) => {
+ if (isStaff) return; // 이해도 체크 입력창에는 이미지 첨부 없음
+ const items = e.clipboardData?.items;
+ if (!items) return;
+ for (const item of items) {
+ if (item.type.startsWith('image/')) {
+ const file = item.getAsFile();
+ if (file) {
+ const merged = [...selectedImages, file].slice(0, 5);
+ setSelectedImages(merged);
+ setImagePreviews(merged.map(f => URL.createObjectURL(f)));
+ }
+ break;
+ }
+ }
+ };
+
// ── 새 질문 등록 ─────────────────────────────────
const handleNewQuestion = async () => {
const text = newQuestion.trim();
@@ -945,6 +962,7 @@ function QnAListPage() {
onKeyDown={e => {
if (e.key === 'Enter') isStaff ? handleNewUnderstandCheck() : handleNewQuestion();
}}
+ onPaste={handleNewQuestionPaste}
disabled={isSubmitting}
/>