Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 704d58d3ac | |||
| 2212195b3c |
@@ -148,7 +148,7 @@ function renderStep1() {
|
|||||||
if (!state.selectedPackKey) return
|
if (!state.selectedPackKey) return
|
||||||
await installerApi.setSelectedPack(state.selectedPackKey)
|
await installerApi.setSelectedPack(state.selectedPackKey)
|
||||||
state.stepDone[1] = true
|
state.stepDone[1] = true
|
||||||
renderAgreement()
|
renderCloseNotice()
|
||||||
})
|
})
|
||||||
|
|
||||||
;(async function () {
|
;(async function () {
|
||||||
@@ -162,6 +162,24 @@ function renderStep1() {
|
|||||||
})()
|
})()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 퀴즈팩 선택 직후 노출하는 "마인크래프트/런처 종료" 강조 안내. 크게 표시.
|
||||||
|
function renderCloseNotice() {
|
||||||
|
setActiveStep(1)
|
||||||
|
clearPage()
|
||||||
|
var section = document.createElement('section')
|
||||||
|
section.className = 'page'
|
||||||
|
section.innerHTML =
|
||||||
|
'<div style="margin:24px 0;padding:30px 24px;border:2px solid var(--danger);border-radius:14px;background:rgba(248,81,73,0.12);text-align:center;">' +
|
||||||
|
' <div style="font-size:15px;font-weight:800;color:var(--danger);letter-spacing:2px;">' + escapeHtml(tt('closeNotice.badge')) + '</div>' +
|
||||||
|
' <div style="font-size:30px;font-weight:900;line-height:1.45;margin-top:16px;">' + escapeHtml(tt('closeNotice.message')) + '</div>' +
|
||||||
|
' <div style="font-size:14px;color:var(--text-muted);margin-top:18px;">' + escapeHtml(tt('closeNotice.sub')) + '</div>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="actionRow"><button class="secondaryBtn" id="back">' + tt('common.back') + '</button><button class="primaryBtn" id="next">' + tt('common.next') + '</button></div>'
|
||||||
|
pageHost.appendChild(section)
|
||||||
|
section.querySelector('#back').addEventListener('click', renderStep1)
|
||||||
|
section.querySelector('#next').addEventListener('click', renderAgreement)
|
||||||
|
}
|
||||||
|
|
||||||
// 약관 동의 페이지: 음악퀴즈 선택 직후, 싱글/멀티 선택(step2) 진입 전에 노출.
|
// 약관 동의 페이지: 음악퀴즈 선택 직후, 싱글/멀티 선택(step2) 진입 전에 노출.
|
||||||
// v0.3.4~ : 어떤 약관을 표시할지는 사이트(/manifest/terms/<pack>/index.json) 가
|
// v0.3.4~ : 어떤 약관을 표시할지는 사이트(/manifest/terms/<pack>/index.json) 가
|
||||||
// 결정. 메인 인스톨러용으로 표시 토글된 항목만 받아 탭을 만든다. 목록이 비어 있는 (terms:[])
|
// 결정. 메인 인스톨러용으로 표시 토글된 항목만 받아 탭을 만든다. 목록이 비어 있는 (terms:[])
|
||||||
|
|||||||
@@ -44,8 +44,6 @@
|
|||||||
"cleanup": "이전 UPnP 매핑 정리 중…",
|
"cleanup": "이전 UPnP 매핑 정리 중…",
|
||||||
"externalIpHttp": "외부 IP(HTTP): {{ip}}",
|
"externalIpHttp": "외부 IP(HTTP): {{ip}}",
|
||||||
"externalIpHttpFail": "외부 IP HTTP 조회 실패 → UPnP 게이트웨이로 폴백",
|
"externalIpHttpFail": "외부 IP HTTP 조회 실패 → UPnP 게이트웨이로 폴백",
|
||||||
"externalIpUpnp": "외부 IP(UPnP): {{ip}}",
|
|
||||||
"externalIpFail": "외부 IP 확인 실패",
|
|
||||||
"probeStart": "외부 포트 점검 시작…",
|
"probeStart": "외부 포트 점검 시작…",
|
||||||
"probeResult": "점검 결과: {{verdict}} ({{detail}})",
|
"probeResult": "점검 결과: {{verdict}} ({{detail}})",
|
||||||
"preForwarded": "이미 외부에서 접속 가능한 상태입니다.",
|
"preForwarded": "이미 외부에서 접속 가능한 상태입니다.",
|
||||||
|
|||||||
@@ -3,6 +3,11 @@
|
|||||||
"heading": "시작하기 전에",
|
"heading": "시작하기 전에",
|
||||||
"message": "마인크래프트 런처를 끄고 시작해주세요."
|
"message": "마인크래프트 런처를 끄고 시작해주세요."
|
||||||
},
|
},
|
||||||
|
"closeNotice": {
|
||||||
|
"badge": "⚠ 매우 중요",
|
||||||
|
"message": "마인크래프트와 마인크래프트 런처를 종료한 뒤 진행해주세요",
|
||||||
|
"sub": "실행 중이면 설치 설정이 제대로 적용되지 않을 수 있습니다."
|
||||||
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"back": "이전",
|
"back": "이전",
|
||||||
"next": "다음",
|
"next": "다음",
|
||||||
@@ -186,7 +191,6 @@
|
|||||||
"fabricLoaderRequired": "Fabric 로더 버전이 음악퀴즈에 지정되지 않았습니다. 관리 사이트에서 platform.loaderVersion 을 설정해 주세요.",
|
"fabricLoaderRequired": "Fabric 로더 버전이 음악퀴즈에 지정되지 않았습니다. 관리 사이트에서 platform.loaderVersion 을 설정해 주세요.",
|
||||||
"fabricInstallerListEmpty": "Fabric installer 목록을 받지 못했습니다.",
|
"fabricInstallerListEmpty": "Fabric installer 목록을 받지 못했습니다.",
|
||||||
"portAllocFail": "포트를 할당할 수 없습니다.",
|
"portAllocFail": "포트를 할당할 수 없습니다.",
|
||||||
"upnpTimeout": "UPnP 응답 없음(타임아웃 15s). 라우터의 UPnP가 꺼져 있거나 SSDP 패킷이 차단됐을 수 있습니다.",
|
|
||||||
"parseResponseFailed": "응답 파싱 실패: {{snippet}}"
|
"parseResponseFailed": "응답 파싱 실패: {{snippet}}"
|
||||||
},
|
},
|
||||||
"log": {
|
"log": {
|
||||||
@@ -217,15 +221,10 @@
|
|||||||
"skipResourcepack": "resourcepackPath가 비어 있어 리소스팩 다운로드를 건너뜁니다.",
|
"skipResourcepack": "resourcepackPath가 비어 있어 리소스팩 다운로드를 건너뜁니다.",
|
||||||
"resourcepackDownload": "리소스팩 다운로드: {{url}}",
|
"resourcepackDownload": "리소스팩 다운로드: {{url}}",
|
||||||
"serverInstallPath": "서버 설치 경로: {{path}}",
|
"serverInstallPath": "서버 설치 경로: {{path}}",
|
||||||
"runBatMissing": "run.bat 이 없어 UPnP 자동 등록 스크립트 주입을 건너뜁니다.",
|
|
||||||
"runBatAlreadyInjected": "run.bat 에 이미 UPnP 자동 등록 스크립트가 들어 있어 건너뜁니다.",
|
|
||||||
"runBatNoJava": "run.bat 에서 java 호출 라인을 찾지 못해 UPnP 자동 등록 주입을 건너뜁니다.",
|
|
||||||
"runBatInjected": "run.bat 에 서버 기동/종료 시 UPnP 자동 등록·해제 스크립트를 추가했습니다.",
|
|
||||||
"mojangEulaFetchFail": "Minecraft EULA 페이지 조회 실패: {{message}}",
|
"mojangEulaFetchFail": "Minecraft EULA 페이지 조회 실패: {{message}}",
|
||||||
"eulaAccepted": "EULA 동의 저장 완료.",
|
"eulaAccepted": "EULA 동의 저장 완료.",
|
||||||
"configEditorOpen": "서버 설정 편집기 실행: {{url}}",
|
"configEditorOpen": "서버 설정 편집기 실행: {{url}}",
|
||||||
"portCheckStart": "포트포워딩 점검 시작: 포트 {{port}}",
|
"portCheckStart": "포트포워딩 점검 시작: 포트 {{port}}",
|
||||||
"upnpCleanup": "이전 실행의 UPnP 매핑이 남아 있으면 제거합니다(중복 방지)...",
|
|
||||||
"externalIpHttp": "외부 IP 확인(HTTP): {{ip}}",
|
"externalIpHttp": "외부 IP 확인(HTTP): {{ip}}",
|
||||||
"externalIpHttpFail": "외부 IP 확인 실패(HTTP). UPnP 게이트웨이를 통한 조회 시도...",
|
"externalIpHttpFail": "외부 IP 확인 실패(HTTP). UPnP 게이트웨이를 통한 조회 시도...",
|
||||||
"externalIpUpnp": "외부 IP 확인(UPnP): {{ip}}",
|
"externalIpUpnp": "외부 IP 확인(UPnP): {{ip}}",
|
||||||
@@ -237,15 +236,6 @@
|
|||||||
"probeVerdictUnknown": "확인 불가",
|
"probeVerdictUnknown": "확인 불가",
|
||||||
"probePreForwarded": "외부에서 {{addr}}:{{port}} 접근 확인됨. 사용자 규칙으로 포워딩 됨.",
|
"probePreForwarded": "외부에서 {{addr}}:{{port}} 접근 확인됨. 사용자 규칙으로 포워딩 됨.",
|
||||||
"ipUnknown": "(IP 미상)",
|
"ipUnknown": "(IP 미상)",
|
||||||
"upnpTryOpen": "UPnP로 포트 {{port}} 자동 개방 시도(TCP)...",
|
|
||||||
"upnpReqOk": "UPnP portMapping 요청 성공. 외부 접근을 재확인합니다.",
|
|
||||||
"upnpTryFail": "UPnP 시도 실패: {{message}}",
|
|
||||||
"upnpFailDetail": "UPnP 실패: {{message}}. 라우터에서 UPnP가 꺼져 있을 수 있습니다. 직접 포트포워딩을 해주세요.",
|
|
||||||
"upnpRecheck": "UPnP 적용 후 재점검 {{attempt}}/3...",
|
|
||||||
"upnpDone": "UPnP로 포트 {{port}} 자동 개방 완료. 테스트 매핑을 제거합니다(실제 개방은 run.bat 이 서버 기동 시 자동으로 처리).",
|
|
||||||
"upnpCleanupTest": "테스트용 UPnP 매핑을 정리합니다.",
|
|
||||||
"upnpFailReason1": "UPnP 매핑은 등록됐지만 외부 포트체크 서비스에서 연결이 닿지 않았습니다. ISP 차단, 이중 NAT, 또는 방화벽 설정을 확인하세요.",
|
|
||||||
"upnpFailReason2": "외부 포트체크 결과를 받지 못했습니다({{detail}}). UPnP 매핑은 등록됐을 수 있습니다.",
|
|
||||||
"upnpClientFail": "UPnP 클라이언트 생성 실패: {{message}}",
|
"upnpClientFail": "UPnP 클라이언트 생성 실패: {{message}}",
|
||||||
"upnpExternalTimeout": "UPnP externalIp 조회 타임아웃(8s).",
|
"upnpExternalTimeout": "UPnP externalIp 조회 타임아웃(8s).",
|
||||||
"upnpExternalErr": "UPnP externalIp 오류: {{message}}",
|
"upnpExternalErr": "UPnP externalIp 오류: {{message}}",
|
||||||
@@ -256,10 +246,6 @@
|
|||||||
"detailIfconfig": "ifconfig.co reachable={{reachable}} ip={{ip}}",
|
"detailIfconfig": "ifconfig.co reachable={{reachable}} ip={{ip}}",
|
||||||
"detailIfconfigFail": "ifconfig.co 실패={{error}}",
|
"detailIfconfigFail": "ifconfig.co 실패={{error}}",
|
||||||
"detailNone": "결과 없음",
|
"detailNone": "결과 없음",
|
||||||
"upnpClientFailRemove": "UPnP 클라이언트 생성 실패(매핑 제거 단계): {{message}}",
|
|
||||||
"upnpRemoveTimeout": "UPnP 매핑 제거 응답 없음(타임아웃 8s). 라우터에 우리가 만든 규칙이 없을 수 있습니다.",
|
|
||||||
"upnpRemoveAttempt": "UPnP 매핑 제거 시도 결과: {{message}} (없으면 정상)",
|
|
||||||
"upnpRemoveDone": "UPnP 매핑 제거 완료(포트 {{port}}).",
|
|
||||||
"platformDownload": "플랫폼({{type}}) 다운로드: {{url}}",
|
"platformDownload": "플랫폼({{type}}) 다운로드: {{url}}",
|
||||||
"platformSaved": "플랫폼 설치파일 저장: {{path}} (사용자가 직접 실행하거나 마인크래프트 런처에서 인식할 수 있습니다.)",
|
"platformSaved": "플랫폼 설치파일 저장: {{path}} (사용자가 직접 실행하거나 마인크래프트 런처에서 인식할 수 있습니다.)",
|
||||||
"platformSkipped": "플랫폼 설치 건너뜀. 바닐라로 진행합니다.",
|
"platformSkipped": "플랫폼 설치 건너뜀. 바닐라로 진행합니다.",
|
||||||
@@ -296,7 +282,8 @@
|
|||||||
"launcherAppsFolderFail": "AppsFolder 실행 실패: {{message}}",
|
"launcherAppsFolderFail": "AppsFolder 실행 실패: {{message}}",
|
||||||
"launcherUrlSchemeFallback": "마지막 시도: minecraft:// URL 스킴 (런처가 없으면 MS Store 가 열릴 수 있음).",
|
"launcherUrlSchemeFallback": "마지막 시도: minecraft:// URL 스킴 (런처가 없으면 MS Store 가 열릴 수 있음).",
|
||||||
"launcherUrlSchemeFail": "URL 스킴 실행 실패: {{message}}.",
|
"launcherUrlSchemeFail": "URL 스킴 실행 실패: {{message}}.",
|
||||||
"launcherAllFail": "Minecraft Launcher 실행 시도가 모두 실패했습니다. minecraft.net 또는 Microsoft Store 에서 \"Minecraft Launcher\" 를 설치한 뒤 다시 시도해 주세요."
|
"launcherAllFail": "Minecraft Launcher 실행 시도가 모두 실패했습니다. minecraft.net 또는 Microsoft Store 에서 \"Minecraft Launcher\" 를 설치한 뒤 다시 시도해 주세요.",
|
||||||
|
"internalError": "내부 오류(무시하고 계속): {{message}}"
|
||||||
},
|
},
|
||||||
"candidates": {
|
"candidates": {
|
||||||
"winProgramFiles86": "Win32 설치(Program Files (x86))",
|
"winProgramFiles86": "Win32 설치(Program Files (x86))",
|
||||||
|
|||||||
@@ -229,6 +229,7 @@
|
|||||||
"ytdlpInstallFailed": "yt-dlp 자동 설치에 실패했습니다: {{message}}",
|
"ytdlpInstallFailed": "yt-dlp 자동 설치에 실패했습니다: {{message}}",
|
||||||
"ytdlpVideoFailed": "yt-dlp 영상 조회 실패 (code={{code}}): {{detail}}",
|
"ytdlpVideoFailed": "yt-dlp 영상 조회 실패 (code={{code}}): {{detail}}",
|
||||||
"ytdlpPlaylistFailed": "yt-dlp 플레이리스트 조회 실패 (code={{code}}): {{detail}}",
|
"ytdlpPlaylistFailed": "yt-dlp 플레이리스트 조회 실패 (code={{code}}): {{detail}}",
|
||||||
"tooManyRedirects": "redirect 가 너무 많습니다."
|
"tooManyRedirects": "redirect 가 너무 많습니다.",
|
||||||
|
"invalidUrl": "올바르지 않은 URL 입니다. http/https 주소만 허용됩니다."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "minecraft-music-quiz-installer",
|
"name": "minecraft-music-quiz-installer",
|
||||||
"version": "0.3.21",
|
"version": "0.3.23",
|
||||||
"description": "마인크래프트 음악퀴즈 간편설치기 + 관리 사이트",
|
"description": "마인크래프트 음악퀴즈 간편설치기 + 관리 사이트",
|
||||||
"main": "dist/installer/main.js",
|
"main": "dist/installer/main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -98,6 +98,16 @@ function sendLog(line: string): void {
|
|||||||
mainWindow.webContents.send('log', stamped)
|
mainWindow.webContents.send('log', stamped)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// nat-upnp(detectExternalIpUpnp) 등이 콜백 밖에서 비동기 소켓 오류를 내면 처리되지
|
||||||
|
// 않아 Electron 메인이 종료되며 창이 갑자기 닫힐 수 있다. 전역 가드로 잡아 로그만
|
||||||
|
// 남기고 앱은 계속 살려 둔다(포트포워딩 전용 도구 v0.3.18 과 동일한 대비책).
|
||||||
|
process.on('uncaughtException', (err) => {
|
||||||
|
try { sendLog(t('log.internalError', { message: (err as Error)?.message || String(err) })) } catch {}
|
||||||
|
})
|
||||||
|
process.on('unhandledRejection', (reason) => {
|
||||||
|
try { sendLog(t('log.internalError', { message: reason instanceof Error ? reason.message : String(reason) })) } catch {}
|
||||||
|
})
|
||||||
|
|
||||||
function fetchBuffer(url: string): Promise<Buffer> {
|
function fetchBuffer(url: string): Promise<Buffer> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const target = new URL(url)
|
const target = new URL(url)
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ app.use((req, res, next) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 모드 폴더 안의 .jar 파일 목록을 JSON으로 반환. 설치기가 자동 다운로드용으로 사용.
|
// 모드 폴더 안의 .jar 파일 목록을 JSON으로 반환. 설치기가 자동 다운로드용으로 사용.
|
||||||
app.get('/file/mods/:folder/index.json', async (req, res) => {
|
app.get('/file/mods/:folder/index.json', async (req, res, next) => {
|
||||||
const folder = req.params.folder
|
const folder = req.params.folder
|
||||||
if (!/^[a-zA-Z0-9_\-]+$/.test(folder)) {
|
if (!/^[a-zA-Z0-9_\-]+$/.test(folder)) {
|
||||||
res.status(404).json({ files: [] })
|
res.status(404).json({ files: [] })
|
||||||
@@ -162,7 +162,8 @@ app.get('/file/mods/:folder/index.json', async (req, res) => {
|
|||||||
res.status(404).json({ files: [] })
|
res.status(404).json({ files: [] })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
throw error
|
// async 핸들러의 throw 는 Express4 가 잡지 못해 unhandledRejection 이 되므로 next 로 위임.
|
||||||
|
next(error)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ indexRouter.get('/', async (_req, res, next) => {
|
|||||||
const manifest = await readManifest()
|
const manifest = await readManifest()
|
||||||
const definitionMap = new Map<string, Awaited<ReturnType<typeof loadPackDefinition>>>()
|
const definitionMap = new Map<string, Awaited<ReturnType<typeof loadPackDefinition>>>()
|
||||||
const keys = await listPackKeys()
|
const keys = await listPackKeys()
|
||||||
for (const key of keys) {
|
// 팩 정의를 병렬 로드(op.ts 와 동일 패턴). 순차 await 보다 빠름.
|
||||||
definitionMap.set(key, await loadPackDefinition(key))
|
const definitions = await Promise.all(keys.map((key) => loadPackDefinition(key)))
|
||||||
}
|
keys.forEach((key, i) => definitionMap.set(key, definitions[i]))
|
||||||
const packs = manifest.packs.map((entry) => ({
|
const packs = manifest.packs.map((entry) => ({
|
||||||
name: entry.name,
|
name: entry.name,
|
||||||
file: entry.file,
|
file: entry.file,
|
||||||
|
|||||||
@@ -299,7 +299,15 @@ async function runYtDlp(args: string[], makeError: (code: string, detail: string
|
|||||||
* 단일 영상 URL 의 메타데이터를 가져온다.
|
* 단일 영상 URL 의 메타데이터를 가져온다.
|
||||||
* `--no-playlist` 로 플레이리스트 URL 이 들어와도 단일 영상 정보만 뽑음.
|
* `--no-playlist` 로 플레이리스트 URL 이 들어와도 단일 영상 정보만 뽑음.
|
||||||
*/
|
*/
|
||||||
|
/** 운영자 입력 URL 이 yt-dlp 인자(플래그)로 오인되지 않도록 http(s) 스킴만 허용. */
|
||||||
|
function assertHttpUrl(url: string): void {
|
||||||
|
if (!/^https?:\/\//i.test(url.trim())) {
|
||||||
|
throw new Error(t('youtube.invalidUrl'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetchVideoMeta(url: string): Promise<YtPlaylistEntry | null> {
|
export async function fetchVideoMeta(url: string): Promise<YtPlaylistEntry | null> {
|
||||||
|
assertHttpUrl(url)
|
||||||
const stdout = await runYtDlp(
|
const stdout = await runYtDlp(
|
||||||
['--dump-json', '--no-warnings', '--no-playlist', '--skip-download', url],
|
['--dump-json', '--no-warnings', '--no-playlist', '--skip-download', url],
|
||||||
(code, detail) => new Error(t('youtube.ytdlpVideoFailed', { code, detail }))
|
(code, detail) => new Error(t('youtube.ytdlpVideoFailed', { code, detail }))
|
||||||
@@ -327,6 +335,7 @@ export async function fetchVideoMeta(url: string): Promise<YtPlaylistEntry | nul
|
|||||||
* `--flat-playlist --dump-json` 출력은 한 줄당 한 JSON.
|
* `--flat-playlist --dump-json` 출력은 한 줄당 한 JSON.
|
||||||
*/
|
*/
|
||||||
export async function fetchPlaylistEntries(url: string): Promise<YtPlaylistEntry[]> {
|
export async function fetchPlaylistEntries(url: string): Promise<YtPlaylistEntry[]> {
|
||||||
|
assertHttpUrl(url)
|
||||||
const stdout = await runYtDlp(
|
const stdout = await runYtDlp(
|
||||||
['--flat-playlist', '--dump-json', '--no-warnings', url],
|
['--flat-playlist', '--dump-json', '--no-warnings', url],
|
||||||
(code, detail) => new Error(t('youtube.ytdlpPlaylistFailed', { code, detail }))
|
(code, detail) => new Error(t('youtube.ytdlpPlaylistFailed', { code, detail }))
|
||||||
|
|||||||
Reference in New Issue
Block a user