diff --git a/README.md b/README.md
index a971451..587affa 100644
--- a/README.md
+++ b/README.md
@@ -41,8 +41,8 @@ src/
├── queries/ TanStack Query 훅 (query-key-factory 기반 키)
├── types/apis/ 서버 DTO 대응 zod 스키마 & 타입
├── app/ App Router 라우트
-│ ├── (with-tabs)/ 하단 탭이 있는 화면 (커뮤니티·육아 정보·홈·챗봇·마이페이지)
-│ ├── (without-tabs)/ 단독 화면 (아이 관리·시설·게시글 상세·알림·회원가입 등)
+│ ├── (with-tabs)/ 하단 탭이 있는 화면 (홈·지원금·어린이집·커뮤니티·마이)
+│ ├── (without-tabs)/ 단독 화면 (아이 관리·챗봇·시설 상세·게시글 상세·알림·회원가입 등)
│ ├── admin/ 관리자 (자체 레이아웃 + AdminGuard)
│ └── auth/ OAuth 콜백 (화면이 아니라 통과 지점이라 그룹 밖)
├── components/
@@ -56,6 +56,32 @@ src/
`(with-tabs)/`** 여야 합니다 — 그룹 밖 경로를 탭에 넣으면 그 탭을 누르는 순간 탭 바가
사라져 다른 탭으로 돌아갈 수 없습니다.
+### 화면 레이아웃 규칙
+
+화면마다 틀이 달라지면 사용자는 "이 화면은 어떻게 생겼을 것"이라는 예측을 못 하고, 같은
+목적지가 여러 입구로 갈라집니다. 진단 기록은 [`docs/ui-audit.md`](./docs/ui-audit.md) 에 있습니다.
+
+**탭 화면의 상단은 `ContextBar`, 하위 화면은 `TopNavBar` 입니다.**
+`ContextBar` 는 화면 이름 대신 **기준**을 보여줍니다 — 지금 보고 있는 아이와 지역. 이 서비스의
+답은 전부 그 둘에 달려 있어서, 기준이 화면에 없으면 사용자는 결과를 잘못 읽습니다. 하위 화면은
+뒤로 가기가 있는 `TopNavBar` 를 씁니다(`hasBackButton`).
+
+**지금 보는 아이는 전역 상태입니다** — `useSelectedChild()` (`src/stores/useSelectedChild.ts`).
+화면마다 `useState` 로 고르면 화면을 옮길 때마다 첫째로 되돌아갑니다. 선택은 기기에 남기되,
+서버 렌더에는 `localStorage` 가 없으므로 `skipHydration` 으로 두고 마운트 뒤에 복구합니다.
+
+**홈의 첫 화면은 "지금 할 일" 입니다.** 기한이 걸린 것만 위에 오고, 찾아보는 수단(검색·퀵메뉴·
+챗봇)은 그 아래입니다. 여기에는 **날짜가 실제로 있는 것만** 올립니다. 정책의 신청 기간은 자유
+문장이라 마감일을 만들 수 없고, 없는 날짜를 지어내면 "추정치는 추정치로" 원칙과 어긋납니다.
+날짜가 있어도 45일보다 먼 것은 아직 할 일이 아닙니다.
+
+**같은 목적지를 두 입구로 주지 않습니다.** 하단 탭에 있는 곳은 홈 퀵메뉴에 넣지 않습니다.
+탭이 곧 이 서비스의 중요도 선언이라, 중복을 두면 그 선언이 흐려집니다.
+
+**목록은 카드, 주요 행동은 화면 아래입니다.** 누를 수 있는 목록은 흰 카드에 담고 오른쪽에
+꺾쇠를 둡니다(`MenuList`). 목록에 무엇을 더하는 행동은 `BottomAction` 으로 아래에 고정해,
+목록이 길어져도 자리가 변하지 않게 합니다.
+
### 의존성 규칙
**import 하는 패키지는 반드시 `package.json` 에 선언합니다.** 한동안 `framer-motion` 과
diff --git a/docs/ui-audit.md b/docs/ui-audit.md
new file mode 100644
index 0000000..0d1630b
--- /dev/null
+++ b/docs/ui-audit.md
@@ -0,0 +1,76 @@
+# 화면 틀 진단 (2026-09)
+
+로그인한 상태로 주요 화면 11개를 390×844(모바일)에서 실제로 띄워 보고 적었다.
+개별 화면의 취향 문제가 아니라 **화면들 사이에서 어긋나는 것**만 남겼다.
+
+이 문서는 기록이다. 고친 것은 "고침"으로 표시하고, 남은 것은 왜 아직 못 고쳤는지 적는다.
+
+---
+
+## 1. 상단바가 가장 중요한 것을 안 보여줬다 — 고침
+
+모든 탭 화면의 상단바가 "화면 이름 + 종 아이콘"뿐이었다. 그런데 이 서비스가 내놓는 답은
+전부 **어느 아이, 어디 사는가**에 달려 있다(월령별 지원금, 접종 일정, 반 편성, 가까운 시설).
+다자녀 가구는 지금 보는 화면이 누구 기준인지 알 방법이 없었고, 주소를 넣지 않은 사용자는
+추천이 비어 있는 이유도 알 수 없었다.
+
+- 아이 선택이 전역 상태로 없었다. `health/page.tsx`, `WaitlistDialog` 등 화면마다 로컬
+ `useState` 로 흩어져 있어서 화면을 옮기면 기준이 첫째로 되돌아갔다.
+- 고침: `src/stores/useSelectedChild.ts`(기기에 저장) + `src/components/common/ContextBar.tsx`
+ (아이 칩 · 지역 칩 · 알림). 탭 화면 네 곳(홈 · 지원금 · 어린이집 · 마이)에 붙였다.
+
+## 2. 홈이 포털이었다 — 고침
+
+첫 화면 위쪽 절반이 검색창 · 아이콘 5개 · 캐릭터 배너로 채워져 있었고, 정작 **놓치면
+사라지는 것**(소급 신청 가능한 지원금, 지난 접종, 대기 순번)은 스크롤 아래에 있었다.
+
+- 고침: `src/components/features/home/TodoSection.tsx` 를 첫 화면 맨 위로. 찾아보는 수단
+ (검색 · 퀵메뉴 · 챗봇)은 그 아래 "찾아보기"로 내렸다.
+- **마감 임박은 넣지 못했다.** 정책의 신청 기간이 서버에서 자유 문장(`applicationPeriod`)
+ 으로 와서 날짜를 뽑을 수 없다. 날짜가 실제로 있는 것만 올린다 — 건강 알림 `dueDate`,
+ 접종 예정일, 대기 순번, 빠진 프로필. 구조화된 마감일은 관리자 타입(`applicationEndDate`)
+ 에만 있어, 정책 API 가 그 값을 열어주면 그때 추가한다.
+- 날짜가 있는 항목은 45일 안(또는 지난 것)만 올린다. 1년 뒤 접종까지 올리면 목록이
+ 달력이 되고 이번 주에 할 일이 묻힌다.
+
+## 3. 같은 목적지가 세 군데에 있었다 — 고침
+
+건강 기록 · 병원 찾기 · 시설 찾기가 홈 퀵메뉴에도, 마이페이지 목록에도 있었다. 반대로
+탭 다섯 자리는 커뮤니티 · 육아 정보 · 홈 · 챗봇 · 마이페이지였다 — **서비스의 핵심인
+지원금과 어린이집이 탭에 없었다.**
+
+- 고침: 탭을 `홈 · 지원금 · 어린이집 · 커뮤니티 · 마이` 로 바꿨다.
+ - `facility/page.tsx` 를 `(with-tabs)` 로 옮겼다(상세 `facility/[id]` 는 그대로).
+ - 챗봇은 탭에서 내리고 `(without-tabs)` 로 옮겼다. 전체 화면 대화라 탭 바가 필요 없고,
+ 홈의 진입점(`ChatSection`)은 남겼다. 대신 나가는 길이 없어지므로 뒤로 가기를 넣었다.
+ - 퀵메뉴에서 탭과 겹치는 "시설 찾기" 를 뺐다.
+
+## 4. 화면마다 틀이 달랐다 — 일부 고침
+
+| 항목 | 이전 | 지금 |
+|---|---|---|
+| 목록 | 홈은 흰 카드, 커뮤니티는 구분선 목록, 마이페이지는 텍스트 나열 | 마이페이지를 카드+꺾쇠로(`MenuList`). 커뮤니티 목록은 그대로 둠 |
+| 주요 버튼 | 건강 기록은 본문 안, 시설 오류는 하단 고정, 커뮤니티는 떠 있는 버튼 | `BottomAction` 을 만들어 건강 기록에 적용. 나머지는 미적용 |
+| 오류 | `ErrorView` 가 항상 `absolute inset-0 z-50` 이라 목록 한 칸이 실패해도 상단바·탭 바까지 덮었다 | 기본은 자리만 채우고, 라우트 오류 경계만 `fullScreen` |
+| 상단 | 화면마다 제목+종 | 탭 화면은 `ContextBar`, 하위 화면은 `TopNavBar`(뒤로 가기) |
+
+남은 것: 커뮤니티 목록과 떠 있는 글쓰기 버튼, 시설 오류 화면의 하단 버튼은 아직 공통
+규칙을 따르지 않는다. 한 번에 바꾸면 회귀 범위가 커져 다음으로 미뤘다.
+
+## 5. 아직 남은 것
+
+- **밀도**: 건강 기록은 칩 한 줄 · 카드 한 장 뒤로 화면이 비고, 칩 줄은 오른쪽에서 잘리는데
+ 더 있다는 표시가 없다.
+- **마이페이지 항목 상태값**: `MenuList` 에 `hint` 를 받도록 열어 뒀다(예: "대기 3곳").
+ 아직 값을 넣지 않았다.
+
+---
+
+## 다시 확인하는 법
+
+```bash
+npm run dev -- --port 3210 # 백엔드 CORS 에 이 주소가 있어야 한다
+```
+
+로그인 화면의 `[DEV]` 버튼으로 들어가 홈 · 지원금 · 어린이집 · 커뮤니티 · 마이를 차례로 본다.
+아이가 둘 이상인 계정으로 아이 칩을 바꿔 가며 홈의 "지금 할 일"이 바뀌는지 확인한다.
diff --git a/src/app/(with-tabs)/error.tsx b/src/app/(with-tabs)/error.tsx
index b3161f9..5ae4ece 100644
--- a/src/app/(with-tabs)/error.tsx
+++ b/src/app/(with-tabs)/error.tsx
@@ -22,7 +22,7 @@ const GroupError = ({
return (
-
+
)
}
diff --git a/src/app/(without-tabs)/facility/page.tsx b/src/app/(with-tabs)/facility/page.tsx
similarity index 97%
rename from src/app/(without-tabs)/facility/page.tsx
rename to src/app/(with-tabs)/facility/page.tsx
index 2b8fcc9..4dac0ad 100644
--- a/src/app/(without-tabs)/facility/page.tsx
+++ b/src/app/(with-tabs)/facility/page.tsx
@@ -2,6 +2,7 @@
import { useRouter } from 'next/navigation'
import { ReactElement, useMemo, useState } from 'react'
import SearchIcon from '@/assets/icons/search.svg'
+import ContextBar from '@/components/common/ContextBar'
import EmptyState from '@/components/common/EmptyState'
import ErrorView from '@/components/common/Error'
import Layout from '@/components/common/Layout'
@@ -98,7 +99,8 @@ const FacilityPage = (): ReactElement => {
}
return (
-
+
+ ({
diff --git a/src/app/(with-tabs)/home/page.tsx b/src/app/(with-tabs)/home/page.tsx
index 53a4a95..ecab334 100644
--- a/src/app/(with-tabs)/home/page.tsx
+++ b/src/app/(with-tabs)/home/page.tsx
@@ -3,29 +3,33 @@
import { format } from 'date-fns'
import { useRouter } from 'next/navigation'
import { ReactElement } from 'react'
-import BellIcon from '@/assets/icons/bell.svg'
import SearchIcon from '@/assets/icons/search.svg'
+import ContextBar from '@/components/common/ContextBar'
import Layout from '@/components/common/Layout'
import MainSection from '@/components/common/MainSection'
import Separator from '@/components/common/Separator'
-import Spacer from '@/components/common/Spacer'
import Input from '@/components/common/input'
import ChatSection from '@/components/features/chat/ChatSection'
import PopularPost from '@/components/features/community/popular-post'
-import MissedBenefitBanner from '@/components/features/home/MissedBenefitBanner'
import QuickMenu from '@/components/features/home/QuickMenu'
+import TodoSection from '@/components/features/home/TodoSection'
import PolicyCard from '@/components/features/policy/PolicyCard'
import RecommendedPolicyCard from '@/components/features/policy/RecommendedPolicyCard'
import { useGetCommunityPopular } from '@/queries/community'
-import { useHasUnreadNotifications } from '@/queries/notification'
import { useGetLatestPolicies, usePolicyRecommendations } from '@/queries/policy'
import { convertPolicyToCardProps } from '@/types/policy'
+/**
+ * 홈.
+ *
+ * 순서가 이 화면의 전부다. 기한이 걸린 것(지금 할 일)이 가장 위에 오고, 그다음이 이 아이에게
+ * 맞는 지원금, 찾아보는 수단(검색·퀵메뉴)은 그 아래다. 예전에는 검색창과 퀵메뉴와 배너가
+ * 첫 화면의 절반을 차지해서, 정작 놓치면 사라지는 것들이 스크롤 아래에 있었다.
+ *
+ * 상단바는 화면 이름 대신 기준(아이 · 지역)을 보여준다 — ContextBar.
+ */
const Home = (): ReactElement => {
const router = useRouter()
- const hasUnread = useHasUnreadNotifications()
- const handleNotificationClick = () => router.push('/notification')
- const handleSearchClick = () => router.push('/search')
const { data: policies, isLoading, error } = useGetLatestPolicies()
const {
data: popularPosts,
@@ -35,110 +39,106 @@ const Home = (): ReactElement => {
const { data: recommendations, isLoading: isRecommendationLoading } = usePolicyRecommendations(3)
return (
-
-