결론부터
| 남기는 것 | 예 | 쓰는 곳 |
|---|---|---|
| 날짜(한국 시간) | 2026-10-23 | 같은 날 푼 사람끼리 비교 |
| 기기별 무작위 번호 | 3bf8…a618 | 한 사람이 하루 한 번만 집계 |
| 걸린 시간 | 24,300ms | 순위 계산 |
| 맞힌 개수 | 10 | 순위 계산 |
| 저장 시각 | 서버 시각 | 기록 확인용 |
- 남기지 않는 것 — 이름, 전화번호, 이메일, IP 주소, 기기 정보.
- 구조 — 정적 사이트(Cloudflare Pages)에 워커 파일 하나를 얹고,
/api/*주소만 워커가 받게 했습니다. 데이터는 Cloudflare의 무료 데이터베이스 D1에 둡니다. - 보이는 것 — 다른 사람의 이름이나 번호는 보이지 않고 "오늘 N명 중 M등" 같은 숫자만 보입니다.
1. 왜 로그인을 받지 않았나
이 게임은 아이부터 어르신까지 쓰도록 만들었습니다. 아이가 쓰는 앱은 구글 플레이의 가족 정책을 지켜야 하고, 이름이 공개되는 랭킹이나 대화 기능, 광고가 들어가면 확인할 것이 크게 늘어납니다. 그래서 처음부터 모을 정보를 순위 계산에 꼭 필요한 것만으로 줄이는 쪽을 택했습니다.
순위에 필요한 건 "누가"가 아니라 "몇 개를 몇 초에 맞혔나"입니다. 사람을 구분할 필요는 "같은 사람이 하루에 두 번 세어지지 않게" 하는 데만 있습니다. 이건 이름 없이도 됩니다.
2. 정적 사이트에 API 하나 얹기
heungai.com은 HTML 파일을 올려 두는 정적 사이트입니다. Cloudflare Pages는 사이트 폴더 맨 위에 _worker.js를 두면 "고급 모드"로 바뀌어, 들어오는 요청을 이 파일이 먼저 받습니다. 모든 요청이 워커를 거치면 무료 요청 한도를 괜히 쓰게 되므로, _routes.json으로 범위를 좁혔습니다.
{"version":1,"include":["/api/*"],"exclude":[]}
이렇게 하면 /api/로 시작하는 주소만 워커가 처리하고, 나머지 페이지와 그림은 예전처럼 파일 그대로 나갑니다. 데이터베이스는 Cloudflare 계정에 D1을 하나 만들고, 사이트 설정에서 DB라는 이름으로 연결했습니다. 워커 안에서는 env.DB로 씁니다.
_worker.js와 _routes.json을 함께 보내지 않으면 새 배포에서 API가 사라져 순위가 끊깁니다. 배포 순서표에 이 두 파일을 고정으로 넣어 두었습니다.3. 같은 사람을 두 번 세지 않는 법
- 번호 만들기 — 게임을 처음 열면 브라우저가
crypto.randomUUID()로 무작위 번호를 하나 만들어 기기 저장소에 넣습니다. 이름이나 기기 정보와는 연결되지 않습니다. - 하루 한 줄 — 표의 기본키를
(날짜, 번호)로 잡았습니다. 같은 날 같은 번호는 한 줄만 있을 수 있습니다. - 첫 기록만 — 저장은
INSERT OR IGNORE로 합니다. 이미 그날 기록이 있으면 새 기록은 조용히 무시되고, 순위는 언제나 그날 첫 기록으로 계산됩니다.
그래서 같은 문제를 다시 풀면 화면에는 새 시간이 나오지만, 순위 옆에는 "(첫 기록)"이 붙습니다. 정답을 외워 두 번째에 빨리 푸는 식으로 순위를 올릴 수 없게 한 것입니다.
4. 순위 계산 한 줄
기준은 맞힌 개수가 많은 순, 같으면 빠른 순입니다. 내 기록보다 앞선 사람 수를 세서 1을 더하면 내 등수가 됩니다.
SELECT COUNT(*) AS n, SUM(CASE WHEN c > ?1 OR (c = ?1 AND ms < ?2) THEN 1 ELSE 0 END) AS ahead FROM gg_daily WHERE date = ?3
같은 날짜 안에서만 세므로 (date, c, ms) 순서로 색인을 하나 더 만들었습니다. 결과로는 오늘 푼 사람 수(count)와 내 등수(rank) 두 숫자만 돌려줍니다.
5. 이상한 값 거르기
주소만 알면 누구나 서버에 값을 보낼 수 있으니, 받기 전에 걸러야 합니다.
| 항목 | 통과 조건 |
|---|---|
| 번호 | 영문·숫자·하이픈 8~64자 |
| 날짜 | 한국 시간 기준 어제·오늘·내일 중 하나(자정 무렵 시차 대비) |
| 걸린 시간 | 2초 이상 1시간 이하의 정수 |
| 맞힌 개수 | 0~10의 정수 |
실제 주소에서 확인한 응답은 이렇습니다. 주소를 열기만 하면(GET) 405, 조건에 안 맞는 값은 400 invalid, JSON이 아닌 본문은 400 body, 없는 API 주소는 404입니다. 서버 안에서 오류가 나도 500과 짧은 문구만 돌려주고 내부 내용은 내보내지 않습니다.
6. 사람 수에 따라 문구 바꾸기
처음에는 무조건 "상위 몇 %"로 보여 줬는데, 푼 사람이 적을 때 어색했습니다. 세 명 중 2등을 "상위 67%"라고 하면 잘한 건지 못한 건지 알기 어렵습니다. 그래서 이렇게 나눴습니다.
- 나 혼자 풀었으면 — "오늘 첫 번째"
- 20명 미만이면 — "N명 중 M등"
- 20명부터 — "N명 중 상위 P%" (P는 등수÷사람 수×100을 올림, 최소 1%)
서버가 응답하지 않거나 인터넷이 끊겨도 게임은 그대로 끝나고, 순위 줄만 나타나지 않게 했습니다. 순위는 덤이지 게임의 조건이 아니기 때문입니다.
7. 모두에게 같은 10문제
순위를 견주려면 같은 문제를 풀어야 합니다. 날짜 문자열(예: gg-daily-2026-10-23)을 숫자로 바꿔 난수의 씨앗으로 쓰면, 같은 날에는 어느 기기에서 열어도 같은 순서의 같은 문제가 나옵니다.
처음에는 2~9단 전체에서 10문제를 뽑았더니 2×2 같은 쉬운 문제가 몰리는 날이 생겼습니다. 지금은 3~9단에서 8문제, 2가 들어간 쉬운 문제 2개를 뽑아 섞습니다.
비용
D1 무료 한도는 하루에 쓰기 10만 행, 읽기 500만 행입니다(UTC 0시, 한국 시간 오전 9시에 다시 채워짐). 한 사람이 하루 10문제를 끝내면 쓰는 행은 1개입니다. 순위 계산은 그날 기록을 훑기 때문에 읽는 양은 푼 사람 수만큼 늘어나지만, 개인 게임 규모에서는 한도에 한참 못 미칩니다.
게임은 매일 구구단에서 바로 할 수 있고, 어떤 정보를 다루는지는 게임 개인정보처리방침에 따로 적어 두었습니다. 같은 게임을 앱으로 포장한 과정은 웹 게임을 플레이 스토어 앱으로에 정리했습니다.
자주 묻는 질문
다른 사람 이름이나 기록이 보이나요?
아니요. 화면에는 오늘 푼 사람 수와 내 등수(또는 상위 몇 %)만 나옵니다. 다른 사람의 번호나 시간은 보내지 않습니다.
하루의 기준은 언제인가요?
한국 시간 자정입니다. 날짜는 기기와 서버 모두 한국 시간(UTC+9)으로 계산합니다.
내 기록을 지우고 싶으면요?
기기 안의 기록은 사이트 데이터(앱 데이터)를 지우면 무작위 번호와 함께 사라집니다. 서버에 남은 기록은 문의 주소로 요청하면 지웁니다.
인터넷이 끊기면 게임이 안 되나요?
게임은 됩니다. 결과를 서버에 보내지 못하면 순위 줄만 나타나지 않습니다.