번호 생성 로직 안내: 확률과 데이터의 조화
이 글의 핵심: 럭키허브가 제공하는 모든 번호 생성은 당첨을 보장하지 않습니다.
로또(6/45)와 연금복권(720+)은 확률 게임이며,
아래 로직은 선택을 도와주는 보조 도구일 뿐입니다.
저희는 완전 무작위성(CSPRNG) 과 과거 데이터 기반 점수화 를 함께 사용해,
“다양성과 재현 가능성 사이” 에서 실용적인 번호 조합을 만들어냅니다.
핵심 철학은 다음 네 가지입니다.
안전한 난수 : crypto.getRandomValues 기반(브라우저/런타임 CSPRNG)
데이터 기반 점수화 : 최근 회차 데이터로 간단한 통계 점수 계산
가중 무작위 선택 : 점수에 비례해 후보를 뽑되, 결과는 항상 랜덤
중복 완화 : 최근 저장한 조합과 4개 이상 겹치면 다른 후보로 회피
1) 난수 & 셔플
function getRandomInt(min: number, max: number) {
const arr = new Uint32Array(1)
crypto.getRandomValues(arr)
return min + (arr[0] % (max - min + 1))
};
JavaScript
function shuffleArray<T>(array: T[]) {
const a = [...array]
for (let i = a.length - 1; i > 0; i--) {
const j = getRandomInt(0, i)
;[a[i], a[j]] = [a[j], a[i]]
}
return a
};
Math.random() 대신 CSPRNG를 사용합니다.
모든 모드의 기본 랜덤 동작은 이 난수에 의존합니다.
2) 가중 랜덤 픽
function weightedRandomPick(candidates: number[], score: Record<number, number>) {
const sum = candidates.reduce((acc, n) => acc + score[n], 0)
const arr = new Uint32Array(1)
crypto.getRandomValues(arr)
let r = (arr[0] / 0xffffffff) * sum
for (const n of candidates) {
r -= score[n]
if (r <= 0) return n
}
return candidates[candidates.length - 1]
}
점수(score) 가 높은 번호일수록 뽑힐 확률이 커집니다.
“확정 선택”이 아니라 “확률을 살짝 기울인 무작위”입니다.
3) 최근 조합과의 유사도 회피
function isSimilar(a: number[], b: number[]) {
const setA = new Set(a)
const overlap = b.filter((n) => setA.has(n)).length
return overlap >= 4 // 4개 이상 겹치면 유사
}
최근 저장된 조합(최대 5개)과 4개 이상 겹치는 순간 다른 후보를 찾습니다.
같은 번호만 계속 뽑히는 체감 중복을 줄이려는 목적입니다.
4) 최종 조합을 만드는 공통 함수
async function generateFromScore(score: Record<number, number>, description: string) {
// 점수 상위권을 넉넉히(예: 25개) 뽑아 셔플 → 다양성 확보
const candidates = Object.entries(score).sort((a,b)=>b[1]-a[1]).map(([n])=>+n)
const pool = shuffleArray(candidates.slice(0, 25))
const recentPicks = await prisma.userPick.findMany({
orderBy: { createdAt: 'desc' }, take: 5, select: { numbers: true },
})
const result: number[] = []
const available = [...pool]
while (result.length < 6 && available.length > 0) {
const pick = weightedRandomPick(available, score)
const next = [...result, pick]
const similar = recentPicks.some(r => isSimilar(r.numbers, next))
if (!similar) result.push(pick)
// 후보에서 제거
available.splice(available.indexOf(pick), 1)
}
return { numbers: result.sort((a,b)=>a-b), reasons: [description] }
}
점수 상위권을 넓게 잡아 셔플 → 동일 패턴 고착화 방지 가중 픽 + 유사도 회피를 거쳐 6개가 모이면 정렬해 반환
로또 6/45: 모드별 로직
1) Hot/Cold 빈도와 최근 미출현 간격을 함께 점수화합니다.
예시 가중치: score = freqScore * 0.5 + gapScore * 0.3 (빈도↑, 오랫동안 안 나온 번호↑ 둘 다 약간 우대)
2) 패턴(번호 쌍 동시출현)
과거 회차에서 서로 자주 같이 나온 번호쌍을 계산합니다. 상위 쌍들을 우선 채택해 점수를 부여하고, 공통 생성 로직으로 최종화.
3) Pure Cold
오랫동안 안 나온 번호에 점수를 높게 부여합니다. “곧 나올 것”이라는 보장은 없고, 다양성 확보에 목적이 있습니다.
4) Gap(출현 주기 기반)
각 번호의 평균 출현 간격과 현재 간격을 비교해, 1 / (|avgGap - lastSeen| + 1) 형태로 점수를 책정합니다. “평균 주기에 근접한 시점의 번호”를 살짝 더 우대합니다.
5) Hybrid(균등 무작위)
모든 번호에 동일한 점수(=동일 확률) → 완전 무작위. 다만 공통 로직의 유사도 회피는 그대로 적용돼, 최근 조합과 과도한 중복은 줄여줍니다.
6) Secret(가벼운 앙상블)
빈도, 미출현 간격, 끝자리 분포(0~9), 번호 쌍 상관을 묶어 점수화. 예) 끝자리 분포를 중앙(5)에 가깝게 선호, 특정 쌍 출현이 비정상적으로 잦으면 소폭 가점.
참고: “range” 같은 실험적 모드는 빈도 기반 정렬로 간단 가중치를 만드는 등, 위의 뼈대를 재활용합니다.
연금복권 720+: 자리수 기반 접근
연금복권은 조(15) + 6자리 숫자(000000999999) 구조로 되어있습니다.
저희는 번호 전체를 하나의 정수로 보지 않고, 각 자리수(포지션)별 분포를 관찰합니다.
자리수별 빈도/간격: 최근 회차에서 각 자리(1의 자리, 10의 자리 … 100000의 자리)마다 0~9의 분포와 간격을 집계
가중 무작위 선택 : 자리별로 점수화된 0~9 중 가중 랜덤으로 한 자리씩 선택
조(Series) : 기본은 균등 무작위(1~5), 향후 데이터가 충분하면 조별 경향도 실험적으로 점수화
저장과 익명성
번호를 생성하면, 다음 회차 기준으로 내 선택 기록이 저장됩니다.
로그인 유저 : Member.id로 저장
비회원 : 익명 ID 생성 후 저장
최근 5개의 기록은 유사도 회피 체크에만 사용하고, 추천 설명(reasons)과 함께 보관
데이터 스noop/착시 : 과거 패턴이 미래를 보장하지 않습니다.
표본 크기 : 최근 회차만 보면 노이즈가 커질 수 있습니다.
무작위성 : CSPRNG를 써도 “우연”은 통제할 수 없습니다.
마무리
저희는 무작위성 + 가벼운 통계라는 현실적인 접근으로 번호 선택을 돕습니다.
재미와 다양성을 위한 도구일 뿐, 절대적인 예측이 아님 을 다시 한 번 강조합니다.
여러 모드를 시도하며 스스로 납득 가능한 전략을 찾는 데 이 글이 도움이 되면 좋겠습니다.
모두 1등 당첨되시길 간절히 바라겠습니다.
LuckyHub 운영자 올림.