KKflow

BUG · 버그 도감 #15

'이 시각부터'라는데 시각이 없었다: 절기가 드는 날 하루 안에서 바뀌는 달

사주 플로우 달력은 절기 목록에 '이 시각부터 월주가 바뀝니다'라고 적으면서 정작 시각을 보여 주지 않았습니다. 오늘의 운세와 달력은 절기가 드는 날에도 정오에 정한 달 하나로 하루를 계산했습니다. 정오 기준 값이 어긋나는 구간과 계산 대신 알림을 택한 해결을 정리했습니다.

· KKflow · 읽는 시간 약 8분

버그 도감은 KKflow 프로젝트에서 실제로 만난 버그를 한 편에 하나씩 기록하는 시리즈입니다. 모든 내용은 해당 저장소의 커밋 기록과 코드를 근거로 합니다.

프로젝트 사주 플로우 · 시기 2026년 9월 15일 · 관련 기술 절기 경계, 하루 단위 계산, 화면 문구

어제 오후 3시 29분에 달이 바뀌었다

2026년 10월 8일 오후 3시 29분(한국 시각)에 한로(寒露)가 들었습니다. 달력으로는 그냥 10월 8일이지만, 사주에서는 이 순간 달이 바뀝니다. 사주의 달은 음력 1일이 아니라 열두 절기(절입)로 갈리기 때문입니다. 한로 전까지는 유월(酉月), 한로부터는 술월(戌月)입니다. 그러니까 10월 8일은 하루 안에 두 달이 들어 있는 날이었습니다.

이런 날이 1년에 열두 번 있습니다. 사주 플로우는 9월 15일 오전 11시 33분에 합쳐진 커밋에서 이 날을 다루는 방식 두 가지를 손봤습니다. 이 커밋은 한 번에 다섯 가지 일을 묶은 것으로, 제목은 "남은 작업 Top 5 — 넷은 고치고 하나는 재 보니 문제가 아니었다"입니다. 이 글은 그 첫 번째 항목 이야기입니다. 커밋은 Claude Code와 함께 작성했습니다.

증상 1: 가리킬 것이 없는 '이 시각'

사주 플로우의 달력 화면에는 그달의 절기를 나열하는 「이 달의 절기」 목록이 있습니다. 한 줄은 이런 모양이었습니다.

10월 8일  한로  이 시각부터 월주가 戌월로 바뀝니다

문장은 "이 시각부터"라고 말하는데, 줄 어디에도 시각이 없습니다. 날짜만 있습니다. 커밋은 이것을 "가리킬 것이 없는 지시어였다"라고 적었습니다. 읽는 사람은 "몇 시부터라는 거지?" 하고 다른 곳을 찾아봐야 합니다. 같은 앱의 원국(사주 결과) 화면에는 절입 시각이 분 단위로 나오고 있었으니, 값이 없어서가 아니라 달력에 옮겨 오지 않았던 것입니다.

증상 2: 정오에 정한 달로 하루 내내

두 번째가 더 근본적입니다. 달력과 「오늘」(오늘의 운세) 화면은 날마다 황도흑도(그날 당번인 열두 신), 건제12신(그날의 성격), 그리고 둘을 합친 하루 점수를 보여 줍니다. 이 값들은 모두 그날의 지지와 그달의 지지(월지)로 계산합니다. 그런데 하루치 값을 만드는 함수는 월지를 이렇게 정했습니다(lib/saju/iljin.ts).

/** 특정 날짜(정오 기준)의 절기 월지 */
function monthBranchOfDate(y, m, d) {
  const utcMs = Date.UTC(y, m - 1, d, 3, 0); // KST 정오
  // 정오 이전에 든 마지막 절기의 월지
  ...
}

날짜 하나에 월지 하나. 그 월지는 그날 정오에 어느 달이었는지로 정합니다. 절기가 없는 날에는 아무 문제가 없습니다. 하루 종일 같은 달이니까요. 하지만 절기가 드는 날에는 하루의 일부가 반드시 실제와 어긋납니다. 커밋 설명은 이렇게 적었습니다. "정오 기준으로 잡아 두었으니 오후에 보는 사람은 이미 다음 달 기운인데 이전 달 값을 본다."

00:00 06:00 12:00 18:00 24:00 정오: 하루 값을 정하는 시각 한로 15:29 절기로 본 실제 달 酉월(유) 戌월(술) 사주 플로우 하루 값 酉월 (정오에 정함) 실제와 다른 8시간 31분 날로 가르는 만세력 戌월 (0시부터 통째로) 실제와 다른 15시간 29분
2026년 10월 8일(한로 15:29) 하루를 세 가지 기준으로 본 그림입니다. 빨간 점선이 실제 절기 시각과 다른 구간입니다. 정오로 값을 정하면 절기가 오후에 드는 날은 그 뒤가, 오전에 드는 날은 그 앞이 어긋납니다.

10월 8일이라면 정오에는 아직 한로 전이므로 하루 값은 유월로 계산됩니다. 오후 3시 29분 이후에 「오늘」을 연 사람은 이미 술월에 들어섰는데도 유월 기준의 황도와 건제를 봅니다. 사주 플로우의 계산 함수(hwangdoOf, geonjeOf)에 그날의 지지 묘(卯)를 넣어 두 달을 비교하면 차이가 분명합니다.

10월 8일(을묘일)황도흑도건제12신하루 날씨
유월로 계산(화면에 나온 값)명당(좋은 쪽)파(깨지는 날)갰다 흐렸다 하는 날
술월로 계산(15:29 이후 실제)구진(나쁜 쪽)집(붙잡는 날)흐린 날

같은 날 같은 시각인데 "깨지는 날"이 "붙잡는 날"로, 좋은 신이 나쁜 신으로 바뀝니다. 이런 값을 믿느냐는 사람마다 다르겠지만, 이 앱이 보여 주겠다고 한 계산 규칙 안에서는 오후 3시 29분 이후의 값이 틀린 셈입니다.

어긋나는 쪽은 절기 시각에 따라 다르다

커밋은 "오후에 보는 사람"을 문제로 짚었습니다. 함께 들어간 시험 코드도 "절입이 정오 뒤에 드는 날"만 골라서, 그날의 정오 기준 월지와 절기가 여는 월지가 실제로 다르다는 것을 확인합니다. 2025년 절입 열두 번 중 정오 뒤에 드는 것은 여덟 번(입춘·경칩·청명·입하·망종·입추·백로·입동)입니다.

코드를 읽어 보면 반대 방향도 있습니다. 절기가 오전에 드는 날에는 정오에 이미 다음 달이므로, 하루 값이 0시부터 다음 달로 계산됩니다. 그러면 자정부터 절입 시각까지가 실제와 어긋납니다. 2026년 망종은 6월 6일 0시 48분이니 어긋나는 시간은 48분뿐이지만, 대설은 12월 7일 11시 53분이라 0시부터 11시간 53분 동안 아직 해월(亥月)인데 자월(子月) 값이 나옵니다. 이 방향은 커밋 설명이나 시험에는 따로 나오지 않고, 위 함수에서 그대로 따라 나오는 결과입니다.

00시 06시 12시 18시 24시 소한 1/5 17:23 입춘 2/4 05:02 경칩 3/5 22:59 청명 4/5 03:40 입하 5/5 20:49 망종 6/6 00:48 소서 7/7 10:57 입추 8/7 20:43 백로 9/7 23:41 한로 10/8 15:29 입동 11/7 18:52 대설 12/7 11:53 분홍 막대 = 정오 기준 하루 값이 실제 달과 다른 구간
2026년 사주의 달을 가르는 12절의 절입 시각입니다(24절기 절입 시각표). 분홍 막대가 정오에 정한 '하루 값'이 실제 달과 다른 구간입니다. 절입이 오후면 절입 뒤부터 자정까지, 오전이면 0시부터 절입까지입니다. 그래서 자정 가까이 드는 백로(23:41)는 19분, 망종(00:48)은 48분뿐이지만, 정오 가까이 드는 대설(11:53)은 11시간 53분이나 됩니다.

정리하면 정오 기준 하루 값은 절기가 정오에 가까이 들수록 오래, 자정에 가까이 들수록 짧게 어긋납니다. 2026년에는 오후에 드는 절기가 일곱 번, 오전이 다섯 번입니다. 어느 쪽이든 절입일마다 하루 중 일부는 계산이 실제 달과 맞지 않습니다.

다른 만세력은 이 날을 어떻게 볼까

이 문제에는 정답이 하나만 있는 것도 아닙니다. 사주 플로우는 절기를 한국천문연구원이 발표한 시각으로 가릅니다. 그런데 널리 쓰이는 만세력 가운데는 절입일 0시부터 통째로 다음 달로 넘기는 곳도 있다고 코드 주석에 적혀 있습니다. 원국 화면 쪽 주석(lib/saju/jieNotice.ts)에는 실제로 확인한 사례도 있습니다. 1975년 9월 8일 오전 11시에 태어난 사람은 백로(그날 15시 33분) 전이라 사주 플로우는 갑신(甲申)월로 보고, 날로 가르는 구현은 을유(乙酉)월로 낸다는 것입니다. 주석의 결론은 이렇습니다. "둘 다 자기 규칙대로는 맞고, 어느 쪽이 틀렸다고 말할 수 없다 — 기준이 다를 뿐이다."

문제는 사용자가 그 사실을 모를 때입니다. 커밋 설명의 표현으로는 "그 말을 우리가 먼저 안 하면 맞대어 본 사람은 이 앱을 의심하는 것 말고 할 수 있는 일이 없다." 절입 당일에 태어난 사람에게 이 차이를 알리는 문구는 원국 화면에 이미 있었습니다. 빠져 있던 것은 날을 보여 주는 두 화면, 달력과 「오늘」이었습니다.

해결: 계산을 쪼개는 대신 시각을 보여 주고 먼저 말한다

커밋은 정오 기준 계산 자체는 바꾸지 않았습니다. 하루치 값을 담는 DayEntry는 지금도 하루에 황도 하나, 건제 하나를 갖는 구조입니다. 대신 두 가지를 더했습니다.

1. 절입 시각을 하루 정보에 넣고 화면에 적기

/**
 * 그 절기가 «몇 시 몇 분»에 드는가 — `14:33` 꼴. 절입일에만 값이 있다.
 */
jieAt: string | null;

절입일을 찾는 함수가 절기 이름과 월지에 더해 시각도 돌려주게 했고, 달력의 「이 달의 절기」 줄에는 날짜와 절기 이름 옆에 15:29 같은 시각이 붙었습니다. 이제 "이 시각부터"가 가리키는 것이 바로 앞에 있습니다. 화면에 적는 시각은 계산에 쓰는 절입 시각과 같은 값에서 잘라 쓰도록 했고, 시험이 이 둘이 같은지 확인합니다.

2. 절입일에만 뜨는 알림

새 컴포넌트 JieBoundaryNote가 「오늘」과 달력 화면에 들어갔습니다. 고른 날이 절입일일 때만 이런 내용이 뜹니다.

오늘 15:29에 한로가 들어요 — 달이 바뀌는 날이에요
이 시각을 넘기면 사주의 달이 戌(술)월로 넘어가요. 달력 날짜가 아니라 절기가 사주의 달을 가르기 때문이에요.
만세력에 따라 이 날 하루를 통째로 다음 달로 넘기는 곳이 있어요. 여기서는 발표된 시각 그대로 갈라요. 다른 곳과 글자가 다르게 나온다면 틀린 것이 아니라 기준이 다른 것이에요.

(지금 운영 중인 컴포넌트 코드의 문구에 10월 8일 값을 넣어 옮긴 것입니다. 달력에서는 '오늘' 대신 '이 날'로 시작합니다.)

절입일이 아닌 날에는 아무것도 뜨지 않습니다. 커밋은 이유를 한 줄로 적었습니다. "한 해에 열두 번만 뜬다. 늘 떠 있는 배너는 사흘이면 안 보이는 것이 된다." 정오 기준으로 정한 값이 하루의 일부에서 실제와 다르다는 사실을, 그 일이 실제로 생기는 날에만 알려 주는 방식입니다. 계산을 시각 단위로 쪼개지 않고 알림을 택한 이유는 커밋에 따로 적혀 있지 않습니다.

시험이 지키는 것

이 항목으로 시험 파일 하나(tests/jieBoundary.test.ts)에 네 가지 확인이 더해졌습니다.

  • 절입일이 아닌 날(2025년 6월 15일)에는 절기 이름·시각·월지가 셋 다 비어 있다.
  • 2025년의 절입일 열둘에는 셋이 모두 차 있다. 날짜를 손으로 적지 않고 절기 표에서 직접 뽑는다. 시험 코드 주석: "날짜를 손으로 적으면 그게 곧 추측이다."
  • 화면에 적는 시각은 HH:mm 꼴이고, 절입 시각과 같다.
  • 절입이 정오 뒤에 드는 날은 정오 기준 월지와 절기가 여는 월지가 실제로 다르다. 즉 알림이 필요한 날이 실제로 있다.

마지막 시험이 흥미롭습니다. 버그를 막는 시험이 아니라 "이 차이가 정말 있다"는 것을 확인하는 시험입니다. 나중에 누가 정오 기준 계산을 바꾸면 이 시험이 먼저 알려 주고, 그때 알림 문구도 함께 다시 봐야 한다는 신호가 됩니다. 커밋 기준으로 전체 시험은 963건이 되었습니다.

교훈

  • 지시어에는 가리킬 것을 붙인다. "이 시각", "아래 표", "이 버튼"이라고 쓰면 그 대상이 같은 자리에 보여야 합니다. 값이 다른 화면에 있다는 것은 읽는 사람에게 아무 도움이 되지 않습니다.
  • 하루에 값 하나를 쓰면 경계일에는 반드시 반대편이 생긴다. 대표 시각을 정오로 잡든 0시로 잡든, 절기가 드는 날에는 하루의 일부가 실제와 다릅니다. 어느 쪽이 얼마나 어긋나는지는 절기 시각에 따라 달라집니다.
  • 기준이 다른 것은 먼저 말한다. 만세력마다 절입일을 다루는 방식이 다르다면, 다른 곳과 비교한 사용자가 의심하기 전에 앱이 먼저 기준을 밝히는 편이 낫습니다.
  • 알림은 필요한 날에만. 매일 떠 있는 안내는 곧 배경이 됩니다. 1년에 열두 번만 뜨기 때문에 뜨는 날에 읽힙니다.

절입 시각 자체를 다룬 버그는 버그 도감 #5: 절기 시각이 41%나 1분씩 틀렸다에 있습니다. 연도별 12절 시각은 24절기 절입 시각표, 절기마다 달의 간지가 어떻게 정해지는지는 육십갑자 일람표의 월두법 표에서 볼 수 있습니다.

함께 읽으면 좋은 글