From c9dd7577b92536c25366fece9673b8d5627194b9 Mon Sep 17 00:00:00 2001 From: Min Jun Cho Date: Wed, 8 Apr 2026 14:52:20 +0900 Subject: [PATCH 1/6] =?UTF-8?q?[#61]=20feat=20:=20notice=20=EC=97=94?= =?UTF-8?q?=EB=93=9C=ED=8F=AC=EC=9D=B8=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/constants/endpoints.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/constants/endpoints.ts b/src/constants/endpoints.ts index dce7864..cfd569e 100644 --- a/src/constants/endpoints.ts +++ b/src/constants/endpoints.ts @@ -58,6 +58,11 @@ export const API_ENDPOINTS = { EDIT_PROFILE: "/api/user/member/profile", CHANGE_PASSWORD: "/api/user/member/password", }, + + NOTICE: { + LIST: "/api/core/notice", + DETAIL: (noticeId: number) => `/api/core/notice/${noticeId}`, + }, }; export default API_ENDPOINTS; From 1b8f9cc26840c1c99466ea7a09c33ed40fad9480 Mon Sep 17 00:00:00 2001 From: Min Jun Cho Date: Wed, 8 Apr 2026 14:55:32 +0900 Subject: [PATCH 2/6] =?UTF-8?q?[#61]=20feat=20:=20notice=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/types/notice.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/types/notice.ts diff --git a/src/types/notice.ts b/src/types/notice.ts new file mode 100644 index 0000000..b3883cd --- /dev/null +++ b/src/types/notice.ts @@ -0,0 +1,22 @@ +export interface NoticePageItem { + noticeId: number; + title: string; +} + +export interface NoticeDetail { + noticeId: number; + noticeTitle: string; + noticeContent: string; + createdAt: string; + updatedAt: string; +} + +export interface NoticePageResponse { + content: NoticePageItem[]; + number: number; + size: number; + isFirst: boolean; + isLast: boolean; + hasNext: boolean; + hasPrevious: boolean; +} \ No newline at end of file From b675cb0dbfd52ab961be8fdb3a1eb8d196463063 Mon Sep 17 00:00:00 2001 From: Min Jun Cho Date: Wed, 8 Apr 2026 14:56:03 +0900 Subject: [PATCH 3/6] =?UTF-8?q?[#61]=20feat=20:=20notice=20API=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/notice/getNoticeApi.ts | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 src/api/notice/getNoticeApi.ts diff --git a/src/api/notice/getNoticeApi.ts b/src/api/notice/getNoticeApi.ts new file mode 100644 index 0000000..b9852d4 --- /dev/null +++ b/src/api/notice/getNoticeApi.ts @@ -0,0 +1,34 @@ +import { BaseResponse } from "@/types/auth"; +import { NoticeDetail, NoticePageResponse } from "@/types/notice"; + +import API_ENDPOINTS from "@/constants/endpoints"; + +import api from "../axios"; + +export const getNoticeList = async ( + page: number = 0, + size: number = 10 +): Promise => { + try { + const response = await api.get>( + API_ENDPOINTS.NOTICE.LIST, + { params: { page, size, sort: "id,desc" } } + ); + return response.data.data; + } catch (error) { + throw error; + } +}; + +export const getNoticeDetail = async ( + noticeId: number +): Promise => { + try { + const response = await api.get>( + API_ENDPOINTS.NOTICE.DETAIL(noticeId) + ); + return response.data.data; + } catch (error) { + throw error; + } +}; \ No newline at end of file From feb1684020b8a0a6bf8c9c68eb2a543b57c4a011 Mon Sep 17 00:00:00 2001 From: Min Jun Cho Date: Wed, 8 Apr 2026 14:56:46 +0900 Subject: [PATCH 4/6] =?UTF-8?q?[#61]=20feat=20:=20notice=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EC=A0=9C=EC=9E=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(tabs)/myPage/notice.tsx | 154 ++++++++++++++++++++++++++--- app/(tabs)/myPage/noticeDetail.tsx | 144 +++++++++++++++++++++++++++ 2 files changed, 285 insertions(+), 13 deletions(-) create mode 100644 app/(tabs)/myPage/noticeDetail.tsx diff --git a/app/(tabs)/myPage/notice.tsx b/app/(tabs)/myPage/notice.tsx index f246bbc..0001372 100644 --- a/app/(tabs)/myPage/notice.tsx +++ b/app/(tabs)/myPage/notice.tsx @@ -1,22 +1,150 @@ -import { styled } from "styled-components/native"; +import { useCallback, useEffect, useState } from "react"; + +import { ActivityIndicator, FlatList } from "react-native"; + +import { useRouter } from "expo-router"; +import { ChevronLeft, ChevronRight } from "lucide-react-native"; +import styled from "styled-components/native"; + +import { NoticePageItem } from "@/types/notice"; + +import { theme } from "@/styles/theme"; + +import { getNoticeList } from "@/api/notice/getNoticeApi"; export default function Notice() { - return ( - - 공지사항 - - ); + const router = useRouter(); + const [notices, setNotices] = useState([]); + const [page, setPage] = useState(0); + const [hasNext, setHasNext] = useState(false); + const [isLoading, setIsLoading] = useState(false); + + const fetchNotices = async (reset: boolean = false) => { + if (isLoading) return; + setIsLoading(true); + try { + const currentPage = reset ? 0 : page; + const data = await getNoticeList(currentPage); + setNotices(reset ? data.content : [...notices, ...data.content]); + setHasNext(data.hasNext); + setPage(currentPage + 1); + } catch (error) { + console.error("공지사항 목록 조회 실패:", error); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + fetchNotices(true); + }, []); + + const handleItemPress = (noticeId: number) => { + router.push({ + pathname: "/myPage/noticeDetail", + params: { noticeId }, + }); + }; + + const renderItem = useCallback( + ({ item }: { item: NoticePageItem }) => ( + handleItemPress(item.noticeId)}> + {item.title} + + + ), + [] + ); + + return ( + +
+ router.back()}> + + + 공지사항 + +
+ + item.noticeId.toString()} + renderItem={renderItem} + onEndReached={() => { + if (hasNext && !isLoading) fetchNotices(false); + }} + onEndReachedThreshold={0.5} + ListFooterComponent={ + isLoading && notices.length > 0 ? ( + + ) : null + } + ListEmptyComponent={ + !isLoading ? ( + + 공지사항이 없습니다 + + ) : null + } + contentContainerStyle={{ flexGrow: 1 }} + /> +
+ ); } const Container = styled.View` - font-family: ${({ theme }) => theme.typography.fontFamily.regular}; flex: 1; - justify-content: "center"; - align-items: "center"; + background-color: ${theme.colors.white}; +`; + +const Header = styled.View` + height: 50px; + flex-direction: row; + align-items: center; + justify-content: space-between; + padding-horizontal: 16px; + border-bottom-width: 1px; + border-bottom-color: ${theme.colors.grey.neutral200}; +`; + +const BackButton = styled.TouchableOpacity` + width: 32px; +`; + +const HeaderTitle = styled.Text` + font-family: ${theme.typography.fontFamily.semiBold}; + font-size: ${theme.typography.fontSize.lg}px; + color: ${theme.colors.text.textPrimary}; +`; + +const Spacer = styled.View` + width: 32px; +`; + +const NoticeItem = styled.TouchableOpacity` + flex-direction: row; + justify-content: space-between; + align-items: center; + padding: 16px 20px; + border-bottom-width: 1px; + border-bottom-color: ${theme.colors.grey.neutral200}; +`; + +const NoticeTitle = styled.Text` + flex: 1; + font-family: ${theme.typography.fontFamily.medium}; + font-size: ${theme.typography.fontSize.sm}px; + color: ${theme.colors.text.textPrimary}; `; -const Title = styled.Text` - font-size: ${({ theme }) => theme.typography.fontSize.xxxl}; - color: ${({ theme }) => theme.colors.text.textPrimary}; - font-family: ${({ theme }) => theme.typography.fontFamily.regular}; +const EmptyContainer = styled.View` + flex: 1; + justify-content: center; + align-items: center; `; + +const EmptyText = styled.Text` + font-family: ${theme.typography.fontFamily.regular}; + font-size: ${theme.typography.fontSize.sm}px; + color: ${theme.colors.text.textTertiary}; +`; \ No newline at end of file diff --git a/app/(tabs)/myPage/noticeDetail.tsx b/app/(tabs)/myPage/noticeDetail.tsx new file mode 100644 index 0000000..88bc124 --- /dev/null +++ b/app/(tabs)/myPage/noticeDetail.tsx @@ -0,0 +1,144 @@ +import { useEffect, useState } from "react"; + +import { ActivityIndicator, ScrollView } from "react-native"; + +import { useLocalSearchParams, useRouter } from "expo-router"; +import { ChevronLeft } from "lucide-react-native"; +import styled from "styled-components/native"; + +import { NoticeDetail as NoticeDetailType } from "@/types/notice"; + +import { theme } from "@/styles/theme"; + +import { getNoticeDetail } from "@/api/notice/getNoticeApi"; + +export default function NoticeDetail() { + const router = useRouter(); + const { noticeId } = useLocalSearchParams<{ noticeId: string }>(); + const [notice, setNotice] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + if (!noticeId) return; + const fetch = async () => { + try { + const data = await getNoticeDetail(Number(noticeId)); + setNotice(data); + } catch (error) { + console.error("공지사항 상세 조회 실패:", error); + } finally { + setIsLoading(false); + } + }; + fetch(); + }, [noticeId]); + + if (isLoading) { + return ( + +
+ router.back()}> + + +
+ + + +
+ ); + } + + if (!notice) { + return ( + +
+ router.back()}> + + +
+ + 공지사항을 찾을 수 없습니다 + +
+ ); + } + + return ( + +
+ router.back()}> + + +
+ + + {notice.createdAt} + {notice.noticeTitle} + + {notice.noticeContent} + +
+ ); +} + +const Container = styled.View` + flex: 1; + background-color: ${theme.colors.white}; +`; + +const Header = styled.View` + height: 50px; + flex-direction: row; + align-items: center; + padding-horizontal: 16px; +`; + +const BackButton = styled.TouchableOpacity` + width: 32px; +`; + +const LoadingContainer = styled.View` + flex: 1; + justify-content: center; + align-items: center; +`; + +const DateText = styled.Text` + font-family: ${theme.typography.fontFamily.regular}; + font-size: ${theme.typography.fontSize.xxs}px; + color: ${theme.colors.text.textTertiary}; + margin-bottom: 8px; +`; + +const TitleText = styled.Text` + font-family: ${theme.typography.fontFamily.bold}; + font-size: ${theme.typography.fontSize.lg}px; + color: ${theme.colors.text.textPrimary}; + margin-bottom: 16px; + line-height: ${theme.typography.fontSize.lg * 1.4}px; +`; + +const Divider = styled.View` + height: 1px; + background-color: ${theme.colors.grey.neutral200}; + margin-bottom: 16px; +`; + +const ContentText = styled.Text` + font-family: ${theme.typography.fontFamily.regular}; + font-size: ${theme.typography.fontSize.xs}px; + color: ${theme.colors.text.textPrimary}; + line-height: ${theme.typography.fontSize.xs * 1.6}px; +`; + +const EmptyContainer = styled.View` + flex: 1; + justify-content: center; + align-items: center; +`; + +const EmptyText = styled.Text` + font-family: ${theme.typography.fontFamily.regular}; + font-size: ${theme.typography.fontSize.sm}px; + color: ${theme.colors.text.textTertiary}; +`; \ No newline at end of file From 62411e0bbd995d8b66bd7eab89cf294eed36e768 Mon Sep 17 00:00:00 2001 From: Min Jun Cho Date: Wed, 8 Apr 2026 15:23:41 +0900 Subject: [PATCH 5/6] =?UTF-8?q?[#61]=20fix=20:=20Pageitem=20=EB=82=A0?= =?UTF-8?q?=EC=A7=9C=20=ED=8F=AC=EB=A7=B7=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(tabs)/myPage/notice.tsx | 23 ++++++++++++++++++----- src/types/notice.ts | 3 ++- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/app/(tabs)/myPage/notice.tsx b/app/(tabs)/myPage/notice.tsx index 0001372..2659615 100644 --- a/app/(tabs)/myPage/notice.tsx +++ b/app/(tabs)/myPage/notice.tsx @@ -49,7 +49,10 @@ export default function Notice() { const renderItem = useCallback( ({ item }: { item: NoticePageItem }) => ( handleItemPress(item.noticeId)}> - {item.title} + + {item.title} + {item.createdAt.replace(/\//g, ".")} + ), @@ -130,11 +133,21 @@ const NoticeItem = styled.TouchableOpacity` border-bottom-color: ${theme.colors.grey.neutral200}; `; +const NoticeInfo = styled.View` + flex: 1; +`; + const NoticeTitle = styled.Text` - flex: 1; - font-family: ${theme.typography.fontFamily.medium}; - font-size: ${theme.typography.fontSize.sm}px; - color: ${theme.colors.text.textPrimary}; + font-family: ${theme.typography.fontFamily.medium}; + font-size: ${theme.typography.fontSize.sm}px; + color: ${theme.colors.text.textPrimary}; +`; + +const NoticeDate = styled.Text` + font-family: ${theme.typography.fontFamily.regular}; + font-size: ${theme.typography.fontSize.xxs}px; + color: ${theme.colors.text.textTertiary}; + margin-top: 4px; `; const EmptyContainer = styled.View` diff --git a/src/types/notice.ts b/src/types/notice.ts index b3883cd..e1321be 100644 --- a/src/types/notice.ts +++ b/src/types/notice.ts @@ -1,8 +1,9 @@ export interface NoticePageItem { noticeId: number; title: string; + createdAt: string; + updatedAt: string; } - export interface NoticeDetail { noticeId: number; noticeTitle: string; From a6501e2fc8d5da46679897762435f90973c1f9d1 Mon Sep 17 00:00:00 2001 From: Min Jun Cho Date: Wed, 8 Apr 2026 16:46:13 +0900 Subject: [PATCH 6/6] =?UTF-8?q?[#61]=20fix=20:=20=EC=83=81=EC=84=B8=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=82=A0=EC=A7=9C=20=ED=8F=AC?= =?UTF-8?q?=EB=A7=B7=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(tabs)/myPage/noticeDetail.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/(tabs)/myPage/noticeDetail.tsx b/app/(tabs)/myPage/noticeDetail.tsx index 88bc124..2804f3e 100644 --- a/app/(tabs)/myPage/noticeDetail.tsx +++ b/app/(tabs)/myPage/noticeDetail.tsx @@ -72,7 +72,7 @@ export default function NoticeDetail() { - {notice.createdAt} + {notice.createdAt.replace(/\//g, ".")} {notice.noticeTitle} {notice.noticeContent}