factcircuit
FactCircuit는 불변 출처와 단계별 검증, 제한된 검색, 재현 가능한 평가를 제공하는 감사 가능한 주장·증거 검증 루프입니다
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
정적 화면(index.html + app.js + styles.css) + 실시간 데이터 API(/api/update) 로 구성된 글로벌 매트리스 시장 인텔리전스 대시보드입니다. API 키를 전혀 쓰지 않고 공개 소스만 사용합니다. • 시몬스 코리아 소식 — Google News RSS (팝업·행사·신제품 등 국내 소식) • 경쟁사 분석 — SEC EDGAR (Sleep Number·Tempur Sealy 재무/매출추이) • 업계 주요 뉴스 — Google News RSS + 제목 한국어 번역 • 원자재·원가 동향 — World Bank 핑크시트(월간 원자재 가격) + 원화 환산 • 환율 — Frankfurter(ECB) USD/KRW 화면은 항상 빈 초기 상태로 시작하며, 사이드바 [업데이트] 버튼을 눌러야 /api/update 에서 데이터를 받아 채웁니다. api/update.py 와 dashboard_server.py 는 같은 수집 로직을 담고 있습니다(하나는 서버리스, 하나는 Flask). 데이터 로직을 고칠 때는 두 파일을 함께 맞춰 주세요.README 전문 보기 ↗
정적 화면(index.html + app.js + styles.css) + 실시간 데이터 API(/api/update) 로 구성된
글로벌 매트리스 시장 인텔리전스 대시보드입니다. API 키를 전혀 쓰지 않고 공개 소스만 사용합니다.
화면은 항상 빈 초기 상태로 시작하며, 사이드바 [업데이트] 버튼을 눌러야
/api/update에서 데이터를 받아 채웁니다.
├─ index.html # 대시보드 화면
├─ app.js # 프런트 로직 (fetch 경로 = 상대경로 '/api/update')
├─ styles.css
├─ api/
│ └─ update.py # ▶ Vercel 서버리스 함수 (배포용). 모든 데이터 수집 로직.
├─ dashboard_server.py # ▶ 로컬 개발용 Flask 서버 (같은 로직, 로컬 테스트 전용)
├─ vercel.json # 라우팅: /api/update → 함수, 그 외 → 정적 파일
├─ requirements.txt # requests / pandas / openpyxl
├─ .gitignore # csv·log·__pycache__ 등 제외
└─ .vercelignore # 배포 번들에서 dashboard_server.py·csv·log 제외
api/update.py와dashboard_server.py는 같은 수집 로직을 담고 있습니다(하나는 서버리스, 하나는 Flask). 데이터 로직을 고칠 때는 두 파일을 함께 맞춰 주세요.
dashboard_server.py 가 정적 파일 + /api/update 를 같은 출처(:5000)로 서빙합니다.
pip install flask flask-cors requests pandas openpyxl
python dashboard_server.py
# 브라우저에서 http://127.0.0.1:5000 접속 → [업데이트] 클릭
Vercel CLI 로 서버리스 함수까지 로컬에서 똑같이 구동할 수 있습니다.
npm i -g vercel
vercel dev
# http://localhost:3000 접속
프로젝트 폴더에서:
git init
git add .
git commit -m "SIMMONS 매트리스 동향 대시보드 (Vercel 구조)"
# GitHub 에서 빈 저장소를 먼저 만든 뒤, 그 주소로 연결
git remote add origin https://github.com/<사용자명>/<저장소명>.git
git branch -M main
git push -u origin main
.gitignore가*.csv/*.log/__pycache__를 자동 제외하므로 배포에 필요한 파일만 올라갑니다.
방법 1 — 웹 대시보드 (권장, 가장 쉬움)
vercel.json 이 라우팅을 처리) Deploy 클릭https://<프로젝트>.vercel.app 주소가 나옵니다 → 접속 후 [업데이트] 클릭방법 2 — Vercel CLI
npm i -g vercel
vercel # 최초: 질문 몇 개에 답하면 프리뷰 배포
vercel --prod # 프로덕션(정식 도메인)으로 배포
이후 GitHub main 에 push 할 때마다 자동 재배포됩니다.
git add .
git commit -m "변경 내용"
git push
/api/update)GET /api/update → 모든 섹션을 모아 아래 JSON 반환(개별 섹션 실패는 그 섹션 status 로만 표시):
{
"updated_at": "2026-07-21 14:58:13",
"sections": {
"simmons_news": { "status": "ok", "items": [ { "title": "...", "source": "...", "date": "...", "link": "...", "image": null } ] },
"news": { "status": "ok", "items": [ { "title": "...", "title_ko": "...", "link": "...", "date": "..." } ] },
"competitors": { "status": "ok", "companies": [ { "ticker": "SNBR", "revenue": ..., "revenue_trend": {...} } ] },
"fx": { "status": "ok", "pair": "USD/KRW", "rate": 1481.18, "change_pct": 0.12, "date": "2026-07-20" }
}
}
스폰지 주원료(PPG·TDI·MDI·PO) 월별 시황은 app.js 의 ICIS_DATA 상수로 그립니다(외부 호출 없음).
oil_crude.py 하나가 담당한다. API 키가 필요 없고 비용도 없다.
| 항목 | 값 |
|---|---|
| 소스 | 한국석유공사 PETRONET · 국제석유가격 > 일일국제원유가격 |
| 표 | https://www.petronet.co.kr/v4/excel/KDFQ0100_x2.jsp (GET · 엑셀용 HTML 표) |
| 유종 | 001 Dubai · 002 Brent(ICE) · 003 WTI(NYMEX) · 004 Oman |
| 기준 | y 년 · m 월 · w 주 · d 일 (사이트에는 q 분기도 있으나 화면에선 뺐다) |
| 단위 | $/배럴 · 갱신 화~토 |
| 키 | 불필요 |
유종·기준 코드는 조회 페이지(fmuId=KDFQSTAT · smuId=KDFQ01 · tmuId=KDFQ0100)의
체크박스·라디오에서 직접 읽어 확인했다. 2026-08 이전에 쓰던 KDFQ0200
(일일국제제품가격 · 휘발유/경유/나프타)과는 다른 표다.
2026-02~08(30행)은 오는데 2026-01~08은 빈 응답이 온다. 과거 구간 단독
조회는 정상이므로 '기간이 과거라서'가 아니라 '행이 많아서' 잘리는 것이다.
그래서 주는 6개월씩 끊어 받아 합친다(_fetch_weekly). 년·월·일은 상한에
걸리지 않는다(일 940행 정상).
PETRONET 은 브라우저에서 직접 부를 수 없다(CORS). 그래서 기준별로 넉넉한 구간을
미리 받아 dashboard.json 에 넣어 두고, 프런트가 고른 기간·유종만 잘라 그린다.
[조회] 를 누르기 전에는 결과가 바뀌지 않는다(실제 사이트와 같다).
| 기준 | 수집 구간 | 행 수(2026-08 기준) |
|---|---|---|
| 년 | 27년 | 28 |
| 월 | 15년 | 188 |
| 주 | 3년 (6개월 × 6회) | 188 |
| 일 | 3년 | 940 |
표 아래 요약행은 받아둔 전 구간에서 비교 대상을 찾아 프런트가 계산한다 — 조회 창 밖의 과거가 필요해서다(전년동일비는 1년 전 값을 본다). 하드코딩은 없다.
| 기준 | 요약행 | 비교 대상 |
|---|---|---|
| 일 | 전일비 · 전주비 · 전월동일비 · 전년동일비 · 평균 | 직전 행 · 7일 전 · 1개월 전 · 1년 전 (휴장일이면 그 이전 영업일) |
| 주 | 전주비 · 전월동주비 · 전년동주비 · 평균 | 직전 행 · 4주 전 · 1년 전 같은 월·같은 주차 |
| 월 | 전월비 · 전년동월비 · 평균 | 직전 행 · 1년 전 같은 달 |
| 년 | 전년비 · 평균 | 직전 행 |
★ 이 규칙은 PETRONET 이 스스로 계산해 준 요약행과 대조해 확정했다.
14개 값 중 13개가 소수점까지 일치한다. 전월동주비 는 처음에 '전월 같은 주차'로
뒀다가 불일치가 나서 역산했더니 4주 전이었다.
★ 유일한 불일치는 일별 초장기 구간(940행)의 평균 이다. PETRONET 은 81.77,
계산값은 79.15 인데, 월별 표로 교차검증한 결과 계산값이 맞다
(겹치는 44개월에 대해 일별 평균 = 월별 표, 평균 절대차 0.000).
짧은 구간에서는 PETRONET 값과 정확히 일치한다 — 초장기 일별 조회에서만
PETRONET 쪽이 어긋난다.
oil_crude 는 FETCHERS 에 등록되어 있어 .github/workflows/collect.yml 이
매일 새벽(UTC 20:00 = KST 05:00) 받아 public/data/dashboard.json 에 커밋한다.
화~토 새 값이 올라오면 다음 실행에서 표와 그래프에 자동 반영된다.
oil_product 와 같은 그룹에 두어 petronet.co.kr 요청이 동시에 나가지 않는다.
수동 확인:
python oil_crude.py # 4개 기준을 모두 받아 행 수·요약행을 출력
oil_product.py. 원유 카드와 같은 조회 UI(기준선택 → 기간 → 제품 → [조회])이고
대상만 석유제품이다. 키·비용 없음.
| 항목 | 값 |
|---|---|
| 페이지 | 국제석유가격 > 일일국제제품가격 (tmuId=KDFQ0200) |
| 제품 | B001 휘발유(95RON) · B007 휘발유(92RON) · C001 등유 · D001 경유(0.5%) · D008 경유(0.05%) · D009 경유(0.001%) · E001 고유황중유(180cst/3.5%) · E008 고유황중유(380cst/3.5%) · F001 나프타 |
| 단위 | $/배럴 (표 머리 (단 위 : $/배럴) 실측) |
| 기준 | 년 · 월 · 주 · 일 (기본 일) · 기간은 일 단위까지 고른다 |
원유는 excel/KDFQ0100_x2.jsp 를 GET 하면 되지만, 제품은 그 엔드포인트가
리다이렉트(v3/return.jsp)만 돌려준다 — term 을 무엇으로 주든 80바이트짜리
스크립트가 오고, 세션을 먼저 만들어도 마찬가지다(실측). 그래서 제품은
sub.jsp 에 조회 POST 를 보내 결과 페이지째 받아 파싱한다.
경유(0.5%)는 2012년 12월 1일 게시 중단이라(페이지 각주) 최근 구간을 조회하면 그 열이 통째로 빠져 9개를 요청해도 8열이 온다. 위치로 맞추면 값이 한 칸씩 밀린다. 그래서 헤더 라벨 → 키로 매핑한다. 기본 체크에서도 이 제품만 빼 두었다(체크박스는 그대로 있어 과거 구간에서 켤 수 있다).
| 표 | 상한 | 대응 |
|---|---|---|
| 원유(KDFQ0100) | 주가 약 30행 | 주를 6개월씩 끊어 받음 |
| 제품(KDFQ0200) | 일이 약 316행 | 일을 6개월씩 끊어 받음 |
같은 PETRONET 이라도 잘리는 축이 다르므로 표별로 실측해서 정한다.
원유에서 확정한 규칙을 그대로 쓰고, 제품 표에서도 PETRONET 자체 계산값과 대조했다. 비교행(전일비·전주비·전월동일비·전년동일비·전월비·전년동월비·전주비·전월동주비·전년동주비·전년비) 10개가 전부 일치한다.
평균에서 어긋난 것 3건은 원인을 규명했고 모두 계산 쪽이 맞다.
| 어긋난 것 | 원인 |
|---|---|
| 일별 평균 | PETRONET 요약은 마지막 청크 구간것이라 비교 대상이 달랐다. 같은 구간으로 맞추면 8개 열 전부 소수점까지 일치 |
년·월 평균의 나프타 0.0 | 결측이 0행인데도 PETRONET 이 0.0 을 준다 — PETRONET 쪽 오류(값 있는 31/188행 평균은 58.32 / 72.39) |
oil_product 를 FETCHERS 에 등록 → collect.yml 이 매일 새벽 받아 커밋한다.
oil_crude 와 같은 그룹에 두어 petronet.co.kr 요청이 순차로 나간다.
python oil_product.py # 4개 기준을 모두 받아 행 수·요약행을 출력
us_ppi.py 하나가 담당한다. API 키가 필요 없고 비용도 없다.
| 항목 | 값 |
|---|---|
| 소스 | 미국 노동통계국(BLS) 공개 API v1 — https://www.bls.gov/developers/ |
| 시리즈 | PCU337910337910 — PPI industry data for Mattress mfg, NSA (NAICS 337910) |
| 기준월 | 1983년 6월 = 100 (data.bls.gov 시리즈 페이지 Base Date = 198306 실측 확인) |
| 구간 | 2016년 1월 ~ 현재 · 월별 → 연평균으로 집계해 선 그래프 하나 |
| 키 | 불필요 |
이 시리즈는 스프링·폼·에어·특수 매트리스를 하나로 평균한 값이고, BLS 는 NAICS 337910 아래에 가격대별 하위 시리즈를 발표하지 않는다. 지수 안에서 "프리미엄만" 떼어낼 방법이 데이터 구조상 없다.
★ 예전 화면의 하이엔드/프리미엄 버튼은 가격대가 아니라 HS 코드(소재) 를
전환하는 것이었다 — 하이엔드=940421(폼·라텍스), 프리미엄=940429(스프링 등).
가격대 근거가 없는 라벨이라 2026-08 에 걷어냈다. 함께 제거한 것:
tiers.py · us_market.py · us_trade.py · italy_trade.py · italy_hicp.py
· europe_flow.py · public/data/{us,italy}-trade.json.
(이탈리아 Eurostat HICP 지표는 이번 개편 대상에서 제외했다 — 다시 필요해지면
별도로 붙인다.)
| 한 요청 기간 | 10년까지 — 2016~2026 을 한 번에 넣으면 "Year range has been reduced to the system-allowed limit of 10 years." 가 붙고 뒤쪽 연도가 잘린다 |
| 그래서 | CHUNK_YEARS = 10 으로 쪼개 2회 호출한다(2016 |
| 일 호출 한도 | 25회 / IP — 하루 1회 자동 수집에서 2회만 쓴다 |
| 잠정치 | 최근 4개월은 footnote code="P" (Preliminary). 공표 후 4개월까지 개정된다 |
증분 저장을 두지 않는다 — 매 실행에서 2016년부터 전 구간을 다시 받으므로 BLS 개정치가 자동으로 반영된다.
차트 아래 13행 설명표는 us_ppi.py 가 payload["table"] ([{k, v}]) 로 계산해
보내고, app.js 의 gPpiTable() 이 그대로 그린다. 숫자를 하드코딩하지 않는다.
| 표의 행 | 어디서 나오나 |
|---|---|
| 최근 값 / 몇 배 | 마지막 월 관측치와 값 ÷ 100 |
2016~2020년 흐름 등 | BAND_EDGES 구간의 첫 달 → 마지막 달 월값 + 연 환산 변화율로 고른 추세 문구 |
정체 구간의 248~255 | 그 구간 월값의 최소~최대 (한 점만 찍으면 '정체'라는 말과 어긋나 보인다) |
| 진행 중인 해 | 부분 연도 첫 달 → 마지막 달 + 잠정치 개월 수 |
| 한 줄 요약 | 전체 방향 + 연 환산 변화율이 가장 컸던 완전 연도 구간 |
구간 경계(BAND_EDGES)만 편집 판단이다. 마지막 구간은 None 이라 마지막 완전
연도까지 스스로 늘어나고, 진행 중인 해 행은 해가 바뀌면 자동으로 다음 해로
옮겨 간다 — 새 데이터가 들어와도 표를 손댈 필요가 없다.
us_ppi 는 FETCHERS 에 등록되어 있어 KOIMA 월간지수(koima_index)와 똑같이
.github/workflows/collect.yml 이 매일 새벽(UTC 20:00 = KST 05:00) 돌면서 받아
public/data/dashboard.json 에 커밋한다. BLS 가 새 달을 발표하면 다음 실행에서
그래프와 설명표에 자동 반영된다(매달보다 촘촘한 주기이고, 내용이 바뀌지 않은
날은 커밋하지 않는다).
수동 확인:
python us_ppi.py # BLS 호출 → 연평균·구간·설명표를 JSON 으로 출력
public/data/simmons-market.json 한 파일이 국내 섹션 세 차트의 유일한 소스다.
차트에 값을 하드코딩하지 않으므로 이 파일만 고치면 배포 시 반영된다.
| 차트 | 내용 | JSON 키 |
|---|---|---|
| A | 시몬스 최근 실적 추이 (매출 막대 + 영업이익 라인) | simmonsPerformance |
| B | 3사 지난해 매출 비교 + 증감률 | competitorRevenueLastYear |
| C | 시장 점유율 도넛 | marketShare2025 |
★ 시몬스는 비상장사다. DART 등 공시 API 로 자동 조회가 되지 않는다. 수치는 실적 발표 기사(헤럴드경제·아주경제·인더스트리뉴스 등)를 보고 수기 입력한 값이며, 같은 사실을 차트 A 우측 상단 ⓘ 툴팁에 표시한다.
isProvisional: true 로 두면 잠정치 배지가 뜬다(확정 실적 발표 전 1~2월용).
연도별로 표시하려면 해당 simmonsPerformance 항목에 "provisional": true 를 넣는다.
.github/workflows/check-simmons-earnings.yml → scripts/check-news-alert.js
★ API 키도 결제도 필요 없다. 예전에 있던 Anthropic API 기반 "자동 검색 + 자동 merge" 파이프라인은 걷어냈다. 이제 이 자동화는 알림만 보낸다 — 데이터 파일은 건드리지 않고, 사람이 Issue 를 보고 손으로 반영한다.
쓰는 것
| 뉴스 검색 | Google 뉴스 RSS news.google.com/rss/search — 키 불필요 |
| HTTP | Node 내장 https 모듈 (라이브러리 설치 없음) |
| XML 파싱 | 정규식으로 <item> 의 <title>·<link> 만 추출 (파서 불필요) |
| Issue 생성 | GitHub REST API · 기본 GITHUB_TOKEN |
실행 시점 — 3월 131일 + 4월 115일 매일 06:00 UTC(15:00 KST).
cron 은 이 구간을 한 줄로 못 쓰므로 두 줄로 나눴고, cron 은 월·일만 거르므로
시즌을 벗어난 실행은 첫 스텝에서 즉시 skip 한다. 수동 실행도 된다 —
dry_run(Issue 안 만듦) / force(시즌 밖에서도 실행).
동작
simmonsPerformance 의 마지막 연도 + 1 을 계산한다.
연도를 하드코딩하지 않으므로 2027년에도 코드를 고칠 필요가 없다.시몬스 {연도}년 매출 /
에이스침대 {연도}년 매출 / 코웨이 {연도}년 매트리스 매출매출 또는 억원 이 있는 것만 남긴다.
(예: 시몬스 "작년 매출 3천239억원" 은 연도 숫자가 없어 걸러진다 — 지난해 기사다)public/data/.seen-news-urls.json 과 대조해 새 링크만 남긴다.
같은 기사로 매일 중복 알림이 가지 않게 한다.Issue 본문에는 검색어별 기사 제목·링크와 반영 체크리스트가 들어간다. RSS 검색은 관련도 기준이라 분기 실적 기사가 섞일 수 있어서, 체크리스트에 "연간(12월 결산) 수치인가", "서로 다른 언론사 2곳 이상에서 같은 수치인가" 를 넣어 뒀다.
필요한 권한 — 기본 GITHUB_TOKEN 으로 충분하다(issues: write + contents: write).
따로 발급할 시크릿이 없다.
로컬 테스트 (Issue 생성·파일 쓰기 없이 미리보기만):
node scripts/check-news-alert.js --dry-run
★ .github/workflows/collect.yml 에는 ANTHROPIC_API_KEY 참조가 남아 있는데
이건 실적 알림과 무관하다. 예측 카드의 해설 문장(icis_forecast.py /
sr_forecast.py)이 쓰는 것이고, 키가 없으면 통계 예측값만
표시하는 폴백으로 동작한다. 그래서 지우지 않았다.
'시몬스 코리아 소식' 카드 바로 아래에 공식 인스타그램 최신 게시물 4개를 카드로 보여준다.
| 파일 | 역할 |
|---|---|
api/instagram.py | Apify 호출 · 필드 매핑 · 캐시 갱신. 서버리스 진입점 + CLI 겸용 |
.github/workflows/collect-instagram.yml | 하루 2회(KST 09:00 / 18:00) 수집 → 커밋 |
public/data/instagram.json | 화면이 읽는 캐시. 워크플로가 만든다 |
app.js renderInstagram() / styles.css .ig-* | 카드 렌더 |
.env.example 에 이름만 적어 뒀다. 실제 값은 세 군데에 각각 넣는다.
| 어디 | 무엇 |
|---|---|
| 로컬 | .env.example → .env 로 복사 후 값 입력 (.env 는 .gitignore 대상) |
| Vercel | Project → Settings → Environment Variables |
| GitHub Actions | Settings → Secrets and variables → Actions → APIFY_TOKEN (시크릿), INSTAGRAM_USERNAME (변수, 선택) |
APIFY_TOKEN 은 https://console.apify.com/settings/integrations 에서 발급한다.
키가 없어도 빌드·실행은 깨지지 않는다 — status='no_key' 를 돌려주고 카드에
"Instagram 연동 준비 중 (APIFY_TOKEN 필요)" 이라고 정직하게 띄운다.
refresh_cache() 는 캐시의 첫 게시물 id 와 새로 받아온 첫 게시물 id 를 비교한다.
같으면 새 글이 없다는 뜻이므로 파일을 다시 쓰지 않고(changed=False), 워크플로의
커밋 단계도 git diff --quiet 에서 걸려 건너뛴다. 수집이 실패하면 기존 캐시를
절대 덮어쓰지 않는다 — 멀쩡한 데이터를 잃지 않기 위해서다.
Vercel 서버리스는 파일시스템이 읽기 전용이다(/tmp 만 쓸 수 있고 인스턴스가 죽으면
사라진다). 그래서 캐시 파일을 남길 수 없다. 저장소에 커밋해 두는 방식이 실제로 남는
유일한 캐시이고, 이 저장소의 다른 수집기(collect.yml, collect-koima-price.yml)와도
같은 구조다. vercel.json 의 crons 는 같은 시각에 /api/instagram 을 깨우는
헬스 핑이며 Apify 를 부르지 않는다(크레딧 0).
python api/instagram.py # 수집 → public/data/instagram.json 갱신
curl localhost:5000/api/instagram # 캐시 그대로 (Apify 호출 없음)
curl "localhost:5000/api/instagram?refresh=1" # 강제 재수집 (크레딧 소모)
displayUrl 은 인스타그램 CDN 의 서명된 주소라 시간이 지나면 만료된다(404/403).
그래서 카드는 로드 실패를 정상 경로로 다뤄 "이미지를 불러올 수 없습니다" 대체 블록을
보여주고, 캡션과 링크는 그대로 남긴다. 하루 2회 수집이 주소를 새로 받아 온다.