diff --git a/app/(tabs)/myPage/notice.tsx b/app/(tabs)/myPage/notice.tsx
index f246bbc..2659615 100644
--- a/app/(tabs)/myPage/notice.tsx
+++ b/app/(tabs)/myPage/notice.tsx
@@ -1,22 +1,163 @@
-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}
+ {item.createdAt.replace(/\//g, ".")}
+
+
+
+ ),
+ []
+ );
+
+ 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 Title = styled.Text`
- font-size: ${({ theme }) => theme.typography.fontSize.xxxl};
- color: ${({ theme }) => theme.colors.text.textPrimary};
- font-family: ${({ theme }) => theme.typography.fontFamily.regular};
+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 NoticeInfo = styled.View`
+ flex: 1;
+`;
+
+const NoticeTitle = styled.Text`
+ 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`
+ 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..2804f3e
--- /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 (
+
+
+
+
+
+
+ );
+ }
+
+ if (!notice) {
+ return (
+
+
+
+ 공지사항을 찾을 수 없습니다
+
+
+ );
+ }
+
+ return (
+
+
+
+
+ {notice.createdAt.replace(/\//g, ".")}
+ {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
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
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;
diff --git a/src/types/notice.ts b/src/types/notice.ts
new file mode 100644
index 0000000..e1321be
--- /dev/null
+++ b/src/types/notice.ts
@@ -0,0 +1,23 @@
+export interface NoticePageItem {
+ noticeId: number;
+ title: string;
+ createdAt: string;
+ updatedAt: 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