op: 데이터팩 출력을 실제 music_quiz zip 으로 교체

가이드 (mc_datapack/launcher_datapack_연동_가이드.txt) 에 따라:
- file/datapacks/music_quiz_template/ 에 mc_datapack 의 music_quiz/ 정적
  파일을 미리 동봉 (data/mq/function/init/songs.mcfunction 제외).
- src/server/datapack.ts: list.music → SNBT (`{title, author, alias}`)
  songs.mcfunction 빌더와 archiver 기반 zip 스트리머 추가.
- /op/datapack/:packName/generate 가 텍스트 placeholder 대신
  music_quiz_<key>.zip 을 Content-Disposition attachment 로 내려준다.
- datapack.ejs 의 코드블록·복사 UI 제거, 곡 수는 서버 렌더 시점에 표시.
- 더 이상 쓰이지 않는 locales 의 datapackOutput.* 키 제거, datapack
  버튼 라벨/상태 문구를 zip 다운로드용으로 정리.
This commit is contained in:
2026-05-13 16:34:34 +09:00
parent 2344c4b8d2
commit af884706d4
66 changed files with 871 additions and 72 deletions

View File

@@ -0,0 +1,7 @@
scoreboard objectives add func.temp dummy
$execute store result score n1 func.temp run data get storage func:temp $(n1)
$execute store result score n2 func.temp run data get storage func:temp $(n2)
execute if score n1 func.temp = n2 func.temp run return 1
return 0

View File

@@ -0,0 +1,15 @@
scoreboard players set two func.temp 2
$data modify storage func:temp half.result set from storage func:temp $(list)
execute store result score length func.temp run data get storage func:temp half.result
scoreboard players operation half func.temp = length func.temp
scoreboard players operation half func.temp /= two func.temp
scoreboard players operation odd func.temp = length func.temp
scoreboard players operation odd func.temp %= two func.temp
scoreboard players operation half func.temp += odd func.temp
function func:half/f1

View File

@@ -0,0 +1 @@
execute if score half func.temp < length func.temp run function func:half/f2

View File

@@ -0,0 +1,5 @@
data remove storage func:temp half.result[-1]
scoreboard players remove length func.temp 1
function func:half/f1

View File

@@ -0,0 +1,18 @@
$function func:length {text:"$(text)"}
# return length
function func:num_list with storage func:temp
# return num_list
function func:shuffle {list:"num_list"}
# return shuffle.result
function func:half {list:"shuffle.result"}
# return half.result
$function func:length {text:"$(text)"}
# return length
function func:text_list with storage func:temp
# return text_list
function func:join {list:"text_list"}
# return join.text
# tellraw @a {"storage":"func:temp","nbt":"join.text"}

View File

@@ -0,0 +1,2 @@
$execute if data storage func:temp {$(l1):{$(l2):[$(index)]}} run return 1
return 0

View File

@@ -0,0 +1,2 @@
$execute if data storage func:temp {space:{text:"$(space)"}} run return 1
return 0

View File

@@ -0,0 +1,4 @@
data modify storage func:temp join.text set value ""
$data modify storage func:temp join.list set from storage func:temp $(list)
function func:join/f1

View File

@@ -0,0 +1,8 @@
execute store result score length func.temp run data get storage func:temp join.list
execute if score length func.temp matches 0 run return 1
data modify storage func:temp join.now set from storage func:temp join.text
data modify storage func:temp join.next set from storage func:temp join.list[0]
function func:join/f2 with storage func:temp join

View File

@@ -0,0 +1,5 @@
$data modify storage func:temp join.text set value "$(now)$(next)"
data remove storage func:temp join.list[0]
function func:join/f1

View File

@@ -0,0 +1,5 @@
$data modify storage func:temp text set value "$(text)"
execute store result storage func:temp length int 1 run data get storage func:temp text
return run data get storage func:temp length

View File

@@ -0,0 +1,13 @@
data modify storage func:temp space.space set value " "
data modify storage func:temp zero set value 0
$data modify storage func:temp length set value $(length)
execute store result score result func.temp run function func:comp_num {n1:"zero",n2:"length"}
execute if score result func.temp matches 1 run tellraw @s {"text":"length는 1이상 이어야 합니다.","color":"red"}
execute if score result func.temp matches 1 run return 0
data modify storage func:temp num_list set value []
data modify storage func:temp index set value 0
data modify storage func:temp index_next set value 1
function func:num_list/f1 with storage func:temp

View File

@@ -0,0 +1,12 @@
$data modify storage func:temp space.text set string storage func:temp text $(index) $(index_next)
execute store result score result func.temp run function func:is_space with storage func:temp space
$execute if score result func.temp matches 0 run data modify storage func:temp num_list append value $(index)
function func:plus {name:"index",plus:1}
function func:plus {name:"index_next",plus:1}
execute store result score result func.temp run function func:comp_num {n1:"index",n2:"length"}
execute if score result func.temp matches 1 run return 1
function func:num_list/f1 with storage func:temp

View File

@@ -0,0 +1,7 @@
scoreboard objectives add func.temp dummy
$execute store result score temp func.temp run data get storage func:temp $(name)
$scoreboard players add temp func.temp $(plus)
$execute store result storage func:temp $(name) int 1 run scoreboard players get temp func.temp

View File

@@ -0,0 +1,9 @@
scoreboard objectives add func.temp dummy
data modify storage func:temp shuffle.result set value []
$data modify storage func:temp shuffle.list set from storage func:temp $(list)
execute store result score length func.temp run data get storage func:temp shuffle.list
function func:shuffle/f1

View File

@@ -0,0 +1,11 @@
execute store result score length func.temp run data get storage func:temp shuffle.list
execute if score length func.temp matches 0 run return 1
execute store result score random func.temp run random value 0..2147483646
scoreboard players operation random func.temp %= length func.temp
execute run function func:shuffle/f2 with storage func:temp {index:0}
execute store result storage func:temp shuffle.index int 1 run scoreboard players get random func.temp
function func:shuffle/f2 with storage func:temp shuffle

View File

@@ -0,0 +1,4 @@
$data modify storage func:temp shuffle.result append from storage func:temp shuffle.list[$(index)]
$data remove storage func:temp shuffle.list[$(index)]
function func:shuffle/f1

View File

@@ -0,0 +1,13 @@
data modify storage func:temp space.space set value " "
data modify storage func:temp zero set value 0
$data modify storage func:temp length set value $(length)
execute store result score result func.temp run function func:comp_num {n1:"zero",n2:"length"}
execute if score result func.temp matches 1 run tellraw @s {"text":"length는 1이상 이어야 합니다.","color":"red"}
execute if score result func.temp matches 1 run return 0
data modify storage func:temp text_list set value []
data modify storage func:temp index set value 0
data modify storage func:temp index_next set value 1
function func:text_list/f1 with storage func:temp

View File

@@ -0,0 +1,16 @@
$data modify storage func:temp space.text set string storage func:temp text $(index) $(index_next)
execute store result score result func.temp run function func:is_space with storage func:temp space
$execute store result score result2 func.temp run function func:is_index {l1:"half",l2:"result",index:$(index)}
execute if score result2 func.temp matches 0 if score result func.temp matches 0 run data modify storage func:temp text_list append value ""
execute if score result2 func.temp matches 0 if score result func.temp matches 1 run data modify storage func:temp text_list append from storage func:temp space.text
execute if score result2 func.temp matches 1 run data modify storage func:temp text_list append from storage func:temp space.text
function func:plus {name:"index",plus:1}
function func:plus {name:"index_next",plus:1}
execute store result score result func.temp run function func:comp_num {n1:"index",n2:"length"}
execute if score result func.temp matches 1 run return 1
function func:text_list/f1 with storage func:temp

View File

@@ -0,0 +1,5 @@
{
"values": [
"mq:load"
]
}

View File

@@ -0,0 +1,5 @@
{
"values": [
"mq:tick"
]
}

View File

@@ -0,0 +1,23 @@
{
"criteria": {
"placed_something": {
"trigger": "minecraft:impossible",
"conditions": {}
}
},
"rewards": {
"function": "mq:players/login"
},
"parent": "minecraft:story/root",
"display": {
"icon": {
"id": "minecraft:stone"
},
"title": "로그인 감지",
"description": "서버 접속을 감지",
"hidden": false,
"announce_to_chat": true,
"show_toast": true,
"frame": "task"
}
}

View File

@@ -0,0 +1,46 @@
{
"type": "minecraft:confirmation",
"title": {
"text": "음악퀴즈",
"bold": true
},
"body": [
{
"type": "minecraft:plain_message",
"contents": {
"text": "음악퀴즈 설명",
"bold": true
}
},
{
"type": "minecraft:plain_message",
"contents": {
"text": "\n1. 정답입력시에 채팅으로 입력해주시면 됩니다.\n[ 띄어쓰기, 영어 대소문자, 특수문자 ]\n상관없이 입력하셔도 인식 됩니다.\n\n2. 모든 소리는 날씨 소리로 조절할수 있습니다.\n\n3. 게임시작후 버튼들은 과반수(절반이상)가 눌러야 작동합니다.\n\n4. 힌트는 특수문자 제외 정답의 절반이 가려져서 나옵니다.\n힌트는 여러번 받을수 있고,\n받을때마다 가려지는 부분이 달라집니다."
},
"width": 300
}
],
"inputs": [],
"can_close_with_escape": true,
"pause": false,
"after_action": "close",
"yes": {
"label": {
"text": "취소",
"type": "text"
},
"action": {
"type": "minecraft:run_command",
"command": "trigger cancel"
}
},
"no": {
"label": {
"text": "다음 페이지 ->"
},
"action": {
"type": "minecraft:show_dialog",
"dialog": "mq:page2"
}
}
}

View File

@@ -0,0 +1,45 @@
{
"type": "minecraft:confirmation",
"title": {
"text": "음악퀴즈",
"bold": true
},
"body": [
{
"type": "minecraft:plain_message",
"contents": {
"text": "음악퀴즈 설명",
"bold": true
}
},
{
"type": "minecraft:plain_message",
"contents": {
"text": "\n5. 다시듣기는 노래가 끝까지 다 재생되었거나,\n다시 처음부분부터 들어보고싶을때\n누르면 좋습니다.\n\n6. 뒤에있는 \"소리 테스트\" 버튼으로\n미리 소리크기를 들어보고 조절할수있습니다."
}
}
],
"inputs": [],
"can_close_with_escape": true,
"pause": false,
"after_action": "close",
"yes": {
"label": {
"text": "<- 이전 페이지",
"type": "text"
},
"action": {
"type": "minecraft:show_dialog",
"dialog": "mq:page1"
}
},
"no": {
"label": {
"text": "다음 페이지 ->"
},
"action": {
"type": "minecraft:show_dialog",
"dialog": "mq:page3"
}
}
}

View File

@@ -0,0 +1,46 @@
{
"type": "minecraft:confirmation",
"title": {
"text": "음악퀴즈",
"bold": true
},
"body": [
{
"type": "minecraft:plain_message",
"contents": {
"text": "음악퀴즈 설명",
"bold": true
}
},
{
"type": "minecraft:plain_message",
"contents": {
"text": "\n재미있게 즐겨주세요."
}
}
],
"inputs": [],
"can_close_with_escape": true,
"pause": false,
"after_action": "close",
"yes": {
"label": {
"text": "<- 이전 페이지",
"type": "text"
},
"action": {
"type": "minecraft:show_dialog",
"dialog": "mq:page2"
}
},
"no": {
"label": {
"text": "준비완료",
"bold": true
},
"action": {
"type": "minecraft:run_command",
"command": "trigger ready"
}
}
}

View File

@@ -0,0 +1,14 @@
scoreboard players reset @a hint
execute if score init main matches 0 run return run function mq:tellraw {"text":"아직 퀴즈가 시작되지 않았습니다.","color":"red",msg:'""'}
execute if score init main matches 1..4 run return run function mq:tellraw {"text":"아직 힌트를 받을 수 없습니다.","color":"red",msg:'""'}
execute if score init main matches 6.. run return run function mq:tellraw {"text":"아직 다음노래가 재생되지 않았습니다.","color":"red",msg:'""'}
execute if score init main matches 10 run return run function mq:tellraw {"text":"퀴즈가 종료되었습니다.","color":"red",msg:'""'}
execute if score init main matches 5 run data modify storage mq:main hint.text set from storage mq:main answer.title
execute if score init main matches 5 run data modify storage mq:main hint.hint set value ""
execute if score init main matches 5 run function func:hint with storage mq:main hint
execute if score init main matches 5 run data modify storage mq:main hint.hint set from storage func:temp join.text
execute if score init main matches 5 run function mq:tellraw {"text":"","color":"black",msg:'""'}
execute if score init main matches 5 run function mq:tellraw {"text":"","color":"black",msg:[{"text":"힌트: ","color":"aqua","bold":true},{"storage":"mq:main","nbt":"hint.hint","color": "yellow","bold": true}]}
execute if score init main matches 5 run function mq:tellraw {"text":"","color":"black",msg:'""'}

View File

@@ -0,0 +1,9 @@
scoreboard players reset @a replay
execute if score init main matches 0 run return run function mq:tellraw {"text":"아직 퀴즈가 시작되지 않았습니다.","color":"red",msg:'""'}
execute if score init main matches 1..4 run return run function mq:tellraw {"text":"아직 노래가 재생되지 않았습니다.","color":"red",msg:'""'}
execute if score init main matches 6.. run return run function mq:tellraw {"text":"아직 다음노래가 재생되지 않았습니다.","color":"red",msg:'""'}
execute if score init main matches 10 run return run function mq:tellraw {"text":"퀴즈가 종료되었습니다.","color":"red",msg:'""'}
execute if score init main matches 5 run function mq:quiz/stop_sound
execute if score init main matches 5 run function mq:quiz/play_sound

View File

@@ -0,0 +1,9 @@
scoreboard players reset @a skip
execute if score init main matches 0 run return run function mq:tellraw {"text":"아직 퀴즈가 시작되지 않았습니다.","color":"red",msg:'""'}
execute if score init main matches 1..4 run return run function mq:tellraw {"text":"아직 스킵 할수없습니다.","color":"red",msg:'""'}
execute if score init main matches 6.. run return run function mq:tellraw {"text":"아직 다음노래가 재생되지 않았습니다.","color":"red",msg:'""'}
execute if score init main matches 10 run return run function mq:tellraw {"text":"퀴즈가 종료되었습니다.","color":"red",msg:'""'}
execute if score init main matches 5 run scoreboard players set skip buttons -2
execute if score init main matches 5 run function mq:quiz/correct

View File

@@ -0,0 +1,10 @@
execute if score init main matches 10 run return run function mq:tellraw {"text":"퀴즈가 완전히 종료된후 시작해주세요.","color":"red","msg":""}
setblock ~ ~ ~ minecraft:air
function mq:quiz/stop_sound
$scoreboard players set max_index main $(max_index)
scoreboard players set init main 1
dialog show @a mq:page1

View File

@@ -0,0 +1,59 @@
scoreboard players set index main 0
$scoreboard players set max_index main $(max_index)
scoreboard players set score main 0
scoreboard players set init main 0
scoreboard players set timer main 0
scoreboard players set start buttons -1
scoreboard players set stop buttons -1
scoreboard players set skip buttons -1
scoreboard players set hint buttons -1
scoreboard players set replay buttons -1
scoreboard players set test buttons -1
scoreboard players reset @a answer
# 트리거 시작
scoreboard objectives remove ready
scoreboard objectives add ready trigger
scoreboard objectives remove cancel
scoreboard objectives add cancel trigger
scoreboard objectives remove stop
scoreboard objectives add stop trigger
scoreboard objectives remove skip
scoreboard objectives add skip trigger
scoreboard objectives remove hint
scoreboard objectives add hint trigger
scoreboard objectives remove replay
scoreboard objectives add replay trigger
# 트리거 끝
scoreboard objectives setdisplay sidebar
scoreboard objectives remove score
scoreboard objectives add score dummy {"text":"점수","bold":true}
scoreboard objectives setdisplay sidebar score
dialog clear @a
bossbar set mq:process name [{"text":"진행도: ","color": "yellow","bold": true},{"score":{"name":"index","objective": "main"},"color": "yellow","bold": true},{"text":"/","color": "yellow","bold": true},{"score":{"name":"max_index","objective": "main"},"color": "yellow","bold": true}]
$bossbar set mq:process max $(max_index)
bossbar set mq:process value 0
bossbar set mq:process color pink
bossbar set mq:process visible false
bossbar set mq:process style notched_10
bossbar set mq:process players @a
# 대기 상태 marker 1개만 소환 (answer.title="음악퀴즈" 가 sentinel)
data modify storage mq:main answer set value {title:"음악퀴즈", alias:[]}
data modify storage mq:tmp marker_call set from storage mq:main marker
data modify storage mq:tmp marker_call.name set value "음악퀴즈"
data modify storage mq:tmp marker_call.alias set value []
function mq:quiz/macro/summon with storage mq:tmp marker_call
function mq:quiz/stop_sound
function mq:images/clear

View File

@@ -0,0 +1,5 @@
stopsound @a block minecraft:block.stone_button.click_on
function mq:tellraw {"text":"띵!!!","color":"white","msg":'""'}
execute as @a at @s run playsound minecraft:block.note_block.bell weather @s ~ ~ ~ 1 0.9
execute as @a at @s run playsound minecraft:block.note_block.bell weather @s ~ ~ ~ 1 0.9
execute as @a at @s run playsound minecraft:block.note_block.bell weather @s ~ ~ ~ 1 0.9

View File

@@ -0,0 +1 @@
kill @e[type=minecraft:painting,tag=mq_cover]

View File

@@ -0,0 +1 @@
$summon minecraft:painting $(x) $(y) $(z) {variant:"$(namespace):$(cover)",facing:$(facing)b,Tags:["mq","mq_cover"]}

View File

@@ -0,0 +1,3 @@
data modify storage mq:tmp painting set from storage mq:main image
data modify storage mq:tmp painting.cover set from storage mq:main answer.cover
function mq:images/macro/show with storage mq:tmp painting

View File

@@ -0,0 +1,7 @@
data modify storage mq:main button_defs set value []
data modify storage mq:main button_defs append value {n:"start", x:140, y:62, z:-225, f:"south", c:"function mq:commands/start with storage mq:main"}
data modify storage mq:main button_defs append value {n:"stop", x:142, y:62, z:-225, f:"south", c:"function mq:commands/stop with storage mq:main"}
data modify storage mq:main button_defs append value {n:"skip", x:144, y:62, z:-225, f:"south", c:"function mq:commands/skip"}
data modify storage mq:main button_defs append value {n:"hint", x:146, y:62, z:-225, f:"south", c:"function mq:commands/hint"}
data modify storage mq:main button_defs append value {n:"replay", x:148, y:62, z:-225, f:"south", c:"function mq:commands/replay"}
data modify storage mq:main button_defs append value {n:"test", x:144, y:62, z:-213, f:"north", c:"function mq:commands/test"}

View File

@@ -0,0 +1,23 @@
# 음악퀴즈 주제 — tellraw 접두사([ 이름 ])와 사이드바 표시에 사용
data modify storage mq:main title set value "음악퀴즈"
# 플레이어 접속 시 텔레포트 위치 (x y z, r=yaw, f=pitch)
data modify storage mq:main spawn set value {x: 144, y: 61, z: -219, r: 180, f: 0}
# 음원 재생 — minecraft_launcher 리소스팩의 musicquiz:track_NN 사운드 이벤트
# namespace — 리소스팩 네임스페이스 (기본 "musicquiz")
# source — /playsound 채널. stopsound 와 동일해야 함 (기본 "weather")
# volume — 기본 음량. 곡별 override 는 init/songs.mcfunction 의 volume 필드 사용
# pitch — 1.0 = 원본 속도
data modify storage mq:main audio set value {namespace: "musicquiz", source: "weather", volume: 1.0, pitch: 1.0}
# 정답 페인팅 — minecraft_launcher 리소스팩의 musicquiz:cover_NN painting_variant
# namespace — painting_variant 네임스페이스 (기본 "musicquiz")
# x,y,z — 페인팅 entity 좌표 (벽면 앞쪽 블록 위치)
# facing — 페인팅이 바라보는 방향: south=0 / west=1 / north=2 / east=3
data modify storage mq:main image set value {namespace: "musicquiz", x: 144, y: 84, z: -261, facing: 0b}
# 정답 입력용 marker entity 소환 좌표
data modify storage mq:main marker set value {x: 144, y: 59, z: -219}
# 곡 개수 max_index 는 init/songs.mcfunction 의 길이로 자동 계산됨

View File

@@ -0,0 +1,5 @@
data modify storage mq:main trigger_defs set value []
data modify storage mq:main trigger_defs append value {n:"stop", n2:"중지", c:"function mq:commands/stop with storage mq:main"}
data modify storage mq:main trigger_defs append value {n:"skip", n2:"스킵", c:"function mq:commands/skip"}
data modify storage mq:main trigger_defs append value {n:"hint", n2:"힌트", c:"function mq:commands/hint"}
data modify storage mq:main trigger_defs append value {n:"replay", n2:"다시재생", c:"function mq:commands/replay"}

View File

@@ -0,0 +1,29 @@
data modify storage mq:main answer set value {title:"", author:"", alias:[]}
data merge storage func:temp {}
data merge storage mq:tmp {}
function mq:init/config
function mq:init/songs
function mq:init/buttons
function mq:init/triggers
function mq:tellraw {"text":"서버 리로드 성공!","color":"white","msg":'""'}
scoreboard objectives remove func.temp
scoreboard objectives remove main
scoreboard objectives remove buttons
scoreboard objectives remove answer
scoreboard objectives remove leave_game
scoreboard objectives add func.temp dummy
scoreboard objectives add main dummy
scoreboard objectives add buttons dummy
scoreboard objectives add answer dummy
scoreboard objectives add leave_game custom:leave_game
scoreboard players set two func.temp 2
bossbar add mq:process [{"text":"진행도: ","color": "yellow","bold": true},{"text":"0","color": "yellow","bold": true},{"text":"/","color": "yellow","bold": true},{"text":"0","color": "yellow","bold": true}]
function mq:commands/stop with storage mq:main
function mq:players/login with storage mq:main spawn

View File

@@ -0,0 +1,10 @@
tag @s add player
scoreboard players reset @s leave_game
title @s times 10t 80t 10t
title @s subtitle ""
title @s title ""
$setworldspawn $(x) $(y) $(z) $(r) $(f)
$tp @s $(x) $(y) $(z) $(r) $(f)
gamemode adventure @s

View File

@@ -0,0 +1,32 @@
scoreboard players set init main 6
scoreboard players set @s answer 2
function mq:tellraw {"text":"","color":"black","msg":""}
function mq:tellraw {"text":"","color":"black",msg:[{"text":"정답: ","color": "aqua"},{"storage":"mq:main","nbt":"answer.title","color": "yellow","bold": true}]}
function mq:tellraw {"text":"","color":"black",msg:[{"text":"가수: ","color":"aqua"},{"storage":"mq:main","nbt":"answer.author","color": "yellow","bold": true}]}
execute if score skip buttons matches -2 run function mq:tellraw {"text":"","color":"black",msg:[{"text":"정답자: ","color": "aqua"},{"text":"스킵","color": "yellow","bold": true}]}
execute unless score skip buttons matches -2 run function mq:tellraw {"text":"","color":"black",msg:[{"text":"정답자: ","color": "aqua"},{"selector":"@s","color": "yellow","bold": true}]}
function mq:tellraw {"text":"","color":"black",msg:[{"text": "( 15초뒤 다음문제로 넘어갑니다. )","color": "gray"}]}
function mq:tellraw {"text":"","color":"black","msg":""}
title @a subtitle [{"text":"정답: ","color": "aqua"},{"storage":"mq:main","nbt":"answer.title","color": "yellow","bold": true}]
title @a title {"text":""}
scoreboard players set @a ready 0
scoreboard players set @a stop 0
scoreboard players set @a skip 0
scoreboard players set @a hint 0
scoreboard players set @a replay 0
execute if score skip buttons matches -2 run scoreboard players add score 1
execute unless score skip buttons matches -2 run scoreboard players add @s score 1
scoreboard players set stop buttons -3
scoreboard players set skip buttons -3
scoreboard players set hint buttons -3
scoreboard players set replay buttons -3
scoreboard players set timer main 1
function mq:images/show

View File

@@ -0,0 +1,3 @@
scoreboard players set init main 10
scoreboard players set timer main 1

View File

@@ -0,0 +1 @@
$execute as @a at @s run playsound $(namespace):$(track) $(source) @s ~ ~ ~ $(volume) $(pitch)

View File

@@ -0,0 +1,3 @@
$data modify storage mq:main answer set from storage mq:main songs[$(idx)]
$data modify storage mq:main answer.track set value "track_$(pad)$(num)"
$data modify storage mq:main answer.cover set value "cover_$(pad)$(num)"

View File

@@ -0,0 +1 @@
$stopsound @a $(source)

View File

@@ -0,0 +1,9 @@
$execute unless data storage mq:main {answer:{title:"음악퀴즈"}} run summon minecraft:marker $(x) $(y) $(z) {Tags:["mq","default"],CustomName:"정답입력시작"}
$summon minecraft:marker $(x) $(y) $(z) {Tags:["mq","default"],CustomName:"$(name)"}
execute store result score length func.temp run data get storage mq:tmp marker_call.alias
execute if score length func.temp matches 1.. run data modify storage mq:tmp marker_call.name set from storage mq:tmp marker_call.alias[0]
execute if score length func.temp matches 1.. run data remove storage mq:tmp marker_call.alias[0]
execute if score length func.temp matches 1.. run function mq:quiz/macro/summon2 with storage mq:tmp marker_call
$execute unless data storage mq:main {answer:{title:"음악퀴즈"}} run summon minecraft:marker $(x) $(y) $(z) {Tags:["mq","default"],CustomName:"정답입력종료"}

View File

@@ -0,0 +1,6 @@
$summon minecraft:marker $(x) $(y) $(z) {Tags:["mq","default"],CustomName:"$(name)"}
execute store result score length func.temp run data get storage mq:tmp marker_call.alias
execute if score length func.temp matches 1.. run data modify storage mq:tmp marker_call.name set from storage mq:tmp marker_call.alias[0]
execute if score length func.temp matches 1.. run data remove storage mq:tmp marker_call.alias[0]
execute if score length func.temp matches 1.. run function mq:quiz/macro/summon2 with storage mq:tmp marker_call

View File

@@ -0,0 +1,5 @@
data modify storage mq:tmp playsound set from storage mq:main audio
data modify storage mq:tmp playsound.track set from storage mq:main answer.track
# 곡 단위 volume override — songs[i].volume 가 없으면 audio.volume 그대로 유지 (no-op)
data modify storage mq:tmp playsound.volume set from storage mq:main answer.volume
function mq:quiz/macro/play_sound with storage mq:tmp playsound

View File

@@ -0,0 +1,20 @@
scoreboard players set timer main 0
execute if score index main >= max_index main run return run function mq:quiz/end with storage mq:main
scoreboard players add index main 1
bossbar set mq:process name [{"text":"진행도: ","color": "yellow","bold": true},{"score":{"name":"index","objective": "main"},"color": "yellow","bold": true},{"text":"/","color": "yellow","bold": true},{"score":{"name":"max_index","objective": "main"},"color": "yellow","bold": true}]
bossbar set mq:process players @a
execute store result bossbar mq:process value run scoreboard players get index main
# tmp.{idx (0-based, songs[] 인덱스), num (1-based, track_NN), pad ("0"|"")} 구성
execute store result storage mq:tmp num int 1 run scoreboard players get index main
scoreboard players operation song_idx func.temp = index main
scoreboard players remove song_idx func.temp 1
execute store result storage mq:tmp idx int 1 run scoreboard players get song_idx func.temp
execute if score index main matches 1..9 run data modify storage mq:tmp pad set value "0"
execute unless score index main matches 1..9 run data modify storage mq:tmp pad set value ""
function mq:quiz/setanswer

View File

@@ -0,0 +1,17 @@
# songs[$(idx)] → answer 로 복사하고, 트랙/커버 id 부여
function mq:quiz/macro/setanswer with storage mq:tmp
# 정답 marker entity 소환 (좌표 + name/alias 합쳐서 macro 호출)
data modify storage mq:tmp marker_call set from storage mq:main marker
data modify storage mq:tmp marker_call.name set from storage mq:main answer.title
data modify storage mq:tmp marker_call.alias set from storage mq:main answer.alias
function mq:quiz/macro/summon with storage mq:tmp marker_call
scoreboard players set stop buttons -1
scoreboard players set skip buttons -1
scoreboard players set hint buttons -1
scoreboard players set replay buttons -1
scoreboard players set init main 5
function mq:quiz/play_sound

View File

@@ -0,0 +1,6 @@
scoreboard players set init main 2
scoreboard players set index main 0
bossbar set mq:process visible true
scoreboard players set timer main 1

View File

@@ -0,0 +1 @@
function mq:quiz/macro/stop_sound with storage mq:main audio

View File

@@ -0,0 +1,28 @@
$execute if score $(n) buttons matches ..-2 run setblock $(x) $(y) $(z) minecraft:air
$execute if score $(n) buttons matches ..-2 run data modify entity @e[type=minecraft:interaction,tag=mq,tag=$(n),limit=1] response set value 0b
$execute if score $(n) buttons matches ..-2 run return 0
$execute unless score $(n) buttons matches -1.. run scoreboard players set $(n) buttons -1
$execute if score $(n) buttons matches -1 run setblock $(x) $(y) $(z) minecraft:stone_button[face=wall,facing=$(f),powered=false]
$execute if score $(n) buttons matches -1 positioned $(x) $(y) $(z) run setblock ~ ~-3 ~ minecraft:redstone_block
$execute if score $(n) buttons matches -1 positioned $(x) $(y) $(z) run setblock ~ ~-3 ~ minecraft:red_wool
$execute if score $(n) buttons matches -1 run scoreboard players set $(n) buttons 0
$execute if block $(x) $(y) $(z) minecraft:stone_button[face=wall,facing=$(f),powered=true] \
if score $(n) buttons matches 0 \
run scoreboard players set $(n) buttons 1
$execute if score $(n) buttons matches 1 unless entity @e[type=minecraft:interaction,tag=mq,tag=$(n),limit=1] positioned $(x) $(y) $(z) run $(c)
$execute if score $(n) buttons matches 1 \
run scoreboard players set $(n) buttons 2
$execute if block $(x) $(y) $(z) minecraft:stone_button[face=wall,facing=$(f),powered=false] \
if score $(n) buttons matches 1.. \
run scoreboard players set $(n) buttons 0
$execute as @e[type=minecraft:interaction,tag=mq,tag=$(n),limit=1] on target as @s positioned $(x) $(y) $(z) run playsound minecraft:block.stone_button.click_on block @s ~ ~ ~ 1 1
$execute as @e[type=minecraft:interaction,tag=mq,tag=$(n),limit=1] on target as @s positioned $(x) $(y) $(z) if score init main matches 0 run $(c)
$execute as @e[type=minecraft:interaction,tag=mq,tag=$(n),limit=1] on target as @s positioned $(x) $(y) $(z) unless score init main matches 0 run trigger $(n)
$execute as @e[type=minecraft:interaction,tag=mq,tag=$(n)] at @s run data remove entity @s attack
$execute as @e[type=minecraft:interaction,tag=mq,tag=$(n)] at @s run data remove entity @s interaction

View File

@@ -0,0 +1,6 @@
function mq:repeat/buttons/btn with storage mq:main button_defs[0]
function mq:repeat/buttons/btn with storage mq:main button_defs[1]
function mq:repeat/buttons/btn with storage mq:main button_defs[2]
function mq:repeat/buttons/btn with storage mq:main button_defs[3]
function mq:repeat/buttons/btn with storage mq:main button_defs[4]
function mq:repeat/buttons/btn with storage mq:main button_defs[5]

View File

@@ -0,0 +1,2 @@
execute as @a[scores={answer=1}] run function mq:quiz/correct with storage mq:main answer
execute as @a[scores={answer=2}] run scoreboard players reset @a answer

View File

@@ -0,0 +1,2 @@
execute as @a[tag=!player] run function mq:players/login with storage mq:main spawn
execute as @a if score @s leave_game matches 1.. run function mq:players/login with storage mq:main spawn

View File

@@ -0,0 +1,49 @@
execute if score timer main matches 1.. run scoreboard players add timer main 1
execute unless score init main matches 2 \
unless score init main matches 6 \
unless score init main matches 10 \
run scoreboard players set timer main 0
# start title timer
execute if score init main matches 2 if score timer main matches 20 run title @a title {"text":"3"}
execute if score init main matches 2 if score timer main matches 20 as @a at @s run playsound minecraft:block.note_block.iron_xylophone weather @s ~ ~ ~ 1 1
execute if score init main matches 2 if score timer main matches 20 as @a at @s run playsound minecraft:block.note_block.iron_xylophone weather @s ~ ~ ~ 1 1
execute if score init main matches 2 if score timer main matches 20 as @a at @s run playsound minecraft:block.note_block.iron_xylophone weather @s ~ ~ ~ 1 1
execute if score init main matches 2 if score timer main matches 40 run title @a title {"text":"2"}
execute if score init main matches 2 if score timer main matches 40 as @a at @s run playsound minecraft:block.note_block.iron_xylophone weather @s ~ ~ ~ 1 1
execute if score init main matches 2 if score timer main matches 40 as @a at @s run playsound minecraft:block.note_block.iron_xylophone weather @s ~ ~ ~ 1 1
execute if score init main matches 2 if score timer main matches 40 as @a at @s run playsound minecraft:block.note_block.iron_xylophone weather @s ~ ~ ~ 1 1
execute if score init main matches 2 if score timer main matches 60 run title @a title {"text":"1"}
execute if score init main matches 2 if score timer main matches 60 as @a at @s run playsound minecraft:block.note_block.iron_xylophone weather @s ~ ~ ~ 1 1
execute if score init main matches 2 if score timer main matches 60 as @a at @s run playsound minecraft:block.note_block.iron_xylophone weather @s ~ ~ ~ 1 1
execute if score init main matches 2 if score timer main matches 60 as @a at @s run playsound minecraft:block.note_block.iron_xylophone weather @s ~ ~ ~ 1 1
execute if score init main matches 2 if score timer main matches 100 run title @a title {"text":""}
execute if score init main matches 2 if score timer main matches 100.. run function mq:quiz/select with storage mq:main
# next song timer
execute if score init main matches 6 if score timer main matches 300 run title @a title {"text":""}
execute if score init main matches 6 if score timer main matches 290 run function mq:images/clear
execute if score init main matches 6 if score timer main matches 300.. run function mq:quiz/select with storage mq:main
# endding timer
execute if score init main matches 10 if score timer main matches 60 run function mq:tellraw {"text":"퀴즈가 종료되었습니다.","color":"white","msg":""}
execute if score init main matches 10 if score timer main matches 60 as @a at @s run playsound minecraft:ui.button.click weather @s ~ ~ ~ 1 1
execute if score init main matches 10 if score timer main matches 60 as @a at @s run playsound minecraft:ui.button.click weather @s ~ ~ ~ 1 1
execute if score init main matches 10 if score timer main matches 60 as @a at @s run playsound minecraft:ui.button.click weather @s ~ ~ ~ 1 1
execute if score init main matches 10 if score timer main matches 180 run function mq:tellraw {"text":"퀴즈를 다시 시작하시려면 종료를 눌러주세요.","color":"white","msg":""}
execute if score init main matches 10 if score timer main matches 120 as @a at @s run scoreboard players set stop buttons -1
execute if score init main matches 10 if score timer main matches 120 as @a at @s run playsound minecraft:ui.button.click weather @s ~ ~ ~ 1 1
execute if score init main matches 10 if score timer main matches 120 as @a at @s run playsound minecraft:ui.button.click weather @s ~ ~ ~ 1 1
execute if score init main matches 10 if score timer main matches 120 as @a at @s run playsound minecraft:ui.button.click weather @s ~ ~ ~ 1 1
execute if score init main matches 10 if score timer main matches 120 run function mq:tellraw {"text":"플레이 해주셔서 감사합니다.","color":"white","msg":""}
execute if score init main matches 10 if score timer main matches 180 as @a at @s run playsound minecraft:ui.button.click weather @s ~ ~ ~ 1 1
execute if score init main matches 10 if score timer main matches 180 as @a at @s run playsound minecraft:ui.button.click weather @s ~ ~ ~ 1 1
execute if score init main matches 10 if score timer main matches 180 as @a at @s run playsound minecraft:ui.button.click weather @s ~ ~ ~ 1 1
execute if score init main matches 10 if score timer main matches 200.. run scoreboard players set init main 11

View File

@@ -0,0 +1,24 @@
execute if score init main matches 0..1 run scoreboard players enable @a ready
execute if score init main matches 0..1 as @a if score @s ready matches 1 run function mq:tellraw {"text":"","color":"black",msg:[{"selector":"@s","color": "yellow","bold": true},{"text":" : ","color":"gray"},{"text":"준비완료","color":"white"}]}
execute if score init main matches 0..1 as @a if score @s ready matches 1 run scoreboard players set @s ready 2
execute if score init main matches 0..1 as @a if score @s ready matches 3 run function mq:tellraw {"text":"","color":"black",msg:[{"selector":"@s","color": "yellow","bold": true},{"text":" : ","color":"gray"},{"text":"이미 준비완료 상태입니다.","color": "red"}]}
execute if score init main matches 0..1 as @a if score @s ready matches 3 run scoreboard players set @s ready 2
execute if score init main matches 0..1 run scoreboard players enable @a cancel
execute if score init main matches 0..1 as @a if score @s cancel matches 1 run function mq:tellraw {"text":"","color":"black",msg:[{"selector":"@s","color": "yellow","bold": true},{"text":" : ","color":"gray"},{"text":"취소를 선택하셨습니다.","color": "red"}]}
execute if score init main matches 0..1 as @a if score @s cancel matches 1 run function mq:commands/stop with storage mq:main
execute if score init main matches 0..1 store result score max_player ready if entity @a
execute if score init main matches 0..1 store result score ready_player ready if entity @a[scores={ready=2..}]
execute if score init main matches 0..1 \
unless score max_player ready matches 0 \
if score max_player ready = ready_player ready \
run function mq:quiz/start with storage mq:main
function mq:repeat/triggers/trigger with storage mq:main trigger_defs[0]
function mq:repeat/triggers/trigger with storage mq:main trigger_defs[1]
function mq:repeat/triggers/trigger with storage mq:main trigger_defs[2]
function mq:repeat/triggers/trigger with storage mq:main trigger_defs[3]

View File

@@ -0,0 +1,27 @@
$scoreboard players enable @a $(n)
$execute unless score init main matches 5 as @a if score @s $(n) matches 1.. run scoreboard players reset @s $(n)
execute unless score init main matches 5 run return 0
$execute store result score real_max_player $(n) if entity @a
$execute store result score rest_player $(n) if entity @a
$execute unless score rest_player $(n) matches 0 run scoreboard players operation rest_player $(n) %= two func.temp
$execute store result score max_player $(n) if entity @a
$execute unless score real_max_player $(n) matches 0 run scoreboard players operation max_player $(n) /= two func.temp
$execute unless score real_max_player $(n) matches 0 run scoreboard players operation max_player $(n) += rest_player $(n)
$execute store result score $(n)_player $(n) if entity @a[scores={$(n)=2..}]
$execute store result score $(n)_player_add $(n) if entity @a[scores={$(n)=2..}]
$execute run scoreboard players add $(n)_player_add $(n) 1
$execute as @a if score @s $(n) matches 1 run function mq:tellraw {"text":"","color":"black",msg:[{"selector":"@s","color": "yellow","bold": true}," : ",{"text":"$(n2) 투표 완료","color": "white"}, \
{"text":" (","color":"gray"},{"score":{"name":"$(n)_player_add","objective": "$(n)"},"color":"gray"},{"text":"/","color":"gray"},{"score":{"name":"max_player","objective": "$(n)"},"color":"gray"},{"text":")","color":"gray"}]}
$execute as @a if score @s $(n) matches 1 run scoreboard players set @s $(n) 2
$execute as @a if score @s $(n) matches 3 run function mq:tellraw {"text":"","color":"black",msg:[{"selector":"@s","color": "yellow","bold": true}," : ",{"text":"이미 $(n2)투표를 하셨습니다.","color": "red"}]}
$execute as @a if score @s $(n) matches 3 run scoreboard players set @s $(n) 2
$execute store result score $(n)_player $(n) if entity @a[scores={$(n)=2..}]
$execute unless score real_max_player $(n) matches 0 \
if score max_player $(n) = $(n)_player $(n) \
run $(c)

View File

@@ -0,0 +1 @@
$tellraw @a ["",{"text":"[ ","bold":true,"color":"gray"},{"storage":"mq:main","nbt":"title","bold":true,"color":"dark_green"},{"text":" ]","bold":true,"color":"gray"},{"text":" "},{"text":"$(text)","color":"$(color)"},$(msg)]

View File

@@ -0,0 +1,6 @@
function mq:repeat/players
function mq:repeat/buttons/handler
function mq:repeat/triggers/handler
execute if score init main matches 2.. run function mq:repeat/timer
execute if score init main matches 5..6 run function mq:repeat/check_answer

View File

@@ -0,0 +1,6 @@
{
"pack": {
"pack_format": 75,
"description": "음악퀴즈용 데이터팩입니다."
}
}

View File

@@ -136,11 +136,9 @@
"pickedNone": "선택된 음악퀴즈 없음",
"pickedLabel": "선택: {{name}}",
"totalCount": "총 {{count}}개의 음악을 찾았습니다.",
"export": "데이터팩 출력",
"copy": "복사",
"copied": "복사됨",
"exporting": "출력 중…",
"exported": "출력 완료",
"export": "데이터팩 zip 다운로드",
"exporting": "다운로드 준비 중…",
"exported": "다운로드를 시작했습니다.",
"failed": "실패: {{message}}",
"modalPickTitle": "음악퀴즈 선택"
},
@@ -161,14 +159,5 @@
"ytdlpVideoFailed": "yt-dlp 영상 조회 실패 (code={{code}}): {{detail}}",
"ytdlpPlaylistFailed": "yt-dlp 플레이리스트 조회 실패 (code={{code}}): {{detail}}",
"tooManyRedirects": "redirect 가 너무 많습니다."
},
"datapackOutput": {
"header": "# === musicquiz: {{name}} ===",
"summary": "# 총 {{musicCount}}곡 / 사진 {{imageCount}}장",
"initLine": "say [musicquiz] 데이터팩 초기화",
"placeholder": "# 곡별 placeholder. 실제 포맷 확정되면 교체 예정.",
"trackLine": "# {{index}}. {{title}} - {{artist}} ({{duration}}s)",
"titleFallback": "(제목 없음)",
"artistFallback": "(가수 미상)"
}
}

79
src/server/datapack.ts Normal file
View File

@@ -0,0 +1,79 @@
import path from 'node:path'
import { Readable } from 'node:stream'
import archiver from 'archiver'
import type { Response } from 'express'
import { fileDatapacksDirPath } from '../shared/paths.js'
import type { MusicListEntry, PackList } from '../shared/types.js'
/** music_quiz/ 정적 템플릿 디렉터리. (songs.mcfunction 만 동적으로 생성) */
const TEMPLATE_DIR = path.join(fileDatapacksDirPath, 'music_quiz_template')
const SONGS_PATH_IN_ZIP = 'music_quiz/data/mq/function/init/songs.mcfunction'
/** SNBT 문자열 리터럴 안에 들어갈 문자열을 escape. */
function escapeSnbtString(input: string): string {
return input.replace(/\\/g, '\\\\').replace(/"/g, '\\"')
}
/** alias 배열을 SNBT 리스트 리터럴로 변환. 빈 배열도 `[]` 로 출력. */
function aliasListSnbt(aliases: string[]): string {
if (!Array.isArray(aliases) || aliases.length === 0) return '[]'
const parts = aliases.map((a) => `"${escapeSnbtString(a)}"`)
return `[${parts.join(',')}]`
}
/** 한 곡(MusicListEntry) → `{title:"...", author:"...", alias:[...]}` SNBT. */
function entrySnbt(entry: MusicListEntry): string {
const title = escapeSnbtString(entry.title ?? '')
// launcher 의 artist → 데이터팩 SNBT 의 author. 빈 값은 빈 문자열로 그대로 둔다.
const author = escapeSnbtString(entry.artist ?? '')
const alias = aliasListSnbt(entry.aliases ?? [])
return `{title:"${title}", author:"${author}", alias:${alias}}`
}
/** list.music 으로부터 `data/mq/function/init/songs.mcfunction` 본문을 생성. */
export function buildSongsMcfunction(list: PackList): string {
const lines: string[] = []
lines.push('# 곡 한 개 = 한 줄.')
lines.push('# 필수 — title, author, alias')
lines.push('# 선택 — volume (이 곡만의 /playsound 음량. 미지정시 init/config.mcfunction')
lines.push('# 의 audio.volume 사용)')
lines.push('# 곡 순서가 리소스팩의 track_NN / cover_NN 인덱스와 1:1 매칭된다.')
lines.push('# 예) {title:"Quiet Song", author:"...", alias:[...], volume:2.0}')
lines.push('data modify storage mq:main songs set value []')
for (const entry of list.music) {
lines.push(`data modify storage mq:main songs append value ${entrySnbt(entry)}`)
}
lines.push('')
lines.push('# 곡 개수는 songs 배열 길이에서 자동 계산됨')
lines.push('execute store result storage mq:main max_index int 1 run data get storage mq:main songs')
return lines.join('\n') + '\n'
}
/** music_quiz 데이터팩 zip 을 Response 로 스트리밍. */
export function streamMusicQuizZip(res: Response, packKey: string, list: PackList): void {
const fileName = `music_quiz_${packKey}.zip`
res.setHeader('Content-Type', 'application/zip')
res.setHeader('Content-Disposition', `attachment; filename="${fileName}"`)
const archive = archiver('zip', { zlib: { level: 9 } })
archive.on('warning', (err) => {
if (err.code !== 'ENOENT') res.destroy(err)
})
archive.on('error', (err) => {
res.destroy(err)
})
archive.pipe(res)
// 정적 템플릿 전체를 music_quiz/ 아래로 묶되 songs.mcfunction 만 제외.
archive.glob('**/*', {
cwd: TEMPLATE_DIR,
dot: false,
ignore: ['data/mq/function/init/songs.mcfunction']
}, { prefix: 'music_quiz/' })
// 동적으로 만든 songs.mcfunction 을 추가.
const songsText = buildSongsMcfunction(list)
archive.append(Readable.from([songsText]), { name: SONGS_PATH_IN_ZIP })
void archive.finalize()
}

View File

@@ -17,6 +17,7 @@ import { fetchPlaylistEntries, fetchVideoMeta, YtDlpUnavailableError } from '../
import { requireAuth } from '../middleware/auth.js'
import type { PackDefinition, PackList } from '../../shared/types.js'
import { t } from '../i18n.js'
import { streamMusicQuizZip } from '../datapack.js'
export const opRouter = Router()
@@ -223,17 +224,19 @@ opRouter.post('/op/list/:packName/playlist', requireAuth, async (req, res) => {
opRouter.get('/op/datapack', requireAuth, async (req, res, next) => {
try {
const keys = await listPackKeys()
const items = await Promise.all(keys.map(async (key) => ({
key,
definition: await loadPackDefinition(key)
})))
const items = await Promise.all(keys.map(async (key) => {
const definition = await loadPackDefinition(key)
const list = await loadPackList(key)
return { key, definition, musicCount: list.music.length }
}))
res.render('op/datapack', { userId: req.session.userId, items })
} catch (error) {
next(error)
}
})
// 데이터팩 출력: 임시 포맷의 mcfunction 텍스트를 반환.
// 데이터팩 출력: mc_datapack 의 music_quiz/ 템플릿을 zip 으로 묶고,
// data/mq/function/init/songs.mcfunction 만 list.music 으로 새로 만들어 덮어쓴다.
opRouter.get('/op/datapack/:packName/generate', requireAuth, async (req, res, next) => {
try {
const packKey = sanitizePackKey(pickFirstValue(req.params.packName))
@@ -243,25 +246,7 @@ opRouter.get('/op/datapack/:packName/generate', requireAuth, async (req, res, ne
return
}
const list = await loadPackList(packKey)
const lines: string[] = []
lines.push(t('datapackOutput.header', { name: definition.name }))
lines.push(t('datapackOutput.summary', {
musicCount: list.music.length,
imageCount: list.images.length
}))
lines.push(t('datapackOutput.initLine'))
lines.push(t('datapackOutput.placeholder'))
list.music.forEach((entry, index) => {
const title = entry.title || t('datapackOutput.titleFallback')
const artist = entry.artist || t('datapackOutput.artistFallback')
lines.push(t('datapackOutput.trackLine', {
index: index + 1,
title,
artist,
duration: entry.durationSec
}))
})
res.type('text/plain; charset=utf-8').send(lines.join('\n') + '\n')
streamMusicQuizZip(res, packKey, list)
} catch (error) {
next(error)
}

View File

@@ -8,6 +8,7 @@ export const manifestDirPath = path.join(projectRoot, 'manifest')
export const accountFilePath = path.join(projectRoot, 'account.json')
export const fileDirPath = path.join(projectRoot, 'file')
export const fileListDirPath = path.join(fileDirPath, 'list')
export const fileDatapacksDirPath = path.join(fileDirPath, 'datapacks')
export const viewsDirPath = path.join(projectRoot, 'views')
export const publicDirPath = path.join(projectRoot, 'public')

View File

@@ -26,11 +26,8 @@
<section class="dpActions" hidden id="dpActions">
<button type="button" class="secondaryButton" id="exportBtn"><%= t('datapack.export') %></button>
<button type="button" class="secondaryButton" id="copyBtn"><%= t('datapack.copy') %></button>
<span class="statusText" id="dp-status"></span>
</section>
<pre class="codeBlock" id="codeOut" hidden></pre>
</main>
<!-- 음악퀴즈 선택 팝업 -->
@@ -42,7 +39,10 @@
<div class="modalBody">
<div class="cardRow horizontalScroll" id="pickList">
<% items.forEach(function (item) { %>
<article class="packCard pickable" data-key="<%= item.key %>" data-name="<%= item.definition ? item.definition.name : item.key %>">
<article class="packCard pickable"
data-key="<%= item.key %>"
data-name="<%= item.definition ? item.definition.name : item.key %>"
data-music-count="<%= item.musicCount %>">
<h2><%= item.definition ? item.definition.name : item.key %></h2>
<p class="muted"><%= item.key %>.json</p>
<% if (item.definition) { %>
@@ -60,8 +60,6 @@
<script>
var I18N = <%- JSON.stringify(localeDict.datapack) %>;
// 데이터팩 출력 본문의 "총 N곡" 패턴은 datapackOutput.summary 와 동일.
var SUMMARY_PATTERN = <%- JSON.stringify(localeDict.datapackOutput.summary) %>;
</script>
<script>
(function () {
@@ -80,43 +78,23 @@
card.addEventListener('click', function () {
pickedKey = card.getAttribute('data-key')
var name = card.getAttribute('data-name')
var count = card.getAttribute('data-music-count') || '0'
document.getElementById('pickedLabel').textContent = I18N.pickedLabel.replace('{{name}}', name)
document.getElementById('countLabel').textContent = I18N.totalCount.replace('{{count}}', count)
pickModal.hidden = true
document.getElementById('dpActions').hidden = false
fetch('/op/list/' + encodeURIComponent(pickedKey)).catch(function () {})
document.getElementById('countLabel').textContent = ''
document.getElementById('codeOut').hidden = true
document.getElementById('dp-status').textContent = ''
document.getElementById('dp-status').classList.remove('error')
})
})
document.getElementById('exportBtn').addEventListener('click', function () {
if (!pickedKey) return
var s = document.getElementById('dp-status')
s.textContent = I18N.exporting; s.classList.remove('error')
fetch('/op/datapack/' + encodeURIComponent(pickedKey) + '/generate')
.then(function (r) { return r.text().then(function (t) { return { ok: r.ok, text: t } }) })
.then(function (res) {
if (!res.ok) {
s.textContent = I18N.failed.replace('{{message}}', res.text); s.classList.add('error')
return
}
var out = document.getElementById('codeOut')
out.textContent = res.text
out.hidden = false
// 첫줄/둘째줄에서 곡 개수를 추출해 카운트 라벨에 표시.
var m = res.text.match(/총\s+(\d+)곡/)
if (m) document.getElementById('countLabel').textContent = I18N.totalCount.replace('{{count}}', m[1])
s.textContent = I18N.exported
})
.catch(function (err) { s.textContent = I18N.failed.replace('{{message}}', err.message); s.classList.add('error') })
})
document.getElementById('copyBtn').addEventListener('click', function () {
var out = document.getElementById('codeOut')
if (out.hidden) return
navigator.clipboard.writeText(out.textContent).then(function () {
var s = document.getElementById('dp-status')
s.textContent = I18N.copied
s.classList.remove('error')
})
// zip 다운로드를 트리거하기 위해 location 으로 이동시킨다.
window.location.href = '/op/datapack/' + encodeURIComponent(pickedKey) + '/generate'
// 다운로드는 비동기 / 별도 응답이므로 약간의 지연 후 "완료" 표시.
setTimeout(function () { s.textContent = I18N.exported }, 500)
})
})()
</script>