Skip to content

Commit 6d016e8

Browse files
authored
feat: 모바일 환경 네브바 구현 (#66) (#75)
* feat: 모바일 환경 네브바 구현 * feat: 모바일 환경용 네브바 구현
1 parent 2316fa2 commit 6d016e8

File tree

8 files changed

+261
-86
lines changed

8 files changed

+261
-86
lines changed

src/api/notice/notice.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import type { TFetchNoticeDetailResponse, TFetchNoticesResponse } from '@/types/notice/notice';
2+
3+
import { axiosInstance } from '../axiosInstance';
4+
5+
// 공지사항 전체 조회 API
6+
export const fetchNotices = async ({
7+
category,
8+
page,
9+
size,
10+
}: {
11+
category: 'SERVICE' | 'SYSTEM';
12+
page: number;
13+
size: number;
14+
}): Promise<TFetchNoticesResponse> => {
15+
const { data } = await axiosInstance.get('/api/v1/notices', {
16+
params: { noticeCategory: category, page, size },
17+
});
18+
return data;
19+
};
20+
21+
// 공지사항 상세 조회 API
22+
export const fetchNoticeDetail = async (noticeId: number): Promise<TFetchNoticeDetailResponse> => {
23+
const { data } = await axiosInstance.get(`/api/v1/notices/${noticeId}`);
24+
return data;
25+
};

src/assets/icons/Burger_fill.svg

Lines changed: 5 additions & 0 deletions
Loading

src/assets/icons/Clear.svg

Lines changed: 4 additions & 0 deletions
Loading

src/components/layout/Header.tsx

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,41 @@
11
import { useState } from 'react';
22
import { Link } from 'react-router-dom';
33

4+
import MobileMenu from './MobileMenu';
45
import SettingsModal from '../modal/SettingModal';
56

7+
import BurgerIcon from '@/assets/icons/Burger_fill.svg?react';
8+
import ClearIcon from '@/assets/icons/Clear.svg?react';
69
import NotificationsIcon from '@/assets/icons/notifications_Blank.svg?react';
710
import SettingsIcon from '@/assets/icons/settings_Blank.svg?react';
811
import NavbarLogo from '@/assets/withTimeLogo/navbarLogo.svg?react';
912

10-
// Header 컴포넌트 props
1113
interface IHeaderProps {
1214
mode?: 'full' | 'minimal'; // full: nav + border | minimal: 로고만
1315
}
1416

1517
export default function Header({ mode = 'full' }: IHeaderProps) {
16-
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
18+
const [isSettingsOpen, setIsSettingsOpen] = useState(false); //설정 모달
19+
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); // 모바일 메뉴
1720

1821
const showNav = mode === 'full';
1922
const showBorder = mode === 'full';
2023

2124
return (
2225
<header className={`w-full ${showBorder ? 'border-b border-gray-200' : ''}`}>
26+
{/* 최상단 네브바 */}
2327
<div className="max-w-7xl mx-auto flex items-center justify-between px-4 lg:px-8 py-4">
2428
{/* 로고 */}
25-
<div className="flex items-center space-x-2">
26-
<Link to="/home">
27-
<NavbarLogo className="w-40 h-auto" />
28-
</Link>
29-
</div>
29+
<Link to="/home">
30+
<NavbarLogo className="w-40 h-auto" />
31+
</Link>
3032

31-
{/* Nav 그룹 */}
33+
{/* 데스크탑 메뉴 */}
3234
{showNav && (
33-
<div className="flex items-center gap-x-10 text-default-gray-00">
34-
{/* 메뉴 */}
35+
<div className="hidden lg:flex items-center gap-x-10 text-default-gray-00">
36+
{/* 네비게이션 링크들 */}
3537
<nav>
36-
<ul className="flex space-x-5 sm:space-x-10 text-sm font-medium">
38+
<ul className="flex space-x-5 sm:space-x-10">
3739
<li>
3840
<Link to="/home" className="font-body1">
3941
메인
@@ -52,7 +54,7 @@ export default function Header({ mode = 'full' }: IHeaderProps) {
5254
</ul>
5355
</nav>
5456

55-
{/* 아이콘 */}
57+
{/* 아이콘 버튼 */}
5658
<div className="hidden lg:flex items-center space-x-5">
5759
<Link to="/">
5860
<NotificationsIcon className="w-5 h-5" fill="none" stroke="#000000" />
@@ -61,12 +63,30 @@ export default function Header({ mode = 'full' }: IHeaderProps) {
6163
<SettingsIcon className="w-5 h-5" fill="none" stroke="#000000" />
6264
</button>
6365
</div>
66+
</div>
67+
)}
6468

65-
{/* 설정 모달 */}
66-
{isSettingsOpen && <SettingsModal onClose={() => setIsSettingsOpen(false)} />}
69+
{/* 모바일 메뉴 토글 버튼 */}
70+
{showNav && (
71+
<div className="lg:hidden">
72+
{!isMobileMenuOpen ? (
73+
<button onClick={() => setIsMobileMenuOpen(true)}>
74+
<BurgerIcon className="w-6 h-6 text-default-gray-800" />
75+
</button>
76+
) : (
77+
<button onClick={() => setIsMobileMenuOpen(false)}>
78+
<ClearIcon className="w-6 h-6 text-default-gray-800" />
79+
</button>
80+
)}
6781
</div>
6882
)}
6983
</div>
84+
85+
{/* 모바일 메뉴 */}
86+
{isMobileMenuOpen && <MobileMenu onClose={() => setIsMobileMenuOpen(false)} onOpenSettings={() => setIsSettingsOpen(true)} />}
87+
88+
{/* 설정 모달 */}
89+
{isSettingsOpen && <SettingsModal onClose={() => setIsSettingsOpen(false)} />}
7090
</header>
7191
);
7292
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { Link } from 'react-router-dom';
2+
3+
import ClearIcon from '@/assets/icons/Clear.svg?react';
4+
import NotificationsIcon from '@/assets/icons/notifications_Blank.svg?react';
5+
import SettingsIcon from '@/assets/icons/settings_Blank.svg?react';
6+
7+
interface IMobileMenuProps {
8+
onClose: () => void;
9+
onOpenSettings: () => void;
10+
}
11+
12+
export default function MobileMenu({ onClose, onOpenSettings }: IMobileMenuProps) {
13+
return (
14+
<>
15+
{/* 검정 반투명 배경 오버레이 */}
16+
<div className="fixed inset-0 bg-black/50 z-40" onClick={onClose} />
17+
18+
{/* 사이드 메뉴 */}
19+
<div className="fixed top-0 right-0 h-full w-[80%] max-w-xs bg-white shadow-lg z-50 transform transition-transform duration-300 ease-in-out translate-x-0">
20+
{/* 닫기 버튼 */}
21+
<div className="flex justify-end p-4">
22+
<button onClick={onClose}>
23+
<ClearIcon className="w-6 h-6 text-default-gray-800" />
24+
</button>
25+
</div>
26+
27+
{/* 메뉴 목록 */}
28+
<nav className="px-6">
29+
<ul className="flex flex-col mt-5 gap-10 font-body1">
30+
<li>
31+
<Link to="/home" onClick={onClose}>
32+
메인
33+
</Link>
34+
</li>
35+
<li>
36+
<Link to="/dateTest" onClick={onClose}>
37+
데이트 취향 테스트
38+
</Link>
39+
</li>
40+
<li>
41+
<Link to="/dateCourse" onClick={onClose}>
42+
데이트 코스
43+
</Link>
44+
</li>
45+
</ul>
46+
47+
{/* 알림, 설정 */}
48+
<div className="flex gap-5 mt-10">
49+
<Link to="/" onClick={onClose}>
50+
<NotificationsIcon className="w-5 h-5" fill="none" stroke="#000000" />
51+
</Link>
52+
<button
53+
type="button"
54+
onClick={() => {
55+
onOpenSettings();
56+
onClose();
57+
}}
58+
>
59+
<SettingsIcon className="w-5 h-5" fill="none" stroke="#000000" />
60+
</button>
61+
</div>
62+
</nav>
63+
</div>
64+
</>
65+
);
66+
}

src/pages/notice/Notice.tsx

Lines changed: 56 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,74 +1,61 @@
1-
import { useState } from 'react';
1+
import { useEffect, useState } from 'react';
22
import { Link } from 'react-router-dom';
33

4+
import type { TNoticeItem } from '@/types/notice/notice';
5+
46
import EditableInputBox from '@/components/common/EditableInputBox';
57
import Navigator from '@/components/common/navigator';
68

7-
const categories = ['서비스 안내', '시스템 안내'];
9+
import { fetchNotices } from '@/api/notice/notice';
810

9-
const dummyNotices = [
10-
{ category: '서비스 안내', title: '서비스 점검 안내 (06월 20일 02:00~04:00)', date: '2025.06.09' },
11-
{ category: '서비스 안내', title: "신규 기능 '코스 저장하기' 오픈 안내", date: '2025.06.09' },
12-
{
13-
category: '서비스 안내',
14-
title: '데이트 추천 정확도 향상을 위한 업데이트 공지',
15-
date: '2025.06.09',
16-
content: `안녕하세요, WithTime 팀입니다.
17-
18-
항상 WithTime을 이용해주시는 모든 사용자 여러분께 진심으로 감사드립니다.
19-
보다 더 정확하고 만족스러운 데이트 코스를 추천해드리기 위해,아래와 같은 기능 개선 및 시스템 업데이트를 진행하였음을 알려드립니다.
20-
21-
🔧 주요 업데이트 내용
22-
1. 사용자 취향 기반 알고리즘 개선
23-
기존에는 간단한 지역 및 활동 선호도 중심으로 코스를 구성했다면,
24-
이번 업데이트부터는 시간대, 최근 행동 패턴, 선택 취소된 장소 이력 등
25-
더 정밀한 데이터를 분석하여 추천의 정확도를 높였습니다.
26-
27-
2. 상황별 추천 강화
28-
- 비 오는 날에는 실내 데이트 중심으로
29-
- 일정 시간이 짧을 경우, 이동 거리를 고려한 코스 구성
30-
이처럼 날씨, 이동 시간, 데이트 시간대를 함께 반영하도록 개선했습니다.
31-
32-
3. 실시간 트렌드 반영
33-
주요 지역별 인기 급상승 장소나 SNS 상에서 언급된 핫플레이스 정보를
34-
추천 코스에 반영하여, 최신 트렌드를 더 빠르게 만나보실 수 있습니다.
35-
36-
🎯 기대 효과
37-
- "오늘 뭐하지?" 고민할 시간 없이 상황 맞춤형 코스를 자동 추천
38-
- MBTI P 유형 사용자도 만족할 만큼 빠르고 간단한 코스 구성
39-
- 더 이상 ‘나랑 안 맞는 장소 추천’으로 불편하지 않도록 개선
40-
41-
📅 적용 일시
42-
- 2025년 6월 21일(금) 00:00부터 순차 적용 예정입니다.
43-
44-
이번 업데이트는 사용자 여러분의 피드백을 바탕으로 진행되었습니다.
45-
앞으로도 더 나은 서비스 제공을 위해 지속적으로 개선해나가겠습니다.
46-
사용 중 불편한 점이나 건의 사항이 있다면, 언제든지 고객센터 또는 [문의하기]를 통해 알려주세요.
47-
감사합니다.
48-
49-
WithTime 팀 드림`,
50-
},
51-
{ category: '서비스 안내', title: '비회원 기능 이용 제한 관련 안내', date: '2025.06.09' },
52-
{ category: '서비스 안내', title: '지도 기반 추천 기능 일시 중단 안내', date: '2025.06.09' },
53-
{ category: '서비스 안내', title: '지도 기반 추천 기능 일시 중단 안내', date: '2025.06.09' },
54-
{ category: '서비스 안내', title: '지도 기반 추천 기능 일시 중단 안내', date: '2025.06.09' },
55-
{ category: '시스템 안내', title: '일부 브라우저에서 발생하는 오류 관련 안내', date: '2025.06.09' },
56-
{ category: '시스템 안내', title: '추천 코스 반영 기준 변경 안내', date: '2025.06.09' },
57-
{ category: '시스템 안내', title: '회원가입 약관 일부 변경 안내', date: '2025.06.09' },
58-
];
11+
const categories = ['서비스 안내', '시스템 안내'];
5912

6013
export default function Notice() {
61-
const [searchValue, setSearchValue] = useState('');
62-
const [activeCategory, setActiveCategory] = useState(categories[0]);
14+
const [searchValue, setSearchValue] = useState(''); //검색어 상태
15+
const [activeCategory, setActiveCategory] = useState(categories[0]); //선택된 카테고리
6316
const [currentPage, setCurrentPage] = useState(1);
17+
18+
const [noticeList, setNoticeList] = useState<TNoticeItem[]>([]); // 공지사항 리스트
19+
const [totalPages, setTotalPages] = useState(1);
20+
const [loading, setLoading] = useState(false);
21+
const [error, setError] = useState('');
22+
6423
const itemsPerPage = 10;
6524

66-
// 필터링 + 페이징
67-
const filteredNotices = dummyNotices.filter(
68-
(notice) => notice.category === activeCategory && notice.title.toLowerCase().includes(searchValue.toLowerCase()),
69-
);
70-
const totalPages = Math.ceil(filteredNotices.length / itemsPerPage);
71-
const paginatedNotices = filteredNotices.slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage);
25+
// 백엔드에 넘길 카테고리 키 - 영어 변환
26+
const categoryKey = activeCategory === '서비스 안내' ? 'SERVICE' : 'SYSTEM';
27+
28+
// 컴포넌트 마운트 시, 카테고리/페이지 변경 시 -> API 호출
29+
useEffect(() => {
30+
const getNotices = async () => {
31+
setLoading(true);
32+
try {
33+
// 공지사항 목록 요청
34+
const response = await fetchNotices({
35+
category: categoryKey,
36+
page: currentPage - 1,
37+
size: itemsPerPage,
38+
});
39+
40+
console.log('API 응답:', response);
41+
42+
// 공지 목록과 페이지 수 설정 (빈 배열도 허용)
43+
setNoticeList(response.result.noticeList ?? []);
44+
setTotalPages(response.result.totalPages ?? 1);
45+
} catch (err) {
46+
// 오류 처리
47+
setError('공지사항을 불러오는 데 실패했습니다.');
48+
console.log(err);
49+
} finally {
50+
setLoading(false);
51+
}
52+
};
53+
54+
getNotices(); // 함수 실행
55+
}, [activeCategory, currentPage]); // 의존성 배열 - 카테고리/페이지 변경 시마다 재호출
56+
57+
// 검색어 필터링 적용된 공지사항
58+
const filteredNotices = noticeList.filter((notice) => notice.title.toLowerCase().includes(searchValue.toLowerCase()));
7259

7360
return (
7461
<div className="max-w-[1000px] mx-auto px-4 py-10">
@@ -102,15 +89,16 @@ export default function Notice() {
10289
))}
10390
</div>
10491

92+
{/* 공지 없을 때 메시지 */}
93+
{!loading && !error && filteredNotices.length === 0 && <p className="text-center text-default-gray-500">공지사항이 없습니다.</p>}
94+
10595
{/* 공지 리스트 */}
10696
<ul className="divide-y divide-default-gray-400 mb-10">
107-
{paginatedNotices.map((notice, index) => (
108-
<li key={index} className="py-4">
109-
<Link to={`/notice/${index}`} state={notice} className="flex items-center justify-between">
110-
<div className="flex items-center gap-4">
111-
<span className="font-body2 text-default-gray-800">{notice.title}</span>
112-
</div>
113-
<span className="text-sm text-default-gray-500">{notice.date}</span>
97+
{filteredNotices.map((notice) => (
98+
<li key={notice.noticeId} className="py-4">
99+
<Link to={`/notice/${notice.noticeId}`} className="flex items-center justify-between">
100+
<span className="font-body2 text-default-gray-800">{notice.title}</span>
101+
<span className="text-sm text-default-gray-500">{new Date(notice.createdAt).toLocaleDateString()}</span>
114102
</Link>
115103
</li>
116104
))}
@@ -122,7 +110,7 @@ export default function Notice() {
122110
current={currentPage}
123111
end={totalPages}
124112
onClick={(page) => {
125-
setCurrentPage(page);
113+
setCurrentPage(page); //페이지 변경
126114
}}
127115
/>
128116
)}

0 commit comments

Comments
 (0)