[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6CBHDP3WAD02W5BDDRADZ1S":3,"skill-same:01M6CBHDP3WAD02W5BDDRADZ1S":51},{"skillNo":4,"name":5,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":7,"ownerLogin":8,"ownerAvatarUrl":9,"ownerAccountNo":10,"ownerDisplayName":11,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":15,"categories":16,"keywords":17,"publishedAt":20,"linkOnly":21,"markdownContent":22,"markdownBody":22,"frontMatter":23,"refName":24,"commitSha":25,"skillDirectory":26,"forksCount":13,"repositoryHtmlUrl":27,"repositoryHomepageUrl":6,"ownerHtmlUrl":28,"ownerBlogUrl":6,"ownerDescription":29,"updatedAt":30,"install":31,"resources":44},"01M6CBHDP3WAD02W5BDDRADZ1S","43-savedata-json-codec",null,"devwinsoft\u002Fdevian","devwinsoft","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F22570160?v=4","01M6C6RR8W05NH8NPKQNCE3Q72","maoshy","01M6C6RRDJ5TJJCZXRTY801YDH",0,"MIT","https:\u002F\u002Fgithub.com\u002Fdevwinsoft\u002Fdevian\u002Fblob\u002Fmain\u002Fskills\u002Fdevian-unity\u002F50-mobile-package\u002F21-savedata-system\u002F43-savedata-json-codec\u002FSKILL.md",[],[18,19],"json","43 savedata json codec","2026-11-10 10:12:30",false,"# 43-savedata-json-codec — SaveDataJsonCodec\n\n\nStatus: ACTIVE\nAppliesTo: v10\nType: Design \u002F SSOT\n\n\n## Purpose\n\nSaveDataManager가 local\u002Fcloud payload에 저장할 게임 상태 JSON의 직렬화\u002F역직렬화 규약을 정의한다.\n\n- JSON 직렬화\u002F역직렬화의 **유일한 진입점은 SaveDataManager** 이다.\n- `AccountManager`, `InventoryManager`, `PurchaseManager`, `GameMessageManager`, `MissionManager`, `AchieveManager`, `LeaderboardManager`, `AttendManager`, `TreasureManager`는 각자 자신의 `Storage`를 소유한다.\n- JSON 구현은 `SaveDataJsonCodec` root codec + section codec으로 분리한다.\n\n\n---\n\n\n## Ownership\n\n- `AccountManager` 소유: `AccountStorage`\n- `InventoryManager` 소유: `InventoryStorage`\n- `PurchaseManager` 소유: `PurchaseStorage`\n- `GameMessageManager` 소유: `GameMessageStorage`\n- `MissionManager` 소유: `MissionStorage`\n- `AchieveManager` 소유: `AchieveStorage`\n- `LeaderboardManager` 소유: `LeaderboardSeasonRewardStorage`\n- `AttendManager` 소유: `AttendStorage`\n- `InventoryStorage` 내부: `TreasureCurrent`, `TreasureCounts` (treasure 상태)\n- `SaveDataManager` 책임:\n  - 위 storage를 수집\n  - primary save binding(local filename + cloud slot) 관리\n  - JSON serialize\u002Fdeserialize 호출\n  - payload encrypt\u002Fdecrypt\n  - local\u002Fcloud save, load, sync, conflict\n\n중요:\n- `GameStorageManager`는 삭제되었다.\n- 각 manager는 자신의 storage를 직접 소유하며, SaveDataManager가 이를 묶어 저장한다.\n\n\n---\n\n\n## Class Boundary\n\n```text\nSaveDataManager : CompoSingleton\u003CSaveDataManager>\n│\n├── ToJson()\n│   └── SaveDataJsonCodec.Serialize(inventory, purchase, shop, account, message, mission, achieve, leaderboardReward, attend, treasure)\n│\n├── LoadFromJson(json)\n│   └── SaveDataJsonCodec.DeserializeInto(json, inventory, purchase, shop, account, message, mission, achieve, leaderboardReward, attend, treasure)\n│\n├── LoadFromPayload(payload)\n│   └── ComplexUtil.Decrypt_Base64(payload) -> LoadFromJson(json)\n│\n└── ClearGameState()\n    ├── AccountManager.Instance.Storage.Clear()\n    ├── InventoryManager.Instance.Storage.Clear()\n    ├── PurchaseManager.Instance.Storage.ClearAll()\n    ├── GameMessageManager.Instance.Storage.Clear()\n    ├── MissionManager.Instance.Storage.Clear()\n    ├── AchieveManager.Instance.Storage.Clear()\n    ├── LeaderboardManager.Instance.Storage.Clear()\n    ├── AttendManager.Instance.Storage.Clear()\n    └── TreasureManager.Instance.Storage.Clear()\n```\n\n- root orchestration: `SaveDataJsonCodec`\n- inventory section: `SaveDataJsonCodecInventory`\n- purchase section: `SaveDataJsonCodecPurchase`\n- account section: `SaveDataJsonCodecAccount`\n- message section: `SaveDataJsonCodecMessage`\n- mission section: `SaveDataJsonCodecMission`\n- achieve section: `SaveDataJsonCodecAchieve`\n- leaderboard reward section: `SaveDataJsonCodecLeaderboardReward`\n- attend section: `SaveDataJsonCodecAttend`\n- treasure section: `SaveDataJsonCodecTreasure`\n\nsection codec은 manager를 직접 알지 않고, `Storage` 타입만 다룬다.\n역직렬화 실패는 `throw` 대신 `CommonResult.Failure(...)`로 root caller에 전달한다.\n\n\n## Error Model\n\n- save\u002Fload는 대표적인 boundary다. parse\u002Fversion\u002Frestore 실패는 `CommonResult`로 반환한다.\n- root caller(`SaveDataManager`)가 clear\u002Fretry\u002Ffallback 정책을 선택한다.\n- section codec은 malformed JSON, lookup miss, restore failure를 조용히 무시하지 않는다.\n- private helper에서만 이미 정규화된 내부 invariant 위반 예외를 제한적으로 허용할 수 있다.\n- 단, helper 예외가 codec boundary를 넘어 public API 기본 모델이 되지 않게 한다.\n\n\n## Primary Save Rule\n\n- public save API는 멀티 슬롯을 노출하지 않는다.\n- `SaveDataManager`는 단일 primary save binding만 가진다.\n- codec은 binding을 모르고, 순수하게 게임 상태 JSON만 다룬다.\n\n\n---\n\n\n## Root JSON Schema\n\n```json\n{\n  \"version\": 20,\n  \"inventory\": {},\n  \"purchase\": {},\n  \"shop\": {},\n  \"account\": {},\n  \"message\": {},\n  \"mission\": {},\n  \"achieve\": {},\n  \"leaderboardReward\": {},\n  \"attend\": {},\n  \"treasure\": {}\n}\n```\n\n- `version`: root schema version\n- `inventory`: `InventoryStorage` 섹션\n- `purchase`: `PurchaseStorage` 섹션\n- `shop`: `ShopStorage` 섹션\n- `account`: `AccountStorage` 섹션\n- `message`: `GameMessageStorage` 섹션\n- `mission`: `MissionStorage` 섹션\n- `achieve`: `AchieveStorage` 섹션\n- `leaderboardReward`: `LeaderboardSeasonRewardStorage` 섹션\n- `attend`: `AttendStorage` 섹션\n- `treasure`: `InventoryStorage` treasure 섹션\n\npayload wrapper(`SaveLocalPayload`, `SaveCloudPayload`)의 `account` 메타는 유지한다.\n이 메타는 sync 초기 판정과 account mirror 용도이며, root JSON의 `account` 섹션과 별개로 존재할 수 있다.\n\n\n---\n\n\n## Section Sourcing\n\nserialize 시 source:\n- `AccountManager.Instance.Storage`\n- `InventoryManager.Instance.Storage`\n- `PurchaseManager.Instance.Storage`\n- `GameMessageManager.Instance.Storage`\n- `MissionManager.Instance.Storage`\n- `AchieveManager.Instance.Storage`\n- `LeaderboardManager.Instance.Storage`\n- `AttendManager.Instance.Storage`\n- `TreasureManager.Instance.Storage`\n\ndeserialize 시 target:\n- `AccountManager.Instance.Storage`\n- `InventoryManager.Instance.Storage`\n- `PurchaseManager.Instance.Storage`\n- `GameMessageManager.Instance.Storage`\n- `MissionManager.Instance.Storage`\n- `AchieveManager.Instance.Storage`\n- `LeaderboardManager.Instance.Storage`\n- `AttendManager.Instance.Storage`\n- `TreasureManager.Instance.Storage`\n\n로드 직후 런타임 재적용:\n- `AccountManager.ApplyStorage(AccountManager.Instance.Storage)`\n- `InventoryStorage.Rentals\u002FPasses`는 payload(local\u002Fcloud)에 저장\u002F복원되며, 로그인\u002F복원 시 `PurchaseManager.SyncEntitlementsAsync()`가 서버 entitlements 값으로 재동기화한다.\n- Mission은 `MissionManager`가 자신의 `MissionStorage`를 기준으로 runtime state를 재구성한다\n- Mission\u002FAchieve\u002FSeasonReward claim 상태 변경 직후에는 `SaveDataManager.SaveGameStorageAsync(true, ct)`로 root payload 저장을 즉시 시도해야 한다\n- 이 local save가 실패하면 mission 시스템은 플레이 불가능 상태로 처리한다(TODO)\n\n\n---\n\n\n## Version \u002F Migration Rules\n\n- schema 변경 시 root `version`을 증가시킨다.\n- 하위호환이 필요하면 root codec에서 version 분기 처리한다.\n- section codec은 자신이 담당하는 섹션의 레거시 필드만 복구한다.\n- 지원하지 않는 version이면 조용히 부분 로드하지 말고, 명시적인 fallback 정책을 문서에 기록한다.\n- malformed JSON \u002F section restore failure는 `CommonResult.Failure(...)`로 반환하고 상위 caller가 전체 상태를 정리한다.\n- save payload\u002Fsection 오류를 public API에서 `throw` 기본 모델로 바꾸지 않는다.\n\n\n---\n\n\n## Hard Rules\n\n- manager가 자신의 storage를 소유하더라도, **payload JSON 직렬화 진입점은 SaveDataManager 하나만 유지**한다.\n- `InventoryStorage`, `PurchaseStorage`, `AccountStorage`, `GameMessageStorage`, `MissionStorage`, `AchieveStorage`, `LeaderboardSeasonRewardStorage`, `AttendStorage`에 `ToJson()` \u002F `FromJson()`을 다시 추가하지 않는다.\n- codec은 domain rule을 가지지 않는다. domain mutation은 각 manager가 담당한다.\n- inventory item save는 generic `stats` blob를 생성 정본으로 쓰지 않는다. item 생성은 `item_id\u002FitemUid\u002Fitem_level`과 table lookup으로 복원하고, mutable 상태는 `amount\u002Fequips` 같은 explicit field로 저장\u002F복원한다.\n- equip owner는 별도 field로 저장하지 않는다. hero equip ownership의 저장\u002F복원 SSOT는 `heroes[*].equips`다.\n- save JSON에서 enum은 숫자 ordinal로 저장하지 않는다. enum key\u002Fvalue가 필요하면 반드시 enum name 문자열을 사용한다.\n- legacy 숫자 enum 파싱은 load 호환용으로만 유지한다. 새 serialize 경로는 숫자 enum을 쓰지 않는다.\n- 새 저장 섹션 추가 시:\n  - root codec 수정\n  - section codec 추가\n  - SaveDataManager sourcing 연결\n  - 관련 manager skill 문서 갱신\n- save\u002Fload 전용 신규 에러 코드는 `COMMON_ERROR` append-only 규칙 안에서만 추가한다.\n\n\n---\n\n\n## Implementation Location (3-path mirror)\n\n- UPM (정본):\n  - `framework-cs\u002Fupm\u002Fcom.devian.foundation\u002FSamples~\u002FMobilePackage\u002FRuntime\u002FSaveData\u002FJsonCodec\u002FSaveDataJsonCodec.cs`\n  - `framework-cs\u002Fupm\u002Fcom.devian.foundation\u002FSamples~\u002FMobilePackage\u002FRuntime\u002FSaveData\u002FJsonCodec\u002FSaveDataJsonCodecInventory.cs`\n  - `framework-cs\u002Fupm\u002Fcom.devian.foundation\u002FSamples~\u002FMobilePackage\u002FRuntime\u002FSaveData\u002FJsonCodec\u002FSaveDataJsonCodecPurchase.cs`\n  - `framework-cs\u002Fupm\u002Fcom.devian.foundation\u002FSamples~\u002FMobilePackage\u002FRuntime\u002FSaveData\u002FJsonCodec\u002FSaveDataJsonCodecAccount.cs`\n  - `framework-cs\u002Fupm\u002Fcom.devian.foundation\u002FSamples~\u002FMobilePackage\u002FRuntime\u002FSaveData\u002FJsonCodec\u002FSaveDataJsonCodecMessage.cs`\n  - `framework-cs\u002Fupm\u002Fcom.devian.foundation\u002FSamples~\u002FMobilePackage\u002FRuntime\u002FSaveData\u002FJsonCodec\u002FSaveDataJsonCodecMission.cs`\n  - `framework-cs\u002Fupm\u002Fcom.devian.foundation\u002FSamples~\u002FMobilePackage\u002FRuntime\u002FSaveData\u002FJsonCodec\u002FSaveDataJsonCodecAchieve.cs`\n  - `framework-cs\u002Fupm\u002Fcom.devian.foundation\u002FSamples~\u002FMobilePackage\u002FRuntime\u002FSaveData\u002FJsonCodec\u002FSaveDataJsonCodecLeaderboardReward.cs`\n  - `framework-cs\u002Fupm\u002Fcom.devian.foundation\u002FSamples~\u002FMobilePackage\u002FRuntime\u002FSaveData\u002FJsonCodec\u002FSaveDataJsonCodecAttend.cs`\n  - `framework-cs\u002Fupm\u002Fcom.devian.foundation\u002FSamples~\u002FMobilePackage\u002FRuntime\u002FSaveData\u002FJsonCodec\u002FSaveDataJsonCodecTreasure.cs`\n- Packages (sync):\n  - `framework-cs\u002Fapps\u002FUnityExample\u002FPackages\u002Fcom.devian.foundation\u002FSamples~\u002FMobilePackage\u002FRuntime\u002FSaveData\u002FJsonCodec\u002F...`\n- Assets\u002FSamples (import):\n  - `framework-cs\u002Fapps\u002FUnityExample\u002FAssets\u002FSamples\u002FDevian Samples\u002F{version}\u002FMobilePackage\u002FRuntime\u002FSaveData\u002FJsonCodec\u002F...`\n\n\n---\n\n\n## Related\n\n- [10-savedata-manager](..\u002F10-savedata-manager\u002FSKILL.md) — SaveDataManager 진입점\n- [41-savedata-savelocal](..\u002F41-savedata-savelocal\u002FSKILL.md) — Local payload 저장\n- [42-savedata-savecloud](..\u002F42-savedata-savecloud\u002FSKILL.md) — Cloud payload 저장\n- [33-account-manager](..\u002F..\u002F20-account-system\u002F33-account-manager\u002FSKILL.md) — AccountStorage 소유자\n- [10-inventory-manager](..\u002F..\u002F22-inventory-system\u002F10-inventory-manager\u002FSKILL.md) — InventoryStorage 소유자\n- [33-purchase-storage](..\u002F..\u002F30-purchase-system\u002F33-purchase-storage\u002FSKILL.md) — PurchaseStorage 규약\n- [45-game-message-system\u002F14-game-message-storage](..\u002F..\u002F45-game-message-system\u002F14-game-message-storage\u002FSKILL.md) — GameMessageStorage 규약\n- [48-mission-system\u002F12-mission-storage](..\u002F..\u002F48-mission-system\u002F12-mission-storage\u002FSKILL.md) — MissionStorage 규약\n- [46-achieve-system\u002F14-achieve-storage](..\u002F..\u002F46-achieve-system\u002F14-achieve-storage\u002FSKILL.md) — AchieveStorage 규약\n- [50-leaderboard\u002F14-leaderboard-season-reward-storage](..\u002F..\u002F50-leaderboard\u002F14-leaderboard-season-reward-storage\u002FSKILL.md) — 시즌 보상 저장 규약\n- [27-attend-system\u002F11-attend-storage](..\u002F..\u002F27-attend-system\u002F11-attend-storage\u002FSKILL.md) — AttendStorage 규약\n- [51-treasure-system\u002F03-ssot](..\u002F..\u002F51-treasure-system\u002F03-ssot\u002FSKILL.md) — Treasure 시스템 SSOT\n",{},"main","8cb5bd877a3a7004867d2f55697cb9e4ff2f1734","skills\u002Fdevian-unity\u002F50-mobile-package\u002F21-savedata-system\u002F43-savedata-json-codec","https:\u002F\u002Fgithub.com\u002Fdevwinsoft\u002Fdevian","https:\u002F\u002Fgithub.com\u002Fdevwinsoft","Kim Hyong Joon","2026-04-12 18:44:10",{"source":7,"skillName":5,"commands":32,"prompt":42,"zipAvailable":43},[33,36,39],{"tool":34,"command":35},"npx","npx skills add devwinsoft\u002Fdevian --skill 43-savedata-json-codec",{"tool":37,"command":38},"bunx","bunx skills add devwinsoft\u002Fdevian --skill 43-savedata-json-codec",{"tool":40,"command":41},"pnpm","pnpm dlx skills add devwinsoft\u002Fdevian --skill 43-savedata-json-codec","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fdevwinsoft\u002Fdevian\u002Fblob\u002Fmain\u002Fskills\u002Fdevian-unity\u002F50-mobile-package\u002F21-savedata-system\u002F43-savedata-json-codec\u002FSKILL.md",true,[45],{"path":46,"resourceType":47,"resourceTypeDesc":48,"sizeBytes":49,"sourceHtmlUrl":50},"agents\u002Fopenai.yaml","REFERENCE","参考文档",103,"https:\u002F\u002Fgithub.com\u002Fmalinskibeniamin\u002Fskills\u002Fblob\u002Fmain\u002Fcodex-skills\u002Faip\u002Fagents\u002Fopenai.yaml",[52,58,66,74,83,90],{"skillNo":53,"name":54,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":7,"ownerLogin":8,"ownerAvatarUrl":9,"ownerAccountNo":10,"ownerDisplayName":11,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":55,"categories":56,"keywords":57,"publishedAt":20},"01M6CBX3K2JJV38ANHVGCAT8A0","devian","https:\u002F\u002Fgithub.com\u002Fdevwinsoft\u002Fdevian\u002Fblob\u002Fmain\u002Fskills\u002Fdevian\u002FSKILL.md",[],[54],{"skillNo":59,"name":60,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":7,"ownerLogin":8,"ownerAvatarUrl":9,"ownerAccountNo":10,"ownerDisplayName":11,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":61,"categories":62,"keywords":63,"publishedAt":20},"01M6CBX210BFMM8AMN770J9WY8","90-project-archive","https:\u002F\u002Fgithub.com\u002Fdevwinsoft\u002Fdevian\u002Fblob\u002Fmain\u002Fskills\u002Fdevian\u002F80-tools\u002F90-project-archive\u002FSKILL.md",[],[64,65],"archive","90 project archive",{"skillNo":67,"name":68,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":7,"ownerLogin":8,"ownerAvatarUrl":9,"ownerAccountNo":10,"ownerDisplayName":11,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":69,"categories":70,"keywords":71,"publishedAt":20},"01M6CBWZSGRR9R3YJZB3177667","30-env-management","https:\u002F\u002Fgithub.com\u002Fdevwinsoft\u002Fdevian\u002Fblob\u002Fmain\u002Fskills\u002Fdevian\u002F80-tools\u002F30-env-management\u002FSKILL.md",[],[72,73],"management","30 env management",{"skillNo":75,"name":76,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":7,"ownerLogin":8,"ownerAvatarUrl":9,"ownerAccountNo":10,"ownerDisplayName":11,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":77,"categories":78,"keywords":79,"publishedAt":20},"01M6CBWYD3TGWBQV7V35551THF","19-page-push-send","https:\u002F\u002Fgithub.com\u002Fdevwinsoft\u002Fdevian\u002Fblob\u002Fmain\u002Fskills\u002Fdevian\u002F80-tools\u002F20-operation\u002F19-page-push-send\u002FSKILL.md",[],[80,81,82],"push","page","19 page push send",{"skillNo":84,"name":85,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":7,"ownerLogin":8,"ownerAvatarUrl":9,"ownerAccountNo":10,"ownerDisplayName":11,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":86,"categories":87,"keywords":88,"publishedAt":20},"01M6CBWWRQ05A7604GHJKTPJTF","18-page-savedata","https:\u002F\u002Fgithub.com\u002Fdevwinsoft\u002Fdevian\u002Fblob\u002Fmain\u002Fskills\u002Fdevian\u002F80-tools\u002F20-operation\u002F18-page-savedata\u002FSKILL.md",[],[81,89],"18 page savedata",{"skillNo":91,"name":92,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":7,"ownerLogin":8,"ownerAvatarUrl":9,"ownerAccountNo":10,"ownerDisplayName":11,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":93,"categories":94,"keywords":95,"publishedAt":20},"01M6CBWVH4S2VF1V7B2GBTESCT","17-page-deobfuscate","https:\u002F\u002Fgithub.com\u002Fdevwinsoft\u002Fdevian\u002Fblob\u002Fmain\u002Fskills\u002Fdevian\u002F80-tools\u002F20-operation\u002F17-page-deobfuscate\u002FSKILL.md",[],[81,96],"17 page deobfuscate"]