Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

This file was deleted.

5 changes: 0 additions & 5 deletions Fantasy-server/Fantasy.Server/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
using Fantasy.Server.Domain.Player.Config;
using Fantasy.Server.Global.Config;
using Fantasy.Server.Global.Infrastructure;
using Fantasy.Server.Global.Security;
using Fantasy.Server.Global.Security.Config;
using Gamism.SDK.Extensions.AspNetCore;
using Microsoft.EntityFrameworkCore;
Expand Down Expand Up @@ -47,10 +46,6 @@
await GameDataSeeder.SeedAsync(db, logger);
}

// Gamism SDK는 환경 구분 없이 Swagger를 노출하므로 Production에서는 Basic Auth로 보호
if (app.Environment.IsProduction())
app.UseMiddleware<SwaggerBasicAuthMiddleware>();

app.UseGamismSdk();
app.UseAuthentication();
app.UseRateLimiter();
Expand Down
1 change: 0 additions & 1 deletion Fantasy-server/Fantasy.Server/deploy/compose.prod.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ services:
- ConnectionStrings__Database=${DB_CONNECTION_STRING}
- ConnectionStrings__Redis=${REDIS_CONNECTION_STRING}
- Jwt__SecretKey=${JWT_SECRET_KEY}
- Swagger__Password=${SWAGGER_PASSWORD}
depends_on:
fantasy-db:
condition: service_healthy
Expand Down
211 changes: 211 additions & 0 deletions docs/client-integration-guide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
# Client Integration Guide

클라이언트(게임 앱)가 Fantasy 서버를 통해 무엇을, 어떤 순서로 호출해야 하는지 정리한 문서입니다.
개별 필드 정의가 아니라 **호출 흐름과 규약**에 초점을 둡니다. 실제 요청/응답 스키마는 서버의 `/swagger`에서 확인할 수 있습니다.

## 1. 기본 규약

| 항목 | 내용 |
|---|---|
| Base 경로 | 모든 엔드포인트는 `/v1` 접두사 사용 |
| 프로토콜 | **HTTP** (배포 환경 포트 제약으로 HTTPS 미사용) |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

보안 위험 안내 (HTTP 사용 및 JWT 전송)

현재 배포 환경의 포트 제약으로 인해 HTTPS 대신 HTTP 프로토콜을 사용한다고 명시되어 있습니다. 하지만 HTTP 환경에서 JWT Bearer 토큰(Authorization 헤더)을 전송할 경우, 네트워크 상에서 토큰이 평문으로 노출되어 중간자 공격(MITM)을 통해 쉽게 탈취될 수 있습니다.

학교 포트 제약이 있더라도, Nginx 등의 역방향 프록시(Reverse Proxy)를 설정하거나 Cloudflare 등을 활용하여 비표준 포트에서도 HTTPS를 적용하는 것을 강력히 권장합니다.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gemini-code-assist[bot] HTTP 사용은 학교 서버 포트포워딩 제약(외부 443 확보 불가로 표준 ACME 검증 불가)으로 인지·수용한 의도된 결정입니다. 문서도 이 제약을 명시하고 있어 결함이 아닙니다. 비표준 포트 HTTPS(DuckDNS+Caddy DNS-01 등) 경로는 추후 별도 검토 예정입니다.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이해했습니다. 학교 서버 환경의 물리적/네트워크적 제약으로 인해 HTTPS 적용이 어려운 상황임을 명확히 인지했습니다. 문서에 해당 제약을 명시하고 의도된 설계임을 밝혀주셨으므로, 해당 이슈는 해결된 것으로 간주하겠습니다. 추후 환경 개선 시 HTTPS 도입을 검토하시겠다는 계획도 확인했습니다.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

프로토콜로 HTTPS 대신 HTTP를 사용하는 것은 로그인 비밀번호 및 JWT Access Token과 같은 민감한 정보가 평문으로 전송되어 중간자 공격(MITM)에 취약해지는 심각한 보안 위험이 있습니다. 학교 포트 제약 등의 환경적 한계가 있더라도, Caddy, Nginx 등의 역방향 프록시를 구성하거나 Cloudflare Tunnel 등을 활용하여 HTTPS를 적용하는 것을 강력히 권장합니다.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gemini-code-assist[bot] HTTP 사용은 동일 라인의 앞선 코멘트와 같이 학교 포트 제약으로 인지·수용한 의도된 결정입니다. 역방향 프록시/터널을 통한 HTTPS 경로는 추후 별도 검토 예정입니다.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

사용자의 상황과 의도를 충분히 이해했습니다. 학교 환경의 제약으로 인해 현재 단계에서 HTTP를 사용해야 한다는 점, 그리고 향후 역방향 프록시 등을 통해 HTTPS를 적용할 계획이 있다는 점을 명확히 인지했습니다. 보안 권고 사항을 수용해주셔서 감사합니다.

| 인증 | JWT Bearer — 보호 엔드포인트는 `Authorization: Bearer {accessToken}` 헤더 필요 |
| 본문 형식 | `application/json` |
| API 문서 | `/swagger` (현재 무인증 공개) |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

보안 위험 안내 (Swagger 무인증 공개)

운영(Production) 환경에서 Swagger UI를 무인증으로 공개하는 것은 API 엔드포인트 구조와 스키마를 외부 공격자에게 노출시켜 공격 표면(Attack Surface)을 넓히는 보안 위험이 있습니다.

비록 클라이언트 연동 편의를 위해 공개하더라도, 최소한 특정 IP 대역만 접근할 수 있도록 방화벽 설정을 하거나, 개발/스테이징 환경에서만 Swagger를 활성화하고 운영 환경에서는 비활성화하는 방안을 검토해 주세요.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gemini-code-assist[bot] Swagger 무인증 공개는 클라이언트 연동용 스키마 참조 제공을 위해 이번 PR에서 의도적으로 결정한 정책입니다(HTTP 환경, 리스크 인지·수용). IP 허용목록 등 강화는 추후 별도 검토하겠습니다.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

의견 주셔서 감사합니다. 보안 위험에 대한 인지 및 수용 정책을 확인했습니다. 추후 환경 개선 시 고려해 주시기 바랍니다.

| 헬스 체크 | `GET /v1/health` — 인증 불필요, `{ status, timestamp }` 반환 |

### 응답 래퍼

모든 응답은 공통 래퍼(`CommonApiResponse`)로 감싸집니다.

```json
{
"status": "...", // 성공/실패 구분 문자열 (SDK가 설정)
"code": 200, // HTTP 상태 코드
"message": "로그인 성공.", // 사람이 읽는 메시지
"data": { } // 실제 페이로드 (없으면 null)
}
```

- 컨트롤러가 DTO를 반환하면 `data`에 그대로 담겨 200으로 래핑됩니다.
- 회원가입처럼 생성 성공은 **201**, 본문 없는 성공은 **204**로 내려갈 수 있습니다.
- 클라이언트는 `data`만 사용하면 되고, `message`는 UI 피드백용으로 활용할 수 있습니다.

### 에러

서버는 예외를 던지면 동일한 래퍼 형태로 에러를 내려줍니다. 주요 코드:

| Code | 의미 | 대표 상황 |
|---|---|---|
| 400 | 잘못된 요청 | 유효성 위반, 티켓 부족, 비정상 클릭 수, 미해금 스킬 장착 등 |
| 401 | 인증 실패 | 로그인 실패, 토큰 만료/무효, 리프레시 토큰 재사용 감지 |
| 403 | 권한 없음 | 타인의 골드 던전 런에 접근 |
| 404 | 없음 | 플레이어/스킬/스테이지 데이터 없음 |
| 409 | 충돌 | 이메일 중복, 다른 직업의 플레이어 존재, 광고 보상 중복 수령 |
| 429 | 요청 과다 | 레이트리밋 초과 (`Too Many Requests` 텍스트) |

### 레이트리밋

| 정책 | 한도 | 기준 | 적용 대상 |
|---|---|---|---|
| `login` | 1분에 5회 | 클라이언트 IP | `POST /v1/auth/login` |
| `game` | 1초에 30회 | 계정 ID(JWT `sub`) | `/v1/player/*`, `/v1/dungeons/*`, 게임 데이터 조회 |

429를 받으면 클라이언트는 재시도 간격을 두어야 합니다.

## 2. 전체 흐름

```
[최초] signup → login → player/init(직업 선택)
[재접속] login(또는 refresh) → player/init(기존 데이터 로드)
[플레이] basic/state ↔ basic/claim · loadout · skill/unlock · weapon · boss · gold-runs
[토큰] accessToken 만료 → auth/refresh 로 갱신
[종료] logout
```

핵심 원칙:

- **로그인 직후 반드시 `player/init`을 호출**해 플레이어 상태를 확보합니다. (없으면 생성, 있으면 로드 — 멱등)
- 상태를 바꾸는 호출(`loadout`, `skill/unlock`, 던전 정산 등)은 응답에 **최신 `player` 전체 스냅샷**을 포함합니다. 클라이언트는 이를 단일 진실 소스로 삼으면 됩니다.
- 변화량은 `changes`(델타)로 함께 내려오므로 획득 연출에 사용합니다.

## 3. 인증 / 계정

| 메서드 | 경로 | 인증 | 설명 |
|---|---|---|---|
| POST | `/v1/account/signup` | ✗ | 회원가입. `email`(≤50, 이메일형식), `password`(8~20). 이메일 중복 시 409 |
| POST | `/v1/auth/login` | ✗ | 로그인. `email`,`password` → 토큰 발급. 레이트리밋 `login` |
| POST | `/v1/auth/refresh` | ✗ | 토큰 갱신. `refreshToken` → 새 토큰 세트 |
| POST | `/v1/auth/logout` | ✓ | 로그아웃. 서버의 리프레시 토큰 폐기 |
| DELETE | `/v1/account` | ✓ | 계정 삭제. 본문 `password` 재확인. 플레이어 데이터·토큰 모두 제거 |

### 토큰 사용 규칙

로그인/갱신 응답의 `data`:

```json
{ "accessToken": "...", "refreshToken": "...", "accessTokenExpiresAt": 1735660800 }
```

- `accessToken`: 보호 API 호출 시 `Authorization: Bearer` 헤더에 사용. 기본 수명 **15분**.
- `accessTokenExpiresAt`: Unix epoch(초). 클라이언트는 이 시각 이전에 갱신을 준비합니다.
- `refreshToken`: 수명 **30일**. **회전식(rotating)** 입니다.
- `refresh` 호출 시마다 **새 refreshToken이 발급되고 이전 토큰은 무효화**됩니다.
- 응답으로 받은 새 토큰을 반드시 저장하고 다음 갱신에 사용해야 합니다.
- 이미 사용한(이전) 토큰을 다시 보내면 **재사용 감지로 401** 처리됩니다 → 재로그인 필요.
Comment on lines +93 to +95

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Refresh Token Rotation (RTR) 적용 시 동시성(Race Condition) 고려

리프레시 토큰 회전(RTR) 방식을 사용할 때, 클라이언트(게임 앱)에서 여러 비동기 요청이 동시에 만료되어 각각 refresh API를 호출하는 상황이 발생할 수 있습니다.

이 경우 첫 번째 요청이 성공하여 토큰이 갱신되면, 거의 동시에 도달한 두 번째 요청은 이미 무효화된 이전 토큰을 사용하게 되므로 재사용 감지(401) 로직이 작동하여 사용자가 강제로 로그아웃되는 현상이 발생할 수 있습니다.

개선 제안:

  1. 서버 측 유예 기간(Grace Period) 도입: 이전 리프레시 토큰이 무효화된 후 아주 짧은 시간(예: 10~30초) 동안은 동시 요청을 감안하여 이전 토큰의 요청도 허용하거나 동일한 새 토큰 세트를 반환하도록 처리합니다.
  2. 클라이언트 측 갱신 락(Locking/Queueing): 클라이언트 연동 가이드에 '토큰 갱신 요청 중에는 다른 갱신 요청을 큐에 대기시키거나 단일 요청만 수행하도록 제한해야 한다'는 주의 사항을 명시해 주는 것이 좋습니다.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gemini-code-assist[bot] RTR 동시 갱신 레이스 컨디션 지적은 타당합니다. 클라이언트 측 갱신 직렬화/큐잉 안내는 곧 진행할 문서 정리에서 함께 검토하고, 서버 측 Grace Period 도입은 별도 서버 변경으로 분리하겠습니다.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네, 알겠습니다. 클라이언트 측 갱신 직렬화/큐잉 안내는 문서에 반영하고, 서버 측 Grace Period 도입은 별도 서버 변경으로 분리하여 진행하는 방향에 동의합니다. 해당 이슈를 잘 인지하고 계시니, 이후 작업 시 참고하시기 바랍니다.


## 4. 플레이어

모든 경로 `/v1/player/*` — 인증 필요, 레이트리밋 `game`.

| 메서드 | 경로 | 본문 | 설명 |
|---|---|---|---|
| POST | `/v1/player/init` | `{ jobType }` | 플레이어 생성/로드 |
| POST | `/v1/player/loadout` | `{ weaponId?, activeSkills[] }` | 장착 무기·액티브 스킬 저장 |
| POST | `/v1/player/skill/unlock` | `{ skillId }` | 스킬 해금 (SP 소모) |

- **init**: 플레이어가 없으면 `jobType`으로 신규 생성(201), 있으면 기존 데이터 로드(200). `jobType`은 **최초 생성 시에만 의미**가 있고, 이미 다른 직업으로 존재하면 **409**. 따라서 재접속 시에도 안전하게 호출 가능합니다.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

플레이어 로드/생성 흐름 설계 검토 (init API)

설명에 따르면 POST /v1/player/init 호출 시 플레이어가 이미 존재하고 다른 직업일 경우 409 에러를 반환한다고 되어 있습니다.

이 경우, 새로운 기기에서 로그인한 클라이언트는 기존 플레이어의 존재 여부나 직업(JobType)을 알지 못하는 상태에서 init을 호출해야 합니다. 만약 잘못된 jobType으로 호출하여 409 에러를 받게 된다면, 클라이언트는 기존 플레이어 데이터를 어떻게 조회하고 로드해야 하는지 흐름이 모호해질 수 있습니다.

개선 제안:

  1. jobType을 선택적으로 받기: init 호출 시 jobType을 누락하거나 null로 보내면, 기존 플레이어가 있을 경우 그대로 로드(200)하고, 없을 경우에만 특정 에러(예: 404)를 반환하여 직업 선택 화면으로 유도하는 방식.
  2. 조회 API 분리: 플레이어 존재 여부와 기본 정보를 조회하는 GET /v1/player API를 추가하여, 플레이어가 있으면 바로 로드하고 없을 때만 POST /v1/player/init을 통해 직업을 선택하여 생성하도록 분리하는 방식.

현재 가이드 문서상에 이 흐름에 대한 해결책(예: 409 응답 바디에 기존 플레이어 정보가 포함되는지 등)이 누락되어 있다면 보완이 필요해 보입니다.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gemini-code-assist[bot] 지적이 타당합니다. player/init의 생성·로드 겸용과 직업 불일치 409 모호함을 해소하기 위해 GET /v1/player(200/404, 로드) + POST /v1/player(201/409, 생성)로 분리하는 재설계를 진행합니다. 별도 스펙/브랜치로 처리합니다.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

제안해주신 GET /v1/player와 POST /v1/player로의 API 분리 설계는 클라이언트의 데이터 조회 및 생성 흐름을 훨씬 명확하게 만들어 줄 것입니다. 특히 기존 플레이어 존재 여부를 사전에 확인할 수 있게 되어, 409 Conflict 예외 처리에 의존하던 기존 방식보다 훨씬 견고한 구현이 가능할 것으로 보입니다. 진행하시는 재설계 방향에 전적으로 동의합니다.

- **loadout**: `weaponId`는 보유 무기여야 하고, `activeSkills`는 **해금된 액티브 스킬**만 허용(패시브·미해금·중복 → 400). 저장과 함께 방치 보상 정산이 함께 일어나 `changes`에 골드/경험치/레벨업이 포함될 수 있습니다.
- **skill/unlock**: 선행 스킬 해금 + SP 충분 + 해당 직업 스킬이어야 함. 이미 해금된 스킬이면 `wasAlreadyUnlocked=true`로 멱등 응답.

## 5. 게임 데이터(레퍼런스)

인증 필요, 레이트리밋 `game`. 정적 테이블이므로 **클라이언트가 1회 조회 후 캐시**하는 것을 권장합니다.

| 메서드 | 경로 | 설명 |
|---|---|---|
| GET | `/v1/jobs/{jobType}/skills` | 직업별 스킬 테이블 |
| GET | `/v1/jobs/{jobType}/weapons` | 직업별 무기 테이블 |
| GET | `/v1/levels` | 레벨별 필요 경험치·보상 SP |
| GET | `/v1/stages` | 스테이지별 몬스터 HP·초당 골드/경험치 |

> 이 엔드포인트들은 enum 값을 **문자열 이름**(`"Warrior"`, `"C"`, `"AtkPercent"`)으로 반환합니다.

## 6. 던전

모든 경로 `/v1/dungeons/*` — 인증 필요, 레이트리밋 `game`.

### 6.1 기본(방치) 던전

| 메서드 | 경로 | 설명 |
|---|---|---|
| GET | `/v1/dungeons/basic/state` | 방치 상태 스냅샷 (읽기 전용) |
| POST | `/v1/dungeons/basic/claim` | 방치 보상 정산 |

- `state`는 `stage`, `lastCalculatedAt`, `serverNow`, `maxOfflineSeconds`(28800초 = **8시간**), `combatPower`(DPS), `goldPerSecond`, `xpPerSecond`를 반환합니다.
- 클라이언트는 `serverNow`와 `lastCalculatedAt`을 기준으로 누적 보상을 **로컬에서 표시**하고, 실제 지급은 `claim`으로 받습니다.
- `claim`은 경과 시간(최대 8시간으로 캡)만큼 골드/경험치를 지급하고, DPS가 현재 스테이지를 클리어할 수 있으면 `maxStage`를 +1 올린 뒤 `lastCalculatedAt`을 현재로 리셋합니다. 앱 포그라운드 진입 시 호출을 권장합니다.

### 6.2 무기 던전 / 보스 던전 (즉시 전투)

| 메서드 | 경로 | 설명 |
|---|---|---|
| POST | `/v1/dungeons/weapon` | 무기 파밍. 클리어(`DPS×30 ≥ 몬스터HP`) 시 확률 드랍 |
| POST | `/v1/dungeons/boss` | 보스 전투. 보스HP = 일반 몬스터HP×5 |

- 무기 던전 드랍 확률(클리어 시): B등급 20%, C등급 70%, 강화 스크롤 30%.
- 보스 클리어 시: 미스릴 +1, 경험치 = `스테이지 초당경험치 × 10`, **A등급 무기 확정 드랍**, 레벨업 가능. 미클리어면 보상 없음.

### 6.3 골드 던전 (클릭형, 티켓 소모)

| 메서드 | 경로 | 설명 |
|---|---|---|
| GET | `/v1/dungeons/tickets` | 티켓 현황 조회 |
| POST | `/v1/dungeons/gold-tickets/ad-reward` | 광고 보상 티켓 +1 (하루 1회) |
| POST | `/v1/dungeons/gold-runs` | 런 시작 (티켓 1장 소모) |
| POST | `/v1/dungeons/gold-runs/{runId}/claim` | 런 결과 정산 |

티켓 규칙:

- 매일(**KST 기준 날짜**) 첫 접근 시 자동으로 **3장 충전**됩니다(지연 지급).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

'지연 지급'이라는 표현은 네트워크 지연이나 의도적인 지급 보류(delayed payment)로 오해하기 쉽습니다. 매일 첫 접근 시점에 필요한 계산을 수행하여 충전하는 방식(Lazy Loading)을 의미한다면, '최초 요청 시점에 충전' 또는 '첫 요청 시점에 실시간 충전'과 같이 표현을 명확히 수정하는 것이 클라이언트 개발자의 혼선을 줄일 수 있습니다.

Suggested change
- 매일(**KST 기준 날짜**) 첫 접근 시 자동으로 **3장 충전**됩니다(지연 지급).
- 매일(**KST 기준 날짜**) 첫 접근 시 자동으로 **3장 충전**됩니다(최초 요청 시점에 충전).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gemini-code-assist[bot] '지연 지급' 표현이 모호하다는 지적 반영해 '최초 요청 시점에 충전'으로 수정했습니다. 이 문서는 곧 플레이어 흐름 재설계 작업에서 함께 정리됩니다.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네, 확인했습니다. 해당 문서는 향후 플레이어 흐름 재설계와 함께 최신화될 예정이라는 점 인지했습니다. 추가로, 문서의 160번 라인에 있는 '지연 지급' 표현을 '최초 요청 시점에 충전'으로 수정해주셔서 감사합니다. 클라이언트 개발자 입장에서 훨씬 명확한 가이드가 될 것입니다.

- 광고 보상으로 하루 1회 +1 가능. 같은 날 두 번 시도하면 **409**.

런 흐름:

1. `gold-runs` 호출 → 티켓 1장 소모, 런 생성. 응답: `runId`, `startedAt`, `durationSeconds`(30), `expiresAt`, `maxClicks`(15×30 = **450**). 티켓이 없으면 **400**.
2. 30초 동안 클라이언트가 클릭 수를 집계.
3. `gold-runs/{runId}/claim`에 `{ clicks }` 전송. 서버 검증:
- 런 소유자 불일치 → 403
- `expiresAt` 초과(시작+30초+여유 30초) → 400
- `clicks > maxClicks` 또는 **경과 시간 대비 과다 클릭** → 400 (어뷰징 방지)
4. 보상: 클릭당 **10골드**, 2% 확률로 미스릴 1.
5. 이미 정산된 런을 다시 호출하면 **동일 보상으로 멱등 응답**(중복 지급 없음).

## 7. 공통 데이터 구조

### PlayerDataResponse (`player`)

상태 변경 응답들이 공통으로 포함하는 플레이어 전체 스냅샷:

```
jobType, level, maxStage, lastWeaponId?, activeSkills[],
gold, exp, enhancementScroll, mithril, sp,
weapons[ { weaponId, count, enhancementLevel, awakeningCount } ],
skills[ { skillId, isUnlocked } ]
```

### ChangesDto (`changes`)

이번 호출로 발생한 변화량(델타). 연출/토스트용:

```
gold, exp, sp, mithril, enhancementScroll, dungeonTickets,
levelUps[], unlockedSkillIds[], acquiredWeaponIds[], maxStage
```

### Enum

| Enum | 값 (순서값) |
|---|---|
| JobType | `Warrior`(0), `Archer`(1), `Mage`(2) |
| WeaponGrade | `C`(0), `B`(1), `A`(2), `S`(3) |
| SkillEffectType | `AtkFlat`, `AtkPercent`, `HpFlat`, `HpPercent`, `CritRate`, `CritDmg`, `CooldownReduce`, `ElementalBoost` |

> 게임 데이터 조회 엔드포인트(5장)는 enum을 **문자열 이름**으로 반환합니다.
> 플레이어/던전 응답의 enum 직렬화 형태(이름 vs 순서값)는 `/swagger`의 실제 응답으로 확인 후 매핑하세요.

## 8. 시간 처리

- 서버 시각은 모두 **UTC**입니다. 응답의 시각 필드(`serverNow`, `lastCalculatedAt`, `expiresAt` 등)는 UTC 기준입니다.
- 단, 일일 리셋(던전 티켓·광고 보상)은 **KST(UTC+9) 날짜** 기준으로 판정됩니다.
- 클라이언트는 로컬 시계 대신 서버가 내려준 시각(`serverNow`)을 기준으로 동기화하는 것을 권장합니다.
Loading