[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6CBWNKSXNWXPAZBF7P0TK5A":3,"skill-same:01M6CBWNKSXNWXPAZBF7P0TK5A":44},{"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":19,"linkOnly":20,"markdownContent":21,"markdownBody":21,"frontMatter":22,"refName":23,"commitSha":24,"skillDirectory":25,"forksCount":13,"repositoryHtmlUrl":26,"repositoryHomepageUrl":6,"ownerHtmlUrl":27,"ownerBlogUrl":6,"ownerDescription":28,"updatedAt":29,"install":30,"resources":43},"01M6CBWNKSXNWXPAZBF7P0TK5A","03-ssot",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\u002F80-tools\u002F20-operation\u002F03-ssot\u002FSKILL.md",[],[18],"03 ssot","2026-11-10 10:12:30",false,"# 03-ssot — 20-operation\n\n\nStatus: ACTIVE\nAppliesTo: v10\nParentSSOT: skills\u002Fdevian\u002F80-tools\u002F03-ssot\u002FSKILL.md\n\n\n## Scope\n\nOperation 웹앱의 **기능(탭) 정의**를 관리한다.\n각 탭의 입출력, 파이프라인, 참조 SSOT를 이 문서에서 정의한다.\n\n\n---\n\n\n## A) Obfuscate\n\nComplexUtil byte-substitution. 평문 -> 난독화.\n\n| 항목 | 값 |\n|------|-----|\n| 입력 | 평문 바이트 |\n| 출력 | 난독화된 바이트 |\n| 버튼 | [Obfuscate] |\n\n> ComplexUtil 정본: [31-variable-complex](..\u002F..\u002F..\u002F10-module\u002F20-core\u002F31-variable-complex\u002FSKILL.md)\n\n\n---\n\n\n## B) Deobfuscate\n\nComplexUtil byte-substitution. 난독화 -> 평문.\n\n| 항목 | 값 |\n|------|-----|\n| 입력 | 난독화된 바이트 |\n| 출력 | 평문 바이트 |\n| 버튼 | [Deobfuscate] |\n\n> ComplexUtil 정본: [31-variable-complex](..\u002F..\u002F..\u002F10-module\u002F20-core\u002F31-variable-complex\u002FSKILL.md)\n\n\n---\n\n\n## C) Save Data\n\nSave Data 편집 워크플로우. 파일 확장자(`.json` \u002F `.dvn`)에 따라 자동 분기.\n\n\n### 입력 형식\n\n| 확장자 | 출처 | 외부 래핑 |\n|--------|------|-----------|\n| `.json` | Unity Editor 로컬 저장 | 없음 (SaveLocalPayload JSON 그대로) |\n| `.dvn` | 모바일 Export | DVN 인코딩 (version byte + ComplexUtil + HMAC) |\n\n두 형식 모두 내부 `payload` 필드는 `ComplexUtil.Encrypt_Base64(게임 상태 JSON)`.\n`.dvn` v2는 HMAC 무결성 검증을 포함한다 (§DVN 파이프라인 참조).\n\n\n### 양방향 프로세스\n\n```\n[SaveDataManager]                    [Operation]\n  ExportDvnAsync  ── .dvn ──→  Import (decode)\n  로컬 저장        ── .json ──→  Import (parse)\n                                   ↓ JSON 편집\n  ImportDvnAsync  ←── .dvn ──  Export (encode)\n  RestoreFromPlainJsonAsync ←── .json ──  Export (serialize)\n```\n\n### DVN 파이프라인\n\nv2 (현재, HMAC 포함):\n```\nExport (.dvn):\n  1. JSON에서 account.socialUserId 추출\n  2. HMAC-SHA256(JSON, APP_SECRET + socialUserId) -> hmac_hex\n  3. signedPayload = JSON + \"\\n\" + hmac_hex\n  4. ComplexUtil.Encrypt_Base64(signedPayload) -> obfuscated\n  5. (char)0x02 + obfuscated -> .dvn\n\nImport (.dvn):\n  1. version parse (0x02)\n  2. ComplexUtil.Decrypt_Base64 -> signedPayload\n  3. signedPayload 분리 -> JSON + hmac_hex\n  4. HMAC 검증 (APP_SECRET + socialUserId)\n  5. Return JSON\n```\n\nv1 (레거시, HMAC 없음):\n```\nImport (.dvn): .dvn -> version parse (0x01) -> ComplexUtil.Decrypt_Base64 -> JSON\n```\n\n- Export는 항상 v2로 생성한다.\n- Import는 v1\u002Fv2 모두 허용한다 (v1은 HMAC 검증 없이 통과).\n- Operation 웹앱은 APP_SECRET을 코드 내 상수로 보유한다 (dvn-codec.ts).\n\n정본: [25-recovery-system\u002F03-ssot](..\u002F..\u002F..\u002F..\u002Fdevian-unity\u002F50-mobile-package\u002F25-recovery-system\u002F03-ssot\u002FSKILL.md)\n- DVN 포맷: §A\n- 인코딩 파이프라인: §B\n- HMAC Integrity: §E\n\n\n---\n\n\n## D) Push Send\n\nFCM 토픽 기반 푸시 알림 발송. PUSH_REMOTE 테이블 데이터를 로드하여 토픽별\u002F언어별로 발송한다.\n\n| 항목 | 값 |\n|------|-----|\n| 데이터 소스 | `PUSH_REMOTE.json` (NDJSON, 파일 로드) |\n| 입력 | Topic (드롭다운), Body × 3언어 (Korean \u002F English \u002F Japanese) |\n| 출력 | FCM 발송 결과 (성공\u002F실패) |\n| 버튼 | [Load Data], [Send] |\n| 백엔드 | Firebase Cloud Function `sendPushNotification` (Callable v2) |\n\n### 발송 흐름\n\n```\n1. [Load Data] → PUSH_REMOTE.json 파일 선택 → NDJSON 파싱\n2. Topic 드롭다운에 고유 Topic 목록 표시 (예: event, test)\n3. 사용자가 Topic 선택 + 언어별 Body 작성\n4. [Send] → 선택 Topic에 해당하는 PUSH_REMOTE 행 필터링\n5. 각 행의 PushId를 FCM 토픽으로, 해당 Language의 Body를 매칭하여 발송\n   예: Topic=\"event\" 선택 시\n     → event_korean (PushId) + Korean body\n     → event_english (PushId) + English body\n6. Body가 비어있거나 매칭 행이 없는 언어는 skip\n```\n\n### Firebase Function: `sendPushNotification`\n\n- Callable v2 (`onCall`), region: asia-northeast3\n- 입력: `{ entries: [{ pushId: string, body: string }] }`\n- 동작: 각 entry에 대해 `admin.messaging().send({ topic: pushId, notification: { body } })`\n- 응답: `{ results: [{ pushId: string, success: boolean, error?: string }] }`\n\n> PUSH_REMOTE 테이블 정본: 52-push-system\u002F03-ssot §D, §F\n\n\n---\n\n\n## Related\n\n- [00-overview](..\u002F00-overview\u002FSKILL.md) — Operation 개요\n- [01-policy](..\u002F01-policy\u002FSKILL.md) — 정책\n- [Tools SSOT](..\u002F..\u002F03-ssot\u002FSKILL.md) — 상위 SSOT\n- [52-push-system\u002F03-ssot](..\u002F..\u002F..\u002F..\u002Fdevian-unity\u002F50-mobile-package\u002F52-push-system\u002F03-ssot\u002FSKILL.md) — Push 시스템 SSOT\n",{},"main","8cb5bd877a3a7004867d2f55697cb9e4ff2f1734","skills\u002Fdevian\u002F80-tools\u002F20-operation\u002F03-ssot","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":31,"prompt":41,"zipAvailable":42},[32,35,38],{"tool":33,"command":34},"npx","npx skills add devwinsoft\u002Fdevian --skill 03-ssot",{"tool":36,"command":37},"bunx","bunx skills add devwinsoft\u002Fdevian --skill 03-ssot",{"tool":39,"command":40},"pnpm","pnpm dlx skills add devwinsoft\u002Fdevian --skill 03-ssot","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fdevwinsoft\u002Fdevian\u002Fblob\u002Fmain\u002Fskills\u002Fdevian\u002F80-tools\u002F20-operation\u002F03-ssot\u002FSKILL.md",true,[],[45,51,59,67,76,83],{"skillNo":46,"name":47,"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":48,"categories":49,"keywords":50,"publishedAt":19},"01M6CBX3K2JJV38ANHVGCAT8A0","devian","https:\u002F\u002Fgithub.com\u002Fdevwinsoft\u002Fdevian\u002Fblob\u002Fmain\u002Fskills\u002Fdevian\u002FSKILL.md",[],[47],{"skillNo":52,"name":53,"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":54,"categories":55,"keywords":56,"publishedAt":19},"01M6CBX210BFMM8AMN770J9WY8","90-project-archive","https:\u002F\u002Fgithub.com\u002Fdevwinsoft\u002Fdevian\u002Fblob\u002Fmain\u002Fskills\u002Fdevian\u002F80-tools\u002F90-project-archive\u002FSKILL.md",[],[57,58],"archive","90 project archive",{"skillNo":60,"name":61,"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":62,"categories":63,"keywords":64,"publishedAt":19},"01M6CBWZSGRR9R3YJZB3177667","30-env-management","https:\u002F\u002Fgithub.com\u002Fdevwinsoft\u002Fdevian\u002Fblob\u002Fmain\u002Fskills\u002Fdevian\u002F80-tools\u002F30-env-management\u002FSKILL.md",[],[65,66],"management","30 env management",{"skillNo":68,"name":69,"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":70,"categories":71,"keywords":72,"publishedAt":19},"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",[],[73,74,75],"push","page","19 page push send",{"skillNo":77,"name":78,"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":79,"categories":80,"keywords":81,"publishedAt":19},"01M6CBWWRQ05A7604GHJKTPJTF","18-page-savedata","https:\u002F\u002Fgithub.com\u002Fdevwinsoft\u002Fdevian\u002Fblob\u002Fmain\u002Fskills\u002Fdevian\u002F80-tools\u002F20-operation\u002F18-page-savedata\u002FSKILL.md",[],[74,82],"18 page savedata",{"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":19},"01M6CBWVH4S2VF1V7B2GBTESCT","17-page-deobfuscate","https:\u002F\u002Fgithub.com\u002Fdevwinsoft\u002Fdevian\u002Fblob\u002Fmain\u002Fskills\u002Fdevian\u002F80-tools\u002F20-operation\u002F17-page-deobfuscate\u002FSKILL.md",[],[74,89],"17 page deobfuscate"]