Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 62 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ npm run build # out/ 에 정적 파일 생성

| 경로 | 내용 |
|------|------|
| `/` | 개요. 월령 축을 옮기면 지원금이 유효·소급가능·지남으로 갈립니다 |
| `/` | 여섯 해. 스크롤이 아이의 개월 수(0 → 72)이고, 뒤의 3D 동네가 그에 맞춰 자랍니다 |
| `/benefits` | 자격 판정, 수령액 계산, 금액 신뢰도 |
| `/facilities` | 정원 관측, 빈자리 알림, 대기 기간 통계 |
| `/data` | 출처 네 곳과 **아는 것 / 모르는 것** 대조 |
Expand Down Expand Up @@ -59,9 +59,66 @@ npm run build # out/ 에 정적 파일 생성

### 시그니처

홈의 **월령 타임라인**입니다. 문구로 설득하는 대신 축을 놓고 직접 옮겨 보게 합니다.
밴드 구성은 실제 정책 데이터 모델(대상 연령 상·하한, 지급액, 소급 개월)을 따랐고,
금액을 모르는 정책은 `미상` 으로 둡니다.
홈 전체가 **아이의 첫 여섯 해**입니다. 스크롤 진행이 곧 개월 수이고, 그 값 하나가 뒤에 깔린
3D 동네의 세 가지를 동시에 정합니다.

- **계절** — 4월생으로 가정합니다. 벚꽃, 초록, 은행잎, 눈이 차례로 지나갑니다
- **성장** — 유모차에서 걸음마로, 마지막에는 책가방을 멥니다. 태어난 봄에 심은 벚나무도 함께 자랍니다
- **시선** — 그 시기에 부모가 드나드는 건물(어린이집 · 소아과 · 행정복지센터)로 카메라가 갑니다.
건물 위의 알림 표식은 그 시기에 살아 있는 알림만큼 떠오르고, 부모급여가 끝나기 직전(20–24개월)에는
도장 빨강으로 바뀝니다

4월생으로 고른 이유는 동네가 보이는 구간(출생 · 6 · 18 · 72개월)이 봄과 가을에 앉기 때문입니다.
3월생이면 전부 초봄이라 계절이 흐르는 게 안 보입니다.

구간 사이에는 **월령 타임라인**(12개월 구간)이 있습니다. 스크롤이 개월 수를 정하는 페이지에서
여기서만은 부모가 직접 축을 옮깁니다. 밴드 구성은 실제 정책 데이터 모델(대상 연령 상·하한, 지급액,
소급 개월)을 따랐고, 금액을 모르는 정책은 `미상` 으로 둡니다.

구조 문법은 [JellySafe 소개 사이트](https://github.com/JellySafe/Introduce)(스크롤 = 하루)에서
가져왔고, 색·서체·도장은 이 사이트의 육아수첩 토큰을 그대로 씁니다.

## 홈을 손볼 때

```
src/features/
├── grow/ 스크롤 엔진(GrowScene) · 구간 목록(stages.ts)
├── stations/ 구간 하나 = 파일 하나. 전부 서버 컴포넌트
└── town/ 3D 동네(three.js). GrowScene 이 동적 import 한다
```

**구간 목록은 두 곳에 있고 순서가 같아야 합니다** — `src/app/page.tsx` 와
`src/features/grow/stages.ts`. 레일 눈금이 stages.ts 에서 나오고, 개월 수의 닻은 페이지의
`[data-month]` 에서 읽습니다. 개발 모드에서는 둘이 어긋나면 콘솔에 경고가 뜹니다.

**개월 수는 문서 진행도가 아니라 구간 위치로 정합니다.** 각 구간 머리가 화면 가운데에 온 순간을
닻으로 삼아 사이를 잇습니다. 구간 길이가 제각각이라 진행도를 선형으로 나누면 "18개월" 도장을 읽는
순간 장면은 15개월이 됩니다.

**스크롤 경로에 React 상태를 두지 않습니다.** GrowScene 은 초당 60회 도는 경로라 값은 ref 로
직접 씁니다.

**3D 는 쓰지 않을 사용자에게 받게 하지 않습니다.** 동작 줄이기(`prefers-reduced-motion`),
WebGL2 미지원, **GPU 가속 없는 소프트웨어 렌더러**(SwiftShader · llvmpipe)면 three.js 청크를
import 하기 **전에** 판정하고 정적 배경을 띄웁니다. 소프트웨어 렌더러에서는 이 장면이 초당 몇
프레임이라 메인 스레드를 잡아먹고 스크롤까지 끊깁니다 — CI 의 헤드리스 Chrome 에서 페이지 로드가
60초를 넘긴 것도 이 때문이었습니다. 카피는 전부 DOM 이라 3D 가 없어도 페이지는 온전합니다.

GPU 없는 환경에서 장면을 확인만 하려면 주소에 `?town=force` 를 붙입니다.

> Windows 에서 "애니메이션 효과"를 꺼 두면 브라우저가 동작 줄이기를 보고해 3D 대신 정적 배경이
> 뜹니다. 고장이 아닙니다.

**프레이밍은 재서 맞춥니다.** 콘솔에서:

```js
carecodeTown.probe() // { home: [67, 32], daycare: [80, 42], … } ← 화면 좌표 %
carecodeTown.stats() // { fps, jsMs, drawCalls, triangles, … }
```

가족이 설 곳 · 카메라가 볼 곳 · 거리는 `town-scene.ts` 의 `BEATS` 한 표에, 계절은 `SEASONS`
한 표에 있습니다. 건물 벽은 면마다 재질을 나누지 않고 벽/옥상 두 번에 그립니다(`wallBox`) —
BoxGeometry 에 재질 배열을 주면 상자 하나에 드로우콜이 여섯 번 듭니다.

## 배포

Expand Down Expand Up @@ -112,4 +169,4 @@ PR 에서 사람이 볼 것은 하나입니다 — **숫자가 갑자기 크게

## 스택

Next.js 15 (App Router, 정적 출력) · React 19 · Tailwind CSS v4 · TypeScript
Next.js 15 (App Router, 정적 출력) · React 19 · Tailwind CSS v4 · TypeScript · three.js (홈의 3D 동네, 지연 로드)
67 changes: 66 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
"dependencies": {
"next": "15.3.5",
"react": "19.0.0",
"react-dom": "19.0.0"
"react-dom": "19.0.0",
"three": "^0.186.0"
},
"devDependencies": {
"@eslint/compat": "^1.4.1",
Expand All @@ -25,6 +26,7 @@
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"@types/three": "^0.186.0",
"eslint": "^9.39.5",
"eslint-config-next": "^15.3.5",
"puppeteer": "^25.5.0",
Expand Down
19 changes: 1 addition & 18 deletions src/app/benefits/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,14 @@ import type { Metadata } from 'next'
import Link from 'next/link'
import { ReactNode } from 'react'
import { Figure, PageHead, Section } from '@/components/Page'
import { VERDICTS } from '@/content/facts'

export const metadata: Metadata = {
title: '지원금',
description:
'놓친 지원금 찾기, 거주지별 비교, 실수령액 제보로 확정하는 금액. 추정치는 추정치라고 표기합니다.',
}

/** 자격 판정 결과. 소득 미입력을 탈락으로 처리하지 않는 것이 이 서비스의 판단이다. */
const VERDICTS = [
{
tone: 'var(--seal-deep)',
head: '대상입니다',
body: '자녀 수와 소득 조건을 모두 만족합니다. 예상 총액에 넣습니다.',
},
{
tone: 'var(--past)',
head: '판단 보류',
body: '소득을 입력하지 않으셨습니다. 탈락으로 처리하면 받을 수 있었던 돈이 목록에서 통째로 사라지므로, 보류로 두고 함께 보여 드립니다.',
},
{
tone: 'var(--stamp)',
head: '대상 아닙니다',
body: '자녀 수가 모자라거나 소득이 기준을 넘습니다. 왜 아닌지 함께 적습니다.',
},
]

export default function BenefitsPage(): ReactNode {
return (
Expand Down
26 changes: 2 additions & 24 deletions src/app/data/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Metadata } from 'next'
import { ReactNode } from 'react'
import { PageHead, Section, Shell } from '@/components/Page'
import { KNOWN_UNKNOWN } from '@/content/facts'
import { MEASURED_LABEL, MEASURED_ON, SOURCES, SYNC_SCHEDULE, formatCount } from '@/content/figures'

export const metadata: Metadata = {
Expand All @@ -9,29 +10,6 @@ export const metadata: Metadata = {
'어디서 받아 오는지, 무엇을 알 수 있고 무엇을 알 수 없는지. 비어 있는 지역과 못 채운 값을 그대로 적었습니다.',
}

/** 이 표가 이 페이지의 이유다. 마케팅 페이지가 보통 쓰지 않는 오른쪽 칸. */
const LEDGER = [
{
known: '시설 전체의 정원과 현원',
unknown: '반별 정원',
why: '공공데이터가 시설 단위로만 줍니다. 0세반이 찼는데 5세반에 자리가 난 것일 수 있어, 알림에 그대로 적습니다.',
},
{
known: '정책의 대상 연령·지역·소득 기준',
unknown: '일부 정책의 지원 금액',
why: '금액이 숫자가 아니라 설명 문장 안에 섞여 옵니다. 기계로 뽑다 실패한 건 미상으로 두고, 받아 보신 분들의 제보로 채웁니다.',
},
{
known: '관측된 정원 변동',
unknown: '대기 순번이 도는 시점',
why: '정부가 공개하지 않습니다. 부모들이 남긴 입소 기록이 세 건 이상 모인 시설만 통계를 냅니다.',
},
{
known: '전국 시군구 200여 곳의 시설',
unknown: '광주와 전남의 시설',
why: '두 정부 API 모두 이 두 지역에서 빈 결과를 돌려줍니다. 우리 쪽 문제가 아니라 확인된 사실이라 적어 둡니다.',
},
]

export default function DataPage(): ReactNode {
return (
Expand Down Expand Up @@ -126,7 +104,7 @@ export default function DataPage(): ReactNode {
</p>

<ul className="mt-10 space-y-px" style={{ backgroundColor: 'var(--rule)' }}>
{LEDGER.map((row) => (
{KNOWN_UNKNOWN.map((row) => (
<li
key={row.unknown}
className="p-6 md:p-7"
Expand Down
8 changes: 1 addition & 7 deletions src/app/facilities/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Metadata } from 'next'
import { ReactNode } from 'react'
import { Figure, PageHead, Section } from '@/components/Page'
import { SILENT_CASES } from '@/content/facts'
import VacancyNotice from '@/components/VacancyNotice'

export const metadata: Metadata = {
Expand All @@ -9,13 +10,6 @@ export const metadata: Metadata = {
'대기 걸어 둔 시설에 자리가 늘면 알립니다. 정원 관측을 쌓아 입소 시점을 추정하되, 근거가 없으면 확률을 만들지 않습니다.',
}

/** 알리지 않는 경우. 이 표가 알림 설계의 핵심이라 앞에 세운다. */
const SILENT_CASES = [
{ when: '관측이 한 번뿐일 때', why: '늘었는지 줄었는지 알 수 없습니다' },
{ when: '빈자리가 그대로일 때', why: '이미 알고 계십니다' },
{ when: '빈자리가 줄었을 때', why: '알릴 내용이 아닙니다' },
{ when: '최근 2주 안에 보냈을 때', why: '같은 자리를 두 번 알리면 믿지 않게 됩니다' },
]

export default function FacilitiesPage(): ReactNode {
return (
Expand Down
Loading
Loading