7ac07a58efd538ad68113028c6af14028b6be6de
Drag-and-drop UX rewrite: - The old "highlight target row + margin-grow animation" approach was driven by per-row dragenter/dragleave events. Those fire in a noisy enter/leave/enter cascade as the pointer crosses sub-elements and as the row itself grows under the pointer, which is why the gap was pulsing open/closed. - New approach: a single container-level dragover handler. On dragstart the source row is briefly cloned into a translucent "placeholder" element (dashed outline, 45% opacity, pointer-events:none) inserted in the source's slot; the original is then hidden (display:none) right after dragstart so the browser can still capture it as the drag image. As the cursor moves over the container we compute which sibling's midpoint the pointer just crossed and insertBefore the placeholder accordingly. The list length stays constant the whole time, so there is no growing/shrinking gap to fight with — what the user sees is the dragged item itself shown semi-transparently at the exact drop slot. On drop, splice the array using the placeholder's index among the non-source children, then re-render. - The bindContainerDnd helper handles both lists; image grid uses vertical Y math (same midpoint rule as the track list since cards flow row-by-row in the auto-fill grid). attachDraggable now only sets up dragstart/dragend/contextmenu per row; no more dragenter/dragleave. Image grid: - Image cards now have a caption below the thumbnail. When the same URL appears in the music list, the music entry's title/artist are borrowed via captionForImage(url); otherwise "(제목 없음)" muted text. Layout changed from a square aspect-ratio card to a flex column: .imgWrap holds the square thumbnail, .cardCaption sits underneath with single-line title + smaller muted artist line. CSS cleanup: - Drop the old .dropAbove margin-grow rules and .dragOver border rule on .trackRow/.imageCard. Replaced with .dragPlaceholder + .hiddenWhileDragging. - .imageCard no longer uses aspect-ratio on itself; aspect lives on .imgWrap so caption can extend the card vertically.
마인크래프트 음악퀴즈 간편설치기 + 관리 사이트 개발 명세서
프로젝트 개요
마인크래프트 음악퀴즈를 .exe 하나로 간편하게 설치할 수 있는 설치기와, 음악퀴즈 정보를 관리하는 웹사이트를 개발한다.
핵심 컨셉
- 이 프로젝트는
%appdata%\.mc_custom폴더를 생성하여 모드 적용 및 서버 실행을 독립적으로 관리한다. - 음악퀴즈 정보는
manifest.json으로 중앙 관리한다.
.mc_custom 폴더 구조
%appdata%\.mc_custom\
├── mods/ ← 모드 (.jar) 저장 및 자동 적용
├── resourcepacks/ ← 리소스팩 (.zip) 저장 및 자동 적용
├── saves/ ← 월드 저장
├── config/ ← 모드 설정 파일
├── screenshots/ ← 스크린샷
└── options.txt ← 게임 설정
- 마인크래프트 런처 프로필의
gameDir을%appdata%\.mc_custom으로 설정하면, 마인크래프트가 이 폴더를 기준으로 모든 파일을 읽고 저장한다. - 버전 파일과 assets는 기존
%appdata%\.minecraft를 그대로 사용한다.
파트 1. 간편설치기 (.exe)
설치기는 아래 단계를 순서대로 페이지 단위로 진행한다. 각 번호 = 1페이지.
1단계: 음악퀴즈 선택
- 음악퀴즈사이트(아래 파트 2 참고)에서
manifest.json을 가져와 등록된 음악퀴즈 목록을 표시한다. - 사용자가 설치할 음악퀴즈를 선택한다.
2단계: 싱글 / 멀티 선택
- 싱글 또는 멀티 중 하나를 선택하는 화면을 표시한다.
- 멀티 선택 시: 3단계(서버 설치)를 거친 후 4단계로 진행한다.
- 싱글 선택 시: 3단계를 건너뛰고 4단계로 바로 진행한다.
3단계: 서버 관련 설정
- 2단계에서 멀티를 선택한 경우에만 진입한다. 싱글 선택 시 이 단계 전체를 건너뛴다.
- 3단계의 각 소항목(3-1 ~ 3-5)은 완료되어도 자동으로 다음으로 넘어가지 않으며, 사용자가 확인 버튼을 눌러야 다음 소항목으로 진행된다.
3-1. 서버 설치 경로 설정
- 서버를 생성할 폴더 경로를 사용자가 직접 지정한다.
- 경로에 한글이 포함되면 안 된다. 한글 포함 시 경고 메시지를 표시하고 다음 단계로 진행 불가.
3-2. JDK 확인 / 설치
- 폴더 선택 UI로 JDK 경로를 지정할 수 있다.
- JDK 자동 탐색 우선순위:
- 시스템 환경변수 (
JAVA_HOME등) - 로컬 환경변수
C:\Program Files\Java(JDK 기본 설치 경로)
- 시스템 환경변수 (
- 위 경로에서 JDK가 발견되면 해당 경로를 기본값으로 자동 설정한다.
- JDK가 없으면 설치를 안내한다.
3-3. 서버 다운로드 및 설치
- 처음 파일 다운로드는 자동으로 시작
3-3-1. 파일 다운로드
- JSON의
packPath필드 값을도메인/file/뒤에 붙여 서버 파일 다운로드 URL을 구성한다.- 예:
packPath가music-quiz/files이면 →도메인/file/music-quiz/files - 해당 경로의 모든 파일을 순서대로 다운로드한다.
- 예:
3-3-2. 설치 로그
- 다른 프로그램 설치 화면처럼 실시간 로그를 표시하는 로그 뷰어를 제공한다.
3-3-3. EULA 동의
- 설치 중간에 Minecraft EULA 동의 화면을 표시하고, 사용자가 직접 동의해야 다음 단계로 진행된다.
- 음악퀴즈 내에
eula.txt가 포함되어 있으면 삭제하고 새로 동의를 받는다.
3-3-4. 램 검사 로직
아래 기준은 모두 JSON의 서버 램 필드(
serverMinRam,serverMaxRam)를 사용한다.
유저 시스템 램 >= serverMaxRam → serverMaxRam으로 설정
유저 시스템 램 >= serverMinRam → serverMinRam으로 설정 (경고 메시지 표시)
유저 시스템 램 < serverMinRam → "플레이 불가" 메시지 출력 후 설치 중단
- 서버 실행 시
-Xmx에serverMaxRam,-Xms에serverMinRam값을 JVM 인자로 사용한다.
3-4. 서버 설정
- 로컬 웹서버를 띄워 브라우저에서 서버 설정 파일을 GUI로 편집할 수 있게 한다.
- 메모장으로 수정해야 했던 파일들을 설명과 함께 편리하게 수정 가능:
bukkit.ymlserver.properties- 기타 설정 파일
- 수정 후 "적용" 버튼으로 실제 파일에 반영한다.
3-5. 서버 포트포워딩 설정
- 이미 포트포워딩 되어 있는 경우 (UPnP 포함): 외부 접속 주소를 화면에 표시하고 다음 단계로 진행.
- 포트포워딩 안 된 경우 → UPnP 시도:
- UPnP로 포트 개방 가능 여부 확인
- 가능하면 자동으로 개방 후 외부 접속 테스트
- 접속 확인되면 다음 단계로 진행 가능
- UPnP 불가 시: "직접 포트포워딩을 해주세요." 메시지를 안내와 함께 표시.
4단계: 유저 클라이언트 설정
- 음악퀴즈 JSON 파일에 명시된 클라이언트 설정을 기반으로 설치한다.
4-1. 모드 플랫폼 설치
- JSON 파일의
platform.type에 명시된 플랫폼 이름을 화면에 표시하고, 설치 / 건너뛰기 버튼으로 사용자가 직접 선택한다.vanilla가 아닌 경우:platform.downloadUrl을 기반으로 다운로드 후 설치한다.- 건너뛰기 선택 시: 플랫폼 설치 없이 바닐라로 진행한다.
4-2. 설치설정 설정
%appdata%\.minecraft\launcher_profiles.json에 프로필을 추가한다.- 이미 동일한 이름의 프로필이 있으면 새로 만들지 않고 기존 프로필을 수정한다.
gameDir을%appdata%\.mc_custom으로 설정한다.- 이 설정으로 인해 모드, 리소스팩, 세이브 등 모든 파일이
.mc_custom기준으로 읽히고 저장된다.
- 이 설정으로 인해 모드, 리소스팩, 세이브 등 모든 파일이
- 램 설정(
javaArgs)을 JSON의 서버 램 필드 기준으로 적용한다.-Xmx에serverMaxRam,-Xms에serverMinRam값을 사용한다.
- 플랫폼(Forge 등) 설치 버전을
lastVersionId로 설정한다.
// launcher_profiles.json 프로필 예시
{
"음악퀴즈": {
"name": "음악퀴즈",
"type": "custom",
"gameDir": "%appdata%\\.mc_custom",
"lastVersionId": "1.20.1-forge-47.2.0",
"javaArgs": "-Xmx4G -Xms2G"
}
}
4-3. 모드 및 리소스팩 설치
- JSON 파일에 명시된 모드와 리소스팩을 다운로드하여
.mc_custom하위 폴더에 저장한다. - 다운로드 진행 상황을 실시간 로그 뷰어로 표시한다. (다른 프로그램 설치 화면과 동일한 형태)
| 파일 종류 | 저장 경로 |
|---|---|
모드 (.jar) |
%appdata%\.mc_custom\mods\ |
리소스팩 (.zip) |
%appdata%\.mc_custom\resourcepacks\ |
- 이미 동일한 파일명이 존재하면 덮어쓴다.
- 다운로드 완료 후 마인크래프트 실행 시 자동으로 적용된다. (별도 설정 불필요)
5단계: 완료 페이지
- 멀티로 진행해서 서버도 설치했다면:
- 서버 폴더 열기 버튼
- 바탕화면에 서버 실행 바로가기 만들기 토글 (기본값: ON)
- 서버 바로 실행 토글 (기본값: ON)
- 마인크래프트 런처 실행 토글 (기본값: ON)
파트 2. 음악퀴즈 관리 웹사이트
기술 스택: Node.js + TypeScript + Express + EJS
라우팅 구조
| 경로 | 설명 |
|---|---|
/ |
메인 페이지 (음악퀴즈 목록) |
/manifest.json |
manifest.json 파일 직접 접근 |
/file/ |
음악퀴즈 파일 제공 경로 |
/op |
관리자 로그인 페이지 |
/op/dashboard |
관리자 대시보드 |
/op/dashboard/:packName |
음악퀴즈 JSON 편집 페이지 |
메인 페이지 (/)
manifest.json에 등록된 음악퀴즈를 가로 한 줄 목록(카드) 형식으로 표시한다.
관리자 인증 (/op)
/op하위 모든 경로는 로그인 없이 접근 불가 (미들웨어로 처리).- 로그인 화면: 아이디 + 비밀번호 입력.
- 계정 정보는
account.json파일에 저장.account.json은 서버 내부에서만 접근 가능, 외부 HTTP 요청으로 절대 노출되지 않아야 함.
- 로그인 성공 시
/op/dashboard로 리다이렉트.
account.json 예시 구조
[
{
"id": "admin",
"password": "admin"
}
]
관리자 대시보드 (/op/dashboard)
공통 레이아웃 (메뉴바)
- 왼쪽: 로고 + "관리자 페이지" 텍스트 → 클릭 시
/op/dashboard로 이동 - 오른쪽: 로그인한 아이디 표시 → 클릭 시 드롭다운 메뉴 표시
- 드롭다운 항목: 로그아웃 버튼
음악퀴즈 목록
/manifest폴더 안의 JSON 파일들을 가져와 가로 한 줄 카드 형식으로 표시.- 카드 클릭 →
/op/dashboard/:packName으로 이동하여 JSON 편집 시작.
음악퀴즈 추가 버튼
/manifest/폴더 안에 새 JSON 파일 생성.- 기본 이름:
new.json - 이미 존재하면:
new2.json,new3.json... 순으로 증가.
- 기본 이름:
- 생성과 동시에
manifest.json에도 자동 등록.
음악퀴즈 삭제 버튼
- 버튼 클릭 시:
- 목록 카드에 체크박스 표시
- 버튼 아래 취소 / 확인 버튼 표시
- 확인 클릭 시 체크된 JSON 파일 삭제 +
manifest.json에서도 자동 제거.
음악퀴즈 편집 페이지 (/op/dashboard/:packName)
- 해당 JSON 파일의 내용을 GUI 폼으로 편집한다.
- JSON 파일 이름 변경 기능 제공.
- 이름 변경 후 적용 클릭 시 현재 브라우저 URL의
:packName부분도 자동 변경 (리다이렉트).
- 이름 변경 후 적용 클릭 시 현재 브라우저 URL의
파트 3. 음악퀴즈 JSON 구조 및 편집 항목
/manifest/*.json파일의 구조. 관리자 편집 페이지에서 아래 항목들을 GUI로 수정 가능.
JSON 필드 정의
| 필드명 | 타입 | 설명 |
|---|---|---|
name |
string |
음악퀴즈 이름 |
mcVersion |
string |
마인크래프트 버전 (스냅샷 제외한 정식 릴리즈만) |
platform |
object |
모드 플랫폼 정보 (아래 참고) |
platform.type |
string |
플랫폼 종류 (vanilla / forge / fabric / neoforge 등) |
platform.downloadUrl |
string |
플랫폼 설치파일 다운로드 URL (바닐라면 생략) |
mods |
array |
설치할 모드 목록 (아래 참고) |
mods[].name |
string |
모드 이름 |
mods[].downloadUrl |
string |
모드 다운로드 URL |
resourcepacks |
array |
설치할 리소스팩 목록 |
resourcepacks[].name |
string |
리소스팩 이름 |
resourcepacks[].downloadUrl |
string |
리소스팩 다운로드 URL |
serverMinRam |
number |
서버 최소 램 (MB 단위) |
serverMaxRam |
number |
서버 최대 램 (MB 단위) |
clientMinRam |
number |
유저(클라이언트) 최소 램 (MB 단위) |
clientRecommendedRam |
number |
유저(클라이언트) 권장 램 (MB 단위) |
packPath |
string |
서버 파일 경로 (/file/ 이후의 경로, 멀티 전용) |
JSON 예시
{
"name": "음악퀴즈 v1",
"mcVersion": "1.20.1",
"platform": {
"type": "forge",
"downloadUrl": "https://example.com/forge-installer.jar"
},
"mods": [
{
"name": "ExampleMod",
"downloadUrl": "https://example.com/examplemod.jar"
}
],
"resourcepacks": [
{
"name": "ExampleResourcePack",
"downloadUrl": "https://example.com/resourcepack.zip"
}
],
"serverMinRam": 2048,
"serverMaxRam": 8192,
"clientMinRam": 4096,
"clientRecommendedRam": 8192,
"packPath": "music-quiz/files"
}
편집 UI 항목별 비고
| 항목 | UI 형태 | 비고 |
|---|---|---|
mcVersion |
드롭다운 | Mojang API에서 정식 릴리즈만 가져와 표시, 스냅샷 제외 |
platform.type |
드롭다운 | vanilla 선택 시 downloadUrl 입력란 숨김 |
mods |
동적 목록 | 항목 추가 / 삭제 가능 |
resourcepacks |
동적 목록 | 항목 추가 / 삭제 가능 |
| 램 관련 필드 | 숫자 입력 | MB 단위, clientMinRam ≤ clientRecommendedRam 유효성 검사 |
packPath |
텍스트 입력 | /file/ 이후 경로만 입력 |
파트 4. manifest.json 구조
사이트 루트의
manifest.json. 설치기와 메인 페이지가 이 파일을 읽는다.
{
"packs": [
{
"name": "음악퀴즈 이름",
"file": "new"
}
]
}
file:/manifest/폴더 안의 JSON 파일 이름 (확장자 제외).- 음악퀴즈 추가/삭제 시 자동으로 이 파일도 업데이트된다.
디렉토리 구조 (웹사이트)
project-root/
├── manifest.json # 음악퀴즈 목록 (외부 접근 가능)
├── account.json # 관리자 계정 정보 (외부 접근 절대 불가)
├── /manifest/ # 음악퀴즈 JSON 파일 저장 폴더
│ ├── new.json
│ └── music-quiz.json
├── /file/ # 서버 파일 제공 경로 (멀티 전용)
├── /src/ # TypeScript 소스
│ ├── app.ts
│ ├── routes/
│ │ ├── index.ts # 메인 페이지
│ │ └── op.ts # 관리자 라우트
│ └── middleware/
│ └── auth.ts # /op 인증 미들웨어
└── /views/ # EJS 템플릿
├── index.ejs
├── op/
│ ├── login.ejs
│ ├── dashboard.ejs
│ └── editor.ejs
└── partials/
└── navbar.ejs
Description