Add launcher catalog workflow and smoke tests
This commit is contained in:
307
app/assets/js/scripts/install.js
Normal file
307
app/assets/js/scripts/install.js
Normal file
@@ -0,0 +1,307 @@
|
||||
const CatalogManager = require('./assets/js/catalogmanager')
|
||||
const ConfigManager = require('./assets/js/configmanager')
|
||||
const ProfileAssetManager = require('./assets/js/profileassetmanager')
|
||||
|
||||
const installCatalogList = document.getElementById('installCatalogList')
|
||||
const installDetailTitle = document.getElementById('installDetailTitle')
|
||||
const installDetailSummary = document.getElementById('installDetailSummary')
|
||||
const installDetailMeta = document.getElementById('installDetailMeta')
|
||||
const installDetailInfo = document.getElementById('installDetailInfo')
|
||||
const installDetailBody = document.getElementById('installDetailBody')
|
||||
const installDetailAddButton = document.getElementById('installDetailAddButton')
|
||||
|
||||
let selectedProfileId = null
|
||||
let latestCatalog = null
|
||||
|
||||
function describeProfileKind(kind){
|
||||
switch(kind){
|
||||
case 'map':
|
||||
return '오리지널 맵'
|
||||
case 'server-pack':
|
||||
return '플러그인 맵 + 서버팩'
|
||||
case 'modpack':
|
||||
default:
|
||||
return '모드팩'
|
||||
}
|
||||
}
|
||||
|
||||
function createInstallBadge(text){
|
||||
const badge = document.createElement('span')
|
||||
badge.className = 'launcherBadge'
|
||||
badge.textContent = text
|
||||
return badge
|
||||
}
|
||||
|
||||
function createInfoLine(label, value){
|
||||
const line = document.createElement('div')
|
||||
line.className = 'launcherInfoLine'
|
||||
|
||||
const labelElement = document.createElement('span')
|
||||
labelElement.className = 'launcherInfoLabel'
|
||||
labelElement.textContent = label
|
||||
|
||||
const valueElement = document.createElement('span')
|
||||
valueElement.className = 'launcherInfoValue'
|
||||
valueElement.textContent = value
|
||||
|
||||
line.appendChild(labelElement)
|
||||
line.appendChild(valueElement)
|
||||
return line
|
||||
}
|
||||
|
||||
function showInstallMessage(title, message){
|
||||
if(typeof setOverlayContent === 'function'){
|
||||
setOverlayContent(title, message, '확인')
|
||||
setOverlayHandler(() => toggleOverlay(false))
|
||||
toggleOverlay(true)
|
||||
}
|
||||
}
|
||||
|
||||
function buildDetailText(profile){
|
||||
if(typeof profile.details === 'string' && profile.details.trim().length > 0){
|
||||
return profile.details.trim()
|
||||
}
|
||||
|
||||
switch(profile.kind){
|
||||
case 'map':
|
||||
return '이 프로필은 싱글플레이 월드를 바로 실행하기 위한 항목입니다. 필요한 클라이언트 배포 파일과 월드 자료는 관리자가 미리 등록해둡니다.'
|
||||
case 'server-pack':
|
||||
return '이 프로필은 서버 실행/접속 흐름을 함께 다루는 항목입니다. 클라이언트 파일과 서버 번들은 관리자가 미리 등록하며, 사용자는 라이브러리에서 실행과 접속만 진행합니다.'
|
||||
case 'modpack':
|
||||
default:
|
||||
return '이 프로필은 일반 모드팩 클라이언트입니다. 필요한 배포 파일은 관리자가 미리 등록하며, 사용자는 라이브러리에 추가한 뒤 실행만 하면 됩니다.'
|
||||
}
|
||||
}
|
||||
|
||||
function renderDetailPanel(profile){
|
||||
const installedIds = new Set(
|
||||
ConfigManager.getInstalledLibraryProfiles().map((installedProfile) => installedProfile.id)
|
||||
)
|
||||
const installed = installedIds.has(profile.id)
|
||||
|
||||
installDetailTitle.textContent = profile.name
|
||||
installDetailSummary.textContent = profile.description || '설명이 없습니다.'
|
||||
installDetailMeta.innerHTML = ''
|
||||
installDetailMeta.appendChild(createInstallBadge(describeProfileKind(profile.kind)))
|
||||
if(installed){
|
||||
installDetailMeta.appendChild(createInstallBadge('라이브러리 보유'))
|
||||
}
|
||||
if(!profile.launchReady){
|
||||
installDetailMeta.appendChild(createInstallBadge('실행 준비 필요'))
|
||||
}
|
||||
if(profile.kind === 'server-pack' && profile.hostReady){
|
||||
installDetailMeta.appendChild(createInstallBadge('로컬 호스팅 가능'))
|
||||
}
|
||||
|
||||
installDetailInfo.innerHTML = ''
|
||||
installDetailInfo.appendChild(createInfoLine('프로필 ID', profile.id))
|
||||
installDetailInfo.appendChild(createInfoLine('종류', describeProfileKind(profile.kind)))
|
||||
installDetailInfo.appendChild(createInfoLine('실행 준비', profile.launchReady ? '완료' : '추가 설정 필요'))
|
||||
|
||||
if(profile.defaultServerAddress){
|
||||
installDetailInfo.appendChild(createInfoLine('기본 주소', profile.defaultServerAddress))
|
||||
}
|
||||
if(profile.kind === 'map' && profile.worldDirectoryName){
|
||||
installDetailInfo.appendChild(createInfoLine('월드 폴더', profile.worldDirectoryName))
|
||||
}
|
||||
if(profile.kind === 'server-pack'){
|
||||
installDetailInfo.appendChild(createInfoLine('로컬 호스팅', profile.hostReady ? '가능' : '관리자 설정 필요'))
|
||||
}
|
||||
if(profile.launchIssues.length > 0){
|
||||
installDetailInfo.appendChild(createInfoLine('확인 필요', profile.launchIssues.join(' / ')))
|
||||
} else if(profile.hostIssues.length > 0){
|
||||
installDetailInfo.appendChild(createInfoLine('호스팅 참고', profile.hostIssues.join(' / ')))
|
||||
}
|
||||
|
||||
installDetailBody.textContent = buildDetailText(profile)
|
||||
installDetailAddButton.disabled = installed || !profile.launchReady
|
||||
installDetailAddButton.textContent = installed ? '이미 라이브러리에 있음' : '라이브러리에 추가'
|
||||
installDetailAddButton.onclick = async () => {
|
||||
try {
|
||||
const installedProfile = await CatalogManager.installProfile(profile.id)
|
||||
await ProfileAssetManager.prefetchProfileAssets(installedProfile)
|
||||
if(installedProfile.kind === 'server-pack' && installedProfile.hostReady){
|
||||
await ProfileAssetManager.ensureServerBundleInstalled(installedProfile)
|
||||
}
|
||||
renderDetailPanel(profile)
|
||||
await renderInstallView()
|
||||
if(typeof refreshLibraryView === 'function'){
|
||||
await refreshLibraryView()
|
||||
}
|
||||
showInstallMessage('추가 완료', `${profile.name} 프로필을 라이브러리에 추가했습니다.`)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
const message = error instanceof Error ? error.message : '프로필 설치 중 오류가 발생했습니다.'
|
||||
showInstallMessage('설치 실패', message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderEmptyDetailPanel(){
|
||||
installDetailTitle.textContent = '프로필을 선택하세요'
|
||||
installDetailSummary.textContent = '왼쪽 목록에서 모드팩, 맵, 서버팩을 고르면 자세한 설명과 설치 조건을 볼 수 있습니다.'
|
||||
installDetailMeta.innerHTML = ''
|
||||
installDetailInfo.innerHTML = ''
|
||||
installDetailBody.textContent = '관리자가 등록한 프로필 상세 설명이 여기에 표시됩니다.'
|
||||
installDetailAddButton.disabled = true
|
||||
installDetailAddButton.textContent = '라이브러리에 추가'
|
||||
installDetailAddButton.onclick = null
|
||||
}
|
||||
|
||||
function selectProfile(profileId){
|
||||
selectedProfileId = profileId
|
||||
if(latestCatalog == null){
|
||||
renderEmptyDetailPanel()
|
||||
return
|
||||
}
|
||||
|
||||
const profile = latestCatalog.profiles.find((entry) => entry.id === profileId)
|
||||
if(profile == null){
|
||||
renderEmptyDetailPanel()
|
||||
return
|
||||
}
|
||||
|
||||
renderDetailPanel(profile)
|
||||
}
|
||||
|
||||
async function renderInstallView(){
|
||||
installCatalogList.innerHTML = ''
|
||||
|
||||
try {
|
||||
const catalog = await CatalogManager.loadCatalog()
|
||||
latestCatalog = catalog
|
||||
const installedIds = new Set(
|
||||
ConfigManager.getInstalledLibraryProfiles().map((profile) => profile.id)
|
||||
)
|
||||
|
||||
if(catalog.sourceError != null){
|
||||
const warningCard = document.createElement('article')
|
||||
warningCard.className = 'launcherCard'
|
||||
warningCard.innerHTML = '<h3 class="launcherCardTitle">카탈로그 로드 경고</h3><p class="launcherCardDescription">관리자가 등록한 카탈로그를 읽지 못했습니다. 현재 보이는 목록이 없다면 배포 주소 또는 로컬 카탈로그 파일을 관리자 측에서 확인해야 합니다.</p>'
|
||||
installCatalogList.appendChild(warningCard)
|
||||
}
|
||||
|
||||
for(const profile of catalog.profiles){
|
||||
const card = document.createElement('article')
|
||||
card.className = 'launcherCard'
|
||||
if(profile.id === selectedProfileId){
|
||||
card.setAttribute('selected', 'true')
|
||||
}
|
||||
|
||||
const header = document.createElement('div')
|
||||
header.className = 'launcherCardHeader'
|
||||
|
||||
const titleGroup = document.createElement('div')
|
||||
titleGroup.className = 'launcherCardTitleGroup'
|
||||
|
||||
const title = document.createElement('h3')
|
||||
title.className = 'launcherCardTitle'
|
||||
title.textContent = profile.name
|
||||
|
||||
const meta = document.createElement('div')
|
||||
meta.className = 'launcherCardMeta'
|
||||
meta.appendChild(createInstallBadge(describeProfileKind(profile.kind)))
|
||||
if(installedIds.has(profile.id)){
|
||||
meta.appendChild(createInstallBadge('설치됨'))
|
||||
}
|
||||
if(profile.kind === 'server-pack' && profile.hostReady){
|
||||
meta.appendChild(createInstallBadge('호스팅 가능'))
|
||||
}
|
||||
|
||||
titleGroup.appendChild(title)
|
||||
titleGroup.appendChild(meta)
|
||||
header.appendChild(titleGroup)
|
||||
|
||||
const description = document.createElement('p')
|
||||
description.className = 'launcherCardDescription'
|
||||
description.textContent = profile.description || '설명이 없습니다.'
|
||||
|
||||
const actions = document.createElement('div')
|
||||
actions.className = 'launcherCardActions'
|
||||
|
||||
const detailButton = document.createElement('button')
|
||||
detailButton.className = 'launcherSecondaryButton'
|
||||
detailButton.textContent = '자세히 보기'
|
||||
detailButton.addEventListener('click', () => {
|
||||
selectProfile(profile.id)
|
||||
renderInstallView()
|
||||
})
|
||||
|
||||
const installButton = document.createElement('button')
|
||||
installButton.className = 'launcherPrimaryButton'
|
||||
installButton.textContent = installedIds.has(profile.id) ? '설치됨' : '라이브러리에 추가'
|
||||
installButton.disabled = installedIds.has(profile.id) || !profile.launchReady
|
||||
installButton.addEventListener('click', async () => {
|
||||
try {
|
||||
const installedProfile = await CatalogManager.installProfile(profile.id)
|
||||
await ProfileAssetManager.prefetchProfileAssets(installedProfile)
|
||||
if(installedProfile.kind === 'server-pack' && installedProfile.hostReady){
|
||||
await ProfileAssetManager.ensureServerBundleInstalled(installedProfile)
|
||||
}
|
||||
selectProfile(profile.id)
|
||||
await renderInstallView()
|
||||
if(typeof refreshLibraryView === 'function'){
|
||||
await refreshLibraryView()
|
||||
}
|
||||
showInstallMessage('추가 완료', `${profile.name} 프로필을 라이브러리에 추가했습니다.`)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
const message = error instanceof Error ? error.message : '프로필 설치 중 오류가 발생했습니다.'
|
||||
showInstallMessage('설치 실패', message)
|
||||
}
|
||||
})
|
||||
|
||||
actions.appendChild(detailButton)
|
||||
actions.appendChild(installButton)
|
||||
|
||||
card.appendChild(header)
|
||||
card.appendChild(description)
|
||||
card.appendChild(actions)
|
||||
installCatalogList.appendChild(card)
|
||||
}
|
||||
|
||||
if(catalog.profiles.length === 0){
|
||||
renderEmptyDetailPanel()
|
||||
return
|
||||
}
|
||||
|
||||
const selectedProfileStillExists = catalog.profiles.some((profile) => profile.id === selectedProfileId)
|
||||
if(!selectedProfileStillExists){
|
||||
selectedProfileId = catalog.profiles[0].id
|
||||
}
|
||||
|
||||
selectProfile(selectedProfileId)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
latestCatalog = null
|
||||
renderEmptyDetailPanel()
|
||||
|
||||
const errorCard = document.createElement('article')
|
||||
errorCard.className = 'launcherCard'
|
||||
errorCard.innerHTML = '<h3 class="launcherCardTitle">카탈로그 로드 실패</h3><p class="launcherCardDescription">관리자가 등록한 카탈로그를 읽지 못했습니다.</p>'
|
||||
installCatalogList.appendChild(errorCard)
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('installOpenSettingsButton').addEventListener('click', async () => {
|
||||
await prepareSettings()
|
||||
switchView(getCurrentView(), VIEWS.settings)
|
||||
})
|
||||
|
||||
document.getElementById('installBackToLibraryButton').addEventListener('click', async () => {
|
||||
if(typeof refreshLibraryView === 'function'){
|
||||
await refreshLibraryView()
|
||||
}
|
||||
switchView(getCurrentView(), VIEWS.library)
|
||||
})
|
||||
|
||||
document.getElementById('installDetailOpenLibraryButton').addEventListener('click', async () => {
|
||||
if(typeof refreshLibraryView === 'function'){
|
||||
await refreshLibraryView()
|
||||
}
|
||||
switchView(getCurrentView(), VIEWS.library)
|
||||
})
|
||||
|
||||
window.refreshInstallView = renderInstallView
|
||||
renderEmptyDetailPanel()
|
||||
renderInstallView()
|
||||
Reference in New Issue
Block a user