From ae6cd2dee6af09faf5fd825d99d9859a5dcfc8ee Mon Sep 17 00:00:00 2001 From: jjunh33 Date: Thu, 8 Oct 2026 16:41:07 +0900 Subject: [PATCH 1/5] =?UTF-8?q?docs:=20=EC=95=8C=EB=A6=BC=20=EC=9A=A9?= =?UTF-8?q?=EC=96=B4=EC=99=80=20=EC=95=8C=EB=A6=BC=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?WDS=20=EB=A7=A4=ED=95=91=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/conventions/terminology.md | 1 + docs/conventions/wds-component-usage.md | 28 +++++++++++++++++++++++++ 2 files changed, 29 insertions(+) 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 연동 때 교체). From 1b887ffa6b176790e9dcb4bbcffd21a932762e0a Mon Sep 17 00:00:00 2001 From: jjunh33 Date: Thu, 8 Oct 2026 16:41:07 +0900 Subject: [PATCH 2/5] =?UTF-8?q?feat:=20=EC=95=8C=EB=A6=BC=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=EB=AA=A9=EB=8D=B0=EC=9D=B4=ED=84=B0=EC=99=80=20API?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notifications/notificationsApi.ts | 8 +++ .../notifications/notificationsMock.ts | 50 +++++++++++++++++++ src/entities/notifications/types.ts | 17 +++++++ 3 files changed, 75 insertions(+) create mode 100644 src/entities/notifications/notificationsApi.ts create mode 100644 src/entities/notifications/notificationsMock.ts create mode 100644 src/entities/notifications/types.ts 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; +} From 6749046f7c2b81162c21227f5264d755561a8fba Mon Sep 17 00:00:00 2001 From: jjunh33 Date: Thu, 8 Oct 2026 16:41:07 +0900 Subject: [PATCH 3/5] =?UTF-8?q?feat:=20=EC=95=8C=EB=A6=BC=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=C2=B7=EC=95=8C=EB=A6=BC=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/router.tsx | 20 +++++ .../icons/notifications/bililge-rental.svg | 6 ++ .../icons/notifications/bililge-return.svg | 6 ++ .../notifications/empty-notifications.svg | 9 ++ src/assets/icons/notifications/event.svg | 9 ++ src/assets/icons/notifications/feedback.svg | 8 ++ src/assets/icons/notifications/locker.svg | 11 +++ src/assets/icons/notifications/notice.svg | 10 +++ .../notifications/NotificationsScreen.tsx | 63 ++++++++++++++ .../NotificationsSettingsScreen.tsx | 87 +++++++++++++++++++ .../components/NotificationsEmptyState.tsx | 39 +++++++++ .../components/NotificationsItem.tsx | 70 +++++++++++++++ .../components/NotificationsList.tsx | 40 +++++++++ .../components/NotificationsListSkeleton.tsx | 57 ++++++++++++ .../components/NotificationsSettingRow.tsx | 45 ++++++++++ .../notifications/constants/notifications.ts | 61 +++++++++++++ src/index.css | 3 + 17 files changed, 544 insertions(+) create mode 100644 src/assets/icons/notifications/bililge-rental.svg create mode 100644 src/assets/icons/notifications/bililge-return.svg create mode 100644 src/assets/icons/notifications/empty-notifications.svg create mode 100644 src/assets/icons/notifications/event.svg create mode 100644 src/assets/icons/notifications/feedback.svg create mode 100644 src/assets/icons/notifications/locker.svg create mode 100644 src/assets/icons/notifications/notice.svg create mode 100644 src/features/notifications/NotificationsScreen.tsx create mode 100644 src/features/notifications/NotificationsSettingsScreen.tsx create mode 100644 src/features/notifications/components/NotificationsEmptyState.tsx create mode 100644 src/features/notifications/components/NotificationsItem.tsx create mode 100644 src/features/notifications/components/NotificationsList.tsx create mode 100644 src/features/notifications/components/NotificationsListSkeleton.tsx create mode 100644 src/features/notifications/components/NotificationsSettingRow.tsx create mode 100644 src/features/notifications/constants/notifications.ts 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/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..c942e43 --- /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 ( + + ); +} + +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..69466cb --- /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); From 8415efd6e7a8b8411c4ccb9636f76f574182ae4f Mon Sep 17 00:00:00 2001 From: jjunh33 Date: Thu, 8 Oct 2026 16:41:07 +0900 Subject: [PATCH 4/5] =?UTF-8?q?feat:=20=ED=97=A4=EB=8D=94=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20=EC=95=84=EC=9D=B4=EC=BD=98=EC=9D=84=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20=ED=99=94=EB=A9=B4=EC=9C=BC=EB=A1=9C=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/features/bililge/BililgeListScreen.tsx | 9 ++++++++- src/features/events/EventsListScreen.tsx | 9 ++++++++- src/features/feedbacks/FeedbacksListScreen.tsx | 9 ++++++++- src/features/home/HomeScreen.tsx | 9 ++++++++- src/features/notices/NoticesListScreen.tsx | 9 ++++++++- 5 files changed, 40 insertions(+), 5 deletions(-) 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" + > From 65e0d8ea090a00f6411ca25ad32b4da9586a494e Mon Sep 17 00:00:00 2001 From: jjunh33 Date: Thu, 8 Oct 2026 23:23:05 +0900 Subject: [PATCH 5/5] =?UTF-8?q?refactor:=20=EC=95=8C=EB=A6=BC=20=EA=B3=A0?= =?UTF-8?q?=EC=A0=95=20px=20=EC=97=AC=EB=B0=B1=EC=9D=84=20Tailwind=20?= =?UTF-8?q?=EC=8A=A4=EC=BC=80=EC=9D=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/notifications/components/NotificationsItem.tsx | 4 ++-- .../notifications/components/NotificationsListSkeleton.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/features/notifications/components/NotificationsItem.tsx b/src/features/notifications/components/NotificationsItem.tsx index c942e43..a2ee52a 100644 --- a/src/features/notifications/components/NotificationsItem.tsx +++ b/src/features/notifications/components/NotificationsItem.tsx @@ -51,8 +51,8 @@ function NotificationsItem({ {timeLabel}
- {/* 아이콘(24) + 간격(8) 만큼 들여 라벨과 같은 줄에서 시작하고, 오른쪽은 38px 비운다 */} -
+ {/* 아이콘(24) + 간격(8) 만큼 들여 라벨과 같은 줄에서 시작하고, 오른쪽은 38px(pr-9.5) 비운다 */} +
-
+