(() => { const CatalogManager = require('./assets/js/catalogmanager') const ConfigManager = require('./assets/js/configmanager') const ProfileAssetManager = require('./assets/js/profileassetmanager') const installCatalogList = document.getElementById('installCatalogList') let expandedProfileId = 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 toggleExpandedProfile(profileId){ expandedProfileId = expandedProfileId === profileId ? null : profileId } async function installProfile(profile){ const installedProfile = await CatalogManager.installProfile(profile.id) await ProfileAssetManager.prefetchProfileAssets(installedProfile) if(installedProfile.kind === 'server-pack' && installedProfile.hostReady){ await ProfileAssetManager.ensureServerBundleInstalled(installedProfile) } if(typeof refreshSelectedProfileButton === 'function'){ refreshSelectedProfileButton() } if(typeof refreshServerStatus === 'function'){ refreshServerStatus(true) } if(typeof refreshLibraryView === 'function'){ await refreshLibraryView() } } function createExpandedDetail(profile, installed){ const detailSection = document.createElement('div') detailSection.className = 'launcherExpandableDetail' detailSection.addEventListener('click', (event) => { event.stopPropagation() }) const badgeRow = document.createElement('div') badgeRow.className = 'launcherExpandableMeta' badgeRow.appendChild(createInstallBadge(describeProfileKind(profile.kind))) if(installed){ badgeRow.appendChild(createInstallBadge('설치됨')) } if(!profile.launchReady){ badgeRow.appendChild(createInstallBadge('실행 준비 필요')) } if(profile.kind === 'server-pack' && profile.hostReady){ badgeRow.appendChild(createInstallBadge('호스팅 가능')) } const infoBlock = document.createElement('div') infoBlock.className = 'launcherInfoBlock' infoBlock.appendChild(createInfoLine('프로필 ID', profile.id)) infoBlock.appendChild(createInfoLine('종류', describeProfileKind(profile.kind))) infoBlock.appendChild(createInfoLine('실행 준비', profile.launchReady ? '완료' : '추가 설정 필요')) if(profile.defaultServerAddress){ infoBlock.appendChild(createInfoLine('기본 주소', profile.defaultServerAddress)) } if(profile.kind === 'map' && profile.worldDirectoryName){ infoBlock.appendChild(createInfoLine('월드 폴더', profile.worldDirectoryName)) } if(profile.kind === 'server-pack'){ infoBlock.appendChild(createInfoLine('로컬 호스팅', profile.hostReady ? '가능' : '관리자 설정 필요')) } if(profile.launchIssues.length > 0){ infoBlock.appendChild(createInfoLine('확인 필요', profile.launchIssues.join(' / '))) } else if(profile.hostIssues.length > 0){ infoBlock.appendChild(createInfoLine('호스팅 참고', profile.hostIssues.join(' / '))) } const bodyGroup = document.createElement('div') bodyGroup.className = 'launcherFieldGroup' const bodyLabel = document.createElement('label') bodyLabel.className = 'launcherFieldLabel' bodyLabel.textContent = '자세한 내용' const body = document.createElement('div') body.className = 'launcherDetailBody' body.textContent = buildDetailText(profile) bodyGroup.appendChild(bodyLabel) bodyGroup.appendChild(body) const actions = document.createElement('div') actions.className = 'launcherCardActions' const installButton = document.createElement('button') installButton.className = 'launcherPrimaryButton' installButton.textContent = installed ? '설치됨' : '라이브러리에 추가' installButton.disabled = installed || !profile.launchReady installButton.addEventListener('click', async (event) => { event.stopPropagation() try { await installProfile(profile) await renderInstallView() showInstallMessage('추가 완료', `${profile.name} 프로필을 라이브러리에 추가했습니다.`) } catch (error) { console.error(error) const message = error instanceof Error ? error.message : '프로필 설치 중 오류가 발생했습니다.' showInstallMessage('설치 실패', message) } }) const openLibraryButton = document.createElement('button') openLibraryButton.className = 'launcherSecondaryButton' openLibraryButton.textContent = '라이브러리 열기' openLibraryButton.addEventListener('click', async (event) => { event.stopPropagation() if(typeof refreshLibraryView === 'function'){ await refreshLibraryView() } switchView(getCurrentView(), VIEWS.library) }) actions.appendChild(installButton) actions.appendChild(openLibraryButton) detailSection.appendChild(badgeRow) detailSection.appendChild(infoBlock) detailSection.appendChild(bodyGroup) detailSection.appendChild(actions) return detailSection } async function renderInstallView(){ installCatalogList.innerHTML = '' try { const catalog = await CatalogManager.loadCatalog() const installedIds = new Set( ConfigManager.getInstalledLibraryProfiles().map((profile) => profile.id) ) if(expandedProfileId != null && !catalog.profiles.some((profile) => profile.id === expandedProfileId)){ expandedProfileId = null } if(catalog.sourceError != null){ const warningCard = document.createElement('article') warningCard.className = 'launcherCard' warningCard.innerHTML = '

카탈로그 로드 경고

관리자가 등록한 카탈로그를 읽지 못했습니다. 현재 보이는 목록이 없다면 배포 주소 또는 로컬 카탈로그 파일을 관리자 측에서 확인해야 합니다.

' installCatalogList.appendChild(warningCard) } for(const profile of catalog.profiles){ const installed = installedIds.has(profile.id) const expanded = expandedProfileId === profile.id const row = document.createElement('article') row.className = 'launcherListItem' if(expanded){ row.setAttribute('selected', 'true') } const top = document.createElement('div') top.className = 'launcherListItemTop' const main = document.createElement('div') main.className = 'launcherListItemMain' const titleRow = document.createElement('div') titleRow.className = 'launcherListTitleRow' const textGroup = document.createElement('div') textGroup.className = 'launcherListTextGroup' const title = document.createElement('h3') title.className = 'launcherListTitle' title.textContent = profile.name const meta = document.createElement('div') meta.className = 'launcherListMeta' meta.appendChild(createInstallBadge(describeProfileKind(profile.kind))) if(installed){ meta.appendChild(createInstallBadge('설치됨')) } if(profile.kind === 'server-pack' && profile.hostReady){ meta.appendChild(createInstallBadge('호스팅 가능')) } const description = document.createElement('p') description.className = 'launcherListDescription' description.textContent = profile.description || '설명이 없습니다.' const actions = document.createElement('div') actions.className = 'launcherListActions' const detailButton = document.createElement('button') detailButton.className = 'launcherSecondaryButton' detailButton.textContent = expanded ? '간단히 보기' : '자세히 보기' detailButton.addEventListener('click', async (event) => { event.stopPropagation() toggleExpandedProfile(profile.id) await renderInstallView() }) const installButton = document.createElement('button') installButton.className = 'launcherPrimaryButton' installButton.textContent = installed ? '설치됨' : '라이브러리에 추가' installButton.disabled = installed || !profile.launchReady installButton.addEventListener('click', async (event) => { event.stopPropagation() try { await installProfile(profile) expandedProfileId = profile.id await renderInstallView() showInstallMessage('추가 완료', `${profile.name} 프로필을 라이브러리에 추가했습니다.`) } catch (error) { console.error(error) const message = error instanceof Error ? error.message : '프로필 설치 중 오류가 발생했습니다.' showInstallMessage('설치 실패', message) } }) textGroup.appendChild(title) titleRow.appendChild(textGroup) titleRow.appendChild(meta) main.appendChild(titleRow) main.appendChild(description) actions.appendChild(detailButton) actions.appendChild(installButton) top.appendChild(main) top.appendChild(actions) row.appendChild(top) if(expanded){ row.appendChild(createExpandedDetail(profile, installed)) } row.addEventListener('click', async () => { toggleExpandedProfile(profile.id) await renderInstallView() }) installCatalogList.appendChild(row) } if(catalog.profiles.length === 0){ const emptyCard = document.createElement('article') emptyCard.className = 'launcherCard' emptyCard.innerHTML = '

등록된 프로필이 없습니다

관리자가 카탈로그에 프로필을 추가하면 여기에 표시됩니다.

' installCatalogList.appendChild(emptyCard) } } catch (error) { console.error(error) const errorCard = document.createElement('article') errorCard.className = 'launcherCard' errorCard.innerHTML = '

카탈로그 로드 실패

관리자가 등록한 카탈로그를 읽지 못했습니다.

' installCatalogList.appendChild(errorCard) } } document.getElementById('installBackButton').addEventListener('click', () => { switchView(getCurrentView(), VIEWS.landing) }) window.refreshInstallView = renderInstallView renderInstallView() })()