Skip to content
sungheeyoonPublic

About

만날지도 — 사주·궁합·인연 서비스. Next.js · Supabase · Expo. AI 코딩 에이전트로 만들고 테스트·권한 규칙으로 검증

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

1,592 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

만세력 (Saju Engine)

만날지도 — 이 저장소가 돌리는 사주 · 궁합 · 인연 서비스다(웹 · iOS · Android, 공개 출시). 구현은 AI 코딩 에이전트가 맡고, 사람은 무엇을 만들지 정하고 결과를 검증한다. 에이전트가 따르는 규칙은 docs/start.md 와 docs/agents/delegation.md, 결정은 docs/adr/ 에 있다.

사람이 처음(또는 오랜만에) 이 저장소를 연다면 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 가 「제출한 생년월일이 어떤 요청에도 실리지 않는다」를 직접 잠근다. 계산은 여전히 브라우저 안에서만 일어난다.

설계에서 핵심이었던 세 가지

1. 사주는 서로 다른 두 시계를 쓴다

절기(연주·월주)는 태양 황경 도달이라는 천문 사건이므로 절대 시각으로 판정한다. 시주·일주는 그 자리의 태양 높이가 정하므로 보정된 지방시로 판정한다.

경도 보정을 절대 시각에 적용하면 절입 판정이 30분씩 밀려 입춘 근처에서 연주가 통째로 뒤집힌다. 그래서 절대 시각은 건드리지 않고, 지방시 보정은 "달력 시각을 읽을 때 더할 오프셋"으로 표현한다.

2025-02-03 23:11 (입춘 23:10:29 직후)
  경도 보정 X   甲子 甲辰 戊寅 乙巳
  경도 보정 O   癸亥 癸卯 戊寅 乙巳
                 ↑    ↑    └────┴── 연주·월주 그대로
               시주·일주만 바뀜

2. 표준시 이력은 손으로 짜면 틀린다

서머타임과 표준자오선을 따로 관리하면 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분으로 자동으로 줄어든다.

3. 판정이 아니라 근거를 반환한다

verdict: 'strong' 만 주지 않고 득령·득지·득세 각각의 충족 여부와 사유를 함께 낸다. 자연어 해석은 이 구조화된 결과를 조회해서 조립하는 별도 층의 몫이다.

학파에 따라 갈리는 지점

전부 옵션이고, 선택한 값이 meta 에 기록된다.

항목 엔진 기본값 비고
경도 보정 켬 지방평균태양시
균시차 끔 ±16분이라 시지를 넘길 수 있다
서머타임 되돌리기 켬
자시 규칙 조자시 (경계 23:00) 야자시는 자정 경계
신강 판정 3기준 중 2개 임계·가중치 조정 가능

화면의 기본값은 엔진의 기본값과 다르다. 폼은 시간 기준을 셋 중 하나로 받고 기본은 진태양시(경도 + 균시차)다 — 국내 만세력이 대체로 그렇게 세우기 때문이다 (TIME_BASIS, ADR 0057). 엔진 쪽 기본을 같이 옮기지 않은 것은, 옵션을 안 적고 부르는 호출부의 답이 조용히 바뀌지 않게 하려는 것이다. 화면은 고른 값을 주소에 그대로 싣는다.

午 지장간 중기 일수, 午未 합화 오행, 지지의 체/용 음양처럼 코드에 주석으로만 남긴 이설도 있다.

저장 — 정책이 든다

계정과 Person 은 Supabase 에 있다(supabase/migrations/). 접근 판정을 앱 코드가 아니라 DB 불변식으로 건 이유는 하나다 — 앱에 조건을 적으면 그 조건을 안 부르는 경로가 언젠가 하나 생기고, 그 경로는 조용히 열려 있다.

  • 가입 관문은 폼 하나다(ADR 0042 · 0225 — 테스트 코드는 걷었다). 구글 로그인은 누구나 되고, 그다음 폼 하나가 닉네임·안내 확인을 한 트랜잭션에 적는다. 그 대신 가입이 안 끝난 계정이 생긴다 — 되돌릴 수 없는 첫 쓰기 둘 (create_self_person·create_managed_person)이 그 상태를 거절한다. 화면 관문은 길만 가리키고, 막는 일은 여전히 DB 가 한다
  • 출생정보는 판본으로 쌓는다. 고치지도 지우지도 않으므로, 어느 사건이 어느 사주를 대상으로 만들어졌는지 나중에 되짚을 수 있다
  • claim 이 편집권을 옮긴다. 어떤 Person 을 자기 자신이라고 지정하면 그 Person 의 기존 편집자는 트리거가 viewer 로 내린다
  • 「고쳐도 되는가」는 한 자리에만 있다(may_edit_person_input). RLS 정책과 RPC 가 같은 함수에 묻는다 — security definer 함수는 RLS 를 지나가므로, 정책에만 적어 두면 RPC 로 들어오는 길이 아무 검사 없이 열린다
  • 아무것도 안 바뀌었으면 판본을 쌓지 않는다. 판본 이력은 무엇이 달라졌는가의 기록이지 저장 버튼을 몇 번 눌렀는가의 기록이 아니다. 지문으로 판정한다
  • 명식은 저장하지 않는다. 저장되는 것은 입력이고 명식은 거기서 계산되는 파생 뷰다. 엔진을 고쳐도 과거 기록의 뜻이 바뀌지 않는다
  • 요청은 후보를 본 데서만 난다. request_match 는 상대와 화면이 그린 요청 종류만 받고(ADR 0216), 그 사람이 내 후보 목록에 실제로 떴다는 기록이 없으면 거절한다. 판본·추천 이유·정책 버전은 앱이 실어 보내지 않고 함수가 그 자리에서 읽는다(ADR 0009)
  • 「지금 이 둘은 서로 후보인가」도 한 자리에만 있다(discovery_eligible). 후보 목록과 요청이 같은 함수에 묻는다 — 두 곳에 적었더니 성별 조건 하나가 요청 쪽에서 빠져, 조건을 바꿔 목록에서 사라진 사람에게 옛 기록으로 청할 수 있었다
  • 수락은 판본과 양쪽 계정을 다시 본다. 요청이 잡아 둔 판본과 지금 판본이 다르면 수락이 아니라 무효이고, 어느 쪽이든 이용이 정지됐으면 Match 가 나지 않는다. 확인·전이·Match 생성·양쪽 알림이 한 트랜잭션이다
  • 자격을 묻고 쓰는 사이는 잠근다. 요청·차단·판본 수정이 두 계정 행을 같은 차례로 잠그고 시작한다 — 잠그지 않으면 차단이 훑고 지나간 직후에 들어온 요청이 그대로 살아남는다
  • 요청·Match·알림 표는 브라우저에 한 줄도 열려 있지 않다. 읽는 길은 definer 함수뿐이고, 그 함수가 내주는 것이 곧 브라우저가 볼 수 있는 것이다 — 닉네임·소개· 사진이 있는지·채우는 오행·균형 한 줄. 생년월일시도 여덟 글자도 점수도 그 반환형에 없다
  • 얼굴도 같은 규율을 지난다(ADR 0040). 사진 바이트는 Postgres 안에 있고 라우트 하나가 내주는데, 「볼 수 있나」는 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)은 창을 둘 연다.

  • 절기 — 한국천문연구원 2025 달력자료와 일치 (망종 6/5 18:57, 소서 7/7 05:05)
  • 일주 앵커 — 2000-01-01 = 戊午 기준. 1900-01-01 甲戌, 2024-01-01 甲子, 2024-02-29 癸亥, 1988-07-15 辛未 로 교차 확인
  • 골든 스냅샷 — 절기·자시·서머타임·자오선 전환·윤년 등 경계 케이스를 고정 (src/lib/saju/golden/golden.snapshot.txt — 몇 건인지는 그 머리의 「케이스 N건」이 찍는다)
  • 자체 검증 — 절기 반환 시각에서 태양 황경을 되짚어 목표와 대조 (오차 1e-4° 이내)
  • 조후 120조합 — 《궁통보감》 원문과 일간×월지 정리표를 대조하고 열 일간×열두 월지를 전수 고정. 상·하반월만 자동 판정한다 — 경계가 중기(절기 +15°)라 천문으로 정해지기 때문이다. 120칸 중 여섯이 이 조건으로 갈리고, 그 칸에서만 절반의 후보를 따로 낸다. "수가 왕하면 戊" 같은 세력 조건은 문턱을 지어내야 해서 판정하지 않는다
  • 억부 외부 대조 — 계통이 다른 네 자료의 명조 37건을 옮겨 출처 주장과 현재 엔진의 일치·불일치를 함께 회귀 고정한다(자료와 성적). 외부 글의 십성 오기는 오행 주장과 분리해 정답으로 승격하지 않는다. 그중 3건은 월간이 연간과, 시간이 일간과 어긋나 실재할 수 없는 명조라(오호둔·오자둔을 다시 세어 테스트로 표시) 채점에서 빼고 서른넷으로 채점한다. 강약은 비교 가능한 26건 중 25건이 맞는다. 추천 오행은 논리를 갈라 센다 — 억부 논리로 고른 20건 중 10건이 맞고, 《천리명고》 衰旺편처럼 극왕·극쇠에 억부와 정반대 처방을 내는 14건은 2건만 맞는다(어긋나는 것이 정상이라 따로 센다). 계통별로 갈리는 것이 요점이다 — 한국 현대 상담 글 2/6, 《적천수천미》 八格 3/3, 《천리명고》 5/11. 표본이 이 정도로는 검증이라 할 수 없어 억부는 실험 상태를 유지한다
  • 종격은 실험 규칙 v2로 판정한다 — 조건과 갈림은 문서로 먼저 고정했고, 그 위에 네 상태(종격 아님·후보·가종·진종)를 낸다. 문턱은 고전이 정한 숫자가 아니다. 무작위 3000건의 세력 분포를 재고 정했고, 축은 자당(비겁+인성) 몫 하나다 — 밖으로 종하면 30% 이하, 안으로 종하면 70% 이상. 그 측정 근거를 dominance.calibration 에 값으로 남긴다. v2 는 문턱을 안 건드리고 세는 법을 셋 고쳤다(국(局)과 합화를 세력에 반영 · 뿌리를 개수가 아니라 질로 · 충에 뽑히거나 국에 끌려간 뿌리는 얕게). 같은 3000건의 발화율이 10.9%에서 10.6%로 오히려 내려간 것이 그 증거다 — 발화율을 함께 재지 않으면 재현율이 오른 것이 세는 법 때문인지 문턱을 헐겁게 해서인지 구별할 수 없다. 억부 후보는 뒤집지 않는다(eokbuOverride: 'disabled'): 억부는 외부 사례를 대조하고도 시험값인데, 그 답을 정반대로 뒤집는 판정이 외부 대조 0건으로 위에 설 수 없다. 진종·가종 어느 쪽으로도 밀기 어려운 명식은 후보로 남긴다 — 억지로 밀어 넣으면 판정이 아니라 반올림이 된다. 축은 자당(비겁+인성) 몫 하나다. 종에는 방향이 둘이라(從財·從殺은 밖으로, 從强·從旺은 안으로) 두 비율이 아니라 한 축의 양끝으로 잰다 — 자당과 이당은 합이 1 이기 때문이다. 처음에는 지배 ÷ (지배 + 일간편) 으로 쟀는데 그 축에서는 안으로 종이 구조적으로 0.5 를 넘을 수 없어 영영 잡히지 않았다(고전 명례로 확인)
  • 종격 외부 대조 — 아직 통과하지 못했다. 세 계통에서 41건을 옮겼다 — 현대 정리 (從殺格·從財格) 18건, 고전(《적천수천미》 임철초 주석) 17건, 민국(《천리명고》) 나머지. 판본 오배로 실재할 수 없는 한 건은 채점에서 빼고 마흔으로 잰다. 종격이라 적힌 33건 중 18건을 잡고, 아니라고 적힌 7건 중 2건을 가종으로 잘못 본다. 계통별로 갈리는 것이 요점이다 — 현대 10/14, 고전 7/16, 민국 1/3. 재현율은 절반을 넘었는데도 게이트는 닫아 둔다(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건이다. 계통이 하나뿐이라 게이트는 닫혀 있다. 통관도 같은 자리에 있다 — 세기는 하되 억부·조후와 갈리는지를 말할 뿐이다. 격국 · 오신 · 조후 조건 · 암합 · 통관 다섯의 근거와 상한은 해석 체계
  • 신살 채택 계통 — 관귀학관은 《삼명통회》 학당사관, 현침은 《오행정기》의 다섯 글자·3자 이상, 태극귀인은 《연해자평》의 년간 기준을 따른다. 천문성은 당사주 天文=辰과 구분해 天門=戌亥의 방위 근거와 현대 명리의 戌亥=天門星를 대조해 두 글자의 동시 성립으로 고정했다. 이름이 비슷한 천의성(天醫星) 은 월지의 앞 지지라는 별개 항목이라 따로 넣었고, 지지에 없으면 천간으로 대신 본다는 변형은 뺐다. 홍염살은 정통표 한 벌만 쓰고 甲乙庚壬에 申을 더하는 계통은 넣지 않는다. 암록은 표를 옮기지 않고 건록의 육합 으로 센다 — 양인·금여·문창과 같은 축이다
  • 없는 것도 없다고 적는다 — 도화·역마·화개는 년지·일지 두 기준으로 각각 세는데, 걸린 자리가 없으면 항목이 사라져 "계산했는데 없다"와 "여기서는 안 본다"가 화면에서 같아 보인다. 그래서 기준별로 걸린 자리가 없다는 사실을 신살 카드에 적는다 (년지 기준만 쓰는 만세력과 결과를 맞춰볼 때 필요한 구분이다)
  • 귀문관살은 원진과 합치지 않는다 — 두 표는 네 쌍이 겹치고 두 쌍이 다르다 (귀문 子酉·寅未 ↔ 원진 子未·寅酉). 합치면 어느 표에서 나온 사실인지 되짚을 수 없고 다른 두 쌍이 조용히 사라지므로, 겹치는 자리에서는 두 줄이 함께 나온다. 자리는 네 지지의 모든 쌍을 본다(ghostGateBasis: 'all-branch-pairs') — 일지 기준으로 좁히는 계통은 채택하지 않았고, 좁혀 읽고 싶으면 participants 의 자리로 거르면 된다. 둘 다 신살 표에도 이름이 오르지만 규칙은 관계 쪽 하나뿐이다 — 값을 옮겨 담기만 하고 자리가 갈릴 수 없음을 60갑자 전수 테스트가 잠근다

상수 테이블은 도메인 불변식으로 검증한다. 예를 들어 지장간은 "정기의 오행 = 그 지지의 오행", "생지·묘지의 중기 = 그 지지가 속한 삼합국의 오행" 을 만족해야 하므로, 표에서 한 글자만 틀려도 테스트가 잡는다.

운영

지금은 공개 출시다(2026-11-05 부터). 운영자 화면은 /ops 에 있고(오늘의 숫자 · 회원 찾기 · 주문 · 신고 열람 · 설문 요약 — 무엇이 있는지는 docs/product/prd/screens/ops.md), 들어갈 때 2단계 인증을 다시 묻는다(ADR 0209). 화면이 없는 절차 — 초대 · 이용 정지 · 탈퇴 대기 처리 · AI 호출량과 실패 조회 — 는 docs/ops/runbook.md 에 SQL 로 적혀 있다.

알려진 한계

  • 국내 출생만 지원한다 (Asia/Seoul 표준시 이력, 서울 · 광역시 · 세종 · 도 · 춘천 · 강릉 19곳의 경도)
  • 1908년 이전은 tzdata 의 지방평균시로 처리되며 실용 범위 밖이다
  • 억부용신은 네 자료 서른넷을 대조하고도 후보만 낸다 — 강약은 26건 중 25건이 맞지만 추천 오행은 억부 논리 20건 중 10건이다. 조후용신은 《궁통보감》 120칸의 후보 천간과 조건 요약을 참고값으로 낸다
  • 종격·격국·통관은 판정하되 억부를 안 뒤집는다. 종격은 마흔 건에서 18/33 을 잡고 발화율이 고전의 희소성과 자릿수가 다르며, 격국은 계통이 하나뿐이다. 게이트를 여는 조건은 문서에 값으로 적혀 있다
  • 화면에 서는 점수는 엔진이 낸 것이 아니다. 후보 카드의 예측 궁합 점수와 궁합 풀이의 점수는 엔진 밖 정책층에서 난다(ADR 0003·0059·0060) — 맞춰볼 외부 정답이 없는 값이라 이름 옆의 종결어미와 목록 머리 한 줄이 그 사실을 든다

스택

Next.js 16 (App Router) · React 19 · TypeScript · Tailwind CSS v4 · Supabase(PostgreSQL · pgTAP) · Expo(React Native, mobile/) · OpenAI(Vercel AI SDK) · Vitest · Playwright · Vercel · astronomy-engine

About

만날지도 — 사주·궁합·인연 서비스. Next.js · Supabase · Expo. AI 코딩 에이전트로 만들고 테스트·권한 규칙으로 검증

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages