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
57 changes: 57 additions & 0 deletions .github/workflows/refresh-figures.yml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
name: Refresh figures

# 사이트에 적힌 수집량은 백엔드가 매주 공공데이터를 새로 받아 오면 낡는다.
# 사람이 직접 고치는 절차를 없애기 위해 주기적으로 받아 와 PR 을 연다.
on:
schedule:
- cron: '0 21 * * 1' # 매주 화요일 06:00 KST (백엔드 시설 동기화 이후)
workflow_dispatch:

permissions:
contents: write
pull-requests: write

jobs:
refresh:
name: 수치 갱신 PR
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 20

# 백엔드가 공개되기 전에는 조용히 건너뛴다. 실패로 울리면 알림이 무의미해진다.
- name: 백엔드 주소 확인
id: guard
run: |
if [ -z "${{ vars.STATS_API_BASE }}" ]; then
echo "STATS_API_BASE 변수가 없어 건너뜁니다."
echo "skip=true" >> "$GITHUB_OUTPUT"
else
echo "skip=false" >> "$GITHUB_OUTPUT"
fi

- name: 수치 받아 오기
if: steps.guard.outputs.skip == 'false'
env:
STATS_API_BASE: ${{ vars.STATS_API_BASE }}
# 넷 중 하나라도 못 받으면 스크립트가 파일을 손대지 않고 실패한다.
# 일부만 갱신하고 날짜를 새로 찍으면 낡은 값에 새 날짜가 붙어 거짓말이 된다.
run: node scripts/refresh-figures.mjs

- name: 변경분 PR 생성
if: steps.guard.outputs.skip == 'false'
uses: peter-evans/create-pull-request@v7
with:
branch: chore/refresh-figures
title: 'chore(content): 수집량 갱신'
commit-message: 'chore(content): 수집량과 기준일 갱신'
body: |
백엔드 공개 통계에서 받아 온 값입니다. 값과 `MEASURED_ON` 이 함께 바뀝니다.

사람이 확인할 것은 하나입니다 — **숫자가 갑자기 크게 줄지 않았는지.**
공공데이터 수집이 부분 실패하면 값이 뚝 떨어질 수 있고, 그대로 올리면
사이트가 실제보다 적은 수를 보여 줍니다.
delete-branch: true
24 changes: 19 additions & 5 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -80,12 +80,26 @@ npm run verify # 로컬에서 같은 검사 (out/ 을 먼저 서빙해야 합
사이트에 적힌 수집량은 `src/content/figures.ts` 한 곳에 있고 `MEASURED_ON` 과 함께 둡니다.
백엔드가 매주 공공데이터를 새로 받아 오므로 값이 낡습니다.

1. `CareCode_Interface` 에서 `./gradlew liveSyncCheck` 로 실측
2. `figures.ts` 의 값과 `MEASURED_ON` 을 **함께** 수정
3. `main` 에 반영하면 자동 배포
**자동**으로 매주 화요일 새벽에 백엔드 공개 통계에서 받아 와 PR 을 엽니다.

값만 고치고 날짜를 안 고치면 거짓말이 됩니다. 매주 화요일 새벽에도 자동으로 다시 빌드하지만,
그건 코드에 적힌 값을 다시 그리는 것일 뿐 실측값을 새로 받아 오지는 않습니다.
| 값 | 출처 |
|---|---|
| 어린이집 · 유치원 | `GET /facilities/statistics` 의 `typeDistribution` |
| 소아청소년과 | `GET /health/hospitals/statistics` |
| 정부지원 서비스 | `GET /policies/statistics` |

넷 중 **하나라도 못 받으면 아무것도 바꾸지 않습니다.** 일부만 갱신하고 날짜를 새로 찍으면
낡은 값에 새 날짜가 붙어 거짓말이 됩니다.

동작하려면 저장소 변수 `STATS_API_BASE` 에 백엔드 공개 주소가 있어야 합니다. 없으면 워크플로가
조용히 건너뜁니다 — 백엔드가 공개되기 전에 실패로 울리면 알림이 무의미해지기 때문입니다.

```bash
STATS_API_BASE=https://api.example.com npm run refresh # 로컬에서 직접
```

PR 에서 사람이 볼 것은 하나입니다 — **숫자가 갑자기 크게 줄지 않았는지.** 공공데이터 수집이
부분 실패하면 값이 뚝 떨어질 수 있습니다.

## 원칙

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,7 +9,8 @@
"start": "next start",
"lint": "next lint",
"typecheck": "tsc --noEmit",
"verify": "node scripts/verify-pages.mjs"
"verify": "node scripts/verify-pages.mjs",
"refresh": "node scripts/refresh-figures.mjs"
},
"dependencies": {
"next": "15.3.5",
Expand Down
90 changes: 90 additions & 0 deletions scripts/refresh-figures.mjs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
/**
* 백엔드 공개 통계에서 수집량을 받아 figures.ts 를 갱신한다.
*
* 이 사이트는 "근거 없는 숫자는 쓰지 않는다" 를 내세운다. 그래서 값을 바꿀 때 기준일도 같이
* 바꿔야 하고, 넷 중 하나라도 못 받으면 **아무것도 바꾸지 않는다.** 일부만 갱신하고 날짜를
* 새로 찍으면 낡은 값에 새 날짜가 붙어 거짓말이 된다.
*
* 사용법:
* STATS_API_BASE=https://api.example.com node scripts/refresh-figures.mjs
*
* 종료 코드
* 0 갱신함 (또는 변경 없음)
* 1 실패 — 파일은 손대지 않았다
* 2 STATS_API_BASE 가 없어 건너뜀
*/
import { readFile, writeFile } from 'node:fs/promises'

const BASE = (process.env.STATS_API_BASE ?? '').replace(/\/$/, '')
const FILE = new URL('../src/content/figures.ts', import.meta.url)

if (!BASE) {
console.log('STATS_API_BASE 가 없어 건너뜁니다. 백엔드가 공개된 뒤 설정하세요.')
process.exit(2)
}

/** 응답이 이상하면 조용히 0 을 쓰지 않고 실패한다. */
const getJson = async (path) => {
const res = await fetch(`${BASE}${path}`, { headers: { accept: 'application/json' } })
if (!res.ok) throw new Error(`${path} → HTTP ${res.status}`)
return res.json()
}

const positive = (value, label) => {
const n = Number(value)
if (!Number.isFinite(n) || n <= 0) throw new Error(`${label} 값이 이상합니다: ${JSON.stringify(value)}`)
return Math.round(n)
}

/** 시설 통계의 typeDistribution 키는 FacilityType enum 이름이다. */
const pickFacility = (distribution, keys, label) => {
for (const key of keys) {
if (distribution?.[key] != null) return positive(distribution[key], label)
}
throw new Error(`${label} 을(를) typeDistribution 에서 찾지 못했습니다: ${Object.keys(distribution ?? {}).join(', ')}`)
}

let counts
try {
const [facilities, policies, hospitals] = await Promise.all([
getJson('/facilities/statistics'),
getJson('/policies/statistics'),
getJson('/health/hospitals/statistics'),
])

const dist = facilities.typeDistribution
counts = {
어린이집: pickFacility(dist, ['DAYCARE', 'CHILDCARE', '어린이집'], '어린이집'),
유치원: pickFacility(dist, ['KINDERGARTEN', '유치원'], '유치원'),
소아청소년과: positive(hospitals.byType?.['소아청소년과'] ?? hospitals.total, '소아청소년과'),
'정부지원 서비스': positive(policies.totalPolicies, '정부지원 서비스'),
}
} catch (error) {
console.error(`수치를 받지 못했습니다: ${error.message}`)
console.error('낡은 값에 새 날짜를 붙이지 않도록 파일을 고치지 않았습니다.')
process.exit(1)
}

const today = new Date().toISOString().slice(0, 10)
let source = await readFile(FILE, 'utf8')
const before = source

for (const [gets, count] of Object.entries(counts)) {
const pattern = new RegExp(`(gets: '${gets}', count: )[\\d_]+`)
if (!pattern.test(source)) {
console.error(`figures.ts 에서 ${gets} 항목을 찾지 못했습니다. 구조가 바뀐 것 같습니다.`)
process.exit(1)
}
source = source.replace(pattern, `$1${count.toLocaleString('en-US').replace(/,/g, '_')}`)
}

source = source.replace(/(export const MEASURED_ON = ')[\d-]+(')/, `$1${today}$2`)

if (source === before) {
console.log('수치와 기준일 모두 그대로입니다.')
process.exit(0)
}

await writeFile(FILE, source)
console.log(`갱신 (${today})`)
Object.entries(counts).forEach(([k, v]) => console.log(` ${k}: ${v.toLocaleString('ko-KR')}`))
Loading