Files
minecraft_launcher/views/op/editor.ejs
claude-bot 7d0f1719f3 fabric: 로더 버전 선택 + fabric-installer CLI 자동 설치
관리 사이트에서 모드 플랫폼으로 fabric 을 선택하면 jar 파일 업로드 대신, 선택한 마인크래프트 버전을 기준으로 Fabric Meta v2 API 에서 호환 로더 목록을 가져와 드롭다운으로 선택하도록 했다. 설치기는 platform.loaderVersion 만 보고 최신 fabric-installer.jar 를 받아 CLI 로 자동 설치(GUI 미표시)한다.

스키마:
- PackPlatform 에 loaderVersion?: string 추가. fabric 일 때만 사용.
- normalizePackDefinition: fabric 이면 downloadUrl 무시하고 loaderVersion 만 저장, 그 외에는 기존 downloadUrl 유지.

웹 UI(views/op/editor.ejs):
- platformType 이 fabric 일 때 platformLoaderVersion select 노출. mcVersion 셀렉트 값을 가지고 https://meta.fabricmc.net/v2/versions/loader/<mcVersion> 호출.
- mcVersion 또는 platformType 변경 시 자동 재조회. 동시 요청 경쟁은 sequence 비교로 무시.
- 이전 저장값을 우선 선택하되 목록에 없으면 최신 stable 자동 선택.
- 폼 제출 시 fabric 인데 로더 미선택이면 경고.
- 라우트(op.ts): platformLoaderVersion 폼 필드 수신.

설치기(installer/main.ts):
- client:install 분기 추가. fabric 이면 installFabricLoader 호출.
- installFabricLoader: Fabric Meta installer 메타 조회 → 최신 stable installer jar 캐시 다운로드 → java -jar fabric-installer.jar client -mcversion <ver> -loader <ver> -dir <.mc_custom> -noprofile 실행. launcher_profiles 갱신은 우리 코드(updateLauncherProfile)가 담당하므로 -noprofile.
- findJavaExecutable: JAVA_HOME → .minecraft\runtime 의 번들 자바(델타/감마/베타 등 우선순위) → PATH 폴백.
- runJavaProcess: stdout/stderr 를 로그 뷰어에 prefix 와 함께 스트리밍. 실패 시 stderr 끝부분을 메시지에 포함.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 01:28:45 +09:00

212 lines
9.2 KiB
Plaintext

<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title><%= pack.name %> 편집</title>
<link rel="stylesheet" href="/static/styles.css" />
</head>
<body class="siteBody">
<%- include('../partials/navbar', { userId }) %>
<main class="pageWrap">
<section class="editorHeader">
<div>
<p class="eyebrow">PACK EDITOR</p>
<h1><%= pack.name %></h1>
</div>
<a class="ghostLink" href="/op/dashboard">목록으로</a>
</section>
<form method="post" class="editorForm" id="editorForm">
<div class="gridTwo">
<label>
<span>음악퀴즈 이름</span>
<input name="displayName" value="<%= pack.name %>" required />
</label>
<label>
<span>JSON 파일 이름 (확장자 제외)</span>
<input name="fileName" value="<%= packKey %>" required pattern="[a-zA-Z0-9_\-]+" />
</label>
</div>
<div class="gridTwo">
<label>
<span>마인크래프트 버전</span>
<select name="mcVersion" required>
<% releases.forEach(function (release) { %>
<option value="<%= release %>" <%= release === pack.mcVersion ? 'selected' : '' %>><%= release %></option>
<% }) %>
</select>
</label>
<label>
<span>모드 플랫폼</span>
<select name="platformType" id="platformType">
<% ['vanilla','forge','fabric','neoforge'].forEach(function (loader) { %>
<option value="<%= loader %>" <%= pack.platform.type === loader ? 'selected' : '' %>><%= loader %></option>
<% }) %>
</select>
</label>
<label class="fullSpan" id="platformDownloadField">
<span>플랫폼 설치파일 URL</span>
<input name="platformDownloadUrl" value="<%= pack.platform.downloadUrl || '' %>" placeholder="/forge-installer.jar 또는 https://example.com/forge-installer.jar" />
<small class="muted">도메인 없이 입력하면 manifest.json 도메인의 <code>/file/platforms/&lt;파일명&gt;</code>으로 해석됩니다.</small>
</label>
<label class="fullSpan" id="platformLoaderField" hidden>
<span>Fabric Loader 버전</span>
<select name="platformLoaderVersion" id="platformLoaderVersion" data-current="<%= pack.platform.loaderVersion || '' %>">
<option value="">불러오는 중...</option>
</select>
<small class="muted">선택한 마인크래프트 버전 기준 Fabric Loader 목록입니다. 설치기는 최신 fabric-installer 를 받아 자동으로 CLI 설치합니다.</small>
</label>
<label>
<span>서버 최소 램 (MB)</span>
<input type="number" name="serverMinRam" value="<%= pack.serverMinRam %>" min="512" required />
</label>
<label>
<span>서버 최대 램 (MB)</span>
<input type="number" name="serverMaxRam" value="<%= pack.serverMaxRam %>" min="512" required />
</label>
<label>
<span>클라이언트 최소 램 (MB)</span>
<input type="number" name="clientMinRam" value="<%= pack.clientMinRam %>" min="512" required />
</label>
<label>
<span>클라이언트 권장 램 (MB)</span>
<input type="number" name="clientRecommendedRam" value="<%= pack.clientRecommendedRam %>" min="512" required />
</label>
<label>
<span>맵 파일 (.zip)</span>
<input name="mapPath" value="<%= pack.mapPath %>" placeholder="my-map.zip" pattern=".+\.zip" />
<small class="muted">/file/maps/ 아래 zip 파일 이름.</small>
</label>
<label>
<span>서버 파일 (.zip)</span>
<input name="serverPath" value="<%= pack.serverPath %>" placeholder="my-server.zip" pattern=".+\.zip" />
<small class="muted">/file/servers/ 아래 zip 파일 이름. 멀티 모드 전용.</small>
</label>
</div>
<div class="gridTwo">
<label>
<span>모드 폴더 이름</span>
<input name="modsFolder" value="<%= pack.modsFolder %>" placeholder="my-pack" pattern="[a-zA-Z0-9_\-]*" />
<small class="muted">/file/mods/&lt;폴더이름&gt;/ 안의 모든 .jar을 자동으로 받습니다. 비워두면 모드를 받지 않습니다.</small>
</label>
<label>
<span>리소스팩 (.zip)</span>
<input name="resourcepackPath" value="<%= pack.resourcepackPath %>" placeholder="my-pack.zip" pattern=".*\.zip|" />
<small class="muted">/file/resourcepacks/ 아래 .zip 파일 이름. 비워두면 리소스팩을 받지 않습니다.</small>
</label>
</div>
<button class="primaryButton" type="submit">저장</button>
</form>
</main>
<script>
(function () {
var platformSelect = document.getElementById('platformType')
var mcVersionSelect = document.querySelector('select[name="mcVersion"]')
var downloadField = document.getElementById('platformDownloadField')
var loaderField = document.getElementById('platformLoaderField')
var loaderSelect = document.getElementById('platformLoaderVersion')
var currentLoader = loaderSelect.getAttribute('data-current') || ''
var loaderCache = {} // mcVersion -> [loader versions]
var loaderFetchSeq = 0
function syncPlatformVisibility() {
var type = platformSelect.value
if (type === 'fabric') {
loaderField.removeAttribute('hidden')
downloadField.setAttribute('hidden', '')
downloadField.querySelector('input').value = ''
loadFabricLoaders()
} else if (type === 'vanilla') {
downloadField.setAttribute('hidden', '')
loaderField.setAttribute('hidden', '')
downloadField.querySelector('input').value = ''
loaderSelect.innerHTML = '<option value=""></option>'
} else {
downloadField.removeAttribute('hidden')
loaderField.setAttribute('hidden', '')
loaderSelect.innerHTML = '<option value=""></option>'
}
}
function populateLoaderOptions(versions, preselect) {
if (!versions || versions.length === 0) {
loaderSelect.innerHTML = '<option value="">호환 로더 없음</option>'
return
}
var html = ''
for (var i = 0; i < versions.length; i++) {
var v = versions[i]
var sel = v.version === preselect ? ' selected' : ''
var label = v.version + (v.stable ? '' : ' (beta)')
html += '<option value="' + v.version + '"' + sel + '>' + label + '</option>'
}
loaderSelect.innerHTML = html
// 사용자가 저장해둔 값이 목록에 없으면 첫 번째(최신) 자동 선택.
if (preselect && !versions.some(function (v) { return v.version === preselect })) {
loaderSelect.value = versions[0].version
}
}
function loadFabricLoaders() {
var mc = (mcVersionSelect && mcVersionSelect.value) || ''
if (!mc) {
loaderSelect.innerHTML = '<option value="">마인크래프트 버전을 먼저 선택하세요</option>'
return
}
if (loaderCache[mc]) {
populateLoaderOptions(loaderCache[mc], currentLoader)
return
}
var seq = ++loaderFetchSeq
loaderSelect.innerHTML = '<option value="">불러오는 중...</option>'
fetch('https://meta.fabricmc.net/v2/versions/loader/' + encodeURIComponent(mc))
.then(function (res) {
if (!res.ok) throw new Error('HTTP ' + res.status)
return res.json()
})
.then(function (list) {
if (seq !== loaderFetchSeq) return // 더 새로운 요청이 들어왔으면 무시
// 응답: [{ loader: { version, stable, ... }, intermediary: {...} }, ...]
var versions = (list || []).map(function (item) {
return { version: item.loader.version, stable: !!item.loader.stable }
})
loaderCache[mc] = versions
populateLoaderOptions(versions, currentLoader)
})
.catch(function (err) {
if (seq !== loaderFetchSeq) return
loaderSelect.innerHTML = '<option value="">로더 목록 로드 실패: ' + (err && err.message ? err.message : err) + '</option>'
})
}
platformSelect.addEventListener('change', syncPlatformVisibility)
if (mcVersionSelect) mcVersionSelect.addEventListener('change', function () {
if (platformSelect.value === 'fabric') loadFabricLoaders()
})
syncPlatformVisibility()
var form = document.getElementById('editorForm')
form.addEventListener('submit', function (event) {
var clientMin = Number(form.clientMinRam.value)
var clientReco = Number(form.clientRecommendedRam.value)
if (clientMin > clientReco) {
event.preventDefault()
alert('클라이언트 최소 램은 권장 램보다 클 수 없습니다.')
return
}
if (platformSelect.value === 'fabric' && !loaderSelect.value) {
event.preventDefault()
alert('Fabric 로더 버전을 선택해 주세요.')
}
})
})()
</script>
</body>
</html>