Easy-to-manage touch screens
Free, open-source touch screens. Guided USB setup, a drag-and-drop editor and local control. Made for Home Assistant.
사주의 근거부터 두 사람의 궁합과 사주풀이까지 차분하게 살펴봅니다.
Translation pending
LifestyleTypeScript · PLpgSQLGitHub @sungheeyoonOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
생년월일시로 사주 8자를 도출하는 만세력 엔진. 천문 계산·시간대 보정을 순수 함수로 분리해 서버 없이 브라우저에서 실행된다.
Translation pending
Taken from the repository README
Loading the full README. The saved excerpt is shown below.
생년월일시로 사주 8자를 도출하는 만세력 엔진. 천문 계산과 시간대 보정을 순수 함수 패키지로 분리해, 서버 없이 브라우저에서 실행된다. 사주 계산은 "정답이 하나"가 아니다. 경도를 보정할지, 23시 출생자를 어느 날로 볼지는 학파마다 갈린다. 그래서 갈리는 지점을 옵션으로 설계하고, 계산 결과에 적용된 규칙을 함께 반환한다. 다른 서비스와 결과가 다를 때 왜 다른지 설명할 수 있다. React/Next 의존성은 app/ 에만 있다. src/lib/saju 는 순수 TypeScript다. 층 넷과 방향은 docs/architecture.md 가 들고, 시험과 린트가 잠근다(ADR 0085). 로그인 없이 열리는 화면은 / 하나다 — 한 사람의 생년월일시를 넣어 만세력을 본다. 거기서 시작하는 궁합(/compat 에서 두 사람을 정하고 /me/compat 에서 두 명식을 본다)과 저장·풀이·인연은 전부 로그인한 사람의 것이다. 비로그인에게 두 사람의 만세력을 나란히 보여 주지 않는다. 화면이 무엇을 하는지는 docs/prd.md 가 답한다 — 이 README 는 엔진과 저장 규율을 든다.Read the full README ↗
사람이 처음(또는 오랜만에) 이 저장소를 연다면
docs/guide.md— 일하는 방식과 사내 용어를 쉬운 말로 풀었다.
생년월일시로 사주 8자를 도출하는 만세력 엔진. 천문 계산과 시간대 보정을 순수 함수 패키지로 분리해, 서버 없이 브라우저에서 실행된다.
사주 계산은 "정답이 하나"가 아니다. 경도를 보정할지, 23시 출생자를 어느 날로 볼지는 학파마다 갈린다. 그래서 갈리는 지점을 옵션으로 설계하고, 계산 결과에 적용된 규칙을 함께 반환한다. 다른 서비스와 결과가 다를 때 왜 다른지 설명할 수 있다.
const saju = computeSaju(
{ year: 1990, month: 5, day: 15, hour: 14, minute: 30, second: 0 },
{ longitude: CITY_LONGITUDES.서울, useLongitude: true },
);
saju.pillars.day.name; // '庚辰'
saju.analysis.strength.verdict; // 'strong'
saju.meta.corrections; // 적용된 보정 내역 (표준자오선·경도·…)
saju.meta.warnings; // 절기 경계 등 결과가 뒤집힐 수 있는 지점
src/lib/saju/
constants/ 천간·지지·60갑자·지장간·합충형해
solarTerms.ts 절기 (태양 황경 도달 시각)
civilTime.ts 절대 시각 ↔ 달력 시각
lunar/ 음력 표 (KASI 에서 뽑아 커밋한다)
timeCorrection/ 표준자오선 이력·서머타임·경도·균시차
pillars/ 4주 도출 (연월일시)
analysis/ 오행 분포·십성·신강신약·억부·조후·통관·격국·종격
relations/ 원국 안의 형충회합
sinsal/ 신살
daeun/ saeun/ wolun/ now/ 운 — 대운·세운·월운, 그리고 지금 도는 칸
compat/ 두 원국 사이의 관계 (궁합)
text/ L3 발화 — 계약·조각·말뭉치·조립기
evidence/ AI 에 넘길 근거 한 벌 (계약을 값으로 데리고 나간다)
golden/ 경계 케이스 골든 스냅샷
src/lib/
input/ 입력 한 벌 ↔ 주소 ↔ 엔진 입력, 저장된 판본 되읽기
matching/ discovery/ 오행 두 축과 후보 노출 정책 (`discovery-v1`)
reading/ 풀이 계약 — 프롬프트·출력 검사·점수 조정폭
people/ profile/ account/ consent/ 저장·프로필·계정 상태·동의 문장
React/Next 의존성은 app/ 에만 있다. src/lib/saju 는 순수 TypeScript다. 층 넷과 방향은
docs/architecture.md 가 들고, 시험과 린트가 잠근다(ADR 0085).
로그인 없이 열리는 화면은 / 하나다 — 한 사람의 생년월일시를 넣어 만세력을 본다.
거기서 시작하는 궁합(/compat 에서 두 사람을 정하고 /me/compat 에서 두 명식을 본다)과
저장·풀이·인연은 전부 로그인한 사람의 것이다. 비로그인에게 두 사람의 만세력을 나란히
보여 주지 않는다. 화면이 무엇을 하는지는 docs/prd.md 가 답한다 — 이
README 는 엔진과 저장 규율을 든다.
입력 폼과 주소 코덱은 화면이 몇이든 같은 것을 쓴다(app/birth-form.tsx·
src/lib/input/query.ts). 같은 값을 넣었는데 화면마다 다른 사주가 나오는 일을 막기 위해서다.
제출한 입력은 컴포넌트가 아니라 주소창이 들고 있다(src/lib/input/query.ts). 그래서 결과
화면을 그대로 링크로 줄 수 있고 새로고침·뒤로가기에도 같은 명식이 나온다. 자시 규칙
하나로 일주가 바뀌므로 기본값도 빼지 않고 전부 적는다 — 나중에 기본값을 옮겨도 이미
나눠 준 링크가 그때 본 것을 그대로 보여줘야 하기 때문이다.
단, 쿼리스트링이 아니라 # 뒤에 싣는다(app/hash-query.ts). 쿼리스트링은 HTTP 요청
라인에 실려 매 요청마다 서버 로그에 남고, 카카오톡·슬랙에 링크를 붙여 넣으면 그 서비스의
미리보기 크롤러가 URL 을 fetch 하면서 두 사람의 생년월일시를 통째로 가져간다. fragment 는
요청에 아예 포함되지 않으므로 링크 공유는 그대로 되면서 서버도 크롤러도 못 본다. 옛 ?
링크는 열자마자 # 으로 갈리고, e2e 가 「제출한 생년월일이 어떤 요청에도 실리지
않는다」를 직접 잠근다. 계산은 여전히 브라우저 안에서만 일어난다.
절기(연주·월주)는 태양 황경 도달이라는 천문 사건이므로 절대 시각으로 판정한다. 시주·일주는 그 자리의 태양 높이가 정하므로 보정된 지방시로 판정한다.
경도 보정을 절대 시각에 적용하면 절입 판정이 30분씩 밀려 입춘 근처에서 연주가 통째로 뒤집힌다. 그래서 절대 시각은 건드리지 않고, 지방시 보정은 "달력 시각을 읽을 때 더할 오프셋"으로 표현한다.
2025-02-03 23:11 (입춘 23:10:29 직후)
경도 보정 X 甲子 甲辰 戊寅 乙巳
경도 보정 O 癸亥 癸卯 戊寅 乙巳
↑ ↑ └────┴── 연주·월주 그대로
시주·일주만 바뀜
서머타임과 표준자오선을 따로 관리하면 1955~60년에서 반드시 어긋난다. 이 시기 서머타임은 UTC+9:00이 아니라 UTC+8:30 위에 얹혀 +9:30이었기 때문이다.
그래서 IANA tz 데이터(Asia/Seoul)에서 29개 구간을 생성해 커밋한다
(scripts/generate-zone-history.mjs). 런타임에 Intl 을 쓰지 않는 이유는 브라우저마다
탑재된 tzdata 버전이 달라 같은 입력에 다른 답이 나올 수 있어서다.
부수 효과로 1954~61년에는 표준자오선이 127.5°가 되어 서울 경도 보정이 -32분에서 -2.1분으로 자동으로 줄어든다.
verdict: 'strong' 만 주지 않고 득령·득지·득세 각각의 충족 여부와 사유를 함께 낸다.
자연어 해석은 이 구조화된 결과를 조회해서 조립하는 별도 층의 몫이다.
전부 옵션이고, 선택한 값이 meta 에 기록된다.
| 항목 | 엔진 기본값 | 비고 |
|---|---|---|
| 경도 보정 | 켬 | 지방평균태양시 |
| 균시차 | 끔 | ±16분이라 시지를 넘길 수 있다 |
| 서머타임 되돌리기 | 켬 | |
| 자시 규칙 | 조자시 (경계 23:00) | 야자시는 자정 경계 |
| 신강 판정 | 3기준 중 2개 | 임계·가중치 조정 가능 |
화면의 기본값은 엔진의 기본값과 다르다. 폼은 시간 기준을 셋 중 하나로 받고
기본은 진태양시(경도 + 균시차)다 — 국내 만세력이 대체로 그렇게 세우기 때문이다
(TIME_BASIS, ADR 0057). 엔진 쪽 기본을 같이 옮기지 않은 것은, 옵션을 안 적고 부르는
호출부의 답이 조용히 바뀌지 않게 하려는 것이다. 화면은 고른 값을 주소에 그대로 싣는다.
午 지장간 중기 일수, 午未 합화 오행, 지지의 체/용 음양처럼 코드에 주석으로만 남긴
이설도 있다.
계정과 Person 은 Supabase 에 있다(supabase/migrations/). 접근 판정을 앱 코드가 아니라
DB 불변식으로 건 이유는 하나다 — 앱에 조건을 적으면 그 조건을 안 부르는 경로가
언젠가 하나 생기고, 그 경로는 조용히 열려 있다.
create_self_person·create_managed_person)이 그 상태를 거절한다. 화면 관문은 길만
가리키고, 막는 일은 여전히 DB 가 한다viewer 로 내린다may_edit_person_input). RLS 정책과 RPC 가 같은
함수에 묻는다 — security definer 함수는 RLS 를 지나가므로, 정책에만 적어 두면 RPC 로
들어오는 길이 아무 검사 없이 열린다request_match 는 상대 하나만 받고, 그 사람이 내
후보 목록에 실제로 떴다는 기록이 없으면 거절한다. 판본·추천 이유·정책 버전은 앱이
실어 보내지 않고 함수가 그 자리에서 읽는다(ADR 0009)discovery_eligible). 후보 목록과
요청이 같은 함수에 묻는다 — 두 곳에 적었더니 성별 조건 하나가 요청 쪽에서 빠져,
조건을 바꿔 목록에서 사라진 사람에게 옛 기록으로 청할 수 있었다definer
함수뿐이고, 그 함수가 내주는 것이 곧 브라우저가 볼 수 있는 것이다 — 닉네임·소개·
사진이 있는지·채우는 오행·균형 한 줄. 생년월일시도 여덟 글자도 점수도 그 반환형에 없다may_see_photo 가 답한다 — 그 조건은 이름이 서는 자리들과
같은 것을 묻는다. 못 보는 사람은 없는 사람과 같은 404 를 받는다정책 시험은 authenticated 역할로 갈아입고 돈다. 표 소유자로 재면 RLS 를 그냥 지나가서
「막힌다」를 한 번도 못 잰 채 전부 통과하기 때문이다. 실제로 정책을 하나 꺼 보고 시험이
무너지는 것까지 확인했다.
DB 안에서만 도는 시험으로는 못 재는 것이 둘 있다. 가입 경로가 관문 함수를 정말
거치는지(설정 한 줄이면 안 거친다), 그리고 화면이 쓰는 질의가 실제로 도는지다 — 정책이
옳아도 질의 모양이 틀리면 화면은 빈 채로 나오고, 빈 화면은 「저장 안 됨」과 구별되지
않는다. 그래서 npm run test:flow 가 진짜 Supabase 에 대고 가입 → 거절 → 저장 →
되읽기 → 남에게 안 보임 → 후보 노출 → 요청·수락을 한 바퀴 돈다.
저장된 입력은 storedChartOf 가 입력 한 벌과 그 명식으로 세운다
(src/lib/input/stored.ts). 이 자리가 틀리면 저장하기 전에 본 사주와 저장한 뒤에 보는
사주가 달라지는데, 사용자가 알아채기 전에는 아무도 모르는 종류의 어긋남이다. 그래서
시험이 값이 같은지가 아니라 여덟 글자가 같은지를 잰다.
문이 둘이다(ADR 0072). 읽는 문(app/me/person-input.ts)이 person 에서 행을 집어
오고 — 없거나 못 보면 null 하나로 답한다 — 세우는 문이 그 값을 받아 명식까지 세운다.
세우는 문은 DB 를 모르므로 얼린 생성 작업(reading_job.birth_a)도 같은 문을 지난다.
못 읽는 입력은 기본값으로 메우지 않고 값으로 못 읽는다고 말한다(unreadable-input).
모르는 출생지를 서울로 치면 저장할 때 본 것과 다른 사주가 같은 화면에 나온다. 반대로 그
밖의 계산 오류는 값으로 안 바꾸고 그대로 던진다 — 사용자가 할 수 있는 것이 없는 오류를
「못 읽습니다」로 얌전히 적으면, 그 행이 어떻게 들어왔는지 아무도 영영 안 묻는다.
명식 계산은 익명 화면과 로그인 화면이 같은 함수를 부른다(src/lib/input/chart.ts). 엔진이 순수
TypeScript 라 브라우저에서도 서버에서도 그대로 돈다.
무엇을 고쳤을 때 무엇을 돌리는가는 docs/agents/test-map.md 가 답한다. 아래는 명령과 엔진의 검증 근거다.
npm test # 단위 시험 — 수는 실행이 찍는다
npm run verify # test + typecheck + lint + production build
npm run db:start # 로컬 Supabase (Docker) — test:db · test:flow · test:e2e:authed 가 쓴다
npm run test:db # pgTAP — 정책이 실제로 막는지. 수는 TAP plan 이 찍는다
npm run test:flow # 흐름 검사 7벌 — 가입부터 요청·수락·풀이까지. 수는 러너가 찍는다
npm run test:e2e # 익명 화면, desktop + mobile Chromium
npm run test:e2e:authed # 로그인·인연·안내 관문 — 전부, 한 워커
npm run test:e2e:signed-in # 차선 하나만 — match · notice 도 같은 꼴 (CI 는 이 차선들을 나란히 돈다, ADR 0082)
e2e 가 두 명령인 것은 계약이다. 로그인하지 않은 사람을 돌려보내는 데 백엔드가 필요하면
그것부터 잘못이라, test:e2e 는 CI 의 껍데기 접속값으로도 돈다. 로그인 흐름
(e2e/signed-in.spec.ts)만 로컬 스택을 요구한다 — 초대된 계정을 로컬에 만들어 그 세션으로
브라우저를 몰고, 스택이 없으면 그렇게 말하고 죽는다. 요청·수락·차단처럼 둘이 있어야
성립하는 흐름(e2e/match.spec.ts)과 채팅(e2e/chat.spec.ts)은 창을 둘 연다.
src/lib/saju/golden/golden.snapshot.txt — 몇 건인지는 그 머리의 「케이스 N건」이 찍는다)종격 아님·후보·가종·진종)를 낸다. 문턱은 고전이
정한 숫자가 아니다. 무작위 3000건의 세력 분포를 재고 정했고, 축은 자당(비겁+인성)
몫 하나다 — 밖으로 종하면 30% 이하, 안으로 종하면 70% 이상. 그 측정 근거를
dominance.calibration 에 값으로 남긴다. v2 는 문턱을 안 건드리고 세는 법을 셋
고쳤다(국(局)과 합화를 세력에 반영 · 뿌리를 개수가 아니라 질로 · 충에 뽑히거나 국에
끌려간 뿌리는 얕게). 같은 3000건의 발화율이 10.9%에서 10.6%로 오히려 내려간 것이
그 증거다 — 발화율을 함께 재지 않으면 재현율이 오른 것이 세는 법 때문인지 문턱을
헐겁게 해서인지 구별할 수 없다. 억부 후보는 뒤집지 않는다(eokbuOverride: 'disabled'): 억부는 외부 사례를
대조하고도 시험값인데, 그 답을 정반대로 뒤집는 판정이 외부 대조 0건으로 위에 설 수 없다.
진종·가종 어느 쪽으로도 밀기 어려운 명식은 후보로 남긴다 — 억지로 밀어 넣으면 판정이
아니라 반올림이 된다.
축은 자당(비겁+인성) 몫 하나다. 종에는 방향이 둘이라(從財·從殺은 밖으로, 從强·從旺은
안으로) 두 비율이 아니라 한 축의 양끝으로 잰다 — 자당과 이당은 합이 1 이기 때문이다.
처음에는 으로 쟀는데 그 축에서는 안으로 종이 구조적으로 0.5 를
넘을 수 없어 영영 잡히지 않았다(고전 명례로 확인)상수 테이블은 도메인 불변식으로 검증한다. 예를 들어 지장간은 "정기의 오행 = 그 지지의 오행", "생지·묘지의 중기 = 그 지지가 속한 삼합국의 오행" 을 만족해야 하므로, 표에서 한 글자만 틀려도 테스트가 잡는다.
폐쇄 초대 MVP 를 운영하는 절차 — 초대·이용 정지·신고 조회·탈퇴 대기 처리·AI 호출량과
실패 조회 — 는 docs/ops/runbook.md 에 SQL 로 적혀 있다. 화면은
아직 없다. prd-archive 가 초기에는 UI 대신 감사 가능한 절차를 쓸 수 있다고 했고 지금이 그
단계다.
Asia/Seoul 표준시 이력, 주요 도시 10곳 경도)Next.js 16 (App Router) · TypeScript · Tailwind CSS v4 · Vitest · Playwright · astronomy-engine
지배 ÷ (지배 + 일간편)eokbuOverride: 'disabled'). 막고
있는 것이 이제 재현율이 아니라 둘이다: 무작위 3000건에서 진종·가종이 10%대로 나와
고전이 말하는 희소성(《적천수천미》 "百無一二")과 자릿수가 다르고, 출처가 종격이 아니라고
못박은 넷 중 하나를 아직 가종으로 본다. 문턱을 자료에 맞춰 낮추지 않은 것도 같은
이유다 — 저자의 판정이 우리가 안 보는 것(합충의 파격, 지장간 여기의 질)에 기대고 있어
압도 비율만 내리면 일반 명식까지 쓸려 들어온다analysis/rootedness.ts) — 억부·종격·격국이 모두 먹고
들어가는 재료다. 음양이 다른 뿌리(甲이 卯의 乙에 두는 것)와 고지의 중기를 거르지 않고
kind·role·days 로 구분해 남긴다. 어디까지 통근으로 볼지가 계통 갈림이라 거르는
쪽을 기본값으로 삼으면 좁게 읽는 쪽이 되살릴 수 없다. 뿌리의 강약은 매기지 않는다COMPAT_POLICY.scoring: 'not-scored') — 맞춰볼 외부
정답이 없다. 두 원국 사이에 무엇이 어떻게 걸렸는지만 내고, 각자의 원국 안에서 닫힌
관계는 뺀다. 계산판이 다르므로 거리·인접은 언제나 null 이라 자리로 말한다.
사용자가 보는 점수는 엔진 밖 두 정책에서 난다(ADR 0003) — 후보 목록의 discovery-v1
이 오행 두 축으로 기준점을 내고(previewScoreOf), 궁합 풀이의 점수는 그 수에서
항목별로 움직인다(ADR 0059·0060). 그 수는 evidence 안이 아니라 옆에 실린다 —
안에 넣으면 엔진이 점수를 낸 것처럼 읽힌다unresolved). 자평 계열 한 계통
74건 중 72건으로 대조해 이름 하나로 맞힌 것이 63건이다. 계통이 하나뿐이라 게이트는
닫혀 있다. 통관도 같은 자리에 있다 — 세기는 하되 억부·조후와 갈리는지를 말할 뿐이다天文=辰과 구분해
天門=戌亥의 방위 근거와
현대 명리의 戌亥=天門星를
대조해 두 글자의 동시 성립으로 고정했다. 이름이 비슷한 천의성(天醫星) 은 월지의
앞 지지라는 별개 항목이라 따로 넣었고, 지지에 없으면 천간으로 대신 본다는 변형은 뺐다.
홍염살은 정통표 한 벌만 쓰고
甲乙庚壬에 申을 더하는 계통은 넣지 않는다. 암록은 표를 옮기지 않고 건록의 육합
으로 센다 — 양인·금여·문창과 같은 축이다子酉·寅未 ↔ 원진 子未·寅酉). 합치면 어느 표에서 나온 사실인지
되짚을 수 없고 다른 두 쌍이 조용히 사라지므로, 겹치는 자리에서는 두 줄이 함께 나온다.
자리는 네 지지의 모든 쌍을 본다(ghostGateBasis: 'all-branch-pairs') — 일지 기준으로
좁히는 계통은 채택하지 않았고, 좁혀 읽고 싶으면 participants 의 자리로 거르면 된다.
둘 다 신살 표에도 이름이 오르지만 규칙은 관계 쪽 하나뿐이다 — 값을 옮겨 담기만 하고
자리가 갈릴 수 없음을 60갑자 전수 테스트가 잠근다