Improve admin distribution editing flow
This commit is contained in:
@@ -9,7 +9,7 @@
|
|||||||
"modsEnabled": false,
|
"modsEnabled": false,
|
||||||
"pluginsEnabled": false,
|
"pluginsEnabled": false,
|
||||||
"serverEnabled": false,
|
"serverEnabled": false,
|
||||||
"distributionUrl": "https://example.com/launcher/map-base.distribution.json",
|
"distributionUrl": "admin/data/distributions/template-map-base.distribution.json",
|
||||||
"worldArchiveUrl": "https://example.com/worlds/map-base.zip",
|
"worldArchiveUrl": "https://example.com/worlds/map-base.zip",
|
||||||
"worldDirectoryName": "Map Base"
|
"worldDirectoryName": "Map Base"
|
||||||
},
|
},
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
"modsEnabled": true,
|
"modsEnabled": true,
|
||||||
"pluginsEnabled": false,
|
"pluginsEnabled": false,
|
||||||
"serverEnabled": false,
|
"serverEnabled": false,
|
||||||
"distributionUrl": "https://example.com/launcher/map-mods.distribution.json",
|
"distributionUrl": "admin/data/distributions/template-map-mods.distribution.json",
|
||||||
"worldArchiveUrl": "https://example.com/worlds/map-mods.zip",
|
"worldArchiveUrl": "https://example.com/worlds/map-mods.zip",
|
||||||
"worldDirectoryName": "Map Mods"
|
"worldDirectoryName": "Map Mods"
|
||||||
},
|
},
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
"modsEnabled": false,
|
"modsEnabled": false,
|
||||||
"pluginsEnabled": true,
|
"pluginsEnabled": true,
|
||||||
"serverEnabled": true,
|
"serverEnabled": true,
|
||||||
"distributionUrl": "https://example.com/launcher/map-plugin-server.distribution.json",
|
"distributionUrl": "admin/data/distributions/template-map-plugin-server.distribution.json",
|
||||||
"worldArchiveUrl": "https://example.com/worlds/plugin-map.zip",
|
"worldArchiveUrl": "https://example.com/worlds/plugin-map.zip",
|
||||||
"worldDirectoryName": "Plugin Map",
|
"worldDirectoryName": "Plugin Map",
|
||||||
"serverJarUrl": "https://example.com/server/paper.jar",
|
"serverJarUrl": "https://example.com/server/paper.jar",
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"version": "1.0.0",
|
||||||
|
"rss": "",
|
||||||
|
"servers": []
|
||||||
|
}
|
||||||
16
admin/data/distributions/template-map-mods.distribution.json
Normal file
16
admin/data/distributions/template-map-mods.distribution.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"version": "1.0.0",
|
||||||
|
"rss": "",
|
||||||
|
"servers": [
|
||||||
|
{
|
||||||
|
"id": "template-map-mods",
|
||||||
|
"name": "Map + Mods Template",
|
||||||
|
"description": "맵과 모드를 함께 쓰는 프로필용 샘플 distribution입니다.",
|
||||||
|
"version": "1.20.1",
|
||||||
|
"minecraftVersion": "1.20.1",
|
||||||
|
"mainServer": true,
|
||||||
|
"autoconnect": false,
|
||||||
|
"modules": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"version": "1.0.0",
|
||||||
|
"rss": "",
|
||||||
|
"servers": [
|
||||||
|
{
|
||||||
|
"id": "template-map-plugin-server",
|
||||||
|
"name": "Map + Plugin Server Template",
|
||||||
|
"description": "맵, 플러그인, 서버를 함께 쓰는 프로필용 샘플 distribution입니다.",
|
||||||
|
"version": "1.20.1",
|
||||||
|
"minecraftVersion": "1.20.1",
|
||||||
|
"mainServer": true,
|
||||||
|
"autoconnect": false,
|
||||||
|
"modules": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@ const editDistributionButton = document.getElementById('editDistributionButton')
|
|||||||
const createDistributionButton = document.getElementById('createDistributionButton')
|
const createDistributionButton = document.getElementById('createDistributionButton')
|
||||||
const distributionEditorModal = document.getElementById('distributionEditorModal')
|
const distributionEditorModal = document.getElementById('distributionEditorModal')
|
||||||
const distributionEditorHint = document.getElementById('distributionEditorHint')
|
const distributionEditorHint = document.getElementById('distributionEditorHint')
|
||||||
|
const distributionEditorStatus = document.getElementById('distributionEditorStatus')
|
||||||
const distributionEditorTextarea = document.getElementById('distributionEditorTextarea')
|
const distributionEditorTextarea = document.getElementById('distributionEditorTextarea')
|
||||||
const closeDistributionEditorButton = document.getElementById('closeDistributionEditorButton')
|
const closeDistributionEditorButton = document.getElementById('closeDistributionEditorButton')
|
||||||
const loadDistributionTemplateButton = document.getElementById('loadDistributionTemplateButton')
|
const loadDistributionTemplateButton = document.getElementById('loadDistributionTemplateButton')
|
||||||
@@ -102,6 +103,18 @@ function clearStatus(){
|
|||||||
delete statusBanner.dataset.tone
|
delete statusBanner.dataset.tone
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showDistributionEditorStatus(message, tone = 'info'){
|
||||||
|
distributionEditorStatus.hidden = false
|
||||||
|
distributionEditorStatus.dataset.tone = tone
|
||||||
|
distributionEditorStatus.textContent = message
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearDistributionEditorStatus(){
|
||||||
|
distributionEditorStatus.hidden = true
|
||||||
|
distributionEditorStatus.textContent = ''
|
||||||
|
delete distributionEditorStatus.dataset.tone
|
||||||
|
}
|
||||||
|
|
||||||
function selectProfile(profileId){
|
function selectProfile(profileId){
|
||||||
state.selectedProfileId = profileId
|
state.selectedProfileId = profileId
|
||||||
renderSidebar()
|
renderSidebar()
|
||||||
@@ -187,7 +200,7 @@ function updateDistributionEditorHint(profile, pathOverride){
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(isRemoteUrl(currentPath)){
|
if(isRemoteUrl(currentPath)){
|
||||||
distributionEditorHint.textContent = `현재 값은 원격 URL입니다: ${currentPath} | 원격 URL은 여기서 직접 수정할 수 없습니다. 샘플을 불러온 뒤 새 파일로 저장하세요.`
|
distributionEditorHint.textContent = `현재 값은 원격 URL입니다: ${currentPath} | 내용을 불러와 수정할 수 있고, 저장하면 이 프로젝트 안의 로컬 distribution 파일로 복사됩니다.`
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -378,11 +391,13 @@ async function uploadIntoField(targetField, accept){
|
|||||||
function openDistributionEditorModal(){
|
function openDistributionEditorModal(){
|
||||||
distributionEditorModal.hidden = false
|
distributionEditorModal.hidden = false
|
||||||
document.body.style.overflow = 'hidden'
|
document.body.style.overflow = 'hidden'
|
||||||
|
clearDistributionEditorStatus()
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeDistributionEditorModal(){
|
function closeDistributionEditorModal(){
|
||||||
distributionEditorModal.hidden = true
|
distributionEditorModal.hidden = true
|
||||||
document.body.style.overflow = ''
|
document.body.style.overflow = ''
|
||||||
|
clearDistributionEditorStatus()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadDistributionTemplate(){
|
async function loadDistributionTemplate(){
|
||||||
@@ -395,6 +410,16 @@ async function loadDistributionTemplate(){
|
|||||||
distributionEditorTextarea.value = result.content
|
distributionEditorTextarea.value = result.content
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadDistributionContent(requestedPath){
|
||||||
|
const response = await fetch(`/api/distribution/content?path=${encodeURIComponent(requestedPath)}`)
|
||||||
|
const result = await response.json()
|
||||||
|
if(!response.ok || result.ok !== true){
|
||||||
|
throw new Error(result.message || 'distribution 파일을 불러오지 못했습니다.')
|
||||||
|
}
|
||||||
|
|
||||||
|
distributionEditorTextarea.value = result.content
|
||||||
|
}
|
||||||
|
|
||||||
async function openDistributionEditor(mode){
|
async function openDistributionEditor(mode){
|
||||||
const profile = getSelectedProfile()
|
const profile = getSelectedProfile()
|
||||||
if(!profile){
|
if(!profile){
|
||||||
@@ -405,6 +430,7 @@ async function openDistributionEditor(mode){
|
|||||||
openDistributionEditorModal()
|
openDistributionEditorModal()
|
||||||
distributionEditorTextarea.value = ''
|
distributionEditorTextarea.value = ''
|
||||||
updateDistributionEditorHint(profile)
|
updateDistributionEditorHint(profile)
|
||||||
|
showDistributionEditorStatus('distribution 내용을 준비하는 중...', 'info')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const currentPath = String(profile.distributionUrl ?? '').trim()
|
const currentPath = String(profile.distributionUrl ?? '').trim()
|
||||||
@@ -413,29 +439,27 @@ async function openDistributionEditor(mode){
|
|||||||
await loadDistributionTemplate()
|
await loadDistributionTemplate()
|
||||||
updateDistributionEditorHint(profile, '')
|
updateDistributionEditorHint(profile, '')
|
||||||
showStatus('distribution 템플릿을 불러왔습니다.', 'success')
|
showStatus('distribution 템플릿을 불러왔습니다.', 'success')
|
||||||
|
showDistributionEditorStatus('샘플을 불러왔습니다. 바로 수정한 뒤 저장하면 됩니다.', 'success')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if(isRemoteUrl(currentPath)){
|
if(isRemoteUrl(currentPath)){
|
||||||
await loadDistributionTemplate()
|
await loadDistributionContent(currentPath)
|
||||||
updateDistributionEditorHint(profile, currentPath)
|
updateDistributionEditorHint(profile, currentPath)
|
||||||
showStatus('원격 URL은 직접 수정할 수 없어 샘플을 대신 불러왔습니다.', 'info')
|
showStatus('원격 distribution 내용을 불러왔습니다.', 'success')
|
||||||
|
showDistributionEditorStatus('원격 distribution 내용을 불러왔습니다. 저장하면 로컬 파일로 복사됩니다.', 'success')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
showStatus('distribution 파일을 불러오는 중...', 'info')
|
showStatus('distribution 파일을 불러오는 중...', 'info')
|
||||||
const response = await fetch(`/api/distribution/content?path=${encodeURIComponent(currentPath)}`)
|
await loadDistributionContent(currentPath)
|
||||||
const result = await response.json()
|
|
||||||
if(!response.ok || result.ok !== true){
|
|
||||||
throw new Error(result.message || 'distribution 파일을 불러오지 못했습니다.')
|
|
||||||
}
|
|
||||||
|
|
||||||
distributionEditorTextarea.value = result.content
|
|
||||||
updateDistributionEditorHint(profile, currentPath)
|
updateDistributionEditorHint(profile, currentPath)
|
||||||
showStatus('distribution 파일을 불러왔습니다.', 'success')
|
showStatus('distribution 파일을 불러왔습니다.', 'success')
|
||||||
|
showDistributionEditorStatus('현재 연결된 distribution 파일을 불러왔습니다.', 'success')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
showStatus(error instanceof Error ? error.message : 'distribution 파일을 불러오지 못했습니다.', 'error')
|
showStatus(error instanceof Error ? error.message : 'distribution 파일을 불러오지 못했습니다.', 'error')
|
||||||
|
showDistributionEditorStatus(error instanceof Error ? error.message : 'distribution 파일을 불러오지 못했습니다.', 'error')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -449,6 +473,7 @@ async function saveDistributionFile(){
|
|||||||
try {
|
try {
|
||||||
saveDistributionFileButton.disabled = true
|
saveDistributionFileButton.disabled = true
|
||||||
showStatus('distribution 파일 저장 중...', 'info')
|
showStatus('distribution 파일 저장 중...', 'info')
|
||||||
|
showDistributionEditorStatus('distribution 파일 저장 중...', 'info')
|
||||||
|
|
||||||
const response = await fetch('/api/distribution/save', {
|
const response = await fetch('/api/distribution/save', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -475,6 +500,7 @@ async function saveDistributionFile(){
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
showStatus(error instanceof Error ? error.message : 'distribution 파일 저장에 실패했습니다.', 'error')
|
showStatus(error instanceof Error ? error.message : 'distribution 파일 저장에 실패했습니다.', 'error')
|
||||||
|
showDistributionEditorStatus(error instanceof Error ? error.message : 'distribution 파일 저장에 실패했습니다.', 'error')
|
||||||
} finally {
|
} finally {
|
||||||
saveDistributionFileButton.disabled = false
|
saveDistributionFileButton.disabled = false
|
||||||
}
|
}
|
||||||
@@ -498,9 +524,11 @@ function bindDistributionEditor(){
|
|||||||
await loadDistributionTemplate()
|
await loadDistributionTemplate()
|
||||||
updateDistributionEditorHint(getSelectedProfile(), '')
|
updateDistributionEditorHint(getSelectedProfile(), '')
|
||||||
showStatus('distribution 템플릿을 다시 불러왔습니다.', 'success')
|
showStatus('distribution 템플릿을 다시 불러왔습니다.', 'success')
|
||||||
|
showDistributionEditorStatus('샘플을 다시 불러왔습니다.', 'success')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
showStatus(error instanceof Error ? error.message : 'distribution 템플릿을 불러오지 못했습니다.', 'error')
|
showStatus(error instanceof Error ? error.message : 'distribution 템플릿을 불러오지 못했습니다.', 'error')
|
||||||
|
showDistributionEditorStatus(error instanceof Error ? error.message : 'distribution 템플릿을 불러오지 못했습니다.', 'error')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -199,6 +199,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<button type="button" id="closeDistributionEditorButton" class="secondaryAction">닫기</button>
|
<button type="button" id="closeDistributionEditorButton" class="secondaryAction">닫기</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="distributionEditorStatus" class="statusBanner" hidden></div>
|
||||||
<textarea id="distributionEditorTextarea" class="distributionTextarea" spellcheck="false"></textarea>
|
<textarea id="distributionEditorTextarea" class="distributionTextarea" spellcheck="false"></textarea>
|
||||||
<div class="modalActions">
|
<div class="modalActions">
|
||||||
<button type="button" id="loadDistributionTemplateButton" class="secondaryAction">샘플 불러오기</button>
|
<button type="button" id="loadDistributionTemplateButton" class="secondaryAction">샘플 불러오기</button>
|
||||||
|
|||||||
@@ -375,6 +375,11 @@ textarea {
|
|||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
background: #111412;
|
background: #111412;
|
||||||
box-shadow: var(--shadow);
|
box-shadow: var(--shadow);
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#distributionEditorStatus {
|
||||||
|
margin-top: -4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modalHeader {
|
.modalHeader {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const RUNTIME_CATALOG_PATH = path.join(RUNTIME_DATA_DIR, 'catalog.json')
|
|||||||
const LAUNCHER_CATALOG_PATH = path.join(PROJECT_ROOT, 'app', 'assets', 'launcher', 'catalog.json')
|
const LAUNCHER_CATALOG_PATH = path.join(PROJECT_ROOT, 'app', 'assets', 'launcher', 'catalog.json')
|
||||||
const PUBLIC_DIR = path.join(__dirname, 'public')
|
const PUBLIC_DIR = path.join(__dirname, 'public')
|
||||||
const SAMPLE_DISTRIBUTION_PATH = path.join(PROJECT_ROOT, 'docs', 'sample_distribution.json')
|
const SAMPLE_DISTRIBUTION_PATH = path.join(PROJECT_ROOT, 'docs', 'sample_distribution.json')
|
||||||
|
const EXAMPLE_DISTRIBUTION_PREFIX = 'https://example.com/launcher/'
|
||||||
|
|
||||||
function normalizeText(value){
|
function normalizeText(value){
|
||||||
return typeof value === 'string' ? value.trim() : ''
|
return typeof value === 'string' ? value.trim() : ''
|
||||||
@@ -163,6 +164,53 @@ async function ensureRuntimeCatalog(){
|
|||||||
await fs.writeJson(RUNTIME_CATALOG_PATH, { version: 1, profiles: [] }, { spaces: 2 })
|
await fs.writeJson(RUNTIME_CATALOG_PATH, { version: 1, profiles: [] }, { spaces: 2 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await migrateExampleCatalog()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureLocalSampleDistribution(profileId, profileName){
|
||||||
|
const fileName = createDistributionFileName(profileId)
|
||||||
|
const targetPath = path.join(DISTRIBUTIONS_DIR, fileName)
|
||||||
|
|
||||||
|
if(!(await fs.pathExists(targetPath))){
|
||||||
|
const template = JSON.parse(await fs.readFile(SAMPLE_DISTRIBUTION_PATH, 'utf8'))
|
||||||
|
template.servers = [
|
||||||
|
{
|
||||||
|
id: profileId,
|
||||||
|
name: profileName || profileId,
|
||||||
|
description: '관리자 사이트 로컬 샘플 distribution입니다.',
|
||||||
|
version: '1.20.1',
|
||||||
|
minecraftVersion: '1.20.1',
|
||||||
|
mainServer: true,
|
||||||
|
autoconnect: false,
|
||||||
|
modules: []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
await fs.writeFile(targetPath, JSON.stringify(template, null, 2) + '\n', 'utf8')
|
||||||
|
}
|
||||||
|
|
||||||
|
return toProjectRelativePath(targetPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function migrateExampleCatalog(){
|
||||||
|
if(!(await fs.pathExists(RUNTIME_CATALOG_PATH))){
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const runtimeCatalog = sanitizeCatalog(await fs.readJson(RUNTIME_CATALOG_PATH))
|
||||||
|
let changed = false
|
||||||
|
|
||||||
|
for(const profile of runtimeCatalog.profiles){
|
||||||
|
if(normalizeText(profile.distributionUrl).startsWith(EXAMPLE_DISTRIBUTION_PREFIX)){
|
||||||
|
profile.distributionUrl = await ensureLocalSampleDistribution(profile.id, profile.name)
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(changed){
|
||||||
|
await fs.writeJson(RUNTIME_CATALOG_PATH, runtimeCatalog, { spaces: 2 })
|
||||||
|
await fs.writeJson(LAUNCHER_CATALOG_PATH, runtimeCatalog, { spaces: 2 })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function readCatalog(){
|
async function readCatalog(){
|
||||||
@@ -248,9 +296,18 @@ async function start(){
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(/^https?:\/\//i.test(requestedPath)){
|
if(/^https?:\/\//i.test(requestedPath)){
|
||||||
res.status(400).json({
|
const response = await fetch(requestedPath, {
|
||||||
ok: false,
|
signal: AbortSignal.timeout(10000)
|
||||||
message: '원격 URL은 사이트에서 직접 수정할 수 없습니다. 업로드하거나 새로 생성하세요.'
|
})
|
||||||
|
if(!response.ok){
|
||||||
|
throw new Error(`원격 distribution을 불러오지 못했습니다. (${response.status})`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = await response.text()
|
||||||
|
JSON.parse(content)
|
||||||
|
res.json({
|
||||||
|
ok: true,
|
||||||
|
content
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
"modsEnabled": false,
|
"modsEnabled": false,
|
||||||
"pluginsEnabled": false,
|
"pluginsEnabled": false,
|
||||||
"serverEnabled": false,
|
"serverEnabled": false,
|
||||||
"distributionUrl": "https://example.com/launcher/map-base.distribution.json",
|
"distributionUrl": "admin/data/distributions/template-map-base.distribution.json",
|
||||||
"worldArchiveUrl": "https://example.com/worlds/map-base.zip",
|
"worldArchiveUrl": "https://example.com/worlds/map-base.zip",
|
||||||
"worldDirectoryName": "Map Base"
|
"worldDirectoryName": "Map Base"
|
||||||
},
|
},
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
"modsEnabled": true,
|
"modsEnabled": true,
|
||||||
"pluginsEnabled": false,
|
"pluginsEnabled": false,
|
||||||
"serverEnabled": false,
|
"serverEnabled": false,
|
||||||
"distributionUrl": "https://example.com/launcher/map-mods.distribution.json",
|
"distributionUrl": "admin/data/distributions/template-map-mods.distribution.json",
|
||||||
"worldArchiveUrl": "https://example.com/worlds/map-mods.zip",
|
"worldArchiveUrl": "https://example.com/worlds/map-mods.zip",
|
||||||
"worldDirectoryName": "Map Mods"
|
"worldDirectoryName": "Map Mods"
|
||||||
},
|
},
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
"modsEnabled": false,
|
"modsEnabled": false,
|
||||||
"pluginsEnabled": true,
|
"pluginsEnabled": true,
|
||||||
"serverEnabled": true,
|
"serverEnabled": true,
|
||||||
"distributionUrl": "https://example.com/launcher/map-plugin-server.distribution.json",
|
"distributionUrl": "admin/data/distributions/template-map-plugin-server.distribution.json",
|
||||||
"worldArchiveUrl": "https://example.com/worlds/plugin-map.zip",
|
"worldArchiveUrl": "https://example.com/worlds/plugin-map.zip",
|
||||||
"worldDirectoryName": "Plugin Map",
|
"worldDirectoryName": "Plugin Map",
|
||||||
"serverJarUrl": "https://example.com/server/paper.jar",
|
"serverJarUrl": "https://example.com/server/paper.jar",
|
||||||
|
|||||||
Reference in New Issue
Block a user