09. 해금 (Unlock)
상위 문서:
게임기획코어.md상태: 골격·데이터 확정 · 작업슬롯(T-038) 서버 완료 · 계정 레벨 서버 완료 · 특성은 해금을 쓰지 않는다(2026-10-02, T-108) 바뀌면 갱신:거래·게임UI·게임기획코어·산업레벨·자원채취·작업슬롯특성
"아직 못 하는 것"을 여는 규칙 하나. 작업슬롯·특성 액티브처럼 콘텐츠마다 따로 있던
해금을 한 표(UnlockTable)와 한 행동(해금 요청) 으로 모은다. 콘텐츠는 늘어나도 해금 규칙은 늘지 않는다.
1. 확정 사항 (2026-09-14)
| # | 항목 | 확정 내용 |
|---|---|---|
| 1 | 모든 해금은 한 표 | UnlockTable 행 하나 = 해금 하나. 콘텐츠별 해금 규칙을 따로 두지 않는다 |
| 2 | 조건은 컬럼 | Gold · AccountLevel · RequiredUnlockTIDs. 0 또는 빈 값 = 그 조건 없음. 여러 조건은 AND |
| 3 | 조건 종류 추가 = 컬럼 추가 | enum·파서를 두지 않는다. 퀘스트 조건이 생기면 QuestTID 컬럼을 더한다 → 3장 |
| 4 | 콘텐츠가 해금을 참조한다 | 콘텐츠 테이블이 UnlockTID 컬럼을 갖는다. UnlockTable은 대상을 모른다 |
| 5 | 해금은 유저의 행동 | 조건을 채워도 저절로 열리지 않는다. C_UnlockRequest를 보내야 열린다 |
| 6 | 한 요청에 원자적으로 | 조건 검사 → 골드 차감(TrySpendGold 재사용) → 기록. 중간 상태가 없다 |
| 7 | 영구 | 한 번 열리면 되돌아가지 않는다. 재검사 없음 |
| 8 | 잠긴 것은 전부 보인다 | 조건도 함께 보인다. 숨기는 콘텐츠를 두지 않는다 |
| 9 | 조건 문구는 클라가 만든다 | 클라가 UnlockTable을 읽고 보유 골드·레벨과 대조한다. 서버는 열린 목록만 준다 |
| 10 | TID 대역 | 작업슬롯 1xxx. 옛 특성 노드 2102~2505 · 3101~3505는 결번이다. 번호는 재사용하지 않는다 |
| 11 | 첫 구현 = 작업슬롯 | 조건은 골드 + 선행. 계정 레벨 조건은 두지 않는다(2026-09-30 · #43) → 4장 |
| 12 | 특성은 해금을 쓰지 않는다 (2026-10-02) | 특성은 레벨형이고 조건(계정 레벨)은 UserTraitLevelTable이 갖는다. 산업 레벨도 개척 특성의 레벨로 연다 → 특성 2.1 |
| 13 | 표시명은 UnlockTable.Name |
선행 해금을 이름으로 보여 준다. 클라가 콘텐츠 테이블을 역색인하지 않는다 (2026-09-16) |
| 14 | 서버 지급 경로 GrantUnlock |
퀘스트 보상·튜토리얼·운영이 조건 없이 연다. 통지는 유저 행동과 같다 (2026-09-16) → 2.4 |
| 15 | 로드 시 검증 3종 | 선행 순환·자기 참조 → 기동 실패 · 한 UnlockTID를 두 콘텐츠가 참조 → 기동 실패 · 미참조 → 경고 (2026-09-16) → 2.5 |
| 16 | 지불 컬럼끼리는 OR | 지불 컬럼이 둘 이상 생기면 유저가 하나를 골라 낸다. 나머지 조건과는 AND (2026-09-16). 지금 지불 컬럼은 Gold 하나다 — 다이아 폐지(2026-09-28) → 3장 |
| 17 | 계정 레벨은 S_AccountLevelResponse |
재화처럼 스냅샷·푸시가 같은 패킷 {Level, Exp, TraitPoint} — 서버 구현 완료 (2026-09-19) → 6장 |
| 18 | 잠긴 칸의 배치 행은 경고 후 무시 | "열렸다"의 원본은 t_user_unlock 하나 (2026-09-16) → 4.1 |
왜 한 표인가 — 해금이 "엄청 많이 쓰일" 것이라서다. 콘텐츠마다 조건 컬럼과 판정 코드를 파면 콘텐츠 수만큼 규칙이 생기고, 클라도 잠긴 칸을 콘텐츠마다 다르게 그려야 한다. 한 표면 서버 판정 한 곳, 클라 조건 문구 한 곳이다.
왜 행동인가 — 자동 해금은 골드를 받을 수 없다. 골드 지불이 첫 조건이므로 유저가 "연다"를 눌러야 한다. 조건이 레벨뿐인 해금도 같은 행동을 거친다 — 규칙이 하나여야 클라가 한 화면으로 처리한다.
2. 구조
UnlockTable (엑셀) 콘텐츠 테이블 (엑셀)
┌──────────┬──────────────┬──────┬──────────────┬─────────────────────┐ ┌────────────┬───────────┐
│ UnlockTID│ Name │ Gold │ AccountLevel │ RequiredUnlockTIDs │ ← │ WorkSlotTID│ UnlockTID │
│ 1003 │ 작업슬롯 3번 │ 1500 │ 0 │ 1002 │ │ 2 │ 1003 │
└──────────┴──────────────┴──────┴──────────────┴─────────────────────┘ └────────────┴───────────┘
유저 → C_UnlockRequest{1003, Gold}
서버 → RequiredUnlockTIDs 전부 열렸나? → AccountLevel 이상인가? → 고른 재화가 이 해금에 있나? → TrySpendGold(1500)?
→ t_user_unlock에 (user, 1003) 기록 → S_UnlockResponse{Ok, 1003}
→ 콘텐츠가 이어서 반응 (슬롯이면 S_WorkStationSlotSyncResponse로 새 칸)
| 층 | 무엇 | 어디 |
|---|---|---|
| 정의 | 해금 하나의 조건 | UnlockTable |
| 참조 | 어떤 콘텐츠가 어떤 해금에 걸리나 | 콘텐츠 테이블의 UnlockTID 컬럼 |
| 상태 | 유저가 무엇을 열었나 | t_user_unlock |
| 행동 | 열기 | C_UnlockRequest / S_UnlockResponse |
2.1 판정 순서
차감이 맨 뒤다. 무료 조건(선행·레벨)을 먼저 거르고, 전부 통과했을 때만 골드를 뺀다. 골드를 먼저 빼면 뒤 조건에서 거절될 때 되돌리는 코드가 필요해진다.
UnlockTID가 테이블에 없다 →InvalidUnlockTID- 이미 열렸다 →
AlreadyUnlocked RequiredUnlockTIDs중 안 열린 것이 있다 →UnlockLocked- 계정 레벨 <
AccountLevel→UnlockLocked - 요청의
Currency가 이 해금의 지불 컬럼에 없다(값 0) →InvalidUnlockTID와 구분해UnlockLocked - 그 재화로
TrySpend…실패 →NotEnoughCurrency - 기록 · 응답 · 콘텐츠 후속
차감은 새로 만들지 않는다. 가챠·상점이 이미 쓰는
User.TrySpendGold를 그대로 부른다. 해금이 재화를 다루는 방식이 다른 곳과 달라지지 않는다.
2.2 콘텐츠의 후속 반응
해금 시스템은 "열렸다"까지만 안다. 열린 뒤 무엇이 달라지는가는 콘텐츠의 몫이다.
| 콘텐츠 | 열리면 |
|---|---|
| 작업슬롯 | 그 칸이 생긴다. WorkStation.Unlock 후 슬롯 스냅샷 푸시 |
| 특성 액티브 | 그 액티브를 쓸 수 있다 (이관 후) |
서버는 S_UnlockResponse 뒤에 콘텐츠별 기존 패킷으로 상태를 밀어 준다. 해금 패킷에 콘텐츠 정보를 싣지 않는다.
2.4 서버가 직접 여는 경로 — GrantUnlock (2026-09-16)
유저 행동 없이 여는 길이 하나 더 있다. 퀘스트 보상·튜토리얼·운영 지급·치트가 쓴다.
TryUnlock |
GrantUnlock |
|
|---|---|---|
| 부르는 쪽 | C_UnlockRequest 핸들러 |
서버 내부 (퀘스트·튜토리얼·치트) |
| 조건 검사 · 차감 | 한다 | 하지 않는다 — 이미 열렸으면 아무것도 안 한다 |
| 기록 · 통지 · 콘텐츠 후속 | 같다 — t_user_unlock + S_UnlockResponse + 콘텐츠 패킷 |
같다 |
이 길이 없으면 퀘스트가 "보상으로 골드를 주고 유저가 사게" 같은 우회를 만들게 된다.
2.5 로드 시 검증 (2026-09-16)
UnlockCatalog가 서버 시작 때 검사한다. 위반은 데이터 오류이므로 기동을 막는다 — 조용히 돌면 영영 못 여는 해금이 생긴다.
| 검사 | 결과 |
|---|---|
RequiredUnlockTIDs 순환(A→B→A) · 자기 참조 |
기동 실패 |
한 UnlockTID를 두 콘텐츠 행이 참조 (1:1 위반) |
기동 실패 — "슬롯 하나 열었는데 산업 레벨도 열림"을 막는다 |
어느 콘텐츠도 참조하지 않는 UnlockTID |
경고 — 오타로 죽은 행 |
묶음 해금(하나 사면 여러 개 열림)은 두지 않는다. 필요하면 선행으로 표현한다 — B가 A를 선행으로 두면 A를 연 뒤 B는 공짜(조건 없음)로 열 수 있다.
3. 조건 컬럼
| 컬럼 | 타입 | 뜻 | 없음 |
|---|---|---|---|
Gold |
int | 이 골드를 낸다 (차감) — 지불 컬럼 | 0 |
AccountLevel |
int | 계정 레벨이 이 이상이다 (차감 없음) | 0 |
RequiredUnlockTIDs |
int[] | 이 해금들이 전부 열려 있다 — 선행. 쉼표 구분 | 빈 셀 |
- 지불 컬럼끼리는 OR, 나머지와는 AND (2026-09-16). 지불 컬럼이 둘 이상 적혀 있으면 유저가 하나를 골라 낸다(지금은
Gold하나뿐 — 다이아는 2026-09-28 폐지). 그래서 요청에 재화 선택이 실린다:C_UnlockRequest{UnlockTID, Currency}. 지불 컬럼이 하나뿐이면 그것만 유효하고, 값이 0인 재화를 고르면 거절한다. - 여러 선행은
1002,1003처럼 나열한다. 전부 열려야 한다(AND). - OR("둘 중 하나")은 두지 않는다. 필요해지면 그때 그룹 컬럼을 더한다 → 8장.
- 새 조건 종류는 컬럼 하나 + 서버 검사 한 줄이다. 후보:
QuestTID(퀘스트 완료), 누적 판정 횟수 같은 실적. 퀘스트가 미작성이라 지금은 넣지 않는다.
⚠️ 조건을 문자열로 적지 않는다 (
"Gold:100;Level:5"같은 것). 파이프라인의Ref·Min/Max검사가 문자열 안까지 못 들어가서 오타가 서버 런타임에서 터진다. 컬럼이면 엑셀 단계에서 잡힌다.
4. 콘텐츠별 적용
| 콘텐츠 | 참조 컬럼 | 조건 | 상태 |
|---|---|---|---|
| 작업슬롯 | WorkSlotTable.UnlockTID |
골드 + 선행(앞 칸). AccountLevel은 0(조건 없음) 확정 |
✅ 데이터 · ✅ 서버 (2026-09-16, T-038) · ❌ 클라 UI (일감 T-039) |
| 산업 레벨 | — (개척 특성 레벨) | 해금을 쓰지 않는다 → 특성 2.1 | ✅ 서버 (2026-10-02, T-108) · ❌ 클라 UI (T-108) |
| 특성 (속도 · 산출량) | — (UserTraitLevelTable) |
해금을 쓰지 않는다 | ✅ 서버 (2026-10-02, T-108) · ❌ 클라 UI (T-108) |
| 특성 액티브 | UserTraitTable (미작성) |
계정 레벨 | ❌ 미착수 |
| 거래소 · 상점 개방 · 보스 | — | 미정 | ❌ 기획 없음 |
4.1 작업슬롯 — 첫 구현
작업슬롯은 골드 단독으로 연다(2026-09-30 · #43) — 이 표의 Gold 컬럼만 쓰고 AccountLevel은 0이다.
시작 2칸(0·1번)은 UnlockTID = 0으로 항상 열려 있고, 2~7번이 해금 대상이다. 상한 8은 WorkSlotTable의 행 수다.
| 칸 | UnlockTID |
Gold |
AccountLevel |
선행 |
|---|---|---|---|---|
| 0 · 1 | 0 (항상 열림) | — | — | — |
| 2 | 1002 | 3,000 | 0 | — |
| 3 | 1003 | 30,000 | 0 | 1002 |
| 4 | 1004 | 300,000 | 0 | 1003 |
| 5 | 1005 | 1,000,000 | 0 | 1004 |
| 6 | 1006 | 5,000,000 | 0 | 1005 |
| 7 | 1007 | 10,000,000 | 0 | 1006 |
⚠️ 골드는 테스트값이다 — 8칸까지 약 한 달(하루 8시간 가동 · 번 골드의 절반을 슬롯에 쓴다는 가정,
9cd5611). 슬롯이 곧 재화 총량의 배수이므로(거래 3.3) 경제와 함께 다시 잡는다.AccountLevel 0은 조건 없음으로 확정한 값이다 — 이중 게이트는 2026-09-30에 버렸다(#43).
재로그인 때 잠긴 칸의 배치 행이 DB에 남아 있으면 경고만 남기고 무시한다 (2026-09-16). 열린 칸은
WorkSlotTable+t_user_unlock으로만 만든다. 미보유 캐릭터를 문 슬롯을 경고만 남기는 기존 규칙과 같은 태도다. 행은 지우지 않는다.
상점의 "작업슬롯 확장권" 품목은 없다 (2026-09-16). 슬롯은 잠긴 칸에서 직접 연다 — 같은 것을 두 곳에서 팔지 않는다. sink로서의 골드 유출은 그대로다 → 거래 3.2.
4.2 특성은 이 표를 쓰지 않는다 (2026-10-02)
특성은 레벨형이라 노드마다 해금 행을 두지 않는다. 레벨 조건은 UserTraitLevelTable, 기록은 t_user_trait이다 → 특성 2.1.
--- | --- | --- | --- | --- |
| 산업 레벨 Lv2~5 | 2000 + 산업×100 + 레벨 | 5 · 15 · 30 · 50 | 앞 레벨 | 특성 포인트 1 |
| 산업 속도 1~5단 | 3000 + 산업×100 + 단 | 0 · 10 · 20 · 30 · 40 | 앞 단 | 특성 포인트 1 |
- 값은 테스트값이다.
- 적성 조건은 없다 — 2026-08-01의 "적성 N AND 계정 레벨 M"에서 적성을 뺐다(2026-09-14).
- 산업별 최대 레벨을 저장하던
t_user_industry_level은 없앴다. 원본은t_user_unlock하나다.
5. 데이터 설계 (GameDesign/Excel/Unlock.xlsx · WorkSlot.xlsx)
5.1 UnlockTable
| 컬럼 | 타입 | 내용 |
|---|---|---|
UnlockTID |
int (키) | 대역: 작업슬롯 1xxx. 재사용 금지 — 옛 특성 노드 2102~2505 · 3101~3505는 결번 |
Name |
string | 표시명 — 조건 문구("작업슬롯 3번 먼저")에 쓴다. 콘텐츠 이름과 같게 적는다 |
Gold |
int (Min 0) | 지불 골드. 0 = 없음 |
AccountLevel |
int (Min 0) | 계정 레벨 요구치. 0 = 없음 |
RequiredUnlockTIDs |
int[] (Ref UnlockTable.UnlockTID?) |
선행 해금. 빈 셀 = 없음 |
Description |
string | 기획 메모 (로직 미사용) |
5.2 WorkSlotTable (WorkSlot.xlsx)
| 컬럼 | 타입 | 내용 |
|---|---|---|
WorkSlotTID |
int (키) | = 슬롯 번호 0~7. 행 수가 곧 상한(8) |
UnlockTID |
int (Ref UnlockTable.UnlockTID?) |
이 칸을 여는 해금. 0 = 항상 열림 |
Description |
string | 기획 메모 |
슬롯은 지금까지 테이블이 없었다. 코드 규칙(
1000 + 칸 번호)으로도 되지만 엑셀만 보고 알 수 없고, 상한 8이 코드 상수로 남는다. 시트 한 장이 둘 다 푼다.
6. 서버 · DB · 패킷
| 대상 | 내용 |
|---|---|
t_user_unlock |
(user_id, unlock_tid, unlocked_at). PK (user_id, unlock_tid). 열린 것만 행이 있다 |
C_UnlockRequest |
{ int UnlockTID, ECurrencyType Currency } — 지불 컬럼이 없는 해금은 Currency를 무시한다 |
S_UnlockResponse |
{ EResultCode Result, int UnlockTID } |
S_UnlockListResponse |
{ List<int> UnlockTIDs } — 로그인 직후 열린 목록 전체 |
S_AccountLevelResponse |
{ int Level, long Exp, int TraitPoint } — 계정 레벨 스냅샷·푸시 (재화와 같은 관례) |
| 특성 패킷 | 해금과 따로 간다 — C_UserTraitLearnRequest · S_UserTraitLearnResponse · S_UserTraitListResponse → 특성 2.1 |
| 결과 코드 | InvalidUnlockTID · AlreadyUnlocked · UnlockLocked(선행·레벨 미충족 · 이 해금에 없는 재화 선택) · NotEnoughCurrency(있음). TraitOnlyUnlock(802)은 쓰지 않는 결번 |
| 서버 | UnlockCatalog(테이블 인덱스 + 로드 검증 2.5) · User.Unlock.cs(TryUnlock · GrantUnlock · IsUnlocked) · SaveUnlockRepository |
- 로그인 시 열린 목록을 먼저 보낸다 — 슬롯 스냅샷보다 앞. 클라가 잠긴 칸을 그릴 때 이미 알고 있어야 한다.
- 콘텐츠는 "내
UnlockTID가 열렸나"만 묻는다.User.IsUnlocked(unlockTid)하나로 끝난다.UnlockTID = 0은 항상 true다. - 골드 차감과 해금 기록은 Repository가 둘이라 원자적이지 않다. 기존 재화 저장과 같은 수준으로 두고, 같은 세션 키로 직렬이라 순서만 보장한다.
7. 서버 / 클라이언트 책임
| 책임 | 주체 |
|---|---|
| 조건 판정 · 골드 차감 · 기록 | 서버 |
| 열린 목록 전달 | 서버 |
| 잠긴 것의 표시 · 조건 문구 · "지금 열 수 있나" 표시 | 클라 — UnlockTable + 보유 골드·레벨 |
| 해금 요청 · 결과 문구 | 클라 |
클라가 "열 수 있다"고 그려도 서버가 다시 검사한다. 골드가 새는 지점이므로 클라 판단을 믿지 않는다(게임기획코어 P4).
표시 규칙 — 전부 보인다. 잠긴 칸도, 그 조건도 그린다. 섬이 어디까지 커질 수 있는지가 목표가 된다(P1·리텐션). 콘텐츠마다 "잠금 표현"은 달라도(칸·목록·버튼) 숨기지는 않는다.
8. 결정 필요 (Open Questions)
슬롯의 계정 레벨 요구치✅ 해소 (2026-09-30 · #43) — 요구치를 두지 않는다. 골드 단독- 퀘스트 완료 조건 — 퀘스트가 생기면
QuestTID컬럼 - OR 조합 — 필요한 콘텐츠가 나오면 그룹 컬럼
이 문서가 답한 것: