한눈에 보기
- 무엇: 여러 명이 함께 쓰는 서버·학급 월드를 운영할 때 필요한 기능을 스크립트 API(JavaScript)로 만든 예제 모음
- 누구: 스크립트 API를 한 번 적용해 본 교사, 고등학생·코딩 동아리(중급~고급)
- 몇 분: 기능 하나당 코드 붙여 넣기 10분, 코드 읽기·고치기 1~2차시
- 결과물: 채팅 명령(
!길드,!저장,!워프등)이나 나침반 UI로 쓰는 행동 팩 - 대부분 GitHub의 코드를 main.js에 그대로 붙여 넣어 쓰고, 문구·값만 바꿔 우리 반 서버로 만듭니다.

기능 한눈에 고르기
| 기능 | 쓰임 | 영상 링크 |
| 입장 메시지 · 기기 표시 | 처음 온 학생에게 규칙 안내, 모바일·PC 확인 | [입장](https://youtu.be/RN9-LahL4tI) · [기기](https://youtu.be/GaN4A8IXUok) |
| 팀 채팅 · 도배 금지 | 모둠끼리만 대화, 채팅 예절 | [팀 채팅](https://youtu.be/KZuXZUz02q8) · [도배 금지](https://youtu.be/D5VeF_cAK2M) |
| 순간이동 요청 · 위치 저장 | 허락받고 모이기, 탐사 지점 저장 | [워프](https://youtu.be/XGtmmp0kiKU) · [위치 저장](https://youtu.be/T4pG6pn2swU) |
| 허용·거부 구역 · 보호 구역 | 작업 구역 나누기, 완성 작품 지키기 | [허용·거부](https://youtu.be/H9DoQttE5_I) · [보호 구역](https://youtu.be/fqjSODEEPXc) |
| 칭호 · 길드 · 길드 전쟁 | 역할 표시, 모둠(길드) 운영, 팀 대항전 | [칭호](https://youtu.be/ZPUvfg-jIkc) · [길드](https://youtu.be/ptWIJbiqj9w) · [길드 전쟁](https://youtu.be/dyfLch_5gD4) |
| 퀘스트 · 퀴즈 | 학습 과제를 임무로, 교과 퀴즈 | [퀘스트](https://youtu.be/8KyXFZekb_Q) · [퀴즈](https://youtu.be/hdPMNhcT43c) |
| 서버에서 서버로 이동 | 로비 서버 → 활동별 서버 | [서버 이동](https://youtu.be/lvg7qufYDoA) |
시작 전 준비: 코드 넣기와 관리자 태그
- Bridge에서 행동 팩 프로젝트를 만들고
scripts/main.js에 GitHub 코드를 붙여 넣기 - manifest.json에
@minecraft/server와@minecraft/server-ui(UI가 있는 기능) 종속성 추가 - 월드 실험 기능에서 ‘베타 API’ 켜고 행동 팩 적용 → 코드를 고친 뒤에는
/reload - 관리자 기능은
admin태그로 구분:/tag @s add admin(지우기:/tag @s remove admin)
- 사용법은 대부분 코드 맨 위 주석에 적혀 있습니다. 먼저 읽고 시작하세요.
- 처음이라면 「스크립트 API 시작하기」「복붙한 코드를 애드온으로 바꾸기」 영상을 먼저 봅니다(자료 링크).
교사용 메모 · 준비 메모: 베타 API와 교육용 에디션
- 이 페이지의 기능 대부분은 채팅 명령을
world.beforeEvents.chatSend(퀴즈는afterEvents.chatSend)로 받습니다. chatSend는 공식 문서에서 실험(베타) 전용으로 표시된 이벤트라, 월드에서 ‘베타 API’ 실험 기능을 켜야 동작합니다. - 허용·거부 구역의 블록 설치 감지(
beforeEvents.playerPlaceBlock)도 베타 전용입니다. 블록 파괴 감지(playerBreakBlock)는 정식(stable) 기능입니다. - 서버 이동에 쓰는
@minecraft/server-admin모듈은 manifest에1.0.0-beta버전으로 넣습니다(영상 화면). - 교육용 에디션은 월드 설정에 ‘베타 API’ 실험 토글이 없고, 베타 버전 종속성을 넣어도 로드되지 않았습니다(2026-07 실측). 그래서 chatSend·playerPlaceBlock(before)·server-admin을 쓰는 예제는 교육용 에디션에서 그대로는 동작하지 않습니다. 이 예제들은 베드락 에디션 월드·전용 서버에서 쓰는 것을 기본으로 하고, 교육용 에디션이 업데이트되면 다시 시험해 보세요.
- 2023~2025년 영상 코드라
runCommandAsync를 씁니다.@minecraft/server2.0.0부터runCommandAsync는 없어졌으므로, manifest를 2.x로 쓰면runCommand로 바꿉니다.player.isOp()는 지금 공식 문서에 없어 쓸 수 없고(GitHub 최신 코드도op·admin태그로 확인하도록 바뀜), 권한 확인이 필요하면 정식 기능인player.commandPermissionLevel(2.1.0부터)을 씁니다. 기기 표시에 쓰는player.clientSystemInfo.platformType은 정식 기능이라 그대로 쓸 수 있습니다(2026-10 공식 문서 기준). - 오류는 설정의 제작자 로그(콘텐츠 로그)로 확인하고, manifest의
@minecraft/server버전을 로그에 나온 버전으로 맞춥니다.
입장 메시지와 접속 기기 표시
- 입장 메시지: 처음 온 플레이어 → “새로운 플레이어 ○○님이 서버에 처음 오셨습니다!” + 개인 안내, 다시 온 플레이어 → “○○님이 서버에 다시 접속하셨습니다.”
- 접속 기록은 월드 동적 속성
joinedPlayers에 저장되어 월드를 껐다 켜도 남습니다. 관리자(OP)가 채팅에!resetplayer를 치면 기록 초기화 - 기기 표시: 이름표 아래에
Mobile·Desktop같은 플랫폼이 빨간 글씨로 나옵니다.
world.afterEvents.playerSpawn.subscribe((eventData) => {
if (!eventData.initialSpawn) return; // 리스폰은 무시
const player = eventData.player;
let joinedPlayers = world.getDynamicProperty("joinedPlayers") || "";
let playerList = joinedPlayers ? joinedPlayers.split(",") : [];
if (!playerList.includes(player.name)) {
playerList.push(player.name);
world.setDynamicProperty("joinedPlayers", playerList.join(","));
world.sendMessage(`§e새로운 플레이어 ${player.name}님이 서버에 처음 오셨습니다!`);
player.sendMessage("§a환영합니다! 서버의 규칙을 확인해주세요!");
} else {
world.sendMessage(`§b${player.name}님이 서버에 다시 접속하셨습니다.`);
}
});
// 기기 표시: 주기적으로 모든 플레이어의 이름표 수정
system.runInterval(() => {
for (const player of world.getAllPlayers()) {
const os = player.clientSystemInfo.platformType;
}
});


팀 채팅: 우리 팀끼리만 대화하기
| 채팅 입력 | 하는 일 |
| `!setteam A` | 내 팀을 A로 정하기 |
| `!adminsetteam 플레이어이름 팀이름` | 관리자(admin 태그)가 다른 학생의 팀 정하기 |
| `!teamlist` | 팀별 소속 플레이어 목록 |
| `!t 메시지` / `!team 메시지` | 같은 팀에게만 `[팀 채팅]`으로 보내기 |

도배 금지: 같은 말·긴 말·빠른 채팅 막기
- 같은 채팅 반복 → “이전 채팅이랑 똑같습니다.” / 10글자 넘음 → “메시지가 너무 깁니다. 최대 (10글자)” / 너무 빠름 → “채팅 속도가 너무 빠릅니다. (0.3초 남음)”
- 조건에 걸리면
data.cancel = true로 채팅을 아예 보내지 않습니다. chat_speed스코어보드를 정상 채팅 때 10틱(0.5초)으로 두고,runInterval로 1틱마다 1씩 줄입니다. 20틱 = 1초
const chat_length = 10 // 채팅 글자 제한
const chat_speed = 10; // 채팅 속도 (1틱 = 0.05초)
if (player.getDynamicProperty("lastChat") == msg) {
data.cancel = true
player.sendMessage(`§c이전 채팅이랑 똑같습니다.`)
} else if (msg.length > chat_length) {
data.cancel = true
player.sendMessage(`§c메시지가 너무 깁니다. 최대 (${chat_length}글자)`)
} else if (chatSpeed > 0) {
data.cancel = true // 채팅 속도가 너무 빠름
} else {
player.runCommandAsync(`scoreboard players set @s chat_speed ${chat_speed}`)
player.setDynamicProperty("lastChat", msg)
}

순간이동 요청: 상대가 허락해야 이동
- 채팅에
!워프→ 채팅창을 닫으면 지금 접속한 플레이어 목록 창이 열림 - 갈 플레이어를 누르면 “○○님에게 워프 요청을 보냈습니다.”
- 상대에게 ‘워프 요청’ 수락·거절 창 → 수락하면 3초 뒤 이동
- 대답이 없으면 요청 만료, 같은 상대에게 이미 요청이 진행 중이면 “이미 다른 워프 요청이 진행 중입니다.”
- 값은
WARP_CONFIG에서 바꿉니다(단위: 초).
const WARP_CONFIG = {
COOLDOWN: 120, // 쿨타임 (초)
REQUEST_TIMEOUT: 60, // 요청 만료 시간 (초)
TELEPORT_DELAY: 3 // 텔레포트 지연 시간 (초)
};
- 영상 설명은 쿨타임 3분이지만 화면 코드와 GitHub 최신 코드(2026-10) 값은 모두 120(2분)입니다. 3분으로 쓰려면 180으로 바꿉니다.

위치 저장: 자주 가는 곳을 나침반으로 이동
- 저장: 그 자리에서 채팅에
!저장 해안마을→ “위치 '해안마을'이(가) 성공적으로 저장되었습니다.” - 이동: 나침반을 들고 우클릭 → ‘저장된 위치’ 창의 버튼(이름과 X·Y·Z 좌표)을 누르면 순간이동
- 삭제: 창의 ‘저장된 위치 삭제’ → 드롭다운에서 골라 보내기
- 위치는 월드 동적 속성
savedLocations에 플레이어별로 저장(JSON) → 월드를 나갔다 와도 유지
if (message.startsWith("!저장")) {
chatEvent.cancel = true;
const locationName = message.slice(3).trim(); // '!저장' 뒤 글자
const pos = player.location;
savedLocations[player.name] = savedLocations[player.name] || {};
savedLocations[player.name][locationName] = {
x: Math.floor(pos.x), y: Math.floor(pos.y), z: Math.floor(pos.z)
};
}
// 버튼을 고르면
player.teleport({ x: coords.x, y: coords.y, z: coords.z });


허용·거부 구역: 좌표 범위로 블록 설치·파괴 제한
- 허용 구역: 정한 범위 안에서만 블록을 놓고 부술 수 있고, 밖에서는 “이 영역에서는 블록을 설치할 수 없습니다.”
- 거부 구역: 변수
isAllowed를isRestricted로 바꾸고!(아니다)만 빼면 반대로 그 구역 안만 금지 - 운영자(OP)는 막지 않습니다(
!player.isOp()일 때만 취소). 교사는 언제든 고칠 수 있음 - 교육용 에디션의 허용·거부 블록은 그 위쪽 전체에 적용 → 좌표 상자로 정하고 싶으면 코드가 편리
const allowRegion = {
min: { x: -10, y: -60, z: -10 }, // 최소 좌표
max: { x: 10, y: -50, z: 10 } // 최대 좌표
};
world.beforeEvents.playerBreakBlock.subscribe((ev) => {
const player = ev.player;
const { x, y, z } = ev.block.location;
const isAllowed = (
x >= allowRegion.min.x && x <= allowRegion.max.x &&
y >= allowRegion.min.y && y <= allowRegion.max.y &&
z >= allowRegion.min.z && z <= allowRegion.max.z
);
if (!isAllowed && !player.isOp()) {
ev.cancel = true;
player.sendMessage(`§c이 영역에서는 블록을 파괴할 수 없습니다.`);
}
});
// 설치는 world.beforeEvents.playerPlaceBlock 으로 똑같이

보호 구역: 나무 도끼로 두 꼭짓점 찍기
- 교사에게
admin태그를 주고 나무 도끼를 듦 - 보호할 곳의 첫 꼭짓점 블록 우클릭 → “첫번째 좌표가 저장되었습니다.”
- 반대쪽 꼭짓점 우클릭 → “새로운 보호 영역이 저장되었습니다.” → admin 태그가 없는 학생은 설치·파괴 불가
- 채팅 명령:
!보호목록(번호와 좌표) ·!보호삭제 2(2번 삭제) ·!보호초기화(모두 삭제) - 코드 위쪽 값으로 도구·태그·문구를 바꿉니다. 영역은 동적 속성
protectedAreas에 저장
const ADMIN_ITEM_NAME = `minecraft:wooden_axe`
const ADMIN_TAG = `admin`
const CANT_DESTROY_MESSAGE = `§c[ system ] 해당 블록은 파괴할 수 없습니다.`
const CANT_PLACE_MESSAGE = `§c[ system ] 해당 블록은 설치할 수 없습니다.`
const DB_KEY = "protectedAreas"
- 구독자 플럼(Plumr)님이 제공한 코드(BlockProtector.js)입니다.


칭호: 채팅과 이름표에 붙는 나만의 이름
- 처음에는 모두
<뉴비>칭호 → 나침반 우클릭 → ‘칭호 설정’에 입력(예: 서버장) →<서버장> <ssakspirit> 서버장입니다. - 삭제에서 ‘예’를 누르면 “칭호를 삭제했습니다”와 함께 뉴비로 돌아감
- 칭호는 1~10글자만 저장(0글자·10글자 초과는 안내 메시지), 플레이어 동적 속성
rank에 저장 - 이름표에도
[ 서버장 ] 이름으로 2틱마다 표시
world.beforeEvents.chatSend.subscribe((data) => {
const rank = data.sender.getDynamicProperty(`rank`)
const name = data.sender.name
const msg = data.message
data.cancel = true;
if (typeof rank == "undefined") {
world.sendMessage(`<뉴비> <${name}> ${msg}`)
} else {
world.sendMessage(`<${rank}> <${name}> ${msg}`)
}
})


길드: 모둠 만들기·가입·길드 채팅
| 채팅 입력 | 누가 | 하는 일 |
| `!길드` | 모두 | 길드 생성(이름·설명) · 가입 요청 · 탈퇴 · 정보 |
| `!길드장` | 길드장 | 길드원 관리(추방) · 정보 수정 · 가입 요청 수락 · 해체 |
| `!관리자` | admin 태그 | 모든 길드 삭제, 길드 채팅 모니터링 |
| `ㅁ메시지` | 길드원 | 같은 길드원만 보는 길드 채팅 |



길드 전쟁: 목표 킬 수와 보상 정하기
- 같은 길드끼리는 공격 불가, 다른 길드끼리는 공격 가능(최대 20개 길드). 이 기능은 GitHub의
player.json을 행동 팩entities폴더에 넣어야 함 !길드전쟁설정: 목표 킬 수·승리 보상 정하기(영상: 3킬, 에메랄드 5개) /!길드전쟁: 시작 /!길드전쟁종료: 결과 발표 — 모두 admin 태그 필요- 시작하면 스코어보드
guildWar에 길드별 킬 수 표시, 목표를 먼저 채운 길드의 모든 길드원이 보상 - 목표 전에 종료하면 보상 없이 최종 순위만. 길드가 2개 이상이어야 시작
function startGuildWar(player) {
if (!player.hasTag('admin')) {
player.sendMessage('§c관리자만 길드 전쟁을 시작할 수 있습니다.');
return;
}
const guilds = getGuilds();
if (Object.keys(guilds).length < 2) {
player.sendMessage('§c길드 전쟁을 시작하려면 최소 2개 이상의 길드가 필요합니다.');
return;
}
// ...
}


퀘스트: 태그로 임무 완료·보상 관리
- 나침반 우클릭 → ‘퀘스트’ 창: 채팅치기 / 플레이어 때리기(미완료·완료 표시)
- 임무를 하면 “퀘스트 "채팅 치기" 완료!” → 창에서 ‘보상받기’ → 에메랄드 10개(채팅 퀘스트)
- 태그 4개로 상태 관리:
chat·hit(수행 완료),chat_compensation·hit_compensation(보상 받음) —/tag @s list로 확인 - 같은 모양으로 블록 캐기 같은 새 퀘스트를 추가해 보는 활동으로 확장
if (player.hasTag("chat")) {
if (player.hasTag(`chat_compensation`)) {
player.sendMessage(`이미 보상을 받았습니다.`)
} else {
player.runCommandAsync(`give @s emerald 10`)
player.runCommandAsync(`tag @s add chat_compensation`)
}
}
// 때리기 퀘스트: world.afterEvents.entityHitEntity 에서 damagingEntity 에 hit 태그


멀티플레이 퀴즈: 채팅으로 문제 풀기
- 채팅에
시작→ 화면에 “1번 문제”(제목)와 문제(부제목) → 채팅으로 정답 → “정답입니다! 다음 문제로 진행합니다.” → 마지막 문제 뒤 “게임이 종료되었습니다. 수고하셨습니다!” 종료를 치면 중간에 끝냄. 여러 명이 함께 풀 수 있음(먼저 맞힌 사람 기준으로 다음 문제)- 문제 목록만 교과 내용으로 바꾸면 우리 반 퀴즈가 됩니다. 정답은
trim으로 앞뒤 공백을 지우고 대소문자 무시
const questions = [
{ question: "마인크래프트 개발사 이름은?", answer: "모장" },
{ question: "마인크래프트 주인공 이름은?", answer: "스티브" },
{ question: "지금 보는 유튜브 채널 이름은?", answer: "스티브코딩" },
// 추가 문제들을 필요에 따라 이어서 나열
];
// 문제 보여 주기 (인덱스는 0부터라 +1)
world.getDimension("overworld").runCommandAsync(`title @a subtitle ${currentQuestion.question}`)
world.getDimension("overworld").runCommandAsync(`title @a title ${currentQuestionIndex + 1}번 문제`)


서버에서 서버로 이동하기
- 나침반 우클릭 → ‘서버 이동’ 창에서 서버를 고르면 그 서버로 접속(로비 서버 → 활동 서버 구조에 활용)
- manifest.json
dependencies에@minecraft/server-admin(1.0.0-beta) 추가 필요 - 내 서버는
servers목록에 이름·주소·포트를 같은 모양으로 추가
import { transferPlayer } from '@minecraft/server-admin';
const servers = {
"GALAXITE": { ip: `play.galaxite.net`, port: 19132 },
// "우리반 서버": { ip: `주소`, port: 19132 },
};
// UI에서 고른 서버로 보내기
transferPlayer(player, serverInfo.ip, serverInfo.port)
- 서버 주소는 바뀔 수 있으니 최신 주소를 씁니다(코드 주석).
@minecraft/server-admin은 공식 문서상 베타(pre-release) 전용이고 “마인크래프트 클라이언트나 Realms에서는 쓸 수 없는” 전용 서버(BDS)용 모듈입니다. 그래서 교육용 에디션·일반 월드에서는 동작하지 않습니다. 지금 문서의 형식은transferPlayer(player, { hostname, port })라서 영상 코드(transferPlayer(player, ip, port))는 고쳐 써야 할 수 있습니다(2026-10 공식 문서 기준).


수업 팁
- 관리자는 교사만:
admin태그·OP는 교사에게만. 길드 삭제·팀 지정·보호 구역 지정을 학생이 하지 않게 합니다. - 한 번에 하나씩: 여러 스크립트를 한 main.js에 그대로 이어 붙이면 import 줄과 같은 이름의 변수·함수가 겹쳐 오류가 나거나, 같은 채팅 명령을 두 코드가 함께 처리할 수 있습니다. 기능을 하나씩 넣고 확인합니다.
- 채팅 예절과 연결: 도배 금지는 학생과 규칙(글자 수·간격)을 함께 정하고 값을 고쳐 보는 활동으로.
- 모둠 활동: 팀 채팅·길드·칭호로 모둠 이름과 역할(건축가·탐험가)을 표시합니다.
- 작품 보호: 완성 작품·공용 시설은 보호 구역, 개인 작업 공간은 허용 구역으로 나눕니다.
- 코드 읽기 수업: 동적 속성(저장), 태그(권한·상태), 이벤트(채팅·아이템 사용) 세 가지가 거의 모든 예제에 반복됩니다.