| 이런 곳에 | 이렇게 넣어요 |
|---|---|
| 블로그·카페·개인 홈페이지 | 게임을 고르고 코드 두 줄을 글에 붙여요 |
| 회사 사이트·이벤트 | 블로그와 같은 코드로 넣고, 기간과 조건은 문의로 정해요 |
| 어린이 앱 (인터넷 연결) | 웹뷰로 냥게임즈 주소를 열고 신호를 받아요 |
| 어린이 앱 (인터넷 없이) | 앱 전용 파일 묶음(SDK)을 앱 안에 넣어요 |
| 맞춤 제작 | 브랜드에 맞춘 화면이나 새 게임을 만들어요 |
블로그·카페에 넣기
게임을 고르고 코드를 블로그 글에 붙이면, 글 안에서 바로 냥게임즈를 할 수 있어요
1게임 고르기
맨 앞 키즈 모드 전체는 아이용 게임 목록을 통째로 넣어요. 상자 안에서 게임을 골라 하고 목록으로 돌아와요.
2모드와 크기
3코드를 글에 붙이기
기본 코드 · 블로거, 워드프레스, 직접 만든 홈페이지의 HTML 편집 화면
간단 코드 · 티스토리처럼 스크립트를 막는 곳의 HTML 모드
알아 두면 좋아요
- 처음엔 가벼운 표지만 보이고, 바로 하기를 누르면 게임을 불러와요. 글이 무거워지지 않아요.
- 게임 위쪽에 냥게임즈 로고가 나와요. 키즈 모드가 아니면 로고를 눌러 냥게임즈에서 더 많은 게임을 볼 수 있어요.
- 점수는 냥게임즈 랭킹에 그대로 올라가요.
- 게임이 화면 밖으로 스크롤되면 혼자 하는 게임은 저절로 멈춰요.
- 게임이 추가되거나 고쳐지면 붙여 둔 게임도 자동으로 최신이 돼요.
- 네이버 블로그·카페는 글에 코드를 넣을 수 없어요. 게임 주소(예: nyang.games/jump/)를 붙이면 그림이 있는 미리보기 카드로 들어가고, 누르면 냥게임즈에서 열려요.
미리보기
블로그 글 안에 이렇게 보여요
회사 사이트·이벤트에 넣기
회사 홈페이지나 이벤트 페이지에 냥게임즈 게임을 넣어요. 붙이는 방법은 블로그와 같고, 쓰는 기간과 조건은 문의로 정해요
- 이벤트 기간이나 달 단위로 쓸 수 있어요.
- 게임 위 냥게임즈 로고 바를 빼거나, 회사 로고를 넣은 화면도 이야기할 수 있어요.
- 아이 고객이 많은 곳이라면 키즈 모드로 넣을 수 있어요. 랭킹·공유·바깥 링크가 모두 꺼져요.
어린이 앱에 넣기
앱에서 보상 조건을 채우면 냥게임즈 한 판을 열어요. 인터넷으로 주소를 열거나, 파일 묶음을 앱 안에 넣어 인터넷 없이 열 수 있어요
ready start end 신호로 앱에 와요close가 와요. 웹뷰를 닫으면 끝이에요두 가지 넣는 방법
| 인터넷 연결 | 인터넷 없이 (SDK) | |
|---|---|---|
| 여는 것 | https://nyang.games/kids/ 주소 | 앱 안에 넣은 파일 |
| 인터넷 | 필요해요 | 필요 없어요 |
| 앱 크기 | 늘지 않아요 | 약 2.5MB 늘어요 (게임 10개 기준, 그중 글꼴이 2MB) |
| 새 게임·고친 점 | 바로 반영돼요 | 새 묶음으로 바꿔 넣으면 반영돼요 |
| 들어가는 게임 | 키즈 게임 전체 (주소로 골라 보여 주기) | 계약한 게임만 묶음에 들어가요 |
신호 (ready·end·close 등) | 똑같아요. 앱 쪽 코드는 여는 주소만 바꾸면 돼요 | |
아이 정보 보호
- 랭킹(닉네임 입력·기록 올리기·순위표), 접속자 수 신호, 방문·한 판 통계 — 냥게임즈 서버로 보내는 정보가 없어요. 기기 번호도 만들지 않아요.
- 카카오톡 자랑하기·공유, 다시 하기, 캐릭터 바꾸기, 냥게임즈 메인으로 가는 링크 등 바깥으로 나가는 길이 없어요.
- 광고(냥게임즈에는 원래 없어요)와 방문 분석 스크립트를 불러오지 않아요.
- 게임 최고 기록과 진행(예: 대탈출 스테이지)은 그 기기 웹뷰 저장소에만 남아요.
키즈 모드에서 쓸 수 있는 게임
| 게임 id | 이름 | 한 판이 끝나는 때 | score 값 |
|---|---|---|---|
sushi | 냥냥 초밥집 | 하트 3개가 다 없어지면 | 받은 손님 수 |
flag | 냥냥 청기백기 | 하트 3개가 다 없어지면 | 맞힌 명령 수 |
odd | 냥냥 다른 그림 찾기 | 시간 막대가 바닥나거나 하트 3개가 다 없어지면 | 찾은 개수 |
rps | 냥냥 눈치 가위바위보 | 하트 3개가 다 없어지면 | 맞힌 판 수 |
color | 냥냥 색깔 맞히기 | 하트 3개가 다 없어지면 | 맞힌 글씨 수 |
cushion | 냥냥 방석탑 | 시간 막대가 바닥나면 | 뺀 방석 수 |
link | 냥냥 이어팡 | 기운 막대가 바닥나면 | 한 판 점수 |
pair | 냥냥 짝꿍 찾기 | 단계 1개를 깨거나 시간이 끝나면 | 깬 단계 번호 (못 깨면 0) |
tiles | 냥냥 짝꿍 블럭 | 판 1개를 깨거나 시간이 끝나면 | 깬 판 번호 (못 깨면 0) |
volley | 냥냥 배구 | 컴퓨터와 한 판(7점)이 끝나면 | 내가 낸 점수 (0~7) |
train | 냥냥 기차놀이 | 울타리나 내 기차에 부딪히면 | 태운 친구 수 |
run | 냥냥 달리기 | 장애물에 부딪히면 | 달린 거리(m) |
jump | 냥이 점프 | 떨어지면 | 올라간 높이(m) |
kung | 쿵쿵 냥이 | 가시·청소기에 닿으면 | 내려간 층수 |
fish | 냥냥 낚시 | 낚시 시간이 끝나면 | 가장 큰 물고기(mm) |
block | 냥냥 블럭 | 블럭이 무너지면 | 쌓은 높이(cm) |
merge | 뭉쳐라 냥냥 | 통이 꽉 차면 | 점수 |
chain | 냥냥 몽글몽글 | 판이 꽉 차면 | 점수 |
trio | 반짝냥 | 판이 꽉 차면 | 점수 |
yarn | 팡팡냥 | 젤리가 선을 넘으면 | 점수 |
escape | 냥냥 대탈출 | 스테이지 1개를 깨거나 실패하면 | 깬 스테이지 번호 (못 깨면 0) |
bung | 냥냥 붕어빵 | 하루 장사가 끝나면 | 번 돈(원) |
인형뽑기(뽑기 방식), 트리(다른 사람이 쓴 쪽지), 오목·윷놀이·댄스 같이하기(링크로 다른 사람과 연결)는 어린이 앱에 맞지 않아 키즈 모드에 넣지 않았어요.
인터넷 연결: 웹뷰로 주소 열기
따로 설치할 것 없이 주소 하나면 돼요.
| 무엇을 | 주소 |
|---|---|
| 아이가 게임을 고르는 화면 | https://nyang.games/kids/?partner=앱이름 |
| 보여 줄 게임만 골라서 | https://nyang.games/kids/?partner=앱이름&games=jump,merge,fish |
| 게임 하나를 바로 | https://nyang.games/jump/?mode=kids&partner=앱이름 |
partner는 영문·숫자·-·_·.로 된 앱 이름이에요(40자까지). 신호에 그대로 실려 와서 어느 앱에서 열었는지 구분할 수 있어요.
연동 방법 자세히 보기 · 신호와 앱 코드 예시
받는 신호
모든 신호는 아래 모양이에요. source가 "nyang.games"인 것만 받으세요.
{ "source": "nyang.games", "type": "end", "partner": "앱이름",
"game": "jump", "score": 132, "scoreText": "245점" }
| type | 언제 | 같이 오는 값 |
|---|---|---|
ready | 화면이 열렸을 때 (고르기 화면은 game: "") | game |
pick | 고르기 화면에서 게임을 눌렀을 때 | game |
start | 시작 버튼을 눌렀을 때 | game |
end | 한 판이 끝났을 때 (한 번만) | game, score(숫자), scoreText(화면에 보이는 글) |
close | "돌아가기" 또는 고르기 화면의 "닫기"를 눌렀을 때 | game |
보상은 앱이 이미 줬으니(게임 한 판 자체가 보상), 보통은 close만 받아서 웹뷰를 닫으면 충분해요. end의 점수는 "오늘의 기록" 같은 재미 요소로 쓰면 좋아요. 점수는 아이 기기에서 만들어지는 값이라, 포인트·현금성 보상의 근거로는 쓰지 마세요.
앱 코드 예시
게임은 아래 다섯 가지 통로로 동시에 신호를 보내요. 앱 종류에 맞는 것 하나만 받으면 돼요.
<iframe id="nyang" src="https://nyang.games/kids/?partner=myapp"
style="width:100%;height:100%;border:0" allow="autoplay"></iframe>
<script>
addEventListener('message', (e) => {
if (e.origin !== 'https://nyang.games') return;
const m = e.data;
if (!m || m.source !== 'nyang.games') return;
if (m.type === 'end') console.log('점수', m.game, m.score, m.scoreText);
if (m.type === 'close') document.getElementById('nyang').remove(); // 게임 창 닫기
});
</script>
// Kotlin · 이름은 꼭 "NyangBridge"
class NyangBridge(private val activity: Activity) {
@JavascriptInterface
fun postMessage(json: String) {
val m = JSONObject(json)
if (m.optString("source") != "nyang.games") return
when (m.optString("type")) {
"end" -> Log.d("nyang", "score=${m.optInt("score")}")
"close" -> activity.runOnUiThread { activity.finish() }
}
}
}
webView.settings.javaScriptEnabled = true
webView.settings.domStorageEnabled = true // 게임 저장(최고 기록)
webView.settings.mediaPlaybackRequiresUserGesture = false
webView.addJavascriptInterface(NyangBridge(this), "NyangBridge")
webView.loadUrl("https://nyang.games/kids/?partner=myapp")
// Swift · 핸들러 이름은 꼭 "nyang"
final class GameVC: UIViewController, WKScriptMessageHandler {
var webView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
let config = WKWebViewConfiguration()
config.userContentController.add(self, name: "nyang")
config.allowsInlineMediaPlayback = true
config.mediaTypesRequiringUserActionForPlayback = []
webView = WKWebView(frame: view.bounds, configuration: config)
view.addSubview(webView)
webView.load(URLRequest(url: URL(string: "https://nyang.games/kids/?partner=myapp")!))
}
func userContentController(_ c: WKUserContentController, didReceive message: WKScriptMessage) {
guard let m = message.body as? [String: Any], m["source"] as? String == "nyang.games" else { return }
switch m["type"] as? String {
case "end": print("score", m["score"] ?? 0)
case "close": dismiss(animated: true)
default: break
}
}
}
// react-native-webview
<WebView
source={{ uri: 'https://nyang.games/kids/?partner=myapp' }}
mediaPlaybackRequiresUserAction={false}
onMessage={(e) => {
const m = JSON.parse(e.nativeEvent.data);
if (m.source !== 'nyang.games') return;
if (m.type === 'end') console.log('score', m.score);
if (m.type === 'close') navigation.goBack();
}}
/>
// webview_flutter · 채널 이름은 꼭 "NyangBridge"
final controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..addJavaScriptChannel('NyangBridge', onMessageReceived: (msg) {
final m = jsonDecode(msg.message);
if (m['source'] != 'nyang.games') return;
if (m['type'] == 'end') debugPrint('score ${m['score']}');
if (m['type'] == 'close') Navigator.of(context).pop();
})
..loadRequest(Uri.parse('https://nyang.games/kids/?partner=myapp'));
붙일 때 알아 두면 좋은 것
- 웹뷰는 화면 전체(세로)로 열어 주세요. 게임은 폰 세로 화면에 맞춰져 있어요.
- 기기 뒤로 가기 버튼은 웹뷰를 닫는 것으로 처리하면 돼요 (게임 안에 따로 나가는 길이 없어요).
- 배경 음악은 처음 탭한 뒤에 나와요. 소리 끄기 버튼은 게임 화면 위쪽에 있어요.
- 한 판 길이는 게임마다 1~5분쯤이에요. 정해진 시간으로 끊고 싶으면 앱에서 타이머로 웹뷰를 닫아도 돼요.
- 글꼴 파일 하나(Pretendard, jsDelivr)를 외부에서 불러와요.
인터넷 없이: 오프라인 SDK
냥게임즈 게임을 파일 묶음으로 앱 안에 넣어요. 비행기 모드나 와이파이 없는 태블릿에서도 열려요.
www/kids/index.html게임 고르기 화면, 여기를 열어요www/<게임 id>/index.html게임 하나를 바로 열 때www/sdk.js사용 허가 확인 (고치지 마세요)www/fonts/글꼴까지 들어 있어 바깥에서 받을 게 없어요README.md · LICENSE.txt연동 요약 · 사용 조건sdk.json · demo.html버전·게임·기한 · PC 시험 페이지
예: 게임 10개를 넣은 묶음 (약 2.5MB)
- 묶음 받기
www폴더를 앱에 넣기- 웹뷰로
kids/index.html열기 close가 오면 닫기
키즈 모드 게임 22가지 가운데 원하는 게임을 골라 묶을 수 있어요. 앱 이름(영문), 넣고 싶은 게임, 쓰실 기간을 알려 주시면 그 앱 전용 묶음을 만들어 드려요.
flag냥냥 다른 그림 찾기odd냥냥 눈치 가위바위보rps
냥냥 색깔 맞히기color냥냥 초밥집sushi냥냥 방석탑cushion
냥냥 이어팡link냥냥 짝꿍 찾기pair냥냥 짝꿍 블럭tiles
냥냥 대탈출escape뭉쳐라 냥냥merge냥냥 몽글몽글chain
반짝냥trio팡팡냥yarn냥냥 블럭block
냥이 점프jump쿵쿵 냥이kung냥냥 달리기run
냥냥 기차놀이train냥냥 배구volley냥냥 낚시fish냥냥 붕어빵bung
SDK 넣는 방법 자세히 보기 · 앱 코드, 신호, 사용 허가
앱에 넣고 열기
앱 종류에 맞는 방법 하나만 하면 돼요. 저장(localStorage)과 자바스크립트를 켜 주세요. 게임 최고 기록이 여기에 남아요.
www 폴더를 app/src/main/assets/nyang/에 넣고, WebViewAssetLoader로 열어요. 앱 안 파일이 https://appassets.androidplatform.net/ 주소로 열려서 사용 허가 확인까지 제대로 동작해요.
// build.gradle: implementation "androidx.webkit:webkit:1.11.0"
val loader = WebViewAssetLoader.Builder()
.addPathHandler("/assets/", WebViewAssetLoader.AssetsPathHandler(this))
.build()
webView.settings.javaScriptEnabled = true
webView.settings.domStorageEnabled = true // 최고 기록 저장
webView.settings.mediaPlaybackRequiresUserGesture = false
webView.webViewClient = object : WebViewClientCompat() {
override fun shouldInterceptRequest(v: WebView, r: WebResourceRequest) =
loader.shouldInterceptRequest(r.url)
}
webView.addJavascriptInterface(NyangBridge(this), "NyangBridge") // 이름은 꼭 "NyangBridge"
webView.loadUrl("https://appassets.androidplatform.net/assets/nyang/kids/index.html")
class NyangBridge(private val activity: Activity) {
@JavascriptInterface
fun postMessage(json: String) {
val m = JSONObject(json)
if (m.optString("source") != "nyang.games") return
when (m.optString("type")) {
"end" -> Log.d("nyang", "score=${m.optInt("score")}")
"close" -> activity.runOnUiThread { activity.finish() }
}
}
}
www 폴더를 Xcode에 폴더 참조(파란 폴더)로 넣고 이름을 nyang으로 해요.
// Swift · 핸들러 이름은 꼭 "nyang"
final class GameVC: UIViewController, WKScriptMessageHandler {
var webView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
let config = WKWebViewConfiguration()
config.userContentController.add(self, name: "nyang")
config.allowsInlineMediaPlayback = true
config.mediaTypesRequiringUserActionForPlayback = []
webView = WKWebView(frame: view.bounds, configuration: config)
view.addSubview(webView)
let dir = Bundle.main.url(forResource: "nyang", withExtension: nil)!
webView.loadFileURL(dir.appendingPathComponent("kids/index.html"), allowingReadAccessTo: dir)
}
func userContentController(_ c: WKUserContentController, didReceive message: WKScriptMessage) {
guard let m = message.body as? [String: Any], m["source"] as? String == "nyang.games" else { return }
switch m["type"] as? String {
case "end": print("score", m["score"] ?? 0)
case "close": dismiss(animated: true)
default: break
}
}
}
www 폴더를 앱 번들에 넣어요 (Android는 android/app/src/main/assets/nyang/, iOS는 번들 폴더 참조).
// react-native-webview
const uri = Platform.OS === 'android'
? 'file:///android_asset/nyang/kids/index.html'
: 'nyang/kids/index.html'; // iOS: 번들 안 경로
<WebView
source={{ uri }}
originWhitelist={['*']}
allowFileAccess
allowingReadAccessToURL="nyang"
mediaPlaybackRequiresUserAction={false}
onMessage={(e) => {
const m = JSON.parse(e.nativeEvent.data);
if (m.source !== 'nyang.games') return;
if (m.type === 'end') console.log('score', m.score);
if (m.type === 'close') navigation.goBack();
}}
/>
www 폴더를 assets/nyang/에 넣고 pubspec.yaml의 assets에 폴더들을 적어요.
// webview_flutter · 채널 이름은 꼭 "NyangBridge"
final controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..addJavaScriptChannel('NyangBridge', onMessageReceived: (msg) {
final m = jsonDecode(msg.message);
if (m['source'] != 'nyang.games') return;
if (m['type'] == 'end') debugPrint('score ${m['score']}');
if (m['type'] == 'close') Navigator.of(context).pop();
})
..loadFlutterAsset('assets/nyang/kids/index.html');
게임 하나만 바로 열 때는 kids/index.html 대신 jump/index.html처럼 게임 폴더의 index.html을 열면 돼요.
받는 신호
인터넷 연결 방식과 똑같은 신호가 와요. 오프라인 SDK에는 error 하나가 더 있어요.
| type | 언제 | 같이 오는 값 |
|---|---|---|
ready · pick · start · end · close | 인터넷 연결 방식과 같아요 | game (end는 score, scoreText도) |
error | 사용 기한이 지났거나, 허가되지 않은 게임·묶음일 때 (게임 대신 안내 화면이 떠요) | reason |
partner에는 묶음에 적힌 앱 이름이 자동으로 실려요.
사용 허가와 표시
- 묶음마다 파트너 키가 들어 있어요. 앱 이름, 넣은 게임, 사용 기한이 적혀 있고 냥게임즈가 서명했어요.
- 게임은 열릴 때마다 인터넷 없이 이 키를 확인해요. 기한이 지나거나, 묶음에 없는 게임이거나, 파일이 바뀌어 서명이 맞지 않으면 게임 대신 "게임을 열 수 없어요" 안내가 뜨고
error신호가 와요. - 게임 결과 화면과 고르기 화면 아래에 작은 냥게임즈 표시가 있어요. 가리지 말아 주세요.
www안 파일을 고치거나 다른 앱·웹사이트로 옮기면 안 돼요. 묶음마다 고유 번호가 들어 있어요 (자세한 건 LICENSE.txt와 계약서).
업데이트
- 새 게임이나 고친 점이 나오면 새 버전 묶음을 드려요.
www폴더를 통째로 바꿔 넣고 앱을 업데이트하면 돼요. - 신호 모양이 바뀌는 큰 변경은 버전 앞자리를 올리고(예: 1.x → 2.0) 미리 알려 드려요. 그 밖의 업데이트는 앱 쪽 코드를 고칠 필요가 없어요.
- 인터넷이 될 때 새 묶음을 알아서 받아 두는 자동 업데이트는 준비 중이에요.
알아 두면 좋은 것
- 소리와 음악은 파일 없이 기기에서 만들어져요. 글꼴도 묶음 안에 있어서 바깥에서 불러오는 파일이 하나도 없어요.
- 웹뷰는 화면 전체(세로)로 열어 주세요. 기기 뒤로 가기 버튼은 웹뷰를 닫는 것으로 처리하면 돼요.
- PC에서 먼저 보고 싶으면 묶음의
demo.html을 로컬 서버로 열어 보세요 (예: 묶음 폴더에서npx serve). 파일을 바로 열면 브라우저가 막을 수 있어요.
맞춤 제작
브랜드나 서비스에 맞춘 게임 화면, 새 미니게임을 냥게임즈 그림체로 만들어요. 일정과 조건은 문의로 정해요
- 지금 있는 게임에 회사 로고·상품·색을 입힌 화면
- 서비스에 맞춘 새 미니게임