10. 우편 (Mail)
상위 문서:
게임기획코어.md상태: 기획 확정(2026-09-22) · 서버 완료(운영 우편 T-081 · 넘침 보관 T-082) · 클라 화면 완료(T-083 · 2026-09-26) 바뀌면 갱신:거래·게임UI·게임기획코어·퀘스트
서버가 유저에게 보상을 맡겨 두는 곳. 운영자가 보내는 보상과, 인벤토리가 가득 차 받지 못한 보상이 여기에 쌓인다. 유저는 원할 때 열어서 받는다. 받지 않아도 사라지지 않는다 — 놓쳐도 손해가 없다(P3).
1. 확정 사항 (2026-09-22)
| # | 항목 | 확정 내용 |
|---|---|---|
| 1 | 역할은 셋 | 운영 지급(점검·사과·이벤트 보상) + 넘침 보관(인벤토리가 가득 차 받지 못한 보상) + 경매 정산(산 물건 · 판매 대금 · 취소·만료 반환 → 거래 4장). 퀘스트·레벨업 보상을 우편으로 돌려 보내는 "수령함"은 두지 않는다 — 받으려고 한 번 더 누르게 만들면 P2와 어긋난다 |
| 2 | 유저 간 우편은 없다 | 유저 사이에 물건이 오가는 곳은 거래뿐이다 |
| 3 | 한 통 = 첨부 여러 줄 | 줄마다 종류 하나 · 수량. 첨부가 없는 공지 우편은 두지 않는다 — 공지는 우편의 몫이 아니다 |
| 4 | 첨부 종류 | 아이템(자원·상자) · 장비 · 캐릭터 · 골드 · 장비 개체(경매 — 인챈트까지 그대로 옮긴다. 템플릿의 장비는 받을 때 새로 만든다) |
| 5 | 골드는 우편에만 허용한다 | 퀘스트 4장의 "골드 직접 지급 금지"를 우편에는 적용하지 않는다. 운영 판단으로 가끔 나가는 지급이라 상시 faucet이 되지 않는다 → 거래 1장 |
| 6 | 받는 대상 = 개인 · 전체 | 개인 우편은 UID 하나로 보낸다. 전체 우편은 기간을 정해 두고, 그 기간 안에 로그인한 모든 계정이 받는다(발송 뒤 가입한 계정 포함). 가입일·레벨 같은 다른 조건은 없다 |
| 7 | 전체 우편 배달 시점 | 로그인할 때 + 발송하는 순간 이미 접속 중인 유저에게도 즉시 → 2.2 |
| 8 | 안 받은 우편은 만료되지 않는다 | 기한이 있으면 "놓치면 손해"가 되어 P3와 충돌한다. 전체 우편의 기간은 보내는 창구의 기간일 뿐이다 — 일단 우편함에 들어온 우편은 사라지지 않는다 |
| 9 | 받은 우편은 7일 뒤 삭제 | 받은 뒤 7일 동안은 "방금 뭘 받았지"를 확인할 수 있게 남긴다 |
| 10 | 수령은 원자적이다 | 첨부가 전부 들어가야 받는다. 하나라도 인벤토리에 안 들어가면 그 우편 전체를 거절하고 "인벤토리 부족" 으로 알린다. 일부만 받는 상태는 없다 → 2.3 |
| 11 | 모두 받기 | 오래된 우편부터 차례로 받다가, 인벤토리에 안 들어가는 우편에서 멈춘다. 받은 수·남은 수를 알려 준다 → 2.3 |
| 12 | 받은 우편만 지울 수 있다 | 안 받은 우편은 지울 수 없다 — 보상을 실수로 지워 버리는 일을 막는다 |
| 13 | 넘침 보관 대상 = 서버가 주는 보상 | 상자 개봉 · 퀘스트 보상만 인벤토리가 가득 차면 우편으로 보낸다. 가챠는 거부한다(T-063) — 비용을 내고 우편함에 쌓는 건 이상하다. 채취 산출은 우편으로 보내지 않는다 — 판정마다 우편이 쌓인다 → 2.4 |
| 14 | 넘침 우편은 지급 1건 = 1통 | 상자 1회 개봉 · 퀘스트 1개 보상이 우편 한 통이다. 여러 건을 합치지 않는다 — 어디서 온 보상인지가 사라진다 |
| 15 | 우편함 상한 = 안 받은 우편 100통 | 값은 공용 상수 시트(T-085). 상한에 받은 우편은 세지 않는다. 상한이 차면 넘침 보관이 멈추고 원래대로 거절한다(상자 개봉 거부 등). 운영 우편은 상한과 관계없이 들어간다 → 2.4 |
| 16 | 문구·첨부는 엑셀 템플릿 | 템플릿 한 줄 = 제목 · 본문 · 발신자 · 첨부 · 기간(일). 운영툴이 생기면 자유 문구로 보낼 수 있게 바꾼다 → 3장 |
| 17 | 발송 = 치트(지금) → 운영툴(나중) | 치트 Arg1 = 템플릿 TID · Arg2 = 받는 UID(0이면 전체). 권한은 치트의 admin_level을 그대로 쓴다 → Server/docs/치트.md |
| 18 | 새 우편 표시 = 우편함 버튼의 점 하나 | 토스트·팝업·소리 없음(P1). 숫자·기한은 적지 않는다. 버튼을 어디에 둘지는 클라가 정한다 → 4장 |
왜 안 받은 우편을 만료시키지 않나 — 만료가 있으면 우편함이 "오늘까지"를 세는 물건이 된다. P3의 금지 목록(미사용분 이월·"오늘까지" 표시)과 같은 이유다. 대신 받은 우편만 7일 뒤 지워 DB가 끝없이 커지지 않게 한다.
왜 우편함에 상한을 두나 — 상한이 없으면 인벤토리가 가득 찬 채로 상자를 계속 열 수 있다. 그러면 우편함이 두 번째 인벤토리가 되어 인벤토리 한도(200칸)가 의미를 잃는다.
2. 설계
2.1 우편 한 통의 상태
[안 받음] ──수령──> [받음] ──7일 경과 또는 유저 삭제──> (삭제)
- 되돌아가는 전이는 없다. 안 받음 → 삭제로 가는 길도 없다(1장 8·12번).
2.2 전체 우편
전체 우편은 한 곳에 한 줄로 저장하고, 받을 자격이 생긴 유저의 개인 우편함으로 복사한다.
| 시점 | 할 일 |
|---|---|
| 발송 | 전체 우편 한 줄 저장(발송 시각 + 템플릿의 기간 일수) · 접속 중인 유저 전원에게 즉시 복사 |
| 로그인 | 기간 안의 전체 우편 중 아직 복사받지 않은 것을 복사 |
| 기간 종료 | 더 이상 복사하지 않는다. 이미 복사된 우편은 그대로 남는다 |
- 한 유저가 같은 전체 우편을 두 번 받지 않는다 — 복사 기록이 곧 중복 방지다.
2.3 수령
| 경우 | 결과 |
|---|---|
| 첨부가 전부 인벤토리에 들어간다 | 한 번에 지급 → 받음으로 바꾼다 |
| 하나라도 안 들어간다 | 아무것도 지급하지 않는다 · 인벤토리 부족 |
| 모두 받기 | 오래된 우편부터 위 규칙으로 하나씩. 처음으로 막힌 우편에서 멈춘다 · 받은 수 / 남은 수 |
- "들어간다"의 판정은 인벤토리 칸 계산(T-063)을 그대로 쓴다. 이미 가진 자원은 칸을 더 먹지 않는다.
- 골드는 칸을 먹지 않는다.
2.4 넘침 보관
서버가 보상을 준다 (상자 개봉 · 퀘스트 보상)
├─ 인벤토리에 전부 들어간다 → 바로 지급 (지금과 같다)
└─ 안 들어간다
├─ 안 받은 우편 < 100통 → 보상 전체를 우편 1통으로 (넘침 템플릿)
└─ 안 받은 우편 = 100통 → 원래대로 거절 (상자 개봉 거부 등)
- 보상을 쪼개지 않는다. 들어가는 것만 인벤토리에 넣고 나머지만 우편으로 보내지 않는다 — 보상 1건 = 우편 1통(1장 14번).
- 넘침 우편의 제목·본문은 넘침 전용 템플릿 한 줄을 쓰고, 첨부만 그때그때 채운다.
- 서버 구현(2026-09-22): 상자 개봉만 이 흐름을 탄다. 응답
S_ItemUseResponse.StoredInMail = true면 보상이 우편으로 갔다는 뜻이다. 퀘스트 보상은 지급 코드가 생길 때 같은 순서(HasStorageFor→HasMailboxRoom→StoreOverflowMail)를 탄다. - ⚠️ 자원 칸 200은 실제로 차지 않는다 — 아이템이 176종뿐이다. 지금 넘침은 장비 칸이 찬 상태에서 상자가 장비를 줄 때 일어난다.
3. 데이터 설계
3.1 엑셀 — Mail.xlsx / MailTemplateTable (2026-09-22)
우편 한 통 = 한 줄. 첨부도 같은 줄에 담는다(시트 하나 — 여러 시트를 오가지 않고 한 줄로 읽는다).
| 컬럼 | 뜻 |
|---|---|
MailTemplateTID |
템플릿 ID |
Title · Body · Sender |
표시 문구. 패킷에 싣지 않는다 — 클라가 이 표에서 읽는다 |
PeriodDays |
전체 우편으로 보낼 때만 쓰는 발송 기간(발송 시각부터 N일). 0이면 전체 우편으로 보낼 수 없다 |
Gold |
첨부 골드. 없으면 0 |
ItemTIDs · ItemCounts |
첨부 아이템과 수량 — 같은 순서·같은 개수. 개수가 다르면 서버 기동이 멈춘다 |
CharacterTIDs · EquipTIDs |
첨부 캐릭터·장비. 한 개체 = 한 원소(2명이면 TID를 두 번 적는다) |
- 넘침 보관용 템플릿(TID 2)은 첨부를 비워 둔 한 줄이다. 서버가 첨부를 채운다(T-082).
- 배열 컬럼에도
Ref가 걸려 있어 없는 TID는 파이프라인이 잡는다.
3.2 상수
| 값 | 기본 | 지금 위치 |
|---|---|---|
| 우편함 상한(안 받은 우편) | 100 | 서버 User.MailboxCapacity — 공용 상수 시트(T-077)가 서면 옮긴다 |
| 받은 우편 보관 일수 | 7 | 서버 Mail.ClaimedRetention — 공용 상수 시트(T-085)가 서면 옮긴다 |
3.3 DB
| 테이블 | 내용 |
|---|---|
t_user_mail |
우편 한 통 — 템플릿 TID · 보낸 순간의 첨부(JSON) · 도착 시각 · 받은 시각(NULL = 안 받음) |
t_global_mail |
전체 우편 한 줄 — 템플릿 TID · 발송 시각 · 종료 시각 |
t_user_global_mail |
전체 우편 복사 기록 (유저, 전체 우편) — 두 번 복사하지 않게 막는다 |
- 첨부를 보낸 순간 복사하므로 템플릿을 나중에 고쳐도 이미 보낸 우편은 그대로다.
4. 서버 / 클라 책임
| 항목 | 담당 |
|---|---|
| 우편 저장 · 전체 우편 복사 · 수령 판정(원자적) · 넘침 보관 · 7일 정리 | 서버 |
치트 발송(Arg1 템플릿 · Arg2 대상) |
서버 · 클라 치트창 |
| 우편함 화면 · 모두 받기 · 받은 우편 삭제 · 새 우편 점 | 클라 — 버튼 위치는 클라가 정한다 |
| 운영툴 발송(자유 문구 · 기간 지정) | 나중 — 운영툴이 생길 때 |
새 우편 점은 게임 UI 2.4의 "버튼에 배지 금지"의 예외다. 숫자·타이머·기한이 없는 점 하나이고, 기다리는 것이 사라지지 않는 보상이라 "지금 누르지 않으면 손해"를 만들지 않는다.
5. 결정 필요
- 없음. 엑셀 이름(3장)은 엑셀 작업 때 확정받는다.
구현 일감: T-081(서버 — 운영 우편) · T-082(서버 — 넘침 보관) · T-083(클라 — 우편함 화면)