diff --git a/src/api/notice/notice.ts b/src/api/notice/notice.ts new file mode 100644 index 0000000..2249296 --- /dev/null +++ b/src/api/notice/notice.ts @@ -0,0 +1,25 @@ +import type { TFetchNoticeDetailResponse, TFetchNoticesResponse } from '@/types/notice/notice'; + +import { axiosInstance } from '../axiosInstance'; + +// 공지사항 전체 조회 API +export const fetchNotices = async ({ + category, + page, + size, +}: { + category: 'SERVICE' | 'SYSTEM'; + page: number; + size: number; +}): Promise => { + const { data } = await axiosInstance.get('/api/v1/notices', { + params: { noticeCategory: category, page, size }, + }); + return data; +}; + +// 공지사항 상세 조회 API +export const fetchNoticeDetail = async (noticeId: number): Promise => { + const { data } = await axiosInstance.get(`/api/v1/notices/${noticeId}`); + return data; +}; diff --git a/src/assets/icons/Burger_fill.svg b/src/assets/icons/Burger_fill.svg new file mode 100644 index 0000000..5fbcfde --- /dev/null +++ b/src/assets/icons/Burger_fill.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/icons/Clear.svg b/src/assets/icons/Clear.svg new file mode 100644 index 0000000..c3b7aa7 --- /dev/null +++ b/src/assets/icons/Clear.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index bd5e2ca..ccb6d17 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -1,39 +1,41 @@ import { useState } from 'react'; import { Link } from 'react-router-dom'; +import MobileMenu from './MobileMenu'; import SettingsModal from '../modal/SettingModal'; +import BurgerIcon from '@/assets/icons/Burger_fill.svg?react'; +import ClearIcon from '@/assets/icons/Clear.svg?react'; import NotificationsIcon from '@/assets/icons/notifications_Blank.svg?react'; import SettingsIcon from '@/assets/icons/settings_Blank.svg?react'; import NavbarLogo from '@/assets/withTimeLogo/navbarLogo.svg?react'; -// Header 컴포넌트 props interface IHeaderProps { mode?: 'full' | 'minimal'; // full: nav + border | minimal: 로고만 } export default function Header({ mode = 'full' }: IHeaderProps) { - const [isSettingsOpen, setIsSettingsOpen] = useState(false); + const [isSettingsOpen, setIsSettingsOpen] = useState(false); //설정 모달 + const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); // 모바일 메뉴 const showNav = mode === 'full'; const showBorder = mode === 'full'; return (
+ {/* 최상단 네브바 */}
{/* 로고 */} -
- - - -
+ + + - {/* Nav 그룹 */} + {/* 데스크탑 메뉴 */} {showNav && ( -
- {/* 메뉴 */} +
+ {/* 네비게이션 링크들 */} - {/* 아이콘 */} + {/* 아이콘 버튼 */}
@@ -61,12 +63,30 @@ export default function Header({ mode = 'full' }: IHeaderProps) {
+
+ )} - {/* 설정 모달 */} - {isSettingsOpen && setIsSettingsOpen(false)} />} + {/* 모바일 메뉴 토글 버튼 */} + {showNav && ( +
+ {!isMobileMenuOpen ? ( + + ) : ( + + )}
)}
+ + {/* 모바일 메뉴 */} + {isMobileMenuOpen && setIsMobileMenuOpen(false)} onOpenSettings={() => setIsSettingsOpen(true)} />} + + {/* 설정 모달 */} + {isSettingsOpen && setIsSettingsOpen(false)} />}
); } diff --git a/src/components/layout/MobileMenu.tsx b/src/components/layout/MobileMenu.tsx new file mode 100644 index 0000000..6fe19e3 --- /dev/null +++ b/src/components/layout/MobileMenu.tsx @@ -0,0 +1,66 @@ +import { Link } from 'react-router-dom'; + +import ClearIcon from '@/assets/icons/Clear.svg?react'; +import NotificationsIcon from '@/assets/icons/notifications_Blank.svg?react'; +import SettingsIcon from '@/assets/icons/settings_Blank.svg?react'; + +interface IMobileMenuProps { + onClose: () => void; + onOpenSettings: () => void; +} + +export default function MobileMenu({ onClose, onOpenSettings }: IMobileMenuProps) { + return ( + <> + {/* 검정 반투명 배경 오버레이 */} +
+ + {/* 사이드 메뉴 */} +
+ {/* 닫기 버튼 */} +
+ +
+ + {/* 메뉴 목록 */} + +
+ + ); +} diff --git a/src/pages/notice/Notice.tsx b/src/pages/notice/Notice.tsx index 1e7ae31..884070a 100644 --- a/src/pages/notice/Notice.tsx +++ b/src/pages/notice/Notice.tsx @@ -1,74 +1,61 @@ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { Link } from 'react-router-dom'; +import type { TNoticeItem } from '@/types/notice/notice'; + import EditableInputBox from '@/components/common/EditableInputBox'; import Navigator from '@/components/common/navigator'; -const categories = ['서비스 안내', '시스템 안내']; +import { fetchNotices } from '@/api/notice/notice'; -const dummyNotices = [ - { category: '서비스 안내', title: '서비스 점검 안내 (06월 20일 02:00~04:00)', date: '2025.06.09' }, - { category: '서비스 안내', title: "신규 기능 '코스 저장하기' 오픈 안내", date: '2025.06.09' }, - { - category: '서비스 안내', - title: '데이트 추천 정확도 향상을 위한 업데이트 공지', - date: '2025.06.09', - content: `안녕하세요, WithTime 팀입니다. - -항상 WithTime을 이용해주시는 모든 사용자 여러분께 진심으로 감사드립니다. - 보다 더 정확하고 만족스러운 데이트 코스를 추천해드리기 위해,아래와 같은 기능 개선 및 시스템 업데이트를 진행하였음을 알려드립니다. - -🔧 주요 업데이트 내용 -1. 사용자 취향 기반 알고리즘 개선 -기존에는 간단한 지역 및 활동 선호도 중심으로 코스를 구성했다면, -이번 업데이트부터는 시간대, 최근 행동 패턴, 선택 취소된 장소 이력 등 -더 정밀한 데이터를 분석하여 추천의 정확도를 높였습니다. - -2. 상황별 추천 강화 -- 비 오는 날에는 실내 데이트 중심으로 -- 일정 시간이 짧을 경우, 이동 거리를 고려한 코스 구성 -이처럼 날씨, 이동 시간, 데이트 시간대를 함께 반영하도록 개선했습니다. - -3. 실시간 트렌드 반영 -주요 지역별 인기 급상승 장소나 SNS 상에서 언급된 핫플레이스 정보를 -추천 코스에 반영하여, 최신 트렌드를 더 빠르게 만나보실 수 있습니다. - -🎯 기대 효과 -- "오늘 뭐하지?" 고민할 시간 없이 상황 맞춤형 코스를 자동 추천 -- MBTI P 유형 사용자도 만족할 만큼 빠르고 간단한 코스 구성 -- 더 이상 ‘나랑 안 맞는 장소 추천’으로 불편하지 않도록 개선 - -📅 적용 일시 -- 2025년 6월 21일(금) 00:00부터 순차 적용 예정입니다. - -이번 업데이트는 사용자 여러분의 피드백을 바탕으로 진행되었습니다. -앞으로도 더 나은 서비스 제공을 위해 지속적으로 개선해나가겠습니다. -사용 중 불편한 점이나 건의 사항이 있다면, 언제든지 고객센터 또는 [문의하기]를 통해 알려주세요. -감사합니다. - -WithTime 팀 드림`, - }, - { category: '서비스 안내', title: '비회원 기능 이용 제한 관련 안내', date: '2025.06.09' }, - { category: '서비스 안내', title: '지도 기반 추천 기능 일시 중단 안내', date: '2025.06.09' }, - { category: '서비스 안내', title: '지도 기반 추천 기능 일시 중단 안내', date: '2025.06.09' }, - { category: '서비스 안내', title: '지도 기반 추천 기능 일시 중단 안내', date: '2025.06.09' }, - { category: '시스템 안내', title: '일부 브라우저에서 발생하는 오류 관련 안내', date: '2025.06.09' }, - { category: '시스템 안내', title: '추천 코스 반영 기준 변경 안내', date: '2025.06.09' }, - { category: '시스템 안내', title: '회원가입 약관 일부 변경 안내', date: '2025.06.09' }, -]; +const categories = ['서비스 안내', '시스템 안내']; export default function Notice() { - const [searchValue, setSearchValue] = useState(''); - const [activeCategory, setActiveCategory] = useState(categories[0]); + const [searchValue, setSearchValue] = useState(''); //검색어 상태 + const [activeCategory, setActiveCategory] = useState(categories[0]); //선택된 카테고리 const [currentPage, setCurrentPage] = useState(1); + + const [noticeList, setNoticeList] = useState([]); // 공지사항 리스트 + const [totalPages, setTotalPages] = useState(1); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + const itemsPerPage = 10; - // 필터링 + 페이징 - const filteredNotices = dummyNotices.filter( - (notice) => notice.category === activeCategory && notice.title.toLowerCase().includes(searchValue.toLowerCase()), - ); - const totalPages = Math.ceil(filteredNotices.length / itemsPerPage); - const paginatedNotices = filteredNotices.slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage); + // 백엔드에 넘길 카테고리 키 - 영어 변환 + const categoryKey = activeCategory === '서비스 안내' ? 'SERVICE' : 'SYSTEM'; + + // 컴포넌트 마운트 시, 카테고리/페이지 변경 시 -> API 호출 + useEffect(() => { + const getNotices = async () => { + setLoading(true); + try { + // 공지사항 목록 요청 + const response = await fetchNotices({ + category: categoryKey, + page: currentPage - 1, + size: itemsPerPage, + }); + + console.log('API 응답:', response); + + // 공지 목록과 페이지 수 설정 (빈 배열도 허용) + setNoticeList(response.result.noticeList ?? []); + setTotalPages(response.result.totalPages ?? 1); + } catch (err) { + // 오류 처리 + setError('공지사항을 불러오는 데 실패했습니다.'); + console.log(err); + } finally { + setLoading(false); + } + }; + + getNotices(); // 함수 실행 + }, [activeCategory, currentPage]); // 의존성 배열 - 카테고리/페이지 변경 시마다 재호출 + + // 검색어 필터링 적용된 공지사항 + const filteredNotices = noticeList.filter((notice) => notice.title.toLowerCase().includes(searchValue.toLowerCase())); return (
@@ -102,15 +89,16 @@ export default function Notice() { ))}
+ {/* 공지 없을 때 메시지 */} + {!loading && !error && filteredNotices.length === 0 &&

공지사항이 없습니다.

} + {/* 공지 리스트 */}
    - {paginatedNotices.map((notice, index) => ( -
  • - -
    - {notice.title} -
    - {notice.date} + {filteredNotices.map((notice) => ( +
  • + + {notice.title} + {new Date(notice.createdAt).toLocaleDateString()}
  • ))} @@ -122,7 +110,7 @@ export default function Notice() { current={currentPage} end={totalPages} onClick={(page) => { - setCurrentPage(page); + setCurrentPage(page); //페이지 변경 }} /> )} diff --git a/src/pages/notice/NoticeDetail.tsx b/src/pages/notice/NoticeDetail.tsx index c1c9a96..a2020a0 100644 --- a/src/pages/notice/NoticeDetail.tsx +++ b/src/pages/notice/NoticeDetail.tsx @@ -1,9 +1,48 @@ +import { useEffect, useState } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; +import type { TNoticeDetail } from '@/types/notice/notice'; + +import { fetchNoticeDetail } from '@/api/notice/notice'; + export default function NoticeDetail() { const navigate = useNavigate(); const location = useLocation(); - const { title, date, content } = location.state || {}; + const noticeId = location.state?.noticeId; + + const [notice, setNotice] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + + useEffect(() => { + if (!noticeId) { + setError('공지사항 ID가 없습니다.'); + return; + } + + const loadNotice = async () => { + setLoading(true); + try { + const res = await fetchNoticeDetail(noticeId); + + if (!res.isSuccess || !res.result) { + throw new Error(res.message); + } + + setNotice(res.result); + } catch (err) { + console.error('📛 공지사항 상세 조회 오류:', err); + setError('공지사항을 불러오는 데 실패했습니다.'); + } finally { + setLoading(false); + } + }; + + loadNotice(); + }, [noticeId]); + + if (loading) return
    로딩 중...
    ; + if (error) return
    {error}
    ; return (
    @@ -12,12 +51,12 @@ export default function NoticeDetail() { {/* 공지 제목 + 날짜 */}
    -

    {title}

    -

    {date}

    +

    {notice?.title}

    +

    {new Date(notice?.createdAt || '').toLocaleDateString()}

    {/* 본문 */} -
    {content || '내용이 없습니다.'}
    +
    {notice?.content || '내용이 없습니다.'}
    {/* 목록으로 돌아가기 */}
    diff --git a/src/types/notice/notice.ts b/src/types/notice/notice.ts new file mode 100644 index 0000000..a9d41e9 --- /dev/null +++ b/src/types/notice/notice.ts @@ -0,0 +1,28 @@ +import type { TCommonResponse } from '../common/common'; + +// 공지사항 전체 조회 +export type TNoticeItem = { + noticeId: number; + title: string; + isPinned: boolean; + createdAt: string; +}; + +export type TFetchNoticesResponse = TCommonResponse<{ + noticeList: TNoticeItem[]; + totalPages: number; + currentPage: number; + currentSize: number; + hasNextPage: boolean; +}>; + +// 공지사항 상세 조회 +export type TNoticeDetail = { + noticeId: number; + title: string; + content: string; + isPinned: boolean; + createdAt: string; +}; + +export type TFetchNoticeDetailResponse = TCommonResponse;