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
2 changes: 2 additions & 0 deletions frontend/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import AdminMemberApproval from './pages/AdminMemberApproval.jsx';
import AdminMemberManage from './pages/AdminMemberManage.jsx';
import AdminExcelUpload from './pages/AdminExcelUpload.jsx';
import AdminDashboard from './pages/AdminDashboard.jsx';
import AdminFeedback from './pages/AdminFeedback.jsx';

// import CheckInPage from './components/attendancemanage/qrmanagement/CheckInPage.jsx';
import QrRenderPage from './components/attendancemanage/qrmanagement/QrRenderPage.jsx';
Expand Down Expand Up @@ -100,6 +101,7 @@ function App() {
element={<AdminExcelUpload />}
/>
<Route path="/admin/dashboard" element={<AdminDashboard />} />
<Route path="/admin/feedbacks" element={<AdminFeedback />} />
</Route>
Comment thread
sangkyu39 marked this conversation as resolved.
</Route>
</Routes>
Expand Down
137 changes: 137 additions & 0 deletions frontend/src/components/AdminFeedback/AdminFeedbackList.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { MessageSquare, RefreshCw } from 'lucide-react';
import styles from './AdminFeedbackList.module.css';
import { getAdminFeedbacks } from '../../utils/adminFeedbackApi';

const PAGE_SIZE = 10;

const formatDateTime = (value) => {
if (!value) return '-';

const date = new Date(value);
if (Number.isNaN(date.getTime())) return '-';

return date.toLocaleString('ko-KR', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
});
};

const AdminFeedbackList = () => {
const [feedbacks, setFeedbacks] = useState([]);
const [page, setPage] = useState(0);
const [totalPages, setTotalPages] = useState(0);
const [totalElements, setTotalElements] = useState(0);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');

const fetchFeedbacks = useCallback(async (nextPage = page) => {
setIsLoading(true);
setError('');

try {
const result = await getAdminFeedbacks({ page: nextPage, size: PAGE_SIZE });
const content = Array.isArray(result?.content) ? result.content : [];

setFeedbacks(content);
setPage(result?.number ?? nextPage);
setTotalPages(result?.totalPages ?? 0);
setTotalElements(result?.totalElements ?? content.length);
} catch (loadError) {
setFeedbacks([]);
Comment thread
sangkyu39 marked this conversation as resolved.
setError(loadError?.message || '피드백을 불러오지 못했습니다. 잠시 후 다시 시도해주세요.');
} finally {
Comment thread
sangkyu39 marked this conversation as resolved.
setIsLoading(false);
}
}, [page]);

useEffect(() => {
fetchFeedbacks(0);
}, [fetchFeedbacks]);
Comment thread
sangkyu39 marked this conversation as resolved.
Comment thread
sangkyu39 marked this conversation as resolved.

const pageLabel = useMemo(() => {
if (totalPages === 0) return '0 / 0';
return `${page + 1} / ${totalPages}`;
}, [page, totalPages]);

const goPrev = () => {
if (isLoading || page <= 0) return;
fetchFeedbacks(page - 1);
};

const goNext = () => {
if (isLoading || page + 1 >= totalPages) return;
fetchFeedbacks(page + 1);
};

return (
<section className={styles.container}>
<div className={styles.headerRow}>
<div className={styles.titleWrap}>
<MessageSquare size={18} />
<h2 className={styles.title}>사용자 피드백</h2>
<span className={styles.count}>총 {totalElements.toLocaleString()}건</span>
</div>

<button
type="button"
className={styles.refreshButton}
onClick={() => fetchFeedbacks(page)}
disabled={isLoading}
>
<RefreshCw size={16} className={isLoading ? styles.spin : ''} />
새로고침
</button>
</div>

{error && <p className={styles.errorText}>{error}</p>}

<div className={styles.listWrap}>
{isLoading && <p className={styles.statusText}>피드백을 불러오는 중입니다...</p>}

{!isLoading && feedbacks.length === 0 && !error && (
<p className={styles.statusText}>등록된 피드백이 없습니다.</p>
)}

{!isLoading && feedbacks.length > 0 && (
<ul className={styles.list}>
{feedbacks.map((item) => (
<li key={item.feedbackId} className={styles.item}>
<div className={styles.itemMeta}>
<span className={styles.feedbackId}>익명</span>
<time className={styles.dateText}>{formatDateTime(item.createdDate)}</time>
</div>
<p className={styles.content}>{item.content || '-'}</p>
</li>
))}
</ul>
)}
</div>

<div className={styles.pagination}>
<button
type="button"
className={styles.pageButton}
onClick={goPrev}
disabled={isLoading || page <= 0}
>
이전
</button>
<span className={styles.pageLabel}>{pageLabel}</span>
<button
type="button"
className={styles.pageButton}
onClick={goNext}
disabled={isLoading || page + 1 >= totalPages}
>
다음
</button>
</div>
</section>
);
};

export default AdminFeedbackList;
191 changes: 191 additions & 0 deletions frontend/src/components/AdminFeedback/AdminFeedbackList.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
.container {
border-radius: 18px;
border: 1px solid #e6ebf2;
background: #ffffff;
box-shadow: 0 10px 30px rgba(19, 33, 68, 0.06);
padding: 20px;
}

.headerRow {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 16px;
}

.titleWrap {
display: flex;
align-items: center;
gap: 8px;
color: #1f2937;
}

.title {
font-size: 18px;
font-weight: 700;
margin: 0;
}

.count {
color: #6b7280;
font-size: 13px;
}

.refreshButton {
border: 1px solid #d7dce5;
background: #ffffff;
color: #1f2937;
border-radius: 10px;
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
}

.refreshButton:hover:not(:disabled) {
background: #f9fafb;
}

.refreshButton:disabled {
cursor: not-allowed;
opacity: 0.6;
}

.spin {
animation: spin 1s linear infinite;
}

@keyframes spin {
from {
transform: rotate(0deg);
}

to {
transform: rotate(360deg);
}
}

.errorText {
margin: 0 0 12px;
color: #b91c1c;
font-size: 14px;
}

.listWrap {
border: 1px solid #edf1f7;
border-radius: 14px;
background: #fcfdff;
min-height: 240px;
}

.statusText {
margin: 0;
padding: 24px;
color: #6b7280;
text-align: center;
}

.list {
list-style: none;
margin: 0;
padding: 0;
}

.item {
padding: 16px;
border-bottom: 1px solid #eef2f7;
}

.item:last-child {
border-bottom: none;
}

.itemMeta {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
margin-bottom: 8px;
}

.feedbackId {
font-size: 12px;
color: #6b7280;
word-break: break-all;
}

.dateText {
font-size: 12px;
color: #6b7280;
white-space: nowrap;
}

.content {
margin: 0;
line-height: 1.6;
color: #111827;
white-space: pre-wrap;
}

.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 12px;
margin-top: 16px;
}

.pageLabel {
min-width: 72px;
text-align: center;
color: #4b5563;
font-weight: 600;
}

.pageButton {
border: 1px solid #d7dce5;
background: #ffffff;
color: #1f2937;
border-radius: 10px;
padding: 8px 14px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
}

.pageButton:hover:not(:disabled) {
background: #f9fafb;
}

.pageButton:disabled {
cursor: not-allowed;
opacity: 0.5;
}

@media (max-width: 768px) {
.container {
padding: 16px;
}

.headerRow {
flex-direction: column;
align-items: stretch;
}

.refreshButton {
justify-content: center;
}

.itemMeta {
flex-direction: column;
align-items: flex-start;
}

.dateText {
white-space: normal;
}
}
2 changes: 2 additions & 0 deletions frontend/src/components/AdminHome/AdminSidebar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
FileText,
Calendar,
Star,
MessageSquare,
Gamepad2,
BarChart3,
Home,
Expand Down Expand Up @@ -31,6 +32,7 @@ const presidentMenuItems = [
{ label: '게시물 관리', href: '/admin/posts', icon: FileText },
{ label: '출석 관리', href: '/admin/attendance', icon: Calendar },
{ label: '포인트 관리', href: '/admin/points', icon: Star },
{ label: '피드백 확인하기', href: '/admin/feedbacks', icon: MessageSquare },
],
},
{
Expand Down
21 changes: 21 additions & 0 deletions frontend/src/pages/AdminFeedback.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import AdminHeader from '../components/AdminHome/AdminHeader';
import AdminSidebar from '../components/AdminHome/AdminSidebar';
import AdminFeedbackList from '../components/AdminFeedback/AdminFeedbackList';
import styles from './AdminFeedback.module.css';

const AdminFeedback = () => {
return (
<div className={styles.layout}>
<AdminSidebar />

<div className={styles.mainArea}>
<AdminHeader title="피드백 확인" />
<div className={styles.contentArea}>
<AdminFeedbackList />
</div>
</div>
</div>
);
};

export default AdminFeedback;
Loading