(() => { const { clipboard } = require('electron') const CatalogManager = require('./assets/js/catalogmanager') const ConfigManager = require('./assets/js/configmanager') const ProfileAssetManager = require('./assets/js/profileassetmanager') const ServerRuntime = require('./assets/js/serverruntime') const { DistroAPI } = require('./assets/js/distromanager') const libraryList = document.getElementById('libraryList') const libraryEmptyState = document.getElementById('libraryEmptyState') function renderLibraryEmptyState(isEmpty){ libraryEmptyState.style.display = isEmpty ? 'flex' : 'none' } function createBadge(text){ const badge = document.createElement('span') badge.className = 'launcherBadge' badge.textContent = text return badge } function describeProfileKind(kind){ switch(kind){ case 'map': return '맵' case 'server-pack': return '서버팩' case 'modpack': default: return '모드팩' } } function createParagraph(className, text){ const element = document.createElement('p') element.className = className element.textContent = text return element } 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 showLibraryMessage(title, message){ if(typeof setOverlayContent === 'function'){ setOverlayContent(title, message, '확인') setOverlayHandler(() => toggleOverlay(false)) toggleOverlay(true) } } function describeAssetState(profile){ const state = ConfigManager.getLibraryProfileAssetState(profile.id) if(profile.kind === 'map'){ if(state.worldInstalledAt){ return `맵 설치 완료 · ${profile.worldDirectoryName}` } if(profile.worldArchiveUrl){ return '맵 아카이브 준비 필요' } } if(profile.kind === 'server-pack'){ if(state.serverBundleInstalledAt){ return '서버 번들 설치 완료' } if(profile.serverBundleUrl){ return '서버 번들 준비 필요' } } return '추가 자산 없음' } async function prepareProfileAssets(profile){ try { await ProfileAssetManager.prefetchProfileAssets(profile) if(profile.kind === 'server-pack' && profile.hostReady){ await ProfileAssetManager.ensureServerBundleInstalled(profile) } await renderLibraryView() showLibraryMessage('자료 준비 완료', `${profile.name} 자료를 준비했습니다.`) } catch (error) { console.error(error) showLibraryMessage('자료 준비 실패', '프로필 자료를 내려받거나 해제하는 중 오류가 발생했습니다.') } } async function activateProfile(profile, launchNow = false){ if(!profile.configured){ const firstIssue = profile.launchIssues?.[0] ?? '이 프로필은 아직 실행 조건이 충족되지 않았습니다.' showLibraryMessage('프로필 설정 필요', firstIssue) return } CatalogManager.selectProfile(profile.id) CatalogManager.applyConfiguredProfile() try { const distro = await DistroAPI.refreshDistributionOrFallback() if(distro == null){ throw new Error('Distribution refresh returned null.') } const currentServer = distro.getServerById(ConfigManager.getSelectedServer()) if(currentServer == null && typeof distro.getMainServer === 'function'){ const mainServer = distro.getMainServer() if(mainServer != null){ ConfigManager.setSelectedServer(mainServer.rawServer.id) ConfigManager.save() } } const selectedServerId = ConfigManager.getSelectedServer() if(selectedServerId != null){ await ProfileAssetManager.prepareProfileForLaunch(profile, selectedServerId) } onDistroRefresh(distro) if(getCurrentView() === VIEWS.landing){ if(launchNow){ document.getElementById('launch_button').click() } return } switchView(getCurrentView(), VIEWS.landing, 250, 250, () => {}, () => { if(launchNow){ document.getElementById('launch_button').click() } }) } catch (error) { console.error(error) showLibraryMessage('프로필 로드 실패', '선택한 프로필의 distribution.json 또는 부가 자산을 불러오지 못했습니다.') } } function appendAddressOverrideField(profile, fieldGroup){ if(!profile.allowCustomServerAddress){ return } const label = document.createElement('label') label.className = 'launcherFieldLabel' label.textContent = '접속 주소' const input = document.createElement('input') input.className = 'launcherFieldInput' input.type = 'text' input.placeholder = profile.defaultServerAddress || 'example.com:25565' input.value = ConfigManager.getLibraryServerAddressOverride(profile.id) ?? '' input.addEventListener('change', () => { CatalogManager.setServerAddressOverride(profile.id, input.value) }) fieldGroup.appendChild(label) fieldGroup.appendChild(input) } function appendPublishedAddressField(profile, hostState, fieldGroup){ if(!hostState.publishedAddress){ return } const label = document.createElement('label') label.className = 'launcherFieldLabel' label.textContent = '호스트 공개 주소' const row = document.createElement('div') row.className = 'launcherInlineField' const input = document.createElement('input') input.className = 'launcherFieldInput' input.type = 'text' input.readOnly = true input.value = hostState.publishedAddress const copyButton = document.createElement('button') copyButton.className = 'launcherSecondaryButton' copyButton.textContent = '주소 복사' copyButton.addEventListener('click', () => { clipboard.writeText(hostState.publishedAddress) }) row.appendChild(input) row.appendChild(copyButton) fieldGroup.appendChild(label) fieldGroup.appendChild(row) } async function renderLibraryView(){ libraryList.innerHTML = '' try { const installedProfiles = await CatalogManager.getInstalledProfiles() const selectedProfileId = CatalogManager.getSelectedProfileId() renderLibraryEmptyState(installedProfiles.length === 0) for(const profile of installedProfiles){ const hostState = ServerRuntime.getHostedProfileState(profile.id) 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(createBadge(describeProfileKind(profile.kind))) if(profile.isCustom){ meta.appendChild(createBadge('커스텀')) } if(profile.id === selectedProfileId){ meta.appendChild(createBadge('선택됨')) } if(profile.kind === 'map' && profile.worldDirectoryName){ meta.appendChild(createBadge(profile.worldDirectoryName)) } if(profile.kind === 'map' && !profile.launchReady){ meta.appendChild(createBadge('맵 설정 필요')) } if(profile.kind === 'server-pack' && !profile.hostReady){ meta.appendChild(createBadge('호스팅 설정 필요')) } if(hostState.running){ meta.appendChild(createBadge(hostState.tunneling ? '서버+터널' : '서버 실행 중')) } titleGroup.appendChild(title) titleGroup.appendChild(meta) header.appendChild(titleGroup) const description = createParagraph('launcherCardDescription', profile.description || '설명이 없습니다.') const infoBlock = document.createElement('div') infoBlock.className = 'launcherInfoBlock' infoBlock.appendChild(createInfoLine('자료 상태', describeAssetState(profile))) infoBlock.appendChild(createInfoLine('실행 준비', profile.launchReady ? '완료' : '추가 설정 필요')) if(profile.defaultServerAddress){ infoBlock.appendChild(createInfoLine('기본 주소', profile.defaultServerAddress)) } if(profile.kind === 'server-pack'){ infoBlock.appendChild(createInfoLine('로컬 호스팅', profile.hostReady ? '가능' : '서버 번들 필요')) } if(hostState.running){ infoBlock.appendChild(createInfoLine('호스트 상태', hostState.tunneling ? '터널 연결 중' : '로컬 서버 실행 중')) } if(profile.launchIssues?.length > 0){ infoBlock.appendChild(createInfoLine('확인 필요', profile.launchIssues[0])) } else if(profile.hostIssues?.length > 0){ infoBlock.appendChild(createInfoLine('호스팅 확인', profile.hostIssues[0])) } const fieldGroup = document.createElement('div') fieldGroup.className = 'launcherFieldGroup' appendAddressOverrideField(profile, fieldGroup) appendPublishedAddressField(profile, hostState, fieldGroup) const actions = document.createElement('div') actions.className = 'launcherCardActions' const prepareButton = document.createElement('button') prepareButton.className = 'launcherSecondaryButton' prepareButton.textContent = '자료 준비' prepareButton.addEventListener('click', async () => { await prepareProfileAssets(profile) }) const selectButton = document.createElement('button') selectButton.className = 'launcherSecondaryButton' selectButton.textContent = '프로필 선택' selectButton.disabled = !profile.configured selectButton.addEventListener('click', async () => { CatalogManager.selectProfile(profile.id) CatalogManager.applyConfiguredProfile() await renderLibraryView() }) const openButton = document.createElement('button') openButton.className = 'launcherSecondaryButton' openButton.textContent = '실행 화면' openButton.disabled = !profile.configured openButton.addEventListener('click', async () => { await activateProfile(profile, false) }) const launchButton = document.createElement('button') launchButton.className = 'launcherPrimaryButton' launchButton.textContent = profile.kind === 'map' ? '맵 실행' : '바로 실행' launchButton.disabled = !profile.configured launchButton.addEventListener('click', async () => { await activateProfile(profile, true) }) actions.appendChild(prepareButton) actions.appendChild(selectButton) actions.appendChild(openButton) actions.appendChild(launchButton) if(profile.kind === 'server-pack'){ const startHostButton = document.createElement('button') startHostButton.className = 'launcherSecondaryButton' startHostButton.textContent = '서버 실행' startHostButton.disabled = hostState.running || !profile.hostReady startHostButton.addEventListener('click', async () => { try { await ServerRuntime.startHostedProfile(profile) await renderLibraryView() } catch (error) { console.error(error) showLibraryMessage('서버 실행 실패', '서버 번들이 준비되지 않았거나 시작 명령을 찾지 못했습니다.') } }) const stopHostButton = document.createElement('button') stopHostButton.className = 'launcherGhostButton' stopHostButton.textContent = '서버 중지' stopHostButton.disabled = !hostState.running stopHostButton.addEventListener('click', async () => { ServerRuntime.stopHostedProfile(profile.id) await renderLibraryView() }) actions.appendChild(startHostButton) actions.appendChild(stopHostButton) } const removeButton = document.createElement('button') removeButton.className = 'launcherGhostButton' removeButton.textContent = '제거' removeButton.addEventListener('click', async () => { ServerRuntime.stopHostedProfile(profile.id) CatalogManager.removeProfile(profile.id) await renderLibraryView() if(typeof refreshInstallView === 'function'){ await refreshInstallView() } }) actions.appendChild(removeButton) card.appendChild(header) card.appendChild(description) card.appendChild(infoBlock) if(fieldGroup.childNodes.length > 0){ card.appendChild(fieldGroup) } card.appendChild(actions) libraryList.appendChild(card) } } catch (error) { console.error(error) renderLibraryEmptyState(false) const errorCard = document.createElement('article') errorCard.className = 'launcherCard' errorCard.innerHTML = '
선택한 카탈로그를 읽지 못했습니다. 설치 페이지에서 카탈로그 경로를 다시 확인하세요.
' libraryList.appendChild(errorCard) } } document.getElementById('libraryOpenInstallButton').addEventListener('click', async () => { if(typeof refreshInstallView === 'function'){ await refreshInstallView() } switchView(getCurrentView(), VIEWS.install) }) document.getElementById('libraryBackButton').addEventListener('click', () => { switchView(getCurrentView(), VIEWS.landing) }) document.getElementById('libraryOpenSettingsButton').addEventListener('click', async () => { await prepareSettings() switchView(getCurrentView(), VIEWS.settings) }) document.getElementById('libraryOpenLaunchButton').addEventListener('click', async () => { const selectedProfile = CatalogManager.getSelectedProfileSync() if(selectedProfile == null){ switchView(getCurrentView(), VIEWS.install) return } await activateProfile(selectedProfile, false) }) setInterval(() => { if(getCurrentView() === VIEWS.library && ServerRuntime.hasRunningProfiles()){ renderLibraryView() } }, 3000) window.refreshLibraryView = renderLibraryView renderLibraryView() })()