From fefa2dcdd2a203e6e58091ad1d822f6aa1600923 Mon Sep 17 00:00:00 2001 From: claude-bot Date: Fri, 17 Jul 2026 14:43:55 +0900 Subject: [PATCH] =?UTF-8?q?feat(server):=20=EA=B6=8C=EC=9E=A5=20JDK=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=EC=9D=84=20Adoptium=20=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EB=8F=99=EC=A0=81=EC=9C=BC=EB=A1=9C=20+=20=EC=9E=90=EC=84=B8?= =?UTF-8?q?=ED=9E=88=20=EB=B3=B4=EA=B8=B0(=EC=8A=A4=EB=83=85=EC=83=B7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 하드코딩 [8,11,17,21,25] 대신 Adoptium(Temurin) API 에서 실제 배포 버전을 가져와 편집기 드롭다운을 채운다. 브라우저 CORS 차단이라 서버가 프록시: GET /op/jdk-versions (메이저 목록), GET /op/jdk-versions/:major (GA/EA 릴리스명). 6h 캐시 + 네트워크 실패 시 정적 폴백. - 편집기: 기본은 LTS(25/21/17/11/8)만 표시, "자세히 보기" 체크 시 비-LTS 메이저까지 펼치고 선택 메이저의 GA/EA 스냅샷 릴리스명을 함께 보여준다. - normalizeRecommendedJdk 를 고정 목록 대신 8~99 범위로 완화(동적 메이저 허용). - 버전 0.4.7→0.4.8. Co-Authored-By: Claude Opus 4.7 --- locales/server/ko-kr.json | 7 ++- package.json | 2 +- src/server/routes/op.ts | 26 +++++++++ src/shared/jdkVersions.ts | 118 ++++++++++++++++++++++++++++++++++++++ src/shared/store.ts | 10 +++- views/op/editor.ejs | 77 +++++++++++++++++++++++-- 6 files changed, 231 insertions(+), 9 deletions(-) create mode 100644 src/shared/jdkVersions.ts diff --git a/locales/server/ko-kr.json b/locales/server/ko-kr.json index e8e74ff..543cca3 100644 --- a/locales/server/ko-kr.json +++ b/locales/server/ko-kr.json @@ -126,7 +126,12 @@ "clientMinRam": "클라이언트 최소 램 (MB)", "clientRecommendedRam": "클라이언트 권장 램 (MB)", "recommendedJdk": "권장 JDK (서버 실행용)", - "recommendedJdkHint": "설치기가 이 버전을 우선 찾고, 없으면 자동 설치합니다. 서버가 요구하는 자바 버전에 맞춰 고르세요(최신 마인크래프트는 보통 Java 25).", + "recommendedJdkHint": "설치기가 이 버전을 우선 찾고, 없으면 자동 설치합니다. 서버가 요구하는 자바 버전에 맞춰 고르세요(최신 마인크래프트는 보통 Java 25). 목록은 Adoptium(Temurin)에서 실제 배포되는 버전을 불러옵니다.", + "jdkShowDetails": "자세히 보기 (모든 버전·스냅샷)", + "jdkDetailsGa": "정식(GA)", + "jdkDetailsEa": "개발 스냅샷(EA)", + "jdkDetailsNone": "상세 버전 정보를 불러오지 못했습니다.", + "jdkLtsSuffix": " (LTS)", "mapPath": "맵 파일 (.zip)", "mapPathHint": "/file/maps/ 아래 zip 파일 이름.", "serverPath": "서버 파일 (.zip)", diff --git a/package.json b/package.json index b6b5dbf..ebc710e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minecraft-music-quiz-installer", - "version": "0.4.7", + "version": "0.4.8", "description": "마인크래프트 음악퀴즈 간편설치기 + 관리 사이트", "main": "dist/installer/main.js", "scripts": { diff --git a/src/server/routes/op.ts b/src/server/routes/op.ts index d48e2cf..4b025b0 100644 --- a/src/server/routes/op.ts +++ b/src/server/routes/op.ts @@ -28,6 +28,7 @@ import { } from '../../shared/store.js' import { hashPassword, isHashed, verifyPassword } from '../password.js' import { fetchReleaseVersions } from '../../shared/mojang.js' +import { fetchJdkAvailability, fetchJdkReleaseNames } from '../../shared/jdkVersions.js' import { fetchPlaylistEntries, fetchVideoMeta, YtDlpUnavailableError } from '../youtube.js' import { requireAuth } from '../middleware/auth.js' import type { PackDefinition, PackList } from '../../shared/types.js' @@ -196,6 +197,31 @@ opRouter.get('/op/dashboard/:packName', requireAuth, async (req, res, next) => { } }) +// ─── /op/jdk-versions ─────────────────────────────────────────────────── +// Adoptium(Temurin) 에서 실제 배포되는 JDK 메이저 목록을 프록시(브라우저 직접 호출은 +// CORS 로 막힘). 편집기의 "권장 JDK" 드롭다운을 이 결과로 채운다. +opRouter.get('/op/jdk-versions', requireAuth, async (_req, res, next) => { + try { + res.json(await fetchJdkAvailability()) + } catch (error) { + next(error) + } +}) + +// 특정 메이저의 상세 릴리스 이름(GA + EA 스냅샷). "자세히 보기" 에서 사용. +opRouter.get('/op/jdk-versions/:major', requireAuth, async (req, res, next) => { + try { + const major = Number(req.params.major) + if (!Number.isInteger(major) || major < 8 || major > 99) { + res.status(400).json({ error: 'invalid major' }) + return + } + res.json(await fetchJdkReleaseNames(major)) + } catch (error) { + next(error) + } +}) + // ─── /op/list ────────────────────────────────────────────────────────── // 음악퀴즈를 카드 한 줄로 표시. 카드 클릭 → /op/list/:packName opRouter.get('/op/list', requireAuth, async (req, res, next) => { diff --git a/src/shared/jdkVersions.ts b/src/shared/jdkVersions.ts new file mode 100644 index 0000000..d1d3bd1 --- /dev/null +++ b/src/shared/jdkVersions.ts @@ -0,0 +1,118 @@ +import https from 'node:https' + +// Adoptium(Temurin) API 로부터 실제 배포되는 JDK 버전을 가져온다. 설치기가 이 벤더를 +// 쓰므로 사이트의 "권장 JDK" 목록도 같은 소스에서 채운다(하드코딩 금지). 네트워크 +// 실패 시에는 정적 폴백을 돌려줘 편집기가 항상 동작하도록 한다. + +const AVAILABLE_URL = 'https://api.adoptium.net/v3/info/available_releases' +const CACHE_TTL_MS = 6 * 60 * 60 * 1000 // 6시간 + +const FALLBACK_MAJORS = [8, 11, 17, 21, 25] +const FALLBACK_LTS = [8, 11, 17, 21, 25] + +export interface JdkAvailability { + /** 배포되는 모든 메이저(예: 8,11,17,21,25,26). 내림차순. */ + available: number[] + /** LTS 메이저만(예: 8,11,17,21,25). 내림차순. */ + lts: number[] + mostRecentLts: number + mostRecentFeature: number +} + +export interface JdkReleaseNames { + major: number + /** 정식(GA) 릴리스 이름들(예: jdk-25.0.3+9). */ + ga: string[] + /** 개발 스냅샷(EA) 릴리스 이름들(예: jdk-25.0.4+6-ea-beta). */ + ea: string[] +} + +let availCache: { data: JdkAvailability; at: number } | null = null +const releaseNamesCache = new Map() + +function fetchJson(url: string): Promise { + return new Promise((resolve, reject) => { + const request = https.get(url, { timeout: 8000, headers: { accept: 'application/json' } }, (response) => { + const code = response.statusCode ?? 0 + if (code !== 200) { + response.resume() + reject(new Error(`Adoptium HTTP ${code}`)) + return + } + const chunks: Buffer[] = [] + response.on('data', (c: Buffer) => chunks.push(c)) + response.on('end', () => { + try { + resolve(JSON.parse(Buffer.concat(chunks).toString('utf8')) as T) + } catch (error) { + reject(error as Error) + } + }) + }) + request.on('error', reject) + request.on('timeout', () => request.destroy(new Error('Adoptium timeout'))) + }) +} + +const sortDesc = (nums: number[]): number[] => [...new Set(nums)].sort((a, b) => b - a) + +export async function fetchJdkAvailability(): Promise { + if (availCache && Date.now() - availCache.at < CACHE_TTL_MS) return availCache.data + try { + const raw = await fetchJson<{ + available_releases?: number[] + available_lts_releases?: number[] + most_recent_lts?: number + most_recent_feature_release?: number + }>(AVAILABLE_URL) + const available = sortDesc( + (Array.isArray(raw.available_releases) ? raw.available_releases : FALLBACK_MAJORS).filter((n) => Number.isInteger(n)) + ) + const lts = sortDesc( + (Array.isArray(raw.available_lts_releases) ? raw.available_lts_releases : FALLBACK_LTS).filter((n) => Number.isInteger(n)) + ) + const data: JdkAvailability = { + available, + lts, + mostRecentLts: Number(raw.most_recent_lts) || (lts[0] ?? 21), + mostRecentFeature: Number(raw.most_recent_feature_release) || (available[0] ?? 25) + } + availCache = { data, at: Date.now() } + return data + } catch { + return ( + availCache?.data ?? { + available: sortDesc(FALLBACK_MAJORS), + lts: sortDesc(FALLBACK_LTS), + mostRecentLts: 25, + mostRecentFeature: 25 + } + ) + } +} + +async function fetchReleaseNames(major: number, type: 'ga' | 'ea'): Promise { + const url = + `https://api.adoptium.net/v3/assets/feature_releases/${major}/${type}` + + '?architecture=x64&image_type=jdk&os=windows&vendor=eclipse&page_size=10' + try { + const arr = await fetchJson>(url) + if (!Array.isArray(arr)) return [] + const names: string[] = [] + for (const r of arr) { + if (typeof r?.release_name === 'string' && !names.includes(r.release_name)) names.push(r.release_name) + } + return names + } catch { + return [] + } +} + +export async function fetchJdkReleaseNames(major: number): Promise { + const cached = releaseNamesCache.get(major) + if (cached && Date.now() - cached.at < CACHE_TTL_MS) return cached.data + const [ga, ea] = await Promise.all([fetchReleaseNames(major, 'ga'), fetchReleaseNames(major, 'ea')]) + const data: JdkReleaseNames = { major, ga, ea } + releaseNamesCache.set(major, { data, at: Date.now() }) + return data +} diff --git a/src/shared/store.ts b/src/shared/store.ts index ca19215..168dddf 100644 --- a/src/shared/store.ts +++ b/src/shared/store.ts @@ -80,17 +80,21 @@ function sanitizeFolderName(input: unknown): string { const ALLOWED_LOADERS: LoaderType[] = ['vanilla', 'forge', 'fabric', 'neoforge'] /** - * 사이트에서 고를 수 있고 설치기가 자동 설치할 수 있는 JDK 메이저 버전 목록(LTS). + * 편집기 드롭다운의 정적 폴백 목록(LTS). 실제 목록은 Adoptium API 에서 가져오며, + * 네트워크 실패 시에만 이 값을 쓴다. * 마인크래프트 버전대별: 8(구버전) / 11(1.17) / 17(1.18~1.20.4) / 21(1.20.5~) / 25(최신). */ export const SUPPORTED_JDK_MAJORS: number[] = [8, 11, 17, 21, 25] /** recommendedJdk 미지정/이상값일 때의 기본 권장 버전. */ export const DEFAULT_JDK_MAJOR = 25 -/** 입력값을 허용된 JDK 메이저로 보정. 목록 밖이면 기본값. */ +/** + * 입력값을 JDK 메이저로 보정. 특정 목록에 얽매이지 않고 실제 배포되는 자바 메이저 + * 범위(8~99)면 허용하고, 벗어나면 기본값. (목록은 Adoptium 에서 동적으로 온다.) + */ export function normalizeRecommendedJdk(input: unknown): number { const n = Math.floor(Number(input)) - return SUPPORTED_JDK_MAJORS.includes(n) ? n : DEFAULT_JDK_MAJOR + return Number.isFinite(n) && n >= 8 && n <= 99 ? n : DEFAULT_JDK_MAJOR } export function normalizePackDefinition(input: Partial & Record): PackDefinition { diff --git a/views/op/editor.ejs b/views/op/editor.ejs index dee5726..1fedae0 100644 --- a/views/op/editor.ejs +++ b/views/op/editor.ejs @@ -82,11 +82,13 @@