714 lines
24 KiB
JavaScript
714 lines
24 KiB
JavaScript
const state = {
|
|
catalog: {
|
|
version: 1,
|
|
profiles: []
|
|
},
|
|
meta: null,
|
|
selectedProfileId: null,
|
|
dirty: false
|
|
}
|
|
|
|
const profileList = document.getElementById('profileList')
|
|
const profileCount = document.getElementById('profileCount')
|
|
const localCatalogUrl = document.getElementById('localCatalogUrl')
|
|
const launcherCatalogPath = document.getElementById('launcherCatalogPath')
|
|
const emptyState = document.getElementById('emptyState')
|
|
const profileEditorForm = document.getElementById('profileEditorForm')
|
|
const statusBanner = document.getElementById('statusBanner')
|
|
const editorHint = document.getElementById('editorHint')
|
|
const saveCatalogButton = document.getElementById('saveCatalogButton')
|
|
const duplicateProfileButton = document.getElementById('duplicateProfileButton')
|
|
const deleteProfileButton = document.getElementById('deleteProfileButton')
|
|
const addProfileButton = document.getElementById('addProfileButton')
|
|
const serverSection = document.getElementById('serverSection')
|
|
const editDistributionButton = document.getElementById('editDistributionButton')
|
|
const createDistributionButton = document.getElementById('createDistributionButton')
|
|
const distributionEditorModal = document.getElementById('distributionEditorModal')
|
|
const distributionEditorHint = document.getElementById('distributionEditorHint')
|
|
const distributionEditorStatus = document.getElementById('distributionEditorStatus')
|
|
const distributionEditorTextarea = document.getElementById('distributionEditorTextarea')
|
|
const closeDistributionEditorButton = document.getElementById('closeDistributionEditorButton')
|
|
const loadDistributionTemplateButton = document.getElementById('loadDistributionTemplateButton')
|
|
const saveDistributionFileButton = document.getElementById('saveDistributionFileButton')
|
|
|
|
const fieldElements = {
|
|
id: document.getElementById('field-id'),
|
|
name: document.getElementById('field-name'),
|
|
description: document.getElementById('field-description'),
|
|
details: document.getElementById('field-details'),
|
|
modsEnabled: document.getElementById('field-modsEnabled'),
|
|
pluginsEnabled: document.getElementById('field-pluginsEnabled'),
|
|
serverEnabled: document.getElementById('field-serverEnabled'),
|
|
distributionUrl: document.getElementById('field-distributionUrl'),
|
|
worldArchiveUrl: document.getElementById('field-worldArchiveUrl'),
|
|
worldDirectoryName: document.getElementById('field-worldDirectoryName'),
|
|
serverJarUrl: document.getElementById('field-serverJarUrl'),
|
|
serverPort: document.getElementById('field-serverPort'),
|
|
serverMemoryMb: document.getElementById('field-serverMemoryMb'),
|
|
serverMaxPlayers: document.getElementById('field-serverMaxPlayers'),
|
|
serverWhitelistEnabled: document.getElementById('field-serverWhitelistEnabled')
|
|
}
|
|
|
|
function slugify(value){
|
|
return String(value ?? '')
|
|
.trim()
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9]+/g, '-')
|
|
.replace(/^-+|-+$/g, '')
|
|
}
|
|
|
|
function isRemoteUrl(value){
|
|
return /^https?:\/\//i.test(String(value ?? '').trim())
|
|
}
|
|
|
|
function createProfile(){
|
|
const timestamp = Date.now()
|
|
return {
|
|
id: `profile-${timestamp}`,
|
|
name: '새 프로필',
|
|
description: '',
|
|
details: '',
|
|
modsEnabled: false,
|
|
pluginsEnabled: false,
|
|
serverEnabled: false,
|
|
distributionUrl: '',
|
|
worldArchiveUrl: '',
|
|
worldDirectoryName: '',
|
|
serverJarUrl: '',
|
|
serverPort: 25565,
|
|
serverMemoryMb: 4096,
|
|
serverMaxPlayers: 20,
|
|
serverWhitelistEnabled: false
|
|
}
|
|
}
|
|
|
|
function getSelectedProfile(){
|
|
return state.catalog.profiles.find((profile) => profile.id === state.selectedProfileId) ?? null
|
|
}
|
|
|
|
function markDirty(nextDirty = true){
|
|
state.dirty = nextDirty
|
|
saveCatalogButton.textContent = nextDirty ? '카탈로그 저장 *' : '카탈로그 저장'
|
|
}
|
|
|
|
function showStatus(message, tone = 'info'){
|
|
statusBanner.hidden = false
|
|
statusBanner.dataset.tone = tone
|
|
statusBanner.textContent = message
|
|
}
|
|
|
|
function clearStatus(){
|
|
statusBanner.hidden = true
|
|
statusBanner.textContent = ''
|
|
delete statusBanner.dataset.tone
|
|
}
|
|
|
|
function showDistributionEditorStatus(message, tone = 'info'){
|
|
if(distributionEditorStatus == null){
|
|
return
|
|
}
|
|
distributionEditorStatus.hidden = false
|
|
distributionEditorStatus.dataset.tone = tone
|
|
distributionEditorStatus.textContent = message
|
|
}
|
|
|
|
function clearDistributionEditorStatus(){
|
|
if(distributionEditorStatus == null){
|
|
return
|
|
}
|
|
distributionEditorStatus.hidden = true
|
|
distributionEditorStatus.textContent = ''
|
|
delete distributionEditorStatus.dataset.tone
|
|
}
|
|
|
|
function selectProfile(profileId){
|
|
state.selectedProfileId = profileId
|
|
renderSidebar()
|
|
populateEditor()
|
|
}
|
|
|
|
function describeProfileFeatures(profile){
|
|
const badges = ['맵']
|
|
if(profile.modsEnabled){
|
|
badges.push('모드')
|
|
}
|
|
if(profile.pluginsEnabled){
|
|
badges.push('플러그인')
|
|
}
|
|
if(profile.serverEnabled){
|
|
badges.push('서버')
|
|
}
|
|
return badges
|
|
}
|
|
|
|
function renderSidebar(){
|
|
profileList.innerHTML = ''
|
|
profileCount.textContent = `${state.catalog.profiles.length}개`
|
|
|
|
for(const profile of state.catalog.profiles){
|
|
const button = document.createElement('button')
|
|
button.type = 'button'
|
|
button.className = 'profileListItem'
|
|
if(profile.id === state.selectedProfileId){
|
|
button.setAttribute('selected', 'true')
|
|
}
|
|
|
|
const title = document.createElement('div')
|
|
title.className = 'profileListName'
|
|
title.textContent = profile.name || profile.id
|
|
|
|
const meta = document.createElement('div')
|
|
meta.className = 'profileListMeta'
|
|
meta.innerHTML = describeProfileFeatures(profile)
|
|
.map((label) => `<span class="badge">${label}</span>`)
|
|
.join('')
|
|
|
|
const description = document.createElement('div')
|
|
description.className = 'profileListDescription'
|
|
description.textContent = profile.description || '설명이 없습니다.'
|
|
|
|
button.appendChild(title)
|
|
button.appendChild(meta)
|
|
button.appendChild(description)
|
|
button.addEventListener('click', () => {
|
|
selectProfile(profile.id)
|
|
})
|
|
|
|
profileList.appendChild(button)
|
|
}
|
|
}
|
|
|
|
function syncFeatureDependencies(profile, showMessage = false){
|
|
if(profile.pluginsEnabled && !profile.serverEnabled){
|
|
profile.serverEnabled = true
|
|
if(showMessage){
|
|
showStatus('플러그인 사용을 켜면 서버 사용도 자동으로 같이 켜집니다.', 'info')
|
|
}
|
|
}
|
|
}
|
|
|
|
function syncServerSection(profile){
|
|
serverSection.hidden = !profile?.serverEnabled
|
|
fieldElements.serverEnabled.disabled = profile?.pluginsEnabled === true
|
|
}
|
|
|
|
function updateDistributionEditorHint(profile, pathOverride){
|
|
if(!profile){
|
|
distributionEditorHint.textContent = '프로필에 연결할 distribution.json 내용을 사이트 안에서 직접 관리합니다.'
|
|
return
|
|
}
|
|
|
|
const currentPath = String(pathOverride ?? profile.distributionUrl ?? '').trim()
|
|
|
|
if(currentPath.length === 0){
|
|
distributionEditorHint.textContent = '연결된 distribution 파일이 없습니다. 샘플을 불러오거나 새로 저장해서 이 프로필에 연결하세요.'
|
|
return
|
|
}
|
|
|
|
if(isRemoteUrl(currentPath)){
|
|
distributionEditorHint.textContent = `현재 값은 원격 URL입니다: ${currentPath} | 내용을 불러와 수정할 수 있고, 저장하면 이 프로젝트 안의 로컬 distribution 파일로 복사됩니다.`
|
|
return
|
|
}
|
|
|
|
distributionEditorHint.textContent = `현재 연결된 distribution 파일: ${currentPath}`
|
|
}
|
|
|
|
function populateEditor(){
|
|
const profile = getSelectedProfile()
|
|
const hasSelection = profile != null
|
|
|
|
emptyState.hidden = hasSelection
|
|
profileEditorForm.hidden = !hasSelection
|
|
duplicateProfileButton.disabled = !hasSelection
|
|
deleteProfileButton.disabled = !hasSelection
|
|
|
|
if(!profile){
|
|
editorHint.textContent = '왼쪽에서 프로필을 선택하거나 새 프로필을 추가하세요.'
|
|
syncServerSection(null)
|
|
updateDistributionEditorHint(null)
|
|
return
|
|
}
|
|
|
|
syncFeatureDependencies(profile)
|
|
|
|
editorHint.textContent = '맵은 기본이고, 모드/플러그인/서버를 체크해서 조합하는 프로필입니다. 저장하면 런처 카탈로그도 같이 갱신됩니다.'
|
|
|
|
fieldElements.id.value = profile.id ?? ''
|
|
fieldElements.name.value = profile.name ?? ''
|
|
fieldElements.description.value = profile.description ?? ''
|
|
fieldElements.details.value = profile.details ?? ''
|
|
fieldElements.modsEnabled.checked = profile.modsEnabled === true
|
|
fieldElements.pluginsEnabled.checked = profile.pluginsEnabled === true
|
|
fieldElements.serverEnabled.checked = profile.serverEnabled === true
|
|
fieldElements.distributionUrl.value = profile.distributionUrl ?? ''
|
|
fieldElements.worldArchiveUrl.value = profile.worldArchiveUrl ?? ''
|
|
fieldElements.worldDirectoryName.value = profile.worldDirectoryName ?? ''
|
|
fieldElements.serverJarUrl.value = profile.serverJarUrl ?? ''
|
|
fieldElements.serverPort.value = profile.serverPort ?? 25565
|
|
fieldElements.serverMemoryMb.value = profile.serverMemoryMb ?? 4096
|
|
fieldElements.serverMaxPlayers.value = profile.serverMaxPlayers ?? 20
|
|
fieldElements.serverWhitelistEnabled.checked = profile.serverWhitelistEnabled === true
|
|
|
|
syncServerSection(profile)
|
|
updateDistributionEditorHint(profile)
|
|
}
|
|
|
|
function updateSelectedProfile(patch){
|
|
const profile = getSelectedProfile()
|
|
if(!profile){
|
|
return
|
|
}
|
|
|
|
Object.assign(profile, patch)
|
|
syncFeatureDependencies(profile)
|
|
markDirty(true)
|
|
renderSidebar()
|
|
syncServerSection(profile)
|
|
|
|
if(Object.prototype.hasOwnProperty.call(patch, 'distributionUrl')){
|
|
updateDistributionEditorHint(profile, patch.distributionUrl)
|
|
}
|
|
}
|
|
|
|
function bindTextField(fieldName){
|
|
fieldElements[fieldName].addEventListener('input', (event) => {
|
|
if(fieldName === 'id'){
|
|
const profile = getSelectedProfile()
|
|
if(!profile){
|
|
return
|
|
}
|
|
|
|
const previousId = profile.id
|
|
profile.id = event.target.value
|
|
state.selectedProfileId = profile.id
|
|
markDirty(true)
|
|
renderSidebar()
|
|
|
|
if(previousId !== profile.id){
|
|
showStatus('프로필 ID를 바꿨습니다. 저장하면 이 ID로 반영됩니다.', 'info')
|
|
}
|
|
return
|
|
}
|
|
|
|
updateSelectedProfile({ [fieldName]: event.target.value })
|
|
})
|
|
}
|
|
|
|
function bindNumberField(fieldName, fallback){
|
|
fieldElements[fieldName].addEventListener('input', (event) => {
|
|
const value = Number.parseInt(event.target.value || String(fallback), 10)
|
|
updateSelectedProfile({
|
|
[fieldName]: Number.isFinite(value) ? value : fallback
|
|
})
|
|
})
|
|
}
|
|
|
|
function bindCheckboxField(fieldName){
|
|
fieldElements[fieldName].addEventListener('change', (event) => {
|
|
updateSelectedProfile({
|
|
[fieldName]: event.target.checked
|
|
})
|
|
|
|
if(fieldName === 'pluginsEnabled' || fieldName === 'serverEnabled'){
|
|
populateEditor()
|
|
}
|
|
})
|
|
}
|
|
|
|
function bindProfileForm(){
|
|
profileEditorForm.addEventListener('submit', (event) => {
|
|
event.preventDefault()
|
|
})
|
|
|
|
bindTextField('id')
|
|
bindTextField('name')
|
|
bindTextField('description')
|
|
bindTextField('details')
|
|
bindTextField('distributionUrl')
|
|
bindTextField('worldArchiveUrl')
|
|
bindTextField('worldDirectoryName')
|
|
bindTextField('serverJarUrl')
|
|
bindNumberField('serverPort', 25565)
|
|
bindNumberField('serverMemoryMb', 4096)
|
|
bindNumberField('serverMaxPlayers', 20)
|
|
bindCheckboxField('modsEnabled')
|
|
bindCheckboxField('pluginsEnabled')
|
|
bindCheckboxField('serverEnabled')
|
|
bindCheckboxField('serverWhitelistEnabled')
|
|
|
|
for(const button of document.querySelectorAll('.uploadButton')){
|
|
button.addEventListener('click', async () => {
|
|
const targetField = button.dataset.uploadTarget
|
|
const accept = button.dataset.uploadAccept
|
|
await uploadIntoField(targetField, accept)
|
|
})
|
|
}
|
|
}
|
|
|
|
async function uploadIntoField(targetField, accept){
|
|
const profile = getSelectedProfile()
|
|
if(!profile){
|
|
return
|
|
}
|
|
|
|
const picker = document.createElement('input')
|
|
picker.type = 'file'
|
|
picker.accept = accept || ''
|
|
picker.addEventListener('change', async () => {
|
|
const file = picker.files?.[0]
|
|
if(!file){
|
|
return
|
|
}
|
|
|
|
const formData = new FormData()
|
|
formData.append('file', file)
|
|
|
|
try {
|
|
showStatus(`${file.name} 업로드 중...`, 'info')
|
|
|
|
const response = await fetch('/api/upload', {
|
|
method: 'POST',
|
|
body: formData
|
|
})
|
|
const result = await response.json()
|
|
if(!response.ok || result.ok !== true){
|
|
throw new Error(result.message || '업로드에 실패했습니다.')
|
|
}
|
|
|
|
profile[targetField] = result.file.path
|
|
fieldElements[targetField].value = result.file.path
|
|
markDirty(true)
|
|
renderSidebar()
|
|
|
|
if(targetField === 'distributionUrl'){
|
|
updateDistributionEditorHint(profile, result.file.path)
|
|
}
|
|
|
|
showStatus(`업로드 완료: ${result.file.path}`, 'success')
|
|
} catch (error) {
|
|
console.error(error)
|
|
showStatus(error instanceof Error ? error.message : '업로드에 실패했습니다.', 'error')
|
|
}
|
|
})
|
|
|
|
picker.click()
|
|
}
|
|
|
|
function openDistributionEditorModal(){
|
|
distributionEditorModal.hidden = false
|
|
document.body.style.overflow = 'hidden'
|
|
clearDistributionEditorStatus()
|
|
}
|
|
|
|
function closeDistributionEditorModal(){
|
|
distributionEditorModal.hidden = true
|
|
document.body.style.overflow = ''
|
|
clearDistributionEditorStatus()
|
|
}
|
|
|
|
async function loadDistributionTemplate(){
|
|
const response = await fetch('/api/distribution/template')
|
|
const result = await response.json()
|
|
if(!response.ok || result.ok !== true){
|
|
throw new Error(result.message || 'distribution 샘플을 불러오지 못했습니다.')
|
|
}
|
|
|
|
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){
|
|
const profile = getSelectedProfile()
|
|
if(!profile){
|
|
showStatus('먼저 프로필을 선택하세요.', 'error')
|
|
return
|
|
}
|
|
|
|
openDistributionEditorModal()
|
|
distributionEditorTextarea.value = ''
|
|
updateDistributionEditorHint(profile)
|
|
showDistributionEditorStatus('distribution 내용을 준비하는 중...', 'info')
|
|
|
|
try {
|
|
const currentPath = String(profile.distributionUrl ?? '').trim()
|
|
|
|
if(mode === 'create' || currentPath.length === 0){
|
|
await loadDistributionTemplate()
|
|
updateDistributionEditorHint(profile, '')
|
|
showStatus('distribution 템플릿을 불러왔습니다.', 'success')
|
|
showDistributionEditorStatus('샘플을 불러왔습니다. 바로 수정한 뒤 저장하면 됩니다.', 'success')
|
|
return
|
|
}
|
|
|
|
if(isRemoteUrl(currentPath)){
|
|
await loadDistributionContent(currentPath)
|
|
updateDistributionEditorHint(profile, currentPath)
|
|
showStatus('원격 distribution 내용을 불러왔습니다.', 'success')
|
|
showDistributionEditorStatus('원격 distribution 내용을 불러왔습니다. 저장하면 로컬 파일로 복사됩니다.', 'success')
|
|
return
|
|
}
|
|
|
|
showStatus('distribution 파일을 불러오는 중...', 'info')
|
|
await loadDistributionContent(currentPath)
|
|
updateDistributionEditorHint(profile, currentPath)
|
|
showStatus('distribution 파일을 불러왔습니다.', 'success')
|
|
showDistributionEditorStatus('현재 연결된 distribution 파일을 불러왔습니다.', 'success')
|
|
} catch (error) {
|
|
console.error(error)
|
|
showStatus(error instanceof Error ? error.message : 'distribution 파일을 불러오지 못했습니다.', 'error')
|
|
showDistributionEditorStatus(error instanceof Error ? error.message : 'distribution 파일을 불러오지 못했습니다.', 'error')
|
|
}
|
|
}
|
|
|
|
async function saveDistributionFile(){
|
|
const profile = getSelectedProfile()
|
|
if(!profile){
|
|
showStatus('먼저 프로필을 선택하세요.', 'error')
|
|
return
|
|
}
|
|
|
|
try {
|
|
saveDistributionFileButton.disabled = true
|
|
showStatus('distribution 파일 저장 중...', 'info')
|
|
showDistributionEditorStatus('distribution 파일 저장 중...', 'info')
|
|
|
|
const response = await fetch('/api/distribution/save', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
profileId: profile.id,
|
|
content: distributionEditorTextarea.value
|
|
})
|
|
})
|
|
const result = await response.json()
|
|
if(!response.ok || result.ok !== true){
|
|
throw new Error(result.message || 'distribution 파일 저장에 실패했습니다.')
|
|
}
|
|
|
|
profile.distributionUrl = result.file.path
|
|
fieldElements.distributionUrl.value = result.file.path
|
|
markDirty(true)
|
|
renderSidebar()
|
|
populateEditor()
|
|
closeDistributionEditorModal()
|
|
showStatus('distribution 파일을 저장했습니다. 카탈로그 저장을 누르면 런처에 반영됩니다.', 'success')
|
|
} catch (error) {
|
|
console.error(error)
|
|
showStatus(error instanceof Error ? error.message : 'distribution 파일 저장에 실패했습니다.', 'error')
|
|
showDistributionEditorStatus(error instanceof Error ? error.message : 'distribution 파일 저장에 실패했습니다.', 'error')
|
|
} finally {
|
|
saveDistributionFileButton.disabled = false
|
|
}
|
|
}
|
|
|
|
function bindDistributionEditor(){
|
|
editDistributionButton.addEventListener('click', async () => {
|
|
await openDistributionEditor('edit')
|
|
})
|
|
|
|
createDistributionButton.addEventListener('click', async () => {
|
|
await openDistributionEditor('create')
|
|
})
|
|
|
|
closeDistributionEditorButton.addEventListener('click', () => {
|
|
closeDistributionEditorModal()
|
|
})
|
|
|
|
loadDistributionTemplateButton.addEventListener('click', async () => {
|
|
try {
|
|
await loadDistributionTemplate()
|
|
updateDistributionEditorHint(getSelectedProfile(), '')
|
|
showStatus('distribution 템플릿을 다시 불러왔습니다.', 'success')
|
|
showDistributionEditorStatus('샘플을 다시 불러왔습니다.', 'success')
|
|
} catch (error) {
|
|
console.error(error)
|
|
showStatus(error instanceof Error ? error.message : 'distribution 템플릿을 불러오지 못했습니다.', 'error')
|
|
showDistributionEditorStatus(error instanceof Error ? error.message : 'distribution 템플릿을 불러오지 못했습니다.', 'error')
|
|
}
|
|
})
|
|
|
|
saveDistributionFileButton.addEventListener('click', async () => {
|
|
await saveDistributionFile()
|
|
})
|
|
|
|
distributionEditorModal.addEventListener('click', (event) => {
|
|
if(event.target === distributionEditorModal){
|
|
closeDistributionEditorModal()
|
|
}
|
|
})
|
|
|
|
document.addEventListener('keydown', (event) => {
|
|
if(event.key === 'Escape' && distributionEditorModal.hidden === false){
|
|
closeDistributionEditorModal()
|
|
}
|
|
})
|
|
}
|
|
|
|
async function loadMeta(){
|
|
const response = await fetch('/api/meta')
|
|
const meta = await response.json()
|
|
state.meta = meta
|
|
launcherCatalogPath.textContent = meta.launcherCatalogPath
|
|
localCatalogUrl.textContent = meta.localCatalogUrl
|
|
localCatalogUrl.href = meta.localCatalogUrl
|
|
}
|
|
|
|
function normalizeLoadedProfile(profile){
|
|
return {
|
|
...createProfile(),
|
|
...profile,
|
|
modsEnabled: profile.modsEnabled === true,
|
|
pluginsEnabled: profile.pluginsEnabled === true,
|
|
serverEnabled: profile.serverEnabled === true || profile.pluginsEnabled === true,
|
|
serverWhitelistEnabled: profile.serverWhitelistEnabled === true
|
|
}
|
|
}
|
|
|
|
async function loadCatalog(){
|
|
const response = await fetch('/api/catalog')
|
|
const catalog = await response.json()
|
|
state.catalog = {
|
|
version: 1,
|
|
profiles: Array.isArray(catalog.profiles) ? catalog.profiles.map(normalizeLoadedProfile) : []
|
|
}
|
|
|
|
if(state.catalog.profiles.length > 0){
|
|
state.selectedProfileId = state.catalog.profiles[0].id
|
|
} else {
|
|
state.selectedProfileId = null
|
|
}
|
|
|
|
markDirty(false)
|
|
renderSidebar()
|
|
populateEditor()
|
|
}
|
|
|
|
function addProfile(){
|
|
const profile = createProfile()
|
|
state.catalog.profiles.push(profile)
|
|
markDirty(true)
|
|
selectProfile(profile.id)
|
|
showStatus(`${profile.name} 프로필을 추가했습니다.`, 'success')
|
|
}
|
|
|
|
function duplicateSelectedProfile(){
|
|
const profile = getSelectedProfile()
|
|
if(!profile){
|
|
return
|
|
}
|
|
|
|
const clonedProfile = structuredClone(profile)
|
|
clonedProfile.id = `${slugify(profile.id || profile.name) || 'profile'}-copy-${Date.now()}`
|
|
clonedProfile.name = `${profile.name} 복제본`
|
|
|
|
state.catalog.profiles.push(clonedProfile)
|
|
markDirty(true)
|
|
selectProfile(clonedProfile.id)
|
|
showStatus('복제본을 만들었습니다.', 'success')
|
|
}
|
|
|
|
function deleteSelectedProfile(){
|
|
const profile = getSelectedProfile()
|
|
if(!profile){
|
|
return
|
|
}
|
|
|
|
const confirmed = window.confirm(`'${profile.name}' 프로필을 삭제할까요?`)
|
|
if(!confirmed){
|
|
return
|
|
}
|
|
|
|
state.catalog.profiles = state.catalog.profiles.filter((entry) => entry.id !== profile.id)
|
|
state.selectedProfileId = state.catalog.profiles[0]?.id ?? null
|
|
markDirty(true)
|
|
renderSidebar()
|
|
populateEditor()
|
|
showStatus('프로필을 삭제했습니다.', 'success')
|
|
}
|
|
|
|
async function saveCatalog(){
|
|
try {
|
|
clearStatus()
|
|
saveCatalogButton.disabled = true
|
|
showStatus('카탈로그 저장 중...', 'info')
|
|
|
|
const response = await fetch('/api/catalog', {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(state.catalog)
|
|
})
|
|
const result = await response.json()
|
|
if(!response.ok || result.ok !== true){
|
|
throw new Error(result.message || '카탈로그 저장에 실패했습니다.')
|
|
}
|
|
|
|
state.catalog = {
|
|
version: 1,
|
|
profiles: result.catalog.profiles.map(normalizeLoadedProfile)
|
|
}
|
|
if(!state.catalog.profiles.some((profile) => profile.id === state.selectedProfileId)){
|
|
state.selectedProfileId = state.catalog.profiles[0]?.id ?? null
|
|
}
|
|
|
|
markDirty(false)
|
|
renderSidebar()
|
|
populateEditor()
|
|
showStatus('카탈로그를 저장했고 런처 카탈로그에도 동기화했습니다.', 'success')
|
|
} catch (error) {
|
|
console.error(error)
|
|
showStatus(error instanceof Error ? error.message : '카탈로그 저장에 실패했습니다.', 'error')
|
|
} finally {
|
|
saveCatalogButton.disabled = false
|
|
}
|
|
}
|
|
|
|
function bindTopLevelActions(){
|
|
addProfileButton.addEventListener('click', () => {
|
|
addProfile()
|
|
})
|
|
|
|
saveCatalogButton.addEventListener('click', async () => {
|
|
await saveCatalog()
|
|
})
|
|
|
|
duplicateProfileButton.addEventListener('click', () => {
|
|
duplicateSelectedProfile()
|
|
})
|
|
|
|
deleteProfileButton.addEventListener('click', () => {
|
|
deleteSelectedProfile()
|
|
})
|
|
}
|
|
|
|
async function bootstrap(){
|
|
bindProfileForm()
|
|
bindDistributionEditor()
|
|
bindTopLevelActions()
|
|
|
|
try {
|
|
await Promise.all([
|
|
loadMeta(),
|
|
loadCatalog()
|
|
])
|
|
showStatus('관리자 사이트를 불러왔습니다. 저장하면 런처 카탈로그가 같이 갱신됩니다.', 'info')
|
|
} catch (error) {
|
|
console.error(error)
|
|
showStatus(error instanceof Error ? error.message : '관리자 사이트를 불러오지 못했습니다.', 'error')
|
|
}
|
|
}
|
|
|
|
bootstrap()
|