diff --git a/docs/conventions/terminology.md b/docs/conventions/terminology.md
index 7b37606..0620a26 100644
--- a/docs/conventions/terminology.md
+++ b/docs/conventions/terminology.md
@@ -21,6 +21,7 @@
| 슬랑제 | `seulrangjes` |
| 아카이빙 | `archives` |
| 검색 | `search` |
+| 알림 | `notifications` |
| 챗봇 | `chat` |
| 파일 | `files` |
| 행사(event) | `events` |
diff --git a/docs/conventions/wds-component-usage.md b/docs/conventions/wds-component-usage.md
index 760bc69..4beaefb 100644
--- a/docs/conventions/wds-component-usage.md
+++ b/docs/conventions/wds-component-usage.md
@@ -621,3 +621,31 @@ Figma는 헤더 배경이 없어서 배너 그림자(`Shadow/Spread/Small`, 60px
### 빈 상태는 WDS `FallbackView`가 아니라 홈 로컬 `HomeEmptyState`
일러스트 44px + 문구 한 줄 + 작은 버튼이라 행사 목록 Empty State 때와 같은 이유(`FallbackView`의 일러스트 폭·패딩 고정)로 맞지 않는다. 그림은 빌릴게 반납 빈 상태(`icons/bililge-empty/return-items.svg`)·행사 목록 빈 상태(`icons/events/empty-events.svg`)와 같은 도형이라 크기만 줄여 그대로 쓴다.
+
+## 알림(`3595:107100` 목록 / `3595:107112` empty / `3628:110837` 설정) 구현 중 확정된 매핑
+
+코드는 `src/features/notifications/` 참고.
+
+| Figma | 코드 | 확인 내용 |
+|---|---|---|
+| `Switch/Switch` (`679:16080`, Small) | `Switch size="small"` | 39×24, 켜짐 배경 `Primary/Normal`(#0066FF), 꺼짐 `Fill/Strong` — 실측으로 Figma와 일치한다. 값은 `checked`/`onCheckedChange`로 제어한다 |
+| `Icon/Normal/Setting` (`492:5735`) | `IconSetting` | 알림 목록 헤더 트레일링 설정 버튼. `ScreenHeader variant="normal"`의 `trailing` + `TopNavigationButton variant="icon"` |
+| `Icon/Normal/Chevron Left` | `IconChevronLeft` | 뒤로가기 — 다른 `variant="normal"` 화면과 같다 |
+| `Divider(new)` 8px (`487:19439`) | `
` | 검색 전체 탭의 섹션 구분 띠와 같은 로컬 처리 |
+| `Icon/Normal/Megaphone`·`Icon/Normal/Ticket` | 쓰지 않음 | 알림 항목의 카테고리 아이콘은 WDS 아이콘이 아니라 24px 배지 전체(`알림 - 공지` 등 6종)가 Stream 로컬 에셋이다. 배경(`#F7F7F8`, radius 6)과 아이콘 색이 종류마다 고정이라 `src/assets/icons/notifications/*.svg`로 받았다 |
+
+### `Notification Item`은 Stream 로컬이다
+
+항목 하나(`3595:107106`)는 카테고리 배지·라벨·상대 시간 줄 + 메시지 줄로 이뤄진 Stream 고유 목록 행이다. WDS `List`/`Card` 계열과 구조가 달라 `NotificationsItem`으로 새로 만들었고 텍스트는 `Typography`로 채웠다(라벨 `caption1`/`regular`, 시간 `caption2`/`regular`, 메시지 `label1-reading`/`medium`).
+
+- 안 읽은 항목만 `Atomic/Blue/99` 배경이라 `index.css`에 `--color-notification-unread` 토큰을 추가했다. **아이콘 색은 읽음 여부와 상관없다** — 처음엔 첫 항목만 진한 아이콘으로 보여 읽음 표현이라고 오해하기 쉬운데, `빌릴게 반납`·`공지`·`행사`는 원래 진한 파랑, `빌릴게 대여`·`열린피드백`은 원래 연한 파랑이다.
+- 항목을 눌렀을 때 이동은 디자인에 없어서 읽음 처리까지만 넣었다.
+- 에셋은 `download_assets`의 whole-node svg 내보내기에 컴포넌트 세트 배경(회색 사각형, 가로 5천 px짜리 프레임 도형)이 같이 딸려 와서, 그중 `알림 - …` 24px 배지 그룹만 꺼내 `viewBox="0 0 24 24"` svg로 다시 감쌌다. 공지·행사·열린피드백은 `download_assets`의 `svgAssets`가 회전한 Ratio 레이어 조각으로 쪼개져 나와서 쓸 수 없었다.
+
+### Empty State는 폭을 고정하지 않는다
+
+행사·검색 empty와 같은 뼈대지만 Figma 컨테이너 폭 203px은 가장 긴 문구("새로운 소식이 있으면 알려 드릴게요.")에 맞춘 값이다. `w-[203px]`로 고정하면 폰트 폭이 0.2px만 넓어도(Pretendard를 못 불러온 환경에서 실제로 203.2px) 마지막 글자 "요."가 다음 줄로 넘어간다 — 문구 폭에 맡긴다. 수직 위치는 프레임 전체 높이 기준 가운데(헤더 56px만큼 `pb-14`로 올림)이고, 812px 뷰포트에서 중심이 406px로 Figma와 같다.
+
+### 설정 화면의 "전체 알림"은 상태를 따로 두지 않는다
+
+Figma 초기 상태가 전체 꺼짐 + 빌릴게만 켜짐이라 "전체"는 독립된 값이 아니라 카테고리가 모두 켜졌는지로 파생한다. 누르면 네 카테고리를 같은 값으로 맞추고, 하나라도 끄면 전체가 꺼진다. 값은 화면 state뿐이라 새로고침하면 초기값으로 돌아간다(서버 API 연동 때 교체).
diff --git a/src/app/router.tsx b/src/app/router.tsx
index 7105d92..1604574 100644
--- a/src/app/router.tsx
+++ b/src/app/router.tsx
@@ -27,6 +27,8 @@ import LockersLockerSelectScreen from "@/features/lockers/LockersLockerSelectScr
import LockersSectionSelectScreen from "@/features/lockers/LockersSectionSelectScreen";
import NoticesDetailScreen from "@/features/notices/NoticesDetailScreen";
import NoticesListScreen from "@/features/notices/NoticesListScreen";
+import NotificationsScreen from "@/features/notifications/NotificationsScreen";
+import NotificationsSettingsScreen from "@/features/notifications/NotificationsSettingsScreen";
import SearchScreen from "@/features/search/SearchScreen";
// 앱의 모든 라우트는 이 객체 배열 한곳에서 정의한다 — 새 화면은 여기에 라우트를 추가한다.
@@ -135,6 +137,24 @@ const routes = [
} satisfies ScreenRouteHandle,
path: "/search",
},
+ {
+ element:
,
+ // Figma 알림 화면에는 Bottom Nav가 없고(뒤로가기로 진입한 화면에 복귀) 배경이 흰 면이다
+ handle: {
+ background: "normal",
+ hasBottomNav: false,
+ } satisfies ScreenRouteHandle,
+ path: "/notifications",
+ },
+ {
+ element:
,
+ // 알림 화면의 설정 아이콘으로 들어가는 화면 — 알림 화면과 같은 이유로 Bottom Nav 없이 흰 면이다
+ handle: {
+ background: "normal",
+ hasBottomNav: false,
+ } satisfies ScreenRouteHandle,
+ path: "/notifications/settings",
+ },
{
element:
,
// Figma 챗봇 진입 화면에는 Bottom Nav가 없다(뒤로가기로 홈에 복귀). 흰 배경 위에
diff --git a/src/assets/icons/notifications/bililge-rental.svg b/src/assets/icons/notifications/bililge-rental.svg
new file mode 100644
index 0000000..8e6d6d7
--- /dev/null
+++ b/src/assets/icons/notifications/bililge-rental.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/src/assets/icons/notifications/bililge-return.svg b/src/assets/icons/notifications/bililge-return.svg
new file mode 100644
index 0000000..d0d036f
--- /dev/null
+++ b/src/assets/icons/notifications/bililge-return.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/src/assets/icons/notifications/empty-notifications.svg b/src/assets/icons/notifications/empty-notifications.svg
new file mode 100644
index 0000000..4a10600
--- /dev/null
+++ b/src/assets/icons/notifications/empty-notifications.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/assets/icons/notifications/event.svg b/src/assets/icons/notifications/event.svg
new file mode 100644
index 0000000..a645896
--- /dev/null
+++ b/src/assets/icons/notifications/event.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/assets/icons/notifications/feedback.svg b/src/assets/icons/notifications/feedback.svg
new file mode 100644
index 0000000..b056a58
--- /dev/null
+++ b/src/assets/icons/notifications/feedback.svg
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/src/assets/icons/notifications/locker.svg b/src/assets/icons/notifications/locker.svg
new file mode 100644
index 0000000..cca8daf
--- /dev/null
+++ b/src/assets/icons/notifications/locker.svg
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/assets/icons/notifications/notice.svg b/src/assets/icons/notifications/notice.svg
new file mode 100644
index 0000000..e15f9c8
--- /dev/null
+++ b/src/assets/icons/notifications/notice.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/entities/notifications/notificationsApi.ts b/src/entities/notifications/notificationsApi.ts
new file mode 100644
index 0000000..94af6b4
--- /dev/null
+++ b/src/entities/notifications/notificationsApi.ts
@@ -0,0 +1,8 @@
+import { NOTIFICATIONS } from "@/entities/notifications/notificationsMock";
+import type { NotificationItem } from "@/entities/notifications/types";
+import { mockupApi } from "@/lib/mockupApi";
+
+// 실 API가 붙으면 함수 안쪽만 요청 코드로 바꾼다 — 화면은 이 함수로만 데이터를 받는다.
+export function fetchNotifications(): Promise
{
+ return mockupApi("notifications", () => NOTIFICATIONS);
+}
diff --git a/src/entities/notifications/notificationsMock.ts b/src/entities/notifications/notificationsMock.ts
new file mode 100644
index 0000000..81fc5ed
--- /dev/null
+++ b/src/entities/notifications/notificationsMock.ts
@@ -0,0 +1,50 @@
+import type { NotificationItem } from "@/entities/notifications/types";
+
+// Figma: 알림 (nodeId 3595:107100) 목업 데이터 — 첫 항목만 안 읽은 알림이다.
+export const NOTIFICATIONS: NotificationItem[] = [
+ {
+ id: "1",
+ isRead: false,
+ message: "우산 반납이 완료됐어요.",
+ timeLabel: "1시간 전",
+ type: "bililgeReturn",
+ },
+ {
+ id: "2",
+ isRead: true,
+ message:
+ "관리자가 대여 신청을 승인했어요. 학생회실에 방문해 우산을 대여해 주세요.",
+ timeLabel: "1시간 전",
+ type: "bililgeRental",
+ },
+ {
+ id: "3",
+ isRead: true,
+ message: "새로운 공지사항이 올라왔어요. 자세한 내용을 확인해 주세요.",
+ timeLabel: "1시간 전",
+ type: "notices",
+ },
+ {
+ id: "4",
+ isRead: true,
+ message:
+ "열린피드백에 새로운 답변이 올라왔어요. 어떤 답변인지 확인해 보세요.",
+ timeLabel: "1시간 전",
+ type: "feedbacks",
+ },
+ {
+ id: "5",
+ isRead: true,
+ message:
+ "‘2026-02 기말고사 간식행사’가 내일 열려요. 행사 일정과 장소를 확인해 주세요.",
+ timeLabel: "1시간 전",
+ type: "events",
+ },
+ {
+ id: "6",
+ isRead: true,
+ message: "사물함을 신청했어요. 배정된 사물함 번호를 확인해 주세요.",
+ timeLabel: "1시간 전",
+ type: "lockers",
+ },
+];
diff --git a/src/entities/notifications/types.ts b/src/entities/notifications/types.ts
new file mode 100644
index 0000000..da8ec33
--- /dev/null
+++ b/src/entities/notifications/types.ts
@@ -0,0 +1,17 @@
+// 알림 종류 — 아이콘·라벨이 종류마다 다르다. 빌릴게는 대여/반납 아이콘이 따로라 둘로 나눴다.
+export type NotificationType =
+ | "bililgeRental"
+ | "bililgeReturn"
+ | "notices"
+ | "feedbacks"
+ | "events"
+ | "lockers";
+
+export interface NotificationItem {
+ id: string;
+ type: NotificationType;
+ message: string;
+ /** 이미 포맷된 표시용 문자열 (예: "1시간 전") — 실 API가 시각을 주면 화면 쪽에서 상대 시간으로 바꾼다 */
+ timeLabel: string;
+ isRead: boolean;
+}
diff --git a/src/features/bililge/BililgeListScreen.tsx b/src/features/bililge/BililgeListScreen.tsx
index ef0f713..944a9c5 100644
--- a/src/features/bililge/BililgeListScreen.tsx
+++ b/src/features/bililge/BililgeListScreen.tsx
@@ -15,6 +15,7 @@ import BililgeItemList from "@/features/bililge/components/BililgeItemList";
import BililgeListSkeleton from "@/features/bililge/components/BililgeListSkeleton";
import BililgeRentalSheet from "@/features/bililge/components/BililgeRentalSheet";
import BililgeReturnSection from "@/features/bililge/components/BililgeReturnSection";
+import { NOTIFICATIONS_PATH } from "@/features/notifications/constants/notifications";
import { getSearchPath } from "@/features/search/constants/search";
// Figma: 빌릴게 (nodeId 1243:73331)
@@ -37,7 +38,13 @@ function BililgeListScreen() {
>
-
+
+ navigate(NOTIFICATIONS_PATH, { viewTransition: true })
+ }
+ variant="icon"
+ >
>
diff --git a/src/features/events/EventsListScreen.tsx b/src/features/events/EventsListScreen.tsx
index ca44c7d..3894b3e 100644
--- a/src/features/events/EventsListScreen.tsx
+++ b/src/features/events/EventsListScreen.tsx
@@ -14,6 +14,7 @@ import { useScreenHeader } from "@/components/ui/useScreenHeader";
import EventsList from "@/features/events/components/EventsList";
import EventsListSkeleton from "@/features/events/components/EventsListSkeleton";
import { EVENT_STATUS_FILTERS } from "@/features/events/constants/events";
+import { NOTIFICATIONS_PATH } from "@/features/notifications/constants/notifications";
import { getSearchPath } from "@/features/search/constants/search";
// Figma: 행사 (nodeId 1243:70854)
@@ -34,7 +35,13 @@ function EventsListScreen() {
>
-
+
+ navigate(NOTIFICATIONS_PATH, { viewTransition: true })
+ }
+ variant="icon"
+ >
>
diff --git a/src/features/feedbacks/FeedbacksListScreen.tsx b/src/features/feedbacks/FeedbacksListScreen.tsx
index 696f234..5fd74d9 100644
--- a/src/features/feedbacks/FeedbacksListScreen.tsx
+++ b/src/features/feedbacks/FeedbacksListScreen.tsx
@@ -24,6 +24,7 @@ import {
FEEDBACK_ROUND_FILTERS,
FEEDBACKS,
} from "@/features/feedbacks/constants/feedbacks";
+import { NOTIFICATIONS_PATH } from "@/features/notifications/constants/notifications";
import { getSearchPath } from "@/features/search/constants/search";
// 카드 한 장이 차지하는 가로 길이(카드끼리 gap이 없어서 카드 폭 그 자체다). 카드가 캐러셀
@@ -372,7 +373,13 @@ function FeedbacksListScreen() {
>
-
+
+ navigate(NOTIFICATIONS_PATH, { viewTransition: true })
+ }
+ variant="icon"
+ >
>
diff --git a/src/features/home/HomeScreen.tsx b/src/features/home/HomeScreen.tsx
index c1c6a47..8b81aa9 100644
--- a/src/features/home/HomeScreen.tsx
+++ b/src/features/home/HomeScreen.tsx
@@ -15,6 +15,7 @@ import {
HOME_HAS_UNREAD_NOTIFICATION,
HOME_MOCK_DATA,
} from "@/features/home/constants/homeMock";
+import { NOTIFICATIONS_PATH } from "@/features/notifications/constants/notifications";
import { getSearchPath } from "@/features/search/constants/search";
// Figma: 홈 2건 이하 (nodeId 3147:146240), 3건 이상 (3147:146297), empty (3147:146366)
@@ -58,7 +59,13 @@ function HomeScreen() {
>
-
+
+ navigate(NOTIFICATIONS_PATH, { viewTransition: true })
+ }
+ variant="icon"
+ >
-
+
+ navigate(NOTIFICATIONS_PATH, { viewTransition: true })
+ }
+ variant="icon"
+ >
>
diff --git a/src/features/notifications/NotificationsScreen.tsx b/src/features/notifications/NotificationsScreen.tsx
new file mode 100644
index 0000000..b2316f1
--- /dev/null
+++ b/src/features/notifications/NotificationsScreen.tsx
@@ -0,0 +1,63 @@
+import { TopNavigationButton } from "@wanteddev/wds";
+import { IconChevronLeft, IconSetting } from "@wanteddev/wds-icon";
+import { Suspense } from "react";
+import { useNavigate } from "react-router-dom";
+
+import ScreenHeader from "@/components/ui/ScreenHeader";
+import { useScreenHeader } from "@/components/ui/useScreenHeader";
+import NotificationsList from "@/features/notifications/components/NotificationsList";
+import NotificationsListSkeleton from "@/features/notifications/components/NotificationsListSkeleton";
+import { NOTIFICATIONS_SETTINGS_PATH } from "@/features/notifications/constants/notifications";
+
+// Figma: 알림 (nodeId 3595:107100), 알림 empty state (3595:107112)
+// 홈·행사·게시판·빌릴게 헤더의 알림 아이콘이 이 화면으로 보낸다. 헤더는 데이터와 상관없어 바로 그리고,
+// 목록 영역만 Suspense로 감싼다.
+function NotificationsScreen() {
+ const navigate = useNavigate();
+
+ // 이 화면으로 바로 들어오면(딥링크·앱 WebView 진입) 뒤로 갈 히스토리가 없어서 navigate(-1)이
+ // 아무 일도 하지 않는다 — 그럴 땐 홈으로 보낸다(행사 신청 화면의 goBack과 같은 규칙).
+ const goBack = () => {
+ const historyIndex = (window.history.state as { idx?: number } | null)?.idx;
+ if (historyIndex !== undefined && historyIndex > 0) {
+ navigate(-1);
+ return;
+ }
+ navigate("/", { replace: true });
+ };
+
+ useScreenHeader(
+
+
+
+ }
+ title="알림"
+ trailing={
+
+ navigate(NOTIFICATIONS_SETTINGS_PATH, { viewTransition: true })
+ }
+ variant="icon"
+ >
+
+
+ }
+ variant="normal"
+ />,
+ );
+
+ return (
+ }>
+
+
+ );
+}
+
+export default NotificationsScreen;
diff --git a/src/features/notifications/NotificationsSettingsScreen.tsx b/src/features/notifications/NotificationsSettingsScreen.tsx
new file mode 100644
index 0000000..c231613
--- /dev/null
+++ b/src/features/notifications/NotificationsSettingsScreen.tsx
@@ -0,0 +1,87 @@
+import { TopNavigationButton } from "@wanteddev/wds";
+import { IconChevronLeft } from "@wanteddev/wds-icon";
+import { useState } from "react";
+import { useNavigate } from "react-router-dom";
+
+import ScreenHeader from "@/components/ui/ScreenHeader";
+import { useScreenHeader } from "@/components/ui/useScreenHeader";
+import NotificationsSettingRow from "@/features/notifications/components/NotificationsSettingRow";
+import {
+ INITIAL_NOTIFICATION_SETTINGS,
+ NOTIFICATION_SETTINGS,
+ NOTIFICATIONS_PATH,
+ type NotificationSettingKey,
+} from "@/features/notifications/constants/notifications";
+
+// Figma: 알림 설정 (nodeId 3628:110837)
+// 스위치 상태는 서버 API가 없어서 이 화면 state로만 들고 있다(새로고침하면 초기값으로 돌아간다).
+// "전체 알림"은 따로 값을 두지 않고 카테고리가 모두 켜졌는지로 파생한다 — 누르면 전부 같은 값으로 맞춘다.
+function NotificationsSettingsScreen() {
+ const navigate = useNavigate();
+ const [settings, setSettings] = useState(INITIAL_NOTIFICATION_SETTINGS);
+
+ const isAllEnabled = NOTIFICATION_SETTINGS.every(({ key }) => settings[key]);
+
+ const setAll = (enabled: boolean) => {
+ setSettings({
+ bililge: enabled,
+ board: enabled,
+ events: enabled,
+ lockers: enabled,
+ });
+ };
+
+ const setOne = (key: NotificationSettingKey, enabled: boolean) => {
+ setSettings((prev) => ({ ...prev, [key]: enabled }));
+ };
+
+ // 알림 목록을 거치지 않고 바로 들어오면(딥링크) 뒤로 갈 히스토리가 없어 알림 목록으로 보낸다.
+ const goBack = () => {
+ const historyIndex = (window.history.state as { idx?: number } | null)?.idx;
+ if (historyIndex !== undefined && historyIndex > 0) {
+ navigate(-1);
+ return;
+ }
+ navigate(NOTIFICATIONS_PATH, { replace: true });
+ };
+
+ useScreenHeader(
+
+
+
+ }
+ title="알림 설정"
+ variant="normal"
+ />,
+ );
+
+ return (
+
+
+
+
+ {NOTIFICATION_SETTINGS.map(({ key, label, description }) => (
+ setOne(key, enabled)}
+ />
+ ))}
+
+
+ );
+}
+
+export default NotificationsSettingsScreen;
diff --git a/src/features/notifications/components/NotificationsEmptyState.tsx b/src/features/notifications/components/NotificationsEmptyState.tsx
new file mode 100644
index 0000000..68e2cee
--- /dev/null
+++ b/src/features/notifications/components/NotificationsEmptyState.tsx
@@ -0,0 +1,39 @@
+import { Typography } from "@wanteddev/wds";
+
+import emptyNotificationsIllustration from "@/assets/icons/notifications/empty-notifications.svg";
+
+// Figma: 알림 empty state > Empty State (nodeId 3595:107512) — Stream 로컬 컴포넌트.
+// 행사·검색 empty와 같은 뼈대(일러스트 80, 간격 12/4)에 일러스트·문구만 다르다.
+// Figma 컨테이너 폭 203px은 가장 긴 문구("새로운 소식이 있으면 알려 드릴게요.")에 맞춘 값이라 고정하지 않고
+// 문구 폭에 맡긴다 — 폰트 폭이 조금만 달라도 마지막 글자가 다음 줄로 넘어가기 때문이다.
+function NotificationsEmptyState() {
+ return (
+
+
+
+
+ 도착한 알림이 없어요
+
+
+ 새로운 소식이 있으면 알려 드릴게요.
+
+
+
+ );
+}
+
+export default NotificationsEmptyState;
diff --git a/src/features/notifications/components/NotificationsItem.tsx b/src/features/notifications/components/NotificationsItem.tsx
new file mode 100644
index 0000000..a2ee52a
--- /dev/null
+++ b/src/features/notifications/components/NotificationsItem.tsx
@@ -0,0 +1,70 @@
+import { Typography } from "@wanteddev/wds";
+
+import type { NotificationType } from "@/entities/notifications/types";
+import { NOTIFICATION_TYPE_META } from "@/features/notifications/constants/notifications";
+
+interface NotificationsItemProps {
+ type: NotificationType;
+ message: string;
+ timeLabel: string;
+ isRead: boolean;
+ onRead: () => void;
+}
+
+// Figma: Notification Item (nodeId 3595:107106 안 읽음 / 3595:107107 읽음) — Stream 로컬 컴포넌트.
+// 안 읽은 알림만 Atomic/Blue/99 배경이고, 아이콘은 종류마다 색이 다를 뿐 읽음 여부와 상관없다.
+// 설계 의도는 "탭하면 읽음 처리"까지다 — 관련 화면으로 이동하는 동작은 디자인에 없어서 넣지 않았다.
+function NotificationsItem({
+ type,
+ message,
+ timeLabel,
+ isRead,
+ onRead,
+}: NotificationsItemProps) {
+ const { icon, label } = NOTIFICATION_TYPE_META[type];
+
+ return (
+
+
+
+
+
+
+ {label}
+
+
+
+ {timeLabel}
+
+
+ {/* 아이콘(24) + 간격(8) 만큼 들여 라벨과 같은 줄에서 시작하고, 오른쪽은 38px(pr-9.5) 비운다 */}
+
+
+ {!isRead && 안 읽은 알림. }
+ {message}
+
+
+
+
+ );
+}
+
+export default NotificationsItem;
diff --git a/src/features/notifications/components/NotificationsList.tsx b/src/features/notifications/components/NotificationsList.tsx
new file mode 100644
index 0000000..5e58d11
--- /dev/null
+++ b/src/features/notifications/components/NotificationsList.tsx
@@ -0,0 +1,40 @@
+import { use, useState } from "react";
+
+import { fetchNotifications } from "@/entities/notifications/notificationsApi";
+import NotificationsEmptyState from "@/features/notifications/components/NotificationsEmptyState";
+import NotificationsItem from "@/features/notifications/components/NotificationsItem";
+
+// 알림 목록 데이터를 use()로 읽는 데이터 컴포넌트 — 응답을 기다리는 동안은 바깥 Suspense가 스켈레톤을 보여준다.
+// 읽음 처리는 서버 API가 없어서 이 화면 state로만 들고 있다(새로고침하면 목데이터 값으로 돌아간다).
+function NotificationsList() {
+ const notifications = use(fetchNotifications());
+ const [readIds, setReadIds] = useState>(new Set());
+
+ if (notifications.length === 0) {
+ // Figma는 Empty State를 프레임 전체 높이 기준 가운데에 둔다 — 헤더(56px)만큼 올려서 맞춘다.
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+ {notifications.map((notification) => (
+
+ setReadIds((prev) => new Set(prev).add(notification.id))
+ }
+ timeLabel={notification.timeLabel}
+ type={notification.type}
+ />
+ ))}
+
+ );
+}
+
+export default NotificationsList;
diff --git a/src/features/notifications/components/NotificationsListSkeleton.tsx b/src/features/notifications/components/NotificationsListSkeleton.tsx
new file mode 100644
index 0000000..ad7d0ae
--- /dev/null
+++ b/src/features/notifications/components/NotificationsListSkeleton.tsx
@@ -0,0 +1,57 @@
+import { Skeleton } from "@wanteddev/wds";
+
+import {
+ SKELETON_ROW_KEYS,
+ useSkeletonAnimation,
+} from "@/components/ui/ScreenSkeleton";
+
+// 알림 목록 데이터를 받는 동안 NotificationsScreen의 목록 자리에 보이는 스켈레톤 — 헤더는 화면이 바로
+// 그리므로 항목(아이콘·라벨·시간 줄 + 메시지 줄)의 배치만 따른다.
+function NotificationsListSkeleton() {
+ const animation = useSkeletonAnimation();
+
+ return (
+
+ {SKELETON_ROW_KEYS.map((key) => (
+
+ ))}
+
+ );
+}
+
+export default NotificationsListSkeleton;
diff --git a/src/features/notifications/components/NotificationsSettingRow.tsx b/src/features/notifications/components/NotificationsSettingRow.tsx
new file mode 100644
index 0000000..82d9720
--- /dev/null
+++ b/src/features/notifications/components/NotificationsSettingRow.tsx
@@ -0,0 +1,45 @@
+import { Switch, Typography } from "@wanteddev/wds";
+
+interface NotificationsSettingRowProps {
+ label: string;
+ /** 있으면 라벨 아래에 설명을 붙인다 — Figma "전체 알림" 행에는 설명이 없다 */
+ description?: string;
+ checked: boolean;
+ onCheckedChange: (checked: boolean) => void;
+}
+
+// Figma: Global Notification Row (nodeId 3628:110843) / Category Notification Row (3628:110849)
+// 스위치는 WDS `Switch size="small"`(Figma Switch/Switch, 39×24)이다.
+function NotificationsSettingRow({
+ label,
+ description,
+ checked,
+ onCheckedChange,
+}: NotificationsSettingRowProps) {
+ return (
+
+
+
+ {label}
+
+ {description !== undefined && (
+
+ {description}
+
+ )}
+
+
+
+ );
+}
+
+export default NotificationsSettingRow;
diff --git a/src/features/notifications/constants/notifications.ts b/src/features/notifications/constants/notifications.ts
new file mode 100644
index 0000000..6d4a683
--- /dev/null
+++ b/src/features/notifications/constants/notifications.ts
@@ -0,0 +1,61 @@
+import bililgeRentalIcon from "@/assets/icons/notifications/bililge-rental.svg";
+import bililgeReturnIcon from "@/assets/icons/notifications/bililge-return.svg";
+import eventIcon from "@/assets/icons/notifications/event.svg";
+import feedbackIcon from "@/assets/icons/notifications/feedback.svg";
+import lockerIcon from "@/assets/icons/notifications/locker.svg";
+import noticeIcon from "@/assets/icons/notifications/notice.svg";
+import type { NotificationType } from "@/entities/notifications/types";
+
+// Figma Notification Category — 24px 아이콘 + 라벨. 빌릴게는 대여·반납이 같은 라벨에 아이콘만 다르다.
+export const NOTIFICATION_TYPE_META = {
+ bililgeRental: { icon: bililgeRentalIcon, label: "빌릴게" },
+ bililgeReturn: { icon: bililgeReturnIcon, label: "빌릴게" },
+ events: { icon: eventIcon, label: "행사" },
+ feedbacks: { icon: feedbackIcon, label: "열린피드백" },
+ lockers: { icon: lockerIcon, label: "사물함" },
+ notices: { icon: noticeIcon, label: "공지" },
+} satisfies Record;
+
+// 알림 설정 카테고리 — Figma 알림 설정(nodeId 3628:110837)의 행 순서다. 게시판은 공지·열린피드백을 같이 켜고 끈다.
+export type NotificationSettingKey = "bililge" | "events" | "board" | "lockers";
+
+export const NOTIFICATION_SETTINGS: {
+ key: NotificationSettingKey;
+ label: string;
+ description: string;
+}[] = [
+ {
+ description: "대여 진행 상황과 반납 일정을 알려 드려요",
+ key: "bililge",
+ label: "빌릴게 알림",
+ },
+ {
+ description: "진행 중인 행사와 신청 현황을 알려 드려요",
+ key: "events",
+ label: "행사 알림",
+ },
+ {
+ description: "새 공지와 열린피드백 답변을 알려 드려요",
+ key: "board",
+ label: "게시판 알림",
+ },
+ {
+ description: "배정 결과와 이용 기간 만료 임박을 알려 드려요",
+ key: "lockers",
+ label: "사물함 알림",
+ },
+];
+
+// Figma 초기 상태 — 빌릴게 알림만 켜져 있다. 실 API가 붙으면 서버에 저장된 값으로 바꾼다.
+export const INITIAL_NOTIFICATION_SETTINGS: Record<
+ NotificationSettingKey,
+ boolean
+> = {
+ bililge: true,
+ board: false,
+ events: false,
+ lockers: false,
+};
+
+export const NOTIFICATIONS_PATH = "/notifications";
+export const NOTIFICATIONS_SETTINGS_PATH = "/notifications/settings";
diff --git a/src/index.css b/src/index.css
index 6814147..2af850f 100644
--- a/src/index.css
+++ b/src/index.css
@@ -43,6 +43,9 @@
--color-primary: var(--semantic-primary-normal);
--color-primary-subtle: var(--atomic-blue-95);
+ /* 알림 목록에서 아직 안 읽은 알림 항목의 배경(Figma "Atomic/Blue/99", 알림 nodeId 3595:107106) */
+ --color-notification-unread: var(--atomic-blue-99);
+
/* 사물함 구역 선택의 마감 구역 배경(Figma "Cool Neutral/97") — 회색 계열이지만
Background/Normal/Alternative(#f7f7f8)보다 한 단계 진해서 잠긴 걸 드러낸다 */
--color-cool-neutral-97: var(--atomic-coolNeutral-97);