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
20 changes: 20 additions & 0 deletions src/commons/ui/button/AnnounceSortButton.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import "styles/commons/ui/button/AnnounceSortButtons.scss";

export default function AnnounceSortButtons({ sortOrder, onChange }) {
return (
<div className="announce-sort">
<button
className={sortOrder === "DATE_ASC" ? "active" : ""}
onClick={() => onChange("DATE_ASC")}
>
오래된 순
</button>
<button
className={sortOrder === "DATE_DESC" ? "active" : ""}
onClick={() => onChange("DATE_DESC")}
>
최신 순
</button>
</div>
);
}
20 changes: 0 additions & 20 deletions src/components/home/AnnounceSortButton.jsx

This file was deleted.

3 changes: 3 additions & 0 deletions src/constants/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,6 @@ export const GAME2_EVALUATE_API = process.env.REACT_APP_GAME2_EVALUATE_API;
// 채팅 소켓 API
export const SOCKET_CHAT_ALL = process.env.REACT_APP_WS_CHAT_ALL_API;
export const SOCKET_CHAT_PRIVATE = process.env.REACT_APP_WS_CHAT_PRIVATE_API;

// 공지사항 API
export const ANNOUNCE_LIST_API = process.env.REACT_APP_ANNOUNCE_LIST_API;
76 changes: 50 additions & 26 deletions src/pages/home/AnnounceDetailPage.jsx
Original file line number Diff line number Diff line change
@@ -1,48 +1,72 @@
import AnnounceBear from "commons/svgs/AnnounceBear";
import CloseButton from "commons/ui/button/CloseButton";
import { useNavigate } from "react-router-dom";
import { useNavigate, useParams } from "react-router-dom";
import "styles/pages/home/AnnounceDetailPage.scss";
import { formatDate } from "utils/time";
import { useEffect, useState } from "react";
import { getAnnounceDetailAPI } from "services/home/announce";

const mockAnnounce = {
id: 1,
title: "서버 점검 및 업데이트",
createdAt: "2025-03-27T16:13:32",
content: `
어쩌고저쩌고어쩌고저쩌고어쩌고저쩌고어쩌
asdfasfasfd a sdfa asdfa asdf assdfasdvasdvas
어쩌고저쩌고어쩌고저쩌고어쩌고저쩌고어쩌
asdfasfasfd a sdfa asdfa asdf assdfasdvasdvas
어쩌고저쩌고어쩌고저쩌고어쩌고저쩌고어쩌
asdfasfasfd a sdfa asdfa asdf assdfasdvasdvas
`,
modifiedAt: "2025-11-16T16:13:32",
writer: "묘묘",
};
/** 공통 상태 컴포넌트 */
const AnnounceStatus = ({ type, message }) => (
<div className="announce-detail-page-container">
<h1 className="announce-detail-title">공지사항</h1>
<div className={`announce-detail-${type}`}>{message}</div>
</div>
);

const AnnounceDetailPage = () => {
const naviate = useNavigate();
const navigate = useNavigate();
const { id } = useParams();
const [notice, setNotice] = useState(null);
const [status, setStatus] = useState({ loading: true, error: null });

const closeDetailPage = () => navigate(-1);

useEffect(() => {
const fetchNoticeDetail = async () => {
try {
setStatus({ loading: true, error: null });
const res = await getAnnounceDetailAPI(id);
setNotice(res);
} catch {
setStatus({
loading: false,
error: "공지사항을 불러오는 중 오류가 발생했습니다.",
});
} finally {
setStatus((prev) => ({ ...prev, loading: false }));
}
};
fetchNoticeDetail();
}, [id]);

if (status.loading)
return <AnnounceStatus type="loading" message="로딩 중..." />;

const closeDetailpage = () => {
naviate(-1);
};
if (status.error)
return <AnnounceStatus type="error" message={status.error} />;

if (!notice)
return (
<AnnounceStatus type="empty" message="공지사항을 찾을 수 없습니다." />
);

return (
<div className="announce-detail-page-container">
<h1 className="announce-detail-title">공지사항</h1>

<article className="announce-detail-container">
<header className="announce-detail-header">
<span className="meta-date">
{formatDate(mockAnnounce.createdAt)}
</span>
<CloseButton onClick={closeDetailpage} />
<span className="meta-date">{formatDate(notice.createdAt)}</span>
<CloseButton onClick={closeDetailPage} />
</header>

<div className="announce-detail-content">
<div className="content-title">
<AnnounceBear />
{mockAnnounce.title}
{notice.title}
</div>
<div className="content-description">{mockAnnounce.content}</div>
<div className="content-description">{notice.content}</div>
</div>
</article>
</div>
Expand Down
172 changes: 79 additions & 93 deletions src/pages/home/AnnouncePage.jsx
Original file line number Diff line number Diff line change
@@ -1,78 +1,13 @@
import { EmptyContent } from "commons/emptyContent/EmptyContent";
import { useState } from "react";
import { Link } from "react-router-dom";
import { useEffect, useState } from "react";
import { Link, useSearchParams } from "react-router-dom";
import "styles/pages/home/AnnouncePage.scss";
import { HOME_ANNOUNCE_TABS } from "constants/home";
import AnnounceSortButtons from "components/home/AnnounceSortButton";
import { formatDate } from "utils/time";
import Pagination from "commons/ui/Pagination";
import { getAnnounceListAPI } from "services/home/announce";
import { EmptyContent } from "commons/emptyContent/EmptyContent";
import AnnounceSortButtons from "commons/ui/button/AnnounceSortButton";

/*
- notificationId: 고유 ID
- title: 공지 제목
- createdAt: 생성일
- type: 공지 분류 (전체 / 이벤트 / 업데이트)
- writer: 작성자
*/
const MOCK_NOTICES = [
{
notificationId: 1,
title: "상대방에게 욕설, 비난이 담긴 채팅 신고",
createdAt: "2025-06-21",
type: "전체",
writer: "묘묘",
},
{
notificationId: 2,
title: "2025. 07. 21 업데이트 안내",
createdAt: "2025-06-21",
type: "업데이트",
writer: "묘묘",
},
{
notificationId: 3,
title: "AI 업그레이드 안내",
createdAt: "2025-06-21",
type: "이벤트",
writer: "묘묘",
},
{
notificationId: 4,
title: "2026. 08. 21 점검 안내",
createdAt: "2025-06-21",
type: "업데이트",
writer: "묘묘",
},
{
notificationId: 5,
title: "상대방에게 욕설, 비난이 담긴 채팅 신고",
createdAt: "2025-06-21",
type: "전체",
writer: "묘묘",
},
{
notificationId: 6,
title: "서비스 정책 변경 안내",
createdAt: "2025-05-30",
type: "이벤트",
writer: "묘묘",
},
{
notificationId: 7,
title: "시스템 안정화 패치",
createdAt: "2025-05-01",
type: "업데이트",
writer: "묘묘",
},
];

const PAGE_SIZE = 5;

/**
* 탭의 활성 상태 및 표시 스타일을 결정하는 헬퍼 함수
* @param {string} tabName - 탭 이름 (전체 / 이벤트 / 업데이트)
* @param {string} currentTab - 현재 선택된 탭
*/
const getTabClassName = (tabName, currentTab) => {
const classes = ["tab"];
if (tabName === currentTab) classes.push("active");
Expand All @@ -81,30 +16,72 @@ const getTabClassName = (tabName, currentTab) => {
};

const AnnouncePage = () => {
const [searchParams, setSearchParams] = useSearchParams();

const [activeTab, setActiveTab] = useState("전체");
const [sortOrder, setSortOrder] = useState("old");
const [currentPage, setCurrentPage] = useState(1);
const [notices, setNotices] = useState([]);
const [totalPages, setTotalPages] = useState(1);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

// URL 파라미터에서 page, sort 가져오기
const currentPage = Number(searchParams.get("page")) || 1;
const sortOrder = searchParams.get("sort") || "DATE_DESC";

const filteredNotices =
activeTab === "전체"
? MOCK_NOTICES
: MOCK_NOTICES.filter((notice) => notice.type === activeTab);
/** 공지사항 API 호출 */
const fetchNotices = async (page = 1, sort = "DATE_DESC") => {
try {
setLoading(true);
setError(null);

const sortedNotices = [...filteredNotices].sort((a, b) => {
if (sortOrder === "new")
return new Date(b.createdAt) - new Date(a.createdAt);
return new Date(a.createdAt) - new Date(b.createdAt);
});
const res = await getAnnounceListAPI({
keyword: "",
page: page - 1, // 서버는 0부터 시작
sort,
});

const totalPages = Math.max(1, Math.ceil(sortedNotices.length / PAGE_SIZE));
const startIndex = (currentPage - 1) * PAGE_SIZE;
const currentItems = sortedNotices.slice(startIndex, startIndex + PAGE_SIZE);
if (res?.content && res.content.length > 0) {
setNotices(res.content);
setTotalPages(res.page.totalPages);
} else {
setNotices([]);
setTotalPages(0);
}
} catch (err) {
setError("공지사항을 불러오는 중 오류가 발생했습니다.");
setTotalPages(0);
} finally {
setLoading(false);
}
};

/** 페이지/정렬 변경 시 URL 갱신 */
const updateSearchParams = (page = currentPage, sort = sortOrder) => {
setSearchParams({ page: String(page), sort });
};

/** 페이지 이동 핸들러 */
const handlePageChange = (newPage) => {
updateSearchParams(newPage, sortOrder);
};

/** 정렬 변경 핸들러 */
const handleSortChange = (order) => {
const newOrder = order === "DATE_DESC" ? "DATE_DESC" : "DATE_ASC";
updateSearchParams(1, newOrder); // 정렬 바꾸면 1페이지로 이동
};

/** 탭 클릭 시 1페이지로 리셋 */
const handleTabClick = (tabName) => {
setActiveTab(tabName);
setCurrentPage(1);
updateSearchParams(1, sortOrder);
};

/** URL 변경 시마다 데이터 다시 불러오기 */
useEffect(() => {
fetchNotices(currentPage, sortOrder);
}, [currentPage, sortOrder]);

return (
<div className="announce-page-container">
<h1 className="announce-title">공지사항</h1>
Expand All @@ -124,35 +101,44 @@ const AnnouncePage = () => {
</ul>
</nav>

{/* 공지 리스트 */}
<div className="announce-list-container">
<AnnounceSortButtons sortOrder={sortOrder} onChange={setSortOrder} />
<AnnounceSortButtons
sortOrder={sortOrder}
onChange={handleSortChange}
/>

{currentItems.length === 0 ? (
{loading ? (
<div className="empty">로딩 중...</div>
) : error ? (
<div className="empty">{error}</div>
) : notices.length === 0 ? (
<div className="empty">
<EmptyContent />
</div>
) : (
currentItems.map((notice, idx) => (
notices.map((notice) => (
<div
key={`${notice.notificationId}-${idx}`}
key={`notice-${notice.notificationId}`}
className="announce-item"
>
<Link className="title" to={`/announce/${notice.notificationId}`}>
{notice.title}
</Link>
<div className="date">{formatDate(notice.createdAt)}</div>
<div className="meta">
<span className="date">{formatDate(notice.createdAt)}</span>
</div>
</div>
))
)}
</div>

{/* 페이지네이션 */}
<div className="announce-pagination">
<Pagination
page={currentPage}
totalPages={totalPages}
onPageChange={setCurrentPage}
isDisabled={currentItems.length === 0}
onPageChange={handlePageChange}
isDisabled={notices.length === 0}
/>
</div>
</div>
Expand Down
10 changes: 10 additions & 0 deletions src/services/home/announce.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { ANNOUNCE_LIST_API } from "constants/api";
import { apiInterface } from "services/axiosForm";

export const getAnnounceListAPI = async (params = {}) => {
return await apiInterface("get", ANNOUNCE_LIST_API, {}, params, false);
};

export const getAnnounceDetailAPI = async (notificationId) => {
return await apiInterface("get", `${ANNOUNCE_LIST_API}/${notificationId}`, {}, {}, false);
};