From 9d9b5183b850d4d3fa49f47b5733adaf572d46a8 Mon Sep 17 00:00:00 2001 From: claude-bot Date: Fri, 17 Jul 2026 12:13:33 +0900 Subject: [PATCH] =?UTF-8?q?feat(server):=20=EA=B6=8C=EC=9E=A5=20JDK=20?= =?UTF-8?q?=ED=83=90=EC=83=89=EC=9D=84=20PF/=ED=99=98=EA=B2=BD=EB=B3=80?= =?UTF-8?q?=EC=88=98=20=EC=A4=91=EC=8B=AC=EC=9C=BC=EB=A1=9C=20+=20?= =?UTF-8?q?=EB=B6=88=EC=9D=BC=EC=B9=98=20=EA=B2=BD=EA=B3=A0=20+=20.mc=5Fcu?= =?UTF-8?q?stom/jdk=20=EC=84=A4=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - JDK 탐색을 C:\Program Files\Java → 환경변수(JAVA_HOME/JDK_HOME) → 설치기 자동설치 위치 순으로 훑어 권장 버전을 우선 선택. 권장이 없으면 가장 높은 버전을 대신 고르되 match=false 로 표시. - 권장과 다른 버전을 찾거나 직접 선택하면 "권장과 달라 정상 실행이 안 될 수 있음" 경고를 띄우고, 동의(확인) 시에만 진행(무조건 차단 대신 경고+동의). - 자동 설치 경로를 %APPDATA%/jdk → .mc_custom/jdk 로 변경(파일제거기가 함께 정리). Adoptium zip 의 중첩 jdk-* 폴더까지 탐색하도록 resolveJdkHome 보강. - 버전 0.4.6→0.4.7. Co-Authored-By: Claude Opus 4.7 --- installer/renderer.js | 45 +++++++++----- locales/installer/ko-kr.json | 2 + package.json | 2 +- src/installer/main.ts | 113 ++++++++++++++++++++++++----------- src/installer/preload.ts | 4 +- 5 files changed, 115 insertions(+), 51 deletions(-) diff --git a/installer/renderer.js b/installer/renderer.js index af977ef..fb04dee 100644 --- a/installer/renderer.js +++ b/installer/renderer.js @@ -641,13 +641,17 @@ function renderSubStep32(host, back, done) { autoBtn.addEventListener('click', async function () { if (installing) return var detect = await installerApi.detectJdk(recommendedJdk) - if (detect.found) { + if (detect.found && detect.match) { input.value = detect.path - msg.textContent = detect.source === 'recommended' - ? tt('step3.sub32.foundRecommended', { path: detect.path, major: recommendedJdk }) - : tt('step3.sub32.found', { path: detect.path }) + msg.textContent = tt('step3.sub32.foundRecommended', { path: detect.path, major: recommendedJdk }) msg.classList.remove('error') msg.classList.add('success') + } else if (detect.found) { + // 권장 버전이 아닌 다른 JDK 를 찾음 → 경고와 함께 채워 넣는다. + input.value = detect.path + msg.textContent = tt('step3.sub32.foundMismatch', { path: detect.path, major: detect.major, required: recommendedJdk }) + msg.classList.remove('success') + msg.classList.add('error') } else { msg.textContent = tt('step3.sub32.notFound', { major: recommendedJdk }) msg.classList.remove('success') @@ -703,21 +707,31 @@ function renderSubStep32(host, back, done) { msg.classList.add('error') return } - // 직접 입력/선택한 JDK 도 서버 실행에 충분한 버전(Java 25+)인지 검증한다. - // 너무 낮으면 run.bat 이 그 자바를 써도 UnsupportedClassVersionError 로 서버가 안 뜬다. + // 직접 입력/선택한 JDK 를 확인한다. 자바를 못 읽으면 진행을 막고, 권장과 다른 + // 버전이면 "정상 실행이 안 될 수 있다"는 경고를 띄운 뒤 사용자가 동의하면 진행한다. nextBtn.disabled = true msg.classList.remove('success', 'error') msg.textContent = tt('step3.sub32.verifying') + var required = recommendedJdk try { var v = await installerApi.verifyJdk(val, recommendedJdk) - if (!v || !v.ok) { + required = (v && v.required) || recommendedJdk + if (!v || !v.major) { msg.classList.add('error') - msg.textContent = (v && v.major) - ? tt('step3.sub32.versionTooLow', { major: v.major, required: (v && v.required) || recommendedJdk }) - : tt('step3.sub32.versionUnknown') + msg.textContent = tt('step3.sub32.versionUnknown') nextBtn.disabled = false return } + if (!v.match) { + // 권장 버전과 다름 → 경고 후 동의 시에만 계속. + var proceed = window.confirm(tt('step3.sub32.mismatchConfirm', { major: v.major, required: required })) + if (!proceed) { + msg.classList.add('error') + msg.textContent = tt('step3.sub32.foundMismatch', { path: val, major: v.major, required: required }) + nextBtn.disabled = false + return + } + } } catch (err) { msg.classList.add('error') msg.textContent = tt('step3.sub32.versionUnknown') @@ -732,10 +746,13 @@ function renderSubStep32(host, back, done) { var detect = await installerApi.detectJdk(recommendedJdk) if (detect.found && !input.value) { input.value = detect.path - msg.textContent = detect.source === 'recommended' - ? tt('step3.sub32.foundRecommended', { path: detect.path, major: recommendedJdk }) - : tt('step3.sub32.autoDetected', { path: detect.path }) - msg.classList.add('success') + if (detect.match) { + msg.textContent = tt('step3.sub32.foundRecommended', { path: detect.path, major: recommendedJdk }) + msg.classList.add('success') + } else { + msg.textContent = tt('step3.sub32.foundMismatch', { path: detect.path, major: detect.major, required: recommendedJdk }) + msg.classList.add('error') + } } else if (!detect.found) { msg.textContent = tt('step3.sub32.notFoundHint', { major: recommendedJdk }) } diff --git a/locales/installer/ko-kr.json b/locales/installer/ko-kr.json index 1941cbe..fc5ac46 100644 --- a/locales/installer/ko-kr.json +++ b/locales/installer/ko-kr.json @@ -93,6 +93,8 @@ "recommended": "권장 JDK: Java {{major}} (이 음악퀴즈 서버 실행용)", "found": "JDK 발견: {{path}}", "foundRecommended": "권장 JDK(Java {{major}}) 발견: {{path}}", + "foundMismatch": "⚠ 권장(Java {{required}})이 아닌 Java {{major}} 를 찾았습니다: {{path}} — 권장 버전과 달라 서버가 정상 실행되지 않을 수 있습니다. 권장 버전을 쓰려면 \"자동 설치\" 를 누르세요.", + "mismatchConfirm": "선택한 Java {{major}} 는 권장 버전(Java {{required}})과 다릅니다. 권장 버전과 달라 서버가 정상 실행되지 않을 수 있습니다. 이대로 계속하시겠습니까?", "autoDetected": "JDK 자동 탐색됨: {{path}}", "notFound": "서버 실행에 필요한 Java {{major}} 이상을 찾지 못했습니다(낮은 버전이 설치돼 있어도 서버가 뜨지 않습니다). \"자동 설치\" 를 눌러 설치하거나 직접 선택해 주세요.", "notFoundHint": "Java {{major}} 이상을 찾지 못했습니다. \"자동 설치\" 를 누르면 Temurin {{major}} 를 받아 설치합니다.", diff --git a/package.json b/package.json index 0c7f4f6..b6b5dbf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minecraft-music-quiz-installer", - "version": "0.4.6", + "version": "0.4.7", "description": "마인크래프트 음악퀴즈 간편설치기 + 관리 사이트", "main": "dist/installer/main.js", "scripts": { diff --git a/src/installer/main.ts b/src/installer/main.ts index 88eebf9..a5531a5 100644 --- a/src/installer/main.ts +++ b/src/installer/main.ts @@ -297,51 +297,95 @@ function findJavaExeInHome(candidate: string): string { return '' } -/** javaExe 로부터 JDK 홈 디렉터리를 되돌린다(/bin/java.exe → ). */ -function homeFromJavaExe(javaExe: string, fallbackHome: string): string { - return path.basename(path.dirname(javaExe)) === 'bin' - ? path.dirname(path.dirname(javaExe)) - : fallbackHome +/** %appdata%/<.mc_custom>. 자동 설치 JDK 는 이 폴더 아래 jdk/ 에 둔다. */ +function customRootDir(): string { + return path.join(getAppDataDir(), getMcCustomDirName()) } +/** 설치기 자동 설치 JDK 의 루트(.mc_custom/jdk). */ +function installerJdkRoot(): string { + return path.join(customRootDir(), 'jdk') +} + +/** dir(또는 그 한 단계 하위 jdk-* 폴더)에서 java 를 찾아 실제 JDK 홈(/bin/java)을 돌려준다. 없으면 ''. */ +function resolveJdkHome(dir: string): string { + const javaExe = findJavaExeInHome(dir) + if (!javaExe) return '' + // findJavaExeInHome 은 항상 /bin/java(.exe) 를 돌려준다 → 상위 두 단계가 홈. + return path.basename(path.dirname(javaExe)) === 'bin' ? path.dirname(path.dirname(javaExe)) : dir +} + +/** + * parent 아래에서 JDK 홈들을 모두 찾는다. parent 자신, 그리고 각 하위 폴더를 검사하되, + * Adoptium zip 처럼 한 단계 더 감싼 `jdk-*` 하위까지 resolveJdkHome 으로 풀어낸다. + * (예: .mc_custom/jdk/temurin-25/jdk-25.0.3+9/bin/java.exe) + */ +function jdkHomesUnder(parent: string): string[] { + const out: string[] = [] + const add = (h: string): void => { if (h) out.push(h) } + add(resolveJdkHome(parent)) + try { + for (const entry of fs.readdirSync(parent)) { + add(resolveJdkHome(path.join(parent, entry))) + } + } catch { + /* 폴더 없음 등 무시 */ + } + return out +} + +/** + * 컴퓨터에 설치된 JDK 홈 목록을 우선순위대로 모은다(중복 제거). + * 순서: ① 기본 자바 설치 위치 C:\Program Files\Java → ② 환경변수(JAVA_HOME/JDK_HOME) + * → ③ 설치기 자동 설치 위치(.mc_custom/jdk, 구버전 %APPDATA%/jdk 호환). + */ +function collectJdkHomes(): string[] { + const homes: string[] = [] + const push = (h: string): void => { + if (h && !homes.some((x) => x.toLowerCase() === h.toLowerCase())) homes.push(h) + } + for (const h of jdkHomesUnder('C:\\Program Files\\Java')) push(h) + for (const env of [process.env.JAVA_HOME, process.env.JDK_HOME]) { + if (env) push(resolveJdkHome(env)) + } + for (const base of [installerJdkRoot(), path.join(getAppDataDir(), 'jdk')]) { + for (const h of jdkHomesUnder(base)) push(h) + } + return homes +} + +// 권장 JDK 를 우선 찾는다. C:\Program Files\Java → 환경변수 → 설치기 자동설치 위치를 +// 훑어, ① 권장 버전과 정확히 일치하는 JDK 가 있으면 그걸 쓰고(match=true), +// ② 없으면 가장 높은 버전을 대신 고르되 "권장과 다름"(match=false)으로 표시해 렌더러가 +// 경고를 띄우게 한다. ③ 아무 JDK 도 없으면 not found → 자동 설치 유도. ipcMain.handle('jdk:detect', async (_event, recommendedInput?: number) => { const required = normalizeRecommendedJdk(recommendedInput) + const javaName = process.platform === 'win32' ? 'java.exe' : 'java' + const found = collectJdkHomes() + .map((home) => ({ home, major: getJavaMajor(path.join(home, 'bin', javaName)) })) + .filter((x) => x.major > 0) - // 1) 권장 JDK(설치기가 자동 설치하는 위치)를 가장 먼저 찾는다. - const recommendedHome = path.join(getAppDataDir(), 'jdk', jdkDirName(required)) - const recJava = findJavaExeInHome(recommendedHome) - if (recJava && getJavaMajor(recJava) >= required) { - return { found: true, path: homeFromJavaExe(recJava, recommendedHome), source: 'recommended', major: required } + const exact = found.find((x) => x.major === required) + if (exact) return { found: true, path: exact.home, major: required, match: true } + if (found.length > 0) { + const best = found.slice().sort((a, b) => b.major - a.major)[0] + return { found: true, path: best.home, major: best.major, match: false } } - - // 2) 없으면 컴퓨터의 JDK(JAVA_HOME/JDK_HOME/Program Files)를 확인한다. - // 권장 버전 미만은 서버 실행에 부족하므로(UnsupportedClassVersionError) "없음" 처리. - const candidates: string[] = [] - if (process.env.JAVA_HOME) candidates.push(process.env.JAVA_HOME) - if (process.env.JDK_HOME) candidates.push(process.env.JDK_HOME) - candidates.push('C:\\Program Files\\Java') - for (const candidate of candidates) { - if (!candidate) continue - const javaExe = findJavaExeInHome(candidate) - if (!javaExe) continue - const major = getJavaMajor(javaExe) - if (major < required) continue - return { found: true, path: homeFromJavaExe(javaExe, candidate), source: 'system', major } - } - return { found: false, path: '', major: required } + return { found: false, path: '', major: required, match: false } }) -// 사용자가 직접 입력/선택한 JDK 경로가 서버 실행에 충분한 버전인지 확인한다. -// ok=true → major >= 요구 버전(25). -// major=0 → 경로에서 java 를 못 찾거나 버전을 못 읽음(실행 실패 등). -ipcMain.handle('jdk:verify', async (_event, jdkPath: string, recommendedInput?: number): Promise<{ ok: boolean; major: number; required: number }> => { +// 사용자가 직접 입력/선택한 JDK 경로의 자바 버전을 확인한다. +// major=0 → 경로에서 java 를 못 찾거나 버전을 못 읽음(진행 차단용). +// match=true → 권장 버전과 정확히 일치. +// match=false → 다른 버전(진행은 허용하되 렌더러가 경고를 띄운다). +ipcMain.handle('jdk:verify', async (_event, jdkPath: string, recommendedInput?: number): Promise<{ ok: boolean; major: number; required: number; match: boolean }> => { const required = normalizeRecommendedJdk(recommendedInput) const javaExe = jdkPath ? findJavaExeInHome(jdkPath) : '' const major = javaExe ? getJavaMajor(javaExe) : 0 - return { ok: major >= required, major, required } + return { ok: major > 0, major, required, match: major === required } }) -// ── JDK 자동 설치(Temurin 25, 취소 가능) ────────────────────────────── +// ── JDK 자동 설치(권장 버전 Temurin, .mc_custom/jdk 에 설치, 취소 가능) ────── interface JdkInstallState { controller: AbortController | null destDir: string | null @@ -423,10 +467,11 @@ ipcMain.handle('jdk:install', async (_event, recommendedInput?: number): Promise jdkInstall.inProgress = true const controller = new AbortController() jdkInstall.controller = controller - const tmpRoot = path.join(getAppDataDir(), 'jdk-cache') + // 자동 설치 JDK 는 .mc_custom/jdk 아래에 둔다(파일제거기가 .mc_custom 통째로 정리 가능). + const tmpRoot = path.join(installerJdkRoot(), '.cache') await fsp.mkdir(tmpRoot, { recursive: true }) const tempZip = path.join(tmpRoot, `${jdkDirName(required)}-${Date.now()}.zip`) - const destDir = path.join(getAppDataDir(), 'jdk', jdkDirName(required)) + const destDir = path.join(installerJdkRoot(), jdkDirName(required)) jdkInstall.destDir = destDir try { // Adoptium API v3: latest GA JDK(Windows x64). 본문은 307 로 GitHub 릴리즈로 리다이렉트. diff --git a/src/installer/preload.ts b/src/installer/preload.ts index e363fae..c412904 100644 --- a/src/installer/preload.ts +++ b/src/installer/preload.ts @@ -24,9 +24,9 @@ const api = { ipcRenderer.invoke('install:validatePath', target), // 3-2 - detectJdk: (recommendedJdk?: number): Promise<{ found: boolean; path: string; source?: string; major?: number }> => + detectJdk: (recommendedJdk?: number): Promise<{ found: boolean; path: string; major: number; match: boolean }> => ipcRenderer.invoke('jdk:detect', recommendedJdk), - verifyJdk: (jdkPath: string, recommendedJdk?: number): Promise<{ ok: boolean; major: number; required: number }> => + verifyJdk: (jdkPath: string, recommendedJdk?: number): Promise<{ ok: boolean; major: number; required: number; match: boolean }> => ipcRenderer.invoke('jdk:verify', jdkPath, recommendedJdk), installJdk: (recommendedJdk?: number): Promise<{ ok: boolean; path?: string; message?: string }> => ipcRenderer.invoke('jdk:install', recommendedJdk),