최종 갱신 (KST)

GameDesign/design/unlock/README.md

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 판정 순서

차감이 맨 뒤다. 무료 조건(선행·레벨)을 먼저 거르고, 전부 통과했을 때만 골드를 뺀다. 골드를 먼저 빼면 뒤 조건에서 거절될 때 되돌리는 코드가 필요해진다.

  1. UnlockTID가 테이블에 없다 → InvalidUnlockTID
  2. 이미 열렸다 → AlreadyUnlocked
  3. RequiredUnlockTIDs 중 안 열린 것이 있다 → UnlockLocked
  4. 계정 레벨 < AccountLevel → UnlockLocked
  5. 요청의 Currency가 이 해금의 지불 컬럼에 없다(값 0) → InvalidUnlockTID와 구분해 UnlockLocked
  6. 그 재화로 TrySpend… 실패 → NotEnoughCurrency
  7. 기록 · 응답 · 콘텐츠 후속

차감은 새로 만들지 않는다. 가챠·상점이 이미 쓰는 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)

  1. 슬롯의 계정 레벨 요구치 ✅ 해소 (2026-09-30 · #43) — 요구치를 두지 않는다. 골드 단독
  2. 퀘스트 완료 조건 — 퀘스트가 생기면 QuestTID 컬럼
  3. OR 조합 — 필요한 콘텐츠가 나오면 그룹 컬럼

이 문서가 답한 것:

  • 해금의 형태 — 한 표 · 조건은 컬럼 · AND ✅
  • 해금의 방식 — 유저의 행동 · 원자적 · 영구 ✅
  • 대상 연결 — 콘텐츠가 UnlockTID를 참조 ✅
  • 표시 — 전부 보임 · 문구는 클라 ✅
  • 첫 구현 — 작업슬롯 ✅
  • 계정 레벨 획득 — 캐릭터 경험치 전량 ✅ (2026-09-19) → 특성 3장 · 캐릭터 3.1