diff --git a/docs/conventions/coding-style.md b/docs/conventions/coding-style.md
index 451dea5..c48b62b 100644
--- a/docs/conventions/coding-style.md
+++ b/docs/conventions/coding-style.md
@@ -41,6 +41,7 @@
새 옵션이 필요하면 `src/app/ScreenLayoutRoute.tsx`의 `ScreenRouteHandle`에 필드를 추가하고, 그 값을 `ScreenLayout` prop으로 넘긴다. 현재 필드는 `hasBottomNav`(하단 탭 표시)와 `background`(375×812 프레임 배경 — 헤더 뒤까지 포함이라 화면 본문에서 칠할 수 없다. 신청 완료처럼 Figma가 흰 배경으로 그린 화면만 `"normal"`)다.
- `ScreenLayout`은 **라우터를 모르는 prop 기반 컴포넌트**로 유지한다. 라우트 정보(`useMatches`)는 `ScreenLayoutRoute`만 읽는다.
+- 헤더는 `useScreenHeader`로 `ScreenLayout`의 헤더 자리에 등록한다. 예외는 홈 하나다 — Figma 홈은 헤더 배경이 없어 배너 그림자가 헤더까지 번지는데, 헤더 자리에 두면 경계에서 그림자가 잘려 선이 생긴다. 그래서 홈만 스크롤 영역 안에 `sticky` 헤더를 직접 그린다(`wds-component-usage.md` 홈 섹션 참고).
- 화면 스택을 쌓는 이동(목록→상세, 상세→신청 등)은 `navigate(to, { viewTransition: true })`·``으로 슬라이드 전환을 켠다. 뒤로가기는 react-router가 그 이동을 기억해 반대 방향으로 자동 적용하므로 `navigate(-1)`은 그대로 둔다. 브라우저 앞으로가기도 POP이라, `ScreenLayoutRoute`는 히스토리 위치(`history.state.idx`)가 줄어든 POP만 뒤로 방향으로 본다. Bottom Nav·상단 탭처럼 형제 화면을 오가는 이동과 홈으로 돌아가는 이동은 켜지 않는다(즉시 전환). 애니메이션은 `index.css`, 방향은 `ScreenLayoutRoute`가 정한다.
- 라우트가 없는 경로는 레이아웃 안의 `path: "*"` 라우트(`ComingSoonScreen`)가 받는다. 하단 탭이 유지돼서 다른 화면으로 돌아갈 수 있다. 구체적인 경로가 `*`보다 항상 우선하므로 배열 순서는 신경 쓰지 않아도 된다.
diff --git a/docs/conventions/component-convention.md b/docs/conventions/component-convention.md
index 16c6996..0951f53 100644
--- a/docs/conventions/component-convention.md
+++ b/docs/conventions/component-convention.md
@@ -51,6 +51,15 @@ interface RentalItemCardProps {
- 색은 `text-[#171719]`처럼 hex를 직접 박지 않는다. `src/index.css`의 `@theme` 블록에 있는 시맨틱 토큰(`text-label-normal`, `bg-background-alternative`, `border-line-solid-neutral`, `text-primary`, `bg-primary-subtle` 등)을 쓴다. 이 토큰들은 `@wanteddev/wds/global.css`가 심어둔 `--semantic-*`/`--atomic-*` CSS 변수를 그대로 별칭 연결한 것이라 다크 테마 전환도 자동으로 따라간다.
- 필요한 색이 아직 토큰으로 없으면, hex를 추측해서 쓰지 말고 `get_variable_defs(fileKey, nodeId)`로 해당 노드의 실제 Figma 변수명·값을 확인한 뒤 `index.css`의 `@theme`에 새 토큰을 추가한다. `Line/Normal/Neutral`(반투명 `#70737c29`)과 `Line/Solid/Neutral`(불투명 `#eaebec`)처럼 이름이 비슷해도 값이 다른 토큰이 있으니 이름만 보고 넘겨짚지 않는다.
- 컴포넌트 인스턴스가 없는 화면 배경/외곽선처럼 Figma 값이 실제로는 안 보이는 경우(예: Bottom Nav 상단 border가 바로 위 배경과 같은 색이라 안 보였던 사례)도 있다 — 이럴 땐 왜 다른 토큰으로 바꿨는지 주석으로 남긴다.
+
+### 크기·위치·그림자
+
+- 크기·위치·간격도 `h-[261px]`처럼 px 임의값을 박지 않는다. 아래 순서로 정한다.
+ 1. **Tailwind 단계**: 정수 px면 단계로 쓴다. Tailwind v4는 0.25 단위(=1px) 소수 단계를 받는다 — `w-[264px]` → `w-66`, `top-[86px]` → `top-21.5`, `size-[42px]` → `size-10.5`.
+ 2. **이름 붙인 토큰**: 화면의 기준이 되는 크기(배너 높이 등)나 Figma 효과 스타일(그림자)은 `index.css`의 `@theme`에 이름을 붙여 쓴다 — `--spacing-notice-banner: 261px` → `h-notice-banner`, `--shadow-spread-small` → `shadow-spread-small`. 색 토큰과 같은 방식이다.
+ 3. **구조로 없애기**: 그림 위치·크기를 숫자로 맞춰야 하는 자리는 에셋을 Figma 배치대로 잘라 받아(카드 크기 그림, 회전이 반영된 SVG 등) `w-full`·고유 크기로 놓는다. 글자 폭처럼 남는 공간을 채우는 값은 `flex-1`·`min-w-0`으로 둔다.
+ 4. **예외**: 0.5px 선처럼 단계·토큰으로 나타낼 수 없는 값만 임의값으로 쓰고, 어느 Figma 값인지 주석을 남긴다.
+
### 타이포그래피
- 글자 크기·굵기를 `text-xs`/`text-[17px]`/`font-semibold`처럼 Tailwind로 직접 짓지 않는다. `@wanteddev/wds`의 `Typography` 컴포넌트를 쓴다 — Figma의 이름 있는 타입 스타일(Headline 2/Bold 등)과 `variant`+`weight` 조합이 1:1로 대응한다(`node_modules/@wanteddev/wds/dist/components/typography/style.js`에서 실측 확인 가능).
@@ -96,6 +105,7 @@ Figma Motion(키프레임 타임라인)이 붙은 요소는 키프레임을 손
- [ ] WDS로 확인된 요소는 전부 import로 대체했다 (raw JSX 없음)
- [ ] 텍스트는 `Typography`(`variant`+`weight`)로 썼다 — 서드파티가 className만 받는 자리가 아닌 이상 `text-xs`/`text-[Npx]` 직접 사용 없음
+- [ ] 크기·위치·그림자에 px 임의값이 없다 — Tailwind 단계·`@theme` 토큰·에셋 배치로 대체했고, 남은 예외에는 주석이 있다
- [ ] Stream 고유 요소만 새 컴포넌트로 작성했다
- [ ] Props가 Figma variant를 유니온 타입으로 반영한다
- [ ] 이미지/아이콘 asset을 다운로드해 커밋했다 (만료되는 Figma URL 미참조)
diff --git a/docs/conventions/wds-component-usage.md b/docs/conventions/wds-component-usage.md
index 3251213..23f2f4e 100644
--- a/docs/conventions/wds-component-usage.md
+++ b/docs/conventions/wds-component-usage.md
@@ -584,3 +584,28 @@ A-1 본문의 `Zone Grid Row`(`2159:110823`)가 `h-px`라 `위쪽 칸/아래쪽
- **재사용(수정 없음)**: 행사 `EventsCard`, 공지 `NoticesCard`, 열린피드백 `FeedbacksCard`
- **빌릴게**: `BililgeItemCard`에 `bordered` prop만 추가했다(흰 배경 위에서 테두리 `border-line-solid-neutral`). 메인 목록은 회색 배경 위라 테두리가 없다
- **아카이빙**: Figma `Search Archiving Card`(`3013:127633`)는 Stream 로컬이고 이 화면 전용이라 `features/search/components/SearchArchivingCard.tsx`로 새로 만들었다
+
+## 홈 화면(`3147:146240` 2건 이하 / `3147:146297` 3건 이상 / `3562:163866` empty) 구현 중 확정된 매핑
+
+| Figma | 코드 | 확인 내용 |
+|---|---|---|
+| Top Navigation `State=Home` (`547:31364`) | `ScreenHeader`(display) + `title`에 로고 | 게시판 헤더와 같은 Stream 로컬 컴포넌트의 상태 하나다. 그래서 display variant의 `title`이 글자 대신 요소(로고)도 받게 넓혔다 |
+| `Button/Icon/Normal` + `Push Badge` (`3147:146473`) | `TopNavigationButton` + `PushBadge variant="dot"` | 알림 점은 WDS `PushBadge` 기본 크기·`top-right`가 Figma 위치와 같다 |
+| `Page Indicator/Counter` (`471:13822`) | `PageCounter size="small" alternative` | 카드마다 자기 순서를 표시한다 |
+| `Content Badge` (신청마감·학생회비·사물함 상태) | `ContentBadge size="small" variant="solid"` | 강조색 redOrange·orange·red·green은 `color="accent"`, 회색(미신청)은 `color="neutral"` + `semantic.label.alternative` |
+| `Menu/Resource/Action Area/Trailing Content/Button` (반납 신청) | `BililgeItemCard` 재사용 | 홈은 흰 카드 안의 줄이라 `surface={false}`, 기한이 지나면 `subtitleTone="negative"` |
+| 빈 상태 버튼 `대여하러 가기` / `행사 둘러보기` | `Button size="small"` | 앞은 `color="assistive" variant="solid"`, 뒤는 반납 신청과 같은 연파랑 `sx` 보정 |
+
+### 공지 배너는 템플릿 6종 — 그림·배경색은 공지마다가 아니라 종류별이다
+
+Figma(`3562:163869`)에 일반공지·행사·제휴·사물함·간식·의견 6종이 있고 카드 구조는 같다. 다른 건 배경색·그림 크기와 위치·그라데이션 위치뿐이라 `HomeNoticeBanner`의 `NOTICE_TEMPLATES` 표 하나로 모았다. 배경색은 Figma에서도 변수가 아닌 raw hex라 `--color-notice-banner-*` 토큰으로 뒀다. 그림은 Figma에서 잘린 모양 그대로 3배로 내보낸 PNG다(`src/assets/images/home/`).
+
+화면설계서 기준으로 5초마다 넘어가고, 마지막 카드 오른쪽에 첫 카드 복제본을 둬서 같은 방향으로 이어진다. 카드 폭은 화면 폭 - 40(Figma 375 기준 335)이고, 카드 사이를 40px 띄워 이전·다음 카드는 화면에 보이지 않게 했다(Figma는 다음 카드가 12px 보이지만 팀 논의로 숨기기로 함).
+
+### 홈 헤더는 레이아웃 헤더 슬롯에 두지 않는다
+
+Figma는 헤더 배경이 없어서 배너 그림자(`Shadow/Spread/Small`, 60px)가 헤더까지 번진다. 레이아웃 헤더 슬롯(`useScreenHeader`)에 두면 스크롤 영역 위 끝에서 그림자가 잘려 헤더와 배너 사이에 선이 생겨서, 홈만 스크롤 영역 안에 `sticky` 헤더를 두고 스크롤하면 배경을 깐다. 같은 이유로 배너 가로 스크롤 영역도 위아래 60px 넓혀 그림자를 자르지 않는다.
+
+### 빈 상태는 WDS `FallbackView`가 아니라 홈 로컬 `HomeEmptyState`
+
+일러스트 44px + 문구 한 줄 + 작은 버튼이라 행사 목록 Empty State 때와 같은 이유(`FallbackView`의 일러스트 폭·패딩 고정)로 맞지 않는다. 그림은 빌릴게 반납 빈 상태(`icons/bililge-empty/return-items.svg`)·행사 목록 빈 상태(`icons/events/empty-events.svg`)와 같은 도형이라 크기만 줄여 그대로 쓴다.
diff --git a/src/assets/icons/home/archive-folder.svg b/src/assets/icons/home/archive-folder.svg
new file mode 100644
index 0000000..24730d4
--- /dev/null
+++ b/src/assets/icons/home/archive-folder.svg
@@ -0,0 +1,11 @@
+
diff --git a/src/assets/icons/home/arrow-right.svg b/src/assets/icons/home/arrow-right.svg
new file mode 100644
index 0000000..be36674
--- /dev/null
+++ b/src/assets/icons/home/arrow-right.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/assets/icons/home/arrow-up-right.svg b/src/assets/icons/home/arrow-up-right.svg
new file mode 100644
index 0000000..b40a4a2
--- /dev/null
+++ b/src/assets/icons/home/arrow-up-right.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/assets/icons/home/info-fee.svg b/src/assets/icons/home/info-fee.svg
new file mode 100644
index 0000000..95959b5
--- /dev/null
+++ b/src/assets/icons/home/info-fee.svg
@@ -0,0 +1,10 @@
+
diff --git a/src/assets/icons/home/info-feedback.svg b/src/assets/icons/home/info-feedback.svg
new file mode 100644
index 0000000..83b0e02
--- /dev/null
+++ b/src/assets/icons/home/info-feedback.svg
@@ -0,0 +1,11 @@
+
diff --git a/src/assets/icons/home/info-locker.svg b/src/assets/icons/home/info-locker.svg
new file mode 100644
index 0000000..463b864
--- /dev/null
+++ b/src/assets/icons/home/info-locker.svg
@@ -0,0 +1,14 @@
+
diff --git a/src/assets/icons/home/logo.svg b/src/assets/icons/home/logo.svg
new file mode 100644
index 0000000..f679443
--- /dev/null
+++ b/src/assets/icons/home/logo.svg
@@ -0,0 +1,12 @@
+
diff --git a/src/assets/images/home/notice-event.jpg b/src/assets/images/home/notice-event.jpg
new file mode 100644
index 0000000..aa472ef
Binary files /dev/null and b/src/assets/images/home/notice-event.jpg differ
diff --git a/src/assets/images/home/notice-feedback.jpg b/src/assets/images/home/notice-feedback.jpg
new file mode 100644
index 0000000..dfc27f5
Binary files /dev/null and b/src/assets/images/home/notice-feedback.jpg differ
diff --git a/src/assets/images/home/notice-general.jpg b/src/assets/images/home/notice-general.jpg
new file mode 100644
index 0000000..0f9640e
Binary files /dev/null and b/src/assets/images/home/notice-general.jpg differ
diff --git a/src/assets/images/home/notice-locker.jpg b/src/assets/images/home/notice-locker.jpg
new file mode 100644
index 0000000..2de9752
Binary files /dev/null and b/src/assets/images/home/notice-locker.jpg differ
diff --git a/src/assets/images/home/notice-partnership.jpg b/src/assets/images/home/notice-partnership.jpg
new file mode 100644
index 0000000..335f5dc
Binary files /dev/null and b/src/assets/images/home/notice-partnership.jpg differ
diff --git a/src/assets/images/home/notice-snack.jpg b/src/assets/images/home/notice-snack.jpg
new file mode 100644
index 0000000..c05f099
Binary files /dev/null and b/src/assets/images/home/notice-snack.jpg differ
diff --git a/src/components/ui/ScreenHeader.tsx b/src/components/ui/ScreenHeader.tsx
index 696f2a9..9a26cc7 100644
--- a/src/components/ui/ScreenHeader.tsx
+++ b/src/components/ui/ScreenHeader.tsx
@@ -9,10 +9,13 @@ interface ScreenHeaderToggleTitle {
type ScreenHeaderTitle = string | ScreenHeaderToggleTitle;
-function isToggleTitle(
- title: ScreenHeaderTitle,
-): title is ScreenHeaderToggleTitle {
- return typeof title !== "string";
+function isToggleTitle(title: unknown): title is ScreenHeaderToggleTitle {
+ return (
+ typeof title === "object" &&
+ title !== null &&
+ "options" in title &&
+ "activeIndex" in title
+ );
}
// Figma: 게시판류 화면의 "공지 | 열린피드백" 같은 2단 탭 타이틀(nodeId 1256:81792 "Board Title").
@@ -47,7 +50,8 @@ function ScreenHeaderToggleTitle({
type ScreenHeaderProps =
| {
variant?: "display";
- title?: ScreenHeaderTitle;
+ // 홈은 글자 대신 로고를 넣는다(Figma Top Navigation `State=Home`, nodeId 547:31364)
+ title?: ScreenHeaderTitle | ReactNode;
trailing?: ReactNode;
}
| {
@@ -102,19 +106,20 @@ function ScreenHeader(props: ScreenHeaderProps) {
diff --git a/src/features/bililge/components/BililgeItemCard.tsx b/src/features/bililge/components/BililgeItemCard.tsx
index 4b48621..3ecdd01 100644
--- a/src/features/bililge/components/BililgeItemCard.tsx
+++ b/src/features/bililge/components/BililgeItemCard.tsx
@@ -12,6 +12,10 @@ interface BililgeItemCardProps {
// 강조(SemiBold) 서체로 붙는다 — "수량 28 · "(Regular)에 이어지는 "9/29까지 반납" 부분.
// 다른 호출부(메인 목록·반납 목록)는 안 넘기면 기존처럼 subtitle 전체가 Regular로 나온다.
subtitleEmphasis?: string;
+ // 반납 기한이 지나면 부제목이 빨갛다(홈 대여 중인 물품, nodeId 3147:146331)
+ subtitleTone?: "normal" | "negative";
+ // 홈은 흰 카드 안에 줄만 들어가서 카드 틀(배경·모서리·여백)을 끈다
+ surface?: boolean;
trailingControl?: "button" | "stepper";
// 흰 배경 화면 위에 놓일 때 카드 경계를 보여주는 테두리 — 검색 결과(Figma nodeId 3147:138737)가
// 이 모양이다. 회색 배경 위의 메인 목록은 테두리가 없고, stepper variant는 항상 테두리가 있다.
@@ -37,6 +41,8 @@ function BililgeItemCard({
itemName,
subtitle,
subtitleEmphasis,
+ subtitleTone = "normal",
+ surface = true,
trailingControl = "button",
bordered = false,
actionLabel = "대여 신청",
@@ -49,7 +55,7 @@ function BililgeItemCard({
return (
@@ -65,7 +71,11 @@ function BililgeItemCard({
diff --git a/src/features/home/HomeScreen.tsx b/src/features/home/HomeScreen.tsx
index 3a7fad0..c1c6a47 100644
--- a/src/features/home/HomeScreen.tsx
+++ b/src/features/home/HomeScreen.tsx
@@ -1,55 +1,109 @@
-import { TopNavigationButton } from "@wanteddev/wds";
-import { IconBell, IconSearch } from "@wanteddev/wds-icon";
-import { Link, useNavigate } from "react-router-dom";
+import { PushBadge, TopNavigationButton } from "@wanteddev/wds";
+import { IconBell, IconSearch, IconSetting } from "@wanteddev/wds-icon";
+import { useState } from "react";
+import { useNavigate } from "react-router-dom";
import chatbotIcon from "@/assets/icons/chat/bot.svg";
+import logo from "@/assets/icons/home/logo.svg";
import ScreenHeader from "@/components/ui/ScreenHeader";
-import { useScreenHeader } from "@/components/ui/useScreenHeader";
+import HomeAppliedEventSection from "@/features/home/components/HomeAppliedEventSection";
+import HomeApplyCard from "@/features/home/components/HomeApplyCard";
+import HomeArchiveBanner from "@/features/home/components/HomeArchiveBanner";
+import HomeInfoList from "@/features/home/components/HomeInfoList";
+import HomeNoticeBanner from "@/features/home/components/HomeNoticeBanner";
+import HomeRentalSection from "@/features/home/components/HomeRentalSection";
+import {
+ HOME_HAS_UNREAD_NOTIFICATION,
+ HOME_MOCK_DATA,
+} from "@/features/home/constants/homeMock";
import { getSearchPath } from "@/features/search/constants/search";
-// 홈 화면 콘텐츠는 아직 없어서, 라우팅이 실제로 동작하는지 확인할 placeholder만 둔다.
-// 챗봇 FAB(Figma nodeId 2443:178457 등, 홈 화면 variant마다 우하단에 고정)만 먼저 구현한다 —
+// Figma: 홈 2건 이하 (nodeId 3147:146240), 3건 이상 (3147:146297), empty (3147:146366)
+// 챗봇 FAB(Figma nodeId 2443:178457 등, 홈 화면 variant마다 우하단에 고정) —
// 우측 20px·바텀 내비 위 24px 고정 위치는 Figma에서 홈 variant별로 다른 화면 높이에서도
// Bottom Nav 상단과의 간격이 항상 24px로 일정한 걸 좌표로 확인해서 얻은 값이다.
function HomeScreen() {
const navigate = useNavigate();
- useScreenHeader(
-
- navigate(getSearchPath())}
- variant="icon"
- >
-
-
-
-
-
- >
- }
- />,
- );
+ const [isScrolled, setIsScrolled] = useState(false);
return (
-
-
- 홈 화면은 아직 준비 중이에요
-
-
- 빌릴게 화면 보기
-
+ {/* 스크롤 영역 자체를 flex로 두면 내용이 길 때 섹션이 줄어들어서(배너 가로 스크롤 영역은 최소 높이가 0) 안쪽에서 쌓는다 */}
+
setIsScrolled(event.currentTarget.scrollTop > 0)}
+ >
+ {/* Figma: Top Navigation State=Home (nodeId 547:31364), 알림 있을 때 (3147:146473)
+ 홈만 헤더를 스크롤 영역 안에 둔다. Figma는 헤더 배경이 없어 배너 그림자가 헤더까지 번지는데,
+ 레이아웃 헤더 슬롯에 두면 그 경계에서 그림자가 잘려 선이 생긴다. 스크롤하면 배경을 깔아
+ 지나가는 내용과 겹치지 않게 한다. */}
+