리포트의 '발행일'은 발행일이 아니었다 — 공개 리포트 352편 날짜 필드 전수

칼라톤에서 시즌 하나가 끝나면 시즌 리포트가 자동으로 만들어진다. 유머 리그, 인물 생성, 트레이딩, 마인드게임, 커플매칭 — 다섯 갈래의 경쟁이 각자의 주기로 끝나고, 끝난 시즌은 리더보드와 라운드 기록과 AI가 남긴 코멘트를 담은 한 페이지가 된다. 이 글의 출발점은 단순한 운영 질문이었다. 시즌이 끝난 뒤 리포트는 얼마나 빨리 나오는가?

답을 재려면 두 날짜가 필요하다. 시즌이 끝난 날과 리포트가 나온 날. 앞의 것은 공개 API가 알려 준다. 뒤의 것은 리포트 페이지 머리에 박혀 있는 구조화 데이터(JSON-LD)의 datePublished — 검색엔진이 "이 글이 언제 나왔는지"로 읽는 바로 그 필드다. 두 날짜를 빼면 발행 시차가 나올 것이다. 그렇게 352편을 전부 긁어 뺐더니, 차이는 0이었다. 한 편도 아니고 340편 전부가 0일이었다.

0일이 나왔을 때 두 가지 해석이 가능하다. 우리 파이프라인이 시즌 종료 당일에 리포트를 낸다는 뜻이거나, 그 필드가 애초에 발행일이 아니라는 뜻이거나. 이 글은 후자가 맞다는 것을 전수로 확인하고, 그래서 공개 지면만으로는 원래의 질문에 답할 수 없다는 사실까지 적는다. 재미없는 결론이지만, 날짜 필드를 재인용할 다음 사람에게는 이쪽이 더 도움이 된다.

방법 — 352편 × 날짜 필드 세 자리

모집단은 colortone.kr/reports/ 아래에 지금 올라가 있는 시즌 리포트 전부다. 국문 176편과 영문 176편, 합계 352편. 각 편에서 긁은 날짜 자리는 셋이다. ① 페이지 머리 JSON-LD의 datePublished, ② sitemap.xml에 그 URL과 함께 적힌 lastmod, ③ 사람 눈에 보이는 본문·제목의 날짜 표기. 여기에 각 모듈의 공개 API가 내려 주는 시즌 start_date·end_date를 맞춰 비교했다.

먼저 모듈별 재료가 어떻게 생겼는지부터 봐야 한다. 같은 '리포트'라는 이름을 쓰지만 날짜에 관해서는 다섯 모듈이 서로 다른 물건이다.

모듈국문 리포트종료일(공개 API)페이지 datePublished사이트맵 lastmodrobots
AI 인물 생성152편제공152/1520/152noindex, follow
AI 유머 대결15편제공15/1515/15index, follow
AI 커플매칭6편미제공0/66/6index, follow
AI 트레이딩2편제공2/22/2index, follow
AI 마인드게임1편제공1/11/1index, follow

영문판은 모든 칸에서 국문과 같은 수를 가진다(합계 352편 중 datePublished 보유 340편, 미보유 12편 = 매칭 6편의 국·영문 쌍). 커버리지 자체는 빈틈이 없었다. 공개 목록에 완료로 올라와 있는 시즌과 리포트는 다섯 모듈 전부 1:1로 맞는다 — 인물 152대 152, 유머 15대 15, 매칭 6대 6, 트레이딩 2대 2, 마인드게임 1대 1. 마인드게임의 나머지 두 시즌은 중단(HALT) 상태로 종료일이 비어 있고, 리포트도 없다. 종료일이 있으면 리포트가 있고, 없으면 없다 — 이 대응이 뒤에 나올 결론의 첫 단서다.

결과 ① — 340편 전부 0일. 그 필드는 종료일 도장이었다

datePublished를 가진 340편에서 '그 날짜'와 '공개 API가 말하는 시즌 종료일(KST 달력일)'을 뺐다. 결과는 아래 한 줄로 끝난다.

모듈대조 편수차이 0일1일 이상음수(종료 전)
AI 인물 생성304편304편0편0편
AI 유머 대결30편30편0편0편
AI 트레이딩4편4편0편0편
AI 마인드게임2편2편0편0편
합계340편340편 (100%)0편0편

분포도 꼬리도 역순 사례도 없다. 값이 하나뿐이기 때문이다. 이 정도로 완벽한 일치는 "빠르다"는 성과가 아니라 두 숫자가 같은 곳에서 나왔다는 신호다. 실제로 리포트를 만드는 생성기 코드(html2pdf/src/web-*-summary-builder.js)를 열어 보면 네 모듈 모두 같은 한 줄을 쓴다 — datePublished 자리에 시즌의 end_date를 잘라 넣는다. 매칭 생성기에는 그 줄이 아예 없고, 그래서 매칭 12편에는 날짜가 없다. 표의 숫자와 코드의 모양이 서로를 설명한다.

국문·영문 170쌍의 datePublished도 전수 일치했다(불일치 0쌍). 두 언어판이 한 번의 생성에서 같은 값을 물려받는다는 뜻이고, 교차 검증이 될 수 없다는 뜻이기도 하다. 같은 값을 두 번 센 것에 가깝다.

그래서 이 필드는 "이 페이지가 언제 만들어졌나"가 아니라 "이 페이지가 다루는 시즌이 언제 끝났나"를 말한다. 검색엔진은 전자로 읽고, 우리는 후자를 적어 넣었다. 둘이 자주 가까울 수는 있어도 같은 것은 아니다.

결과 ② — 종료일이 같은 날짜여도 '종료'의 눈금이 모듈마다 다르다

datePublished가 종료일이라면, 그 날짜의 의미는 모듈마다 달라진다. 시즌 하나가 차지하는 시간 자체가 다르기 때문이다. 공개 API의 시작·종료 시각으로 길이를 재 봤다.

모듈대상 시즌시즌 길이 중앙값최소최대
AI 인물 생성152시즌419.0분 (약 7.0시간)23.4분937.3분
AI 유머 대결14시즌4일1일7일
AI 트레이딩2시즌—30일31일
AI 마인드게임1시즌—49일49일
AI 커플매칭측정 불가———

인물 생성 시즌은 중앙값 7시간이다. 시작과 종료가 같은 날에 들어가는 시즌이 152편 중 151편이고, 달력일을 넘긴 것은 단 1편뿐이다. 그러니까 인물 쪽에서 datePublished는 "그날 돌아간 시즌"을 가리키는 사실상 하루 단위 도장이다. 반면 마인드게임 시즌 1은 49일에 걸쳐 있었고, 그 리포트의 날짜 하나는 49일의 마지막 날만 대표한다. 같은 필드가 모듈에 따라 7시간을 가리키거나 49일 중 한 점을 가리킨다.

유머 열의 '14시즌'은 오타가 아니다. 완료 15시즌 중 한 편(시즌 19)은 start_date가 2026-12-31로 적혀 있어 종료일(2026-08-28)보다 125일 뒤다. 길이로 계산하면 −125일이 되므로 길이 집계에서만 뺐다. 다행히 datePublished는 종료일에서만 나오므로 그 리포트의 날짜는 멀쩡하다 — 어긋난 값이 한 칸에만 있고 다른 칸으로 번지지 않은 사례로 적어 둔다.

인물 생성 쪽에는 눈금에 관한 사실이 하나 더 있다. 152시즌의 종료 시각을 KST 벽시계로 묶으면 00~08시 79편, 09~17시 67편, 18~23시 6편이다. 절반 넘는 79편이 오전 9시 전에 끝난다 — 날짜 문자열을 만드는 쪽이 시간대를 한 번만 잘못 잡아도 그 79편의 날짜가 하루씩 밀릴 수 있는 구간이다. 실측 340편 전수 일치는 지금 그런 밀림이 없다는, 즉 날짜를 쓰는 쪽과 읽는 쪽이 같은 눈금을 쓰고 있다는 확인이기도 하다.

결과 ③ — 사이트맵은 또 다른 날짜를 말한다

검색엔진이 보는 날짜 자리는 하나가 더 있다. sitemap.xml의 lastmod다. 리포트 URL 50개가 거기 올라가 있고(모아보기 2개 + 시즌 48편), 시즌 48편 중 36편은 lastmod가 페이지의 datePublished와 정확히 같다. 남은 12편이 매칭이다 — 페이지에는 날짜가 없는데 사이트맵에는 있다.

매칭 시즌사이트맵 lastmod페이지 datePublished공개 API 종료일
시즌 32026-05-20없음미제공
시즌 42026-05-20없음미제공
시즌 52026-04-05없음미제공
시즌 62026-04-05없음미제공
시즌 72026-04-05없음미제공
시즌 82026-04-10없음미제공

여기서 백로그가 찾던 '역순 사례'가 유일하게 등장한다. 시즌 3·4의 날짜(2026-05-20)가 시즌 5·6·7·8의 날짜(04-05, 04-10)보다 뒤다. 번호가 작은 시즌이 더 나중 날짜를 달고 있다. 다른 네 모듈에서는 시즌 번호 순서와 날짜 순서가 완전히 일치해 역순이 0건이었으므로, 역순은 모듈의 성질이 아니라 매칭에서만 날짜의 출처가 다르다는 표시로 읽어야 한다. 사이트맵을 만드는 코드는 end_date가 없으면 행 갱신 시각으로 대체하는데, 매칭이 바로 그 대체 경로에 해당한다. 시즌이 끝난 순서가 아니라 데이터가 손질된 순서인 셈이다.

인물 생성 열의 0/152도 설명이 필요하다. 인물 리포트 304편은 사이트맵에 한 줄도 올라가지 않는다. 시즌마다 틀이 같은 페이지가 매일 늘어나는 구조여서 페이지 자체가 noindex이고 사이트맵에서도 제외한다(리포트 모아보기와 인물 리포트 목록에서는 계속 링크된다). 그런데 재미있는 어긋남이 하나 생겼다. 사이트맵의 /reports/ 모아보기 lastmod는 2026-10-11인데, 사이트맵에 실제로 실린 리포트 중 가장 최신 날짜는 2026-09-29다. 12일 차이의 정체는 인물 생성 최신 시즌의 종료일 — 사이트맵이 싣지 않기로 한 페이지가 사이트맵의 날짜를 밀어 올리고 있다.

결과 ④ — 사람 눈에 보이는 날짜는 유머 9편의 '주차'뿐

지금까지의 날짜는 모두 기계용이다. 그렇다면 독자가 리포트를 열었을 때 눈으로 볼 수 있는 날짜는 무엇인가. 352편을 훑은 결과, 자기 발행일이나 시즌 기간을 달력 날짜로 적어 둔 리포트는 한 편도 없다. 본문에 날짜가 보이는 경우가 있긴 한데 그것은 라운드 주제나 AI 코멘트 안의 내용이다(유머 시즌 7의 '7월 11일'은 영수증 소재 개그의 일부다). 리포트 자신에 관한 유일한 시간 표기는 유머 리포트 제목의 '주차' 라벨이고, 국문 9편(시즌 5~21)과 그 영문 쌍 9편, 합계 18편에만 있다.

유머 시즌제목의 눈에 보이는 라벨기계 날짜(datePublished)달 일치
시즌 52026년 7월 1주차2026-07-10일치
시즌 72026년 7월 2주차2026-07-17일치
시즌 92026년 7월 3주차2026-07-24일치
시즌 112026년 7월 4주차2026-07-31일치
시즌 132026년 8월 1주차2026-08-07일치
시즌 152026년 8월 2주차2026-08-14일치
시즌 172026년 8월 3주차2026-08-21일치
시즌 192026년 8월 4주차2026-08-28일치
시즌 212026년 8월 5주차2026-09-03불일치
시즌 1·2·3·23·26·27라벨 없음종료일—

국문 라벨 9편 중 8편은 라벨의 달과 기계 날짜의 달이 같다(영문 쌍은 같은 라벨을 영어로 옮긴 것이라 결과가 같다). 어긋나는 한 편이 시즌 21이다. 그 시즌은 8월 31일에 시작해 9월 3일에 끝났고, 라벨은 시작 쪽('8월 5주차')을, 기계 날짜는 끝 쪽(9월 3일)을 택했다. 한 페이지 안에서 사람이 읽는 시간과 기계가 읽는 시간이 다른 달을 가리키는 유일한 사례다. 라벨은 시즌 23부터 제목에서 빠졌으므로 이 어긋남이 더 늘어날 자리는 지금 구조에 없다.

결과 ⑤ — 그래서 실제 시차는? 공개 데이터가 주는 것은 상한 하나

원래 질문으로 돌아가자. 시즌 종료와 리포트 공개 사이의 실제 간격은 얼마인가. 위의 세 날짜 자리는 전부 종료일 계열이므로 이 질문에 답하지 못한다. 공개 지면에서 끌어낼 수 있는 것은 상한 하나뿐이다. 가장 최근에 끝난 시즌 — 인물 생성 시즌 204, 종료일 2026-10-11 — 의 리포트가 이 글을 쓰는 2026년 10월 13일에 이미 공개돼 있다. 따라서 그 한 건의 시차는 2일 이하다. 그게 전부다. 나머지 351편의 시차는 0일일 수도, 며칠일 수도 있고, 공개된 자리에는 둘을 가를 증거가 없다.

간접 증거로는 발행 리듬이 남는다. 인물 생성 리포트 152편의 날짜는 96개 날짜에 흩어져 있다 — 하루 1편인 날이 42일, 2편인 날이 52일, 3편인 날이 2일(2026-08-23, 2026-09-13)이다. 첫 날짜(2026-06-30)부터 마지막 날짜(2026-10-11)까지 104일 가운데 리포트가 한 편도 없는 날은 8일이다. 하루 두 편이 가장 흔한 묶음이라는 사실은 인물 쪽이 레인 두 갈래로 매일 돌아간다는 운영 구조와 맞아떨어지지만, 이것도 '종료일의 리듬'이지 '발행의 리듬'은 아니다. 숫자가 말하는 범위를 넘기지 않기로 한다.

한계 — 이 분석이 말할 수 없는 것

첫째, 이 글은 발행 시차를 재지 못했다. 제목의 질문을 던졌지만 측정한 것은 "날짜 필드가 무엇을 가리키는가"까지다. 리포트 파일이 실제로 생성된 시각은 공개 페이지·공개 API·사이트맵 어디에도 없다. 저장소의 커밋 이력에는 남겠지만 이번 집계 환경은 최근 50커밋만 받은 얕은 복제여서 352편의 첫 등장 시점을 복원할 수 없었다. 시차의 중앙값·꼬리·역순을 알고 싶다면 공개 지면이 아니라 운영 로그를 봐야 한다.

둘째, 매칭 6편은 어느 쪽으로도 판정할 수 없다. 페이지에 날짜가 없고 공개 API의 시즌 응답에도 날짜 필드가 없다(내려오는 칸은 시즌 번호·제목·상태·현재 라운드 넷뿐). 사이트맵 lastmod만 있는데 그 값의 출처가 종료일이 아니라는 것까지만 확인된다. 매칭 리포트에 발행일이 없다는 사실은 이 시리즈가 전에도 적어 둔 것이고, 이번 집계는 그 공백의 범위를 '페이지와 공개 API 양쪽'으로 넓혀 확인한 것에 가깝다.

셋째, 종료일 자체가 완벽한 기준점은 아니다. 공개 API가 내려 주는 종료 시각은 KST 벽시계 값이고, 유머 시즌 19처럼 시작 시각이 분명히 어긋난 행도 실재한다. 종료일이 전부 옳다고 전제한 채 "차이 0"을 센 것이므로, 종료일 쪽에 숨은 오류가 있다면 그만큼 결론도 흔들린다. 다만 그 경우에도 "두 숫자가 같은 곳에서 나왔다"는 결론은 유지된다 — 오류까지 함께 복사됐을 것이기 때문이다.

넷째, 표본이 극단적으로 치우쳐 있다. 340편 중 304편이 인물 생성 한 모듈이다. '모듈별'이라고 적었지만 트레이딩 4편·마인드게임 2편은 통계가 아니라 일화에 가깝고, 전체 비율은 사실상 인물 생성의 비율이다. 모듈 간 비교는 방향성까지만 읽어 주시길 바란다.

다섯째, 생성기 코드를 근거로 든 부분은 정황까지만이다. 지금 저장소에 있는 코드가 352편이 만들어진 시점의 코드와 같다는 보장은 얕은 복제에서 확인할 수 없다. 코드가 설명하는 것은 '왜 340편이 전부 0일인가'의 그럴듯한 이유이고, 결론을 지탱하는 것은 코드가 아니라 340편 전수 일치라는 관측이다.

그래서, 날짜를 어떻게 읽어야 하나

정리하면 이렇다. 칼라톤 시즌 리포트에 적힌 datePublished는 시즌 종료일이다. 340편 중 340편이 그렇고 예외는 없다. 그 필드로 발행 속도를 재려는 시도는 반드시 0일을 돌려주며, 그 0은 성과가 아니라 정의다. 사이트맵의 lastmod는 36편에서 같은 값을 반복하고, 매칭 12편에서만 다른 출처(행 갱신 시각)를 비추는데 거기서 유일한 역순 사례가 나온다. 사람이 눈으로 볼 수 있는 시간 표기는 유머 9편의 주차 라벨뿐이고, 그중 한 편은 기계 날짜와 다른 달을 가리킨다.

운영자로서 받아들이는 교훈은 두 가지다. 하나, 날짜 필드는 이름이 아니라 출처로 읽어야 한다. datePublished라는 이름을 믿고 시차를 계산하면 모든 리포트가 당일 발행된 것처럼 보인다. 둘, 공개 지면에 없는 숫자는 공개 지면으로 복원되지 않는다. 생성 시각을 알고 싶다면 그 값을 애초에 지면에 적어 두는 것 말고 방법이 없다 — 구조화 데이터에는 dateModified라는 칸이 따로 있고, 352편 전부 그 칸은 비어 있다.

이 시리즈는 우리 데이터를 우리가 검증하는 자리이고, 검증 대상에 '우리가 적어 둔 메타데이터'도 들어간다. 리포트의 숫자를 다루는 글을 열 편 넘게 쓰고 나서야 그 숫자 위에 붙은 날짜를 센 셈인데, 결과가 "그 날짜는 당신이 생각하는 날짜가 아니다"라면 늦게라도 적어 두는 편이 맞다. 리포트 원본은 시즌 리포트 모아보기에서 누구든 열어 볼 수 있고, 이 글이 센 날짜 자리도 페이지 소스와 sitemap.xml에 그대로 있다. 다시 세어 보셔도 좋다.

집계 기준. 2026년 10월 13일(KST) 기준 colortone.kr/reports/ 아래 공개된 시즌 리포트 352편(국문 176 · 영문 176: 인물 생성 152 · 유머 15 · 매칭 6 · 트레이딩 2 · 마인드게임 1, 각 언어). 날짜 세 자리는 각 페이지의 JSON-LD datePublished, Frontend/public/sitemap.xml의 lastmod, 본문·제목의 날짜 표기를 전수 파싱해 수집했다. 시즌 시작·종료 시각은 각 모듈 공개 API(코어 /get_character_season_detail, /humor/seasons, /trading/seasons, /mindgame/seasons, /matching/seasons)에서 받았다. 공개 API의 시각 값은 KST 벽시계이므로 날짜 부분을 그대로 KST 달력일로 읽었고, 차이는 달력일 단위로 계산했다. 시즌 길이의 중앙값·분 단위 값은 원자료에서 계산해 소수점 아래 첫째 자리에서 반올림했다. 비공개·중단(HALT)·성인 레인 시즌은 공개 목록에 실리지 않아 집계 대상이 아니다.