diff --git a/README.md b/README.md index 54bfa88..790b98e 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ npm run admin ``` - 기본 주소: `http://127.0.0.1:8787` -- `distribution.json` 업로드 / 새로 만들기 / 직접 편집 가능 +- `distribution.json` 업로드 / 새로 만들기 / 입력 폼 편집 가능 - 월드 ZIP 업로드 가능 - 서버용 버킷 JAR 업로드 가능 - 서버 메모리, 최대 인원수, 화이트리스트, 포트 설정 가능 diff --git a/admin/public/app.js b/admin/public/app.js index 34b107c..d35d592 100644 --- a/admin/public/app.js +++ b/admin/public/app.js @@ -5,7 +5,10 @@ const state = { }, meta: null, selectedProfileId: null, - dirty: false + dirty: false, + distributionEditor: { + document: null + } } const profileList = document.getElementById('profileList') @@ -26,7 +29,9 @@ const createDistributionButton = document.getElementById('createDistributionButt const distributionEditorModal = document.getElementById('distributionEditorModal') const distributionEditorHint = document.getElementById('distributionEditorHint') const distributionEditorStatus = document.getElementById('distributionEditorStatus') -const distributionEditorTextarea = document.getElementById('distributionEditorTextarea') +const distributionEditorForm = document.getElementById('distributionEditorForm') +const distributionModuleCount = document.getElementById('distributionModuleCount') +const distributionAdditionalServerCount = document.getElementById('distributionAdditionalServerCount') const closeDistributionEditorButton = document.getElementById('closeDistributionEditorButton') const loadDistributionTemplateButton = document.getElementById('loadDistributionTemplateButton') const saveDistributionFileButton = document.getElementById('saveDistributionFileButton') @@ -49,6 +54,18 @@ const fieldElements = { serverWhitelistEnabled: document.getElementById('field-serverWhitelistEnabled') } +const distributionFieldElements = { + version: document.getElementById('distribution-field-version'), + rss: document.getElementById('distribution-field-rss'), + serverId: document.getElementById('distribution-field-serverId'), + serverName: document.getElementById('distribution-field-serverName'), + serverDescription: document.getElementById('distribution-field-serverDescription'), + serverVersion: document.getElementById('distribution-field-serverVersion'), + minecraftVersion: document.getElementById('distribution-field-minecraftVersion'), + mainServer: document.getElementById('distribution-field-mainServer'), + autoconnect: document.getElementById('distribution-field-autoconnect') +} + function slugify(value){ return String(value ?? '') .trim() @@ -61,6 +78,94 @@ function isRemoteUrl(value){ return /^https?:\/\//i.test(String(value ?? '').trim()) } +function deepClone(value){ + return value == null ? value : JSON.parse(JSON.stringify(value)) +} + +function getDistributionTemplateProfileDefaults(profile){ + const safeId = slugify(profile?.id || profile?.name || 'example-profile') || 'example-profile' + return { + version: '1.0.0', + rss: '', + servers: [ + { + id: safeId, + name: profile?.name || '새 프로필', + description: profile?.description || '관리자 사이트에서 만든 distribution 템플릿입니다.', + version: '1.0.0', + minecraftVersion: '1.20.1', + mainServer: true, + autoconnect: false, + modules: [] + } + ] + } +} + +function normalizeDistributionDocument(rawDocument, profile){ + const baseDocument = deepClone(rawDocument) ?? {} + const defaultDocument = getDistributionTemplateProfileDefaults(profile) + const servers = Array.isArray(baseDocument.servers) && baseDocument.servers.length > 0 + ? baseDocument.servers + : defaultDocument.servers + const primaryServer = { + ...defaultDocument.servers[0], + ...(servers[0] ?? {}) + } + + baseDocument.version = typeof baseDocument.version === 'string' && baseDocument.version.trim().length > 0 + ? baseDocument.version.trim() + : defaultDocument.version + baseDocument.rss = typeof baseDocument.rss === 'string' ? baseDocument.rss.trim() : '' + baseDocument.servers = [primaryServer, ...servers.slice(1)] + + return baseDocument +} + +function populateDistributionEditorForm(documentData, profile){ + const normalized = normalizeDistributionDocument(documentData, profile) + const primaryServer = normalized.servers[0] + + state.distributionEditor.document = normalized + distributionFieldElements.version.value = normalized.version ?? '' + distributionFieldElements.rss.value = normalized.rss ?? '' + distributionFieldElements.serverId.value = primaryServer.id ?? '' + distributionFieldElements.serverName.value = primaryServer.name ?? '' + distributionFieldElements.serverDescription.value = primaryServer.description ?? '' + distributionFieldElements.serverVersion.value = primaryServer.version ?? '' + distributionFieldElements.minecraftVersion.value = primaryServer.minecraftVersion ?? '' + distributionFieldElements.mainServer.checked = primaryServer.mainServer !== false + distributionFieldElements.autoconnect.checked = primaryServer.autoconnect === true + + const moduleCount = Array.isArray(primaryServer.modules) ? primaryServer.modules.length : 0 + const additionalServerCount = Math.max(0, normalized.servers.length - 1) + distributionModuleCount.textContent = `${moduleCount}개` + distributionAdditionalServerCount.textContent = `${additionalServerCount}개` +} + +function buildDistributionDocumentFromForm(profile){ + const baseDocument = normalizeDistributionDocument(state.distributionEditor.document, profile) + const primaryServer = { + ...baseDocument.servers[0] + } + + baseDocument.version = distributionFieldElements.version.value.trim() || '1.0.0' + baseDocument.rss = distributionFieldElements.rss.value.trim() + + primaryServer.id = distributionFieldElements.serverId.value.trim() || slugify(profile?.id || profile?.name || 'example-profile') || 'example-profile' + primaryServer.name = distributionFieldElements.serverName.value.trim() || profile?.name || '새 프로필' + primaryServer.description = distributionFieldElements.serverDescription.value.trim() + primaryServer.version = distributionFieldElements.serverVersion.value.trim() || '1.0.0' + primaryServer.minecraftVersion = distributionFieldElements.minecraftVersion.value.trim() || '1.20.1' + primaryServer.mainServer = distributionFieldElements.mainServer.checked + primaryServer.autoconnect = distributionFieldElements.autoconnect.checked + primaryServer.modules = Array.isArray(primaryServer.modules) ? primaryServer.modules : [] + + baseDocument.servers = [primaryServer, ...baseDocument.servers.slice(1)] + state.distributionEditor.document = baseDocument + return baseDocument +} + function createProfile(){ const timestamp = Date.now() return { @@ -415,7 +520,7 @@ async function loadDistributionTemplate(){ throw new Error(result.message || 'distribution 샘플을 불러오지 못했습니다.') } - distributionEditorTextarea.value = result.content + return JSON.parse(result.content) } async function loadDistributionContent(requestedPath){ @@ -425,7 +530,7 @@ async function loadDistributionContent(requestedPath){ throw new Error(result.message || 'distribution 파일을 불러오지 못했습니다.') } - distributionEditorTextarea.value = result.content + return JSON.parse(result.content) } async function openDistributionEditor(mode){ @@ -436,7 +541,8 @@ async function openDistributionEditor(mode){ } openDistributionEditorModal() - distributionEditorTextarea.value = '' + distributionEditorForm.reset() + state.distributionEditor.document = null updateDistributionEditorHint(profile) showDistributionEditorStatus('distribution 내용을 준비하는 중...', 'info') @@ -444,7 +550,8 @@ async function openDistributionEditor(mode){ const currentPath = String(profile.distributionUrl ?? '').trim() if(mode === 'create' || currentPath.length === 0){ - await loadDistributionTemplate() + const templateDocument = await loadDistributionTemplate() + populateDistributionEditorForm(templateDocument, profile) updateDistributionEditorHint(profile, '') showStatus('distribution 템플릿을 불러왔습니다.', 'success') showDistributionEditorStatus('샘플을 불러왔습니다. 바로 수정한 뒤 저장하면 됩니다.', 'success') @@ -452,7 +559,8 @@ async function openDistributionEditor(mode){ } if(isRemoteUrl(currentPath)){ - await loadDistributionContent(currentPath) + const remoteDocument = await loadDistributionContent(currentPath) + populateDistributionEditorForm(remoteDocument, profile) updateDistributionEditorHint(profile, currentPath) showStatus('원격 distribution 내용을 불러왔습니다.', 'success') showDistributionEditorStatus('원격 distribution 내용을 불러왔습니다. 저장하면 로컬 파일로 복사됩니다.', 'success') @@ -460,7 +568,8 @@ async function openDistributionEditor(mode){ } showStatus('distribution 파일을 불러오는 중...', 'info') - await loadDistributionContent(currentPath) + const localDocument = await loadDistributionContent(currentPath) + populateDistributionEditorForm(localDocument, profile) updateDistributionEditorHint(profile, currentPath) showStatus('distribution 파일을 불러왔습니다.', 'success') showDistributionEditorStatus('현재 연결된 distribution 파일을 불러왔습니다.', 'success') @@ -479,6 +588,7 @@ async function saveDistributionFile(){ } try { + const distributionDocument = buildDistributionDocumentFromForm(profile) saveDistributionFileButton.disabled = true showStatus('distribution 파일 저장 중...', 'info') showDistributionEditorStatus('distribution 파일 저장 중...', 'info') @@ -490,7 +600,7 @@ async function saveDistributionFile(){ }, body: JSON.stringify({ profileId: profile.id, - content: distributionEditorTextarea.value + content: JSON.stringify(distributionDocument, null, 2) }) }) const result = await response.json() @@ -515,6 +625,10 @@ async function saveDistributionFile(){ } function bindDistributionEditor(){ + distributionEditorForm.addEventListener('submit', (event) => { + event.preventDefault() + }) + distributionEditorModal.addEventListener('click', (event) => { if(event.target === distributionEditorModal){ closeDistributionEditorModal() @@ -538,7 +652,8 @@ window.__launcherAdminCloseDistributionEditor = () => { window.__launcherAdminLoadDistributionTemplate = async () => { try { - await loadDistributionTemplate() + const templateDocument = await loadDistributionTemplate() + populateDistributionEditorForm(templateDocument, getSelectedProfile()) updateDistributionEditorHint(getSelectedProfile(), '') showStatus('distribution 템플릿을 다시 불러왔습니다.', 'success') showDistributionEditorStatus('샘플을 다시 불러왔습니다.', 'success') diff --git a/admin/public/index.html b/admin/public/index.html index 843a825..53d2aa2 100644 --- a/admin/public/index.html +++ b/admin/public/index.html @@ -116,12 +116,12 @@
- +
- distribution.json은 여기서 직접 업로드하거나 JSON 편집기로 새로 만들 수 있습니다. + distribution.json은 여기서 직접 업로드하거나, 아래 편집기에서 설명을 보며 입력 폼으로 만들 수 있습니다.
@@ -195,12 +195,88 @@
Distribution Editor

distribution.json 편집

-

프로필에 연결할 distribution.json 내용을 사이트 안에서 직접 관리합니다.

+

프로필에 연결할 distribution.json 내용을 사이트 안에서 입력 폼으로 관리합니다.

- +
+
+

현재 보존되는 고급 정보

+
+
+
+ 모듈 수 +
0개
+
+
+ 추가 서버 수 +
0개
+
+
+ 이 편집기는 첫 번째 서버의 기본 정보만 수정합니다. 기존 모듈 목록과 추가 서버 정보는 저장할 때 그대로 보존됩니다. +
+
+
+
+
+
+

기본 정보

+
+
+ + +
+
+
+
+

서버 기본 정보

+
+
+ + + + + + + +
+
+
diff --git a/admin/public/styles.css b/admin/public/styles.css index f05743a..b9a436b 100644 --- a/admin/public/styles.css +++ b/admin/public/styles.css @@ -376,12 +376,30 @@ textarea { background: #111412; box-shadow: var(--shadow); pointer-events: auto; + overflow-y: auto; } #distributionEditorStatus { margin-top: -4px; } +.distributionSummary { + padding-bottom: 16px; +} + +.distributionSummaryValue { + width: 100%; + padding: 14px 16px; + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 14px; + background: var(--panel-strong); + color: var(--text); +} + +.distributionEditorForm { + gap: 16px; +} + .modalHeader { display: flex; justify-content: space-between; diff --git a/docs/admin-site.md b/docs/admin-site.md index dfbd419..418ab56 100644 --- a/docs/admin-site.md +++ b/docs/admin-site.md @@ -21,7 +21,7 @@ npm run admin - 프로필 추가 / 수정 / 삭제 / 복제 - `맵` 기본 + `모드`, `플러그인`, `서버` 체크 조합 -- `distribution.json` 업로드 / 새로 만들기 / 직접 편집 +- `distribution.json` 업로드 / 새로 만들기 / 입력 폼 편집 - 월드 ZIP 업로드 - 서버용 버킷 JAR 업로드 - 서버 포트 / 메모리 / 최대 인원수 / 화이트리스트 설정 @@ -32,8 +32,10 @@ npm run admin ## distribution 편집 - `distribution 파일` 칸에서 JSON 업로드 가능 -- `JSON 편집` 버튼으로 현재 연결된 로컬 `distribution.json` 수정 가능 +- `폼 편집` 버튼으로 현재 연결된 `distribution.json`을 입력 폼으로 수정 가능 - `새로 만들기` 버튼으로 샘플 템플릿에서 새 `distribution.json` 생성 가능 +- 편집기에서는 버전, RSS, 서버 이름, 마인크래프트 버전 같은 기본 정보를 설명과 함께 입력합니다. +- 기존 모듈 목록과 추가 서버 정보는 저장 시 그대로 보존됩니다. - 저장된 distribution 파일은 아래에 생성됩니다. - `admin/data/distributions/`