diff --git a/installer/renderer.js b/installer/renderer.js index 796af7a..e42fa58 100644 --- a/installer/renderer.js +++ b/installer/renderer.js @@ -820,16 +820,20 @@ function renderSubStep35(host, back, done) { var result = await installerApi.checkPortForward(port) state.serverInstall.portStatus = result var address = formatServerAddress(result.externalIp, result.port) - if (result.status === 'preForwarded') { - resultMsg.innerHTML = tt('step3.sub35.preForwarded', { address: address }) - resultMsg.classList.add('success') - } else if (result.status === 'upnpOk') { - resultMsg.innerHTML = tt('step3.sub35.upnpOk', { address: address }) - resultMsg.classList.add('success') + if (result.status === 'open') { + // 열려 있으면 외부 접속 주소를 크게 표시. + resultMsg.innerHTML = + '
' + + '
' + escapeHtml(tt('step3.sub35.openTitle')) + '
' + + '
' + escapeHtml(address) + '
' + + '
' } else { - resultMsg.innerHTML = (result.message || tt('step3.sub35.manualHint')) + - tt('step3.sub35.manualDetail', { address: address }) - resultMsg.classList.add('warn') + // 안 열려 있으면 "직접 포트포워딩 해주세요" 를 크게 표시. + resultMsg.innerHTML = + '
' + + '
' + escapeHtml(tt('step3.sub35.notOpenBig')) + '
' + + '
' + escapeHtml(tt('step3.sub35.notOpenHint', { address: address, port: result.port })) + '
' + + '
' } nextBtn.disabled = false } catch (err) { diff --git a/locales/installer/ko-kr.json b/locales/installer/ko-kr.json index 79e7544..e1fe45f 100644 --- a/locales/installer/ko-kr.json +++ b/locales/installer/ko-kr.json @@ -124,14 +124,13 @@ }, "sub35": { "heading": "포트포워딩", - "description": "UPNP를 개방해 외부 접속을 허용합니다.", + "description": "포트가 외부에서 열려 있는지 확인합니다. (설치기가 직접 열지는 않습니다)", "portLabel": "포트", "recheck": "재점검", "checking": "확인 중...", - "preForwarded": "포트포워딩 성공! 친구는 {{address}} 주소로 서버에 접속할 수 있습니다. (이미 외부 개방되어 있음)", - "upnpOk": "포트포워딩 성공! 친구는 {{address}} 주소로 서버에 접속할 수 있습니다. (UPnP로 자동 개방 완료)", - "manualHint": "직접 포트포워딩을 해주세요.", - "manualDetail": "
외부 주소: {{address}}", + "openTitle": "포트가 열려 있습니다! 친구 접속 주소", + "notOpenBig": "직접 포트포워딩 해주세요.", + "notOpenHint": "공유기에서 외부 TCP {{port}} 를 이 PC 로 포워딩하세요. (외부 주소: {{address}})", "checkFailed": "점검 실패: {{message}}", "ipUnknown": "확인 불가" } diff --git a/package.json b/package.json index 1950912..ee10330 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minecraft-music-quiz-installer", - "version": "0.3.18", + "version": "0.3.19", "description": "마인크래프트 음악퀴즈 간편설치기 + 관리 사이트", "main": "dist/installer/main.js", "scripts": { diff --git a/src/installer/main.ts b/src/installer/main.ts index e390bc1..bb5c695 100644 --- a/src/installer/main.ts +++ b/src/installer/main.ts @@ -860,12 +860,7 @@ ipcMain.handle('server:portForward', async (_event, port: number): Promise 0 ? port : 25565 sendLog(t('log.portCheckStart', { port: targetPort })) - // 1차 점검 전에 우리가 이전 실행에서 만든 UPnP 매핑이 남아 있으면 먼저 제거한다. - // 이렇게 해야 "사용자 라우터 규칙이 활성화돼서 외부 접근이 가능한 상태" 와 "UPnP 매핑 덕분에 접근 가능한 상태" 가 구별된다. - // 사용자 규칙이 비활성/없으면 1차 점검은 false 가 되어 UPnP 시도 단계로 자연스럽게 넘어간다. - sendLog(t('log.upnpCleanup')) - await removeUpnpMapping(targetPort) - + // 설치기는 포트를 직접 열지 않는다. 외부에서 이미 열려 있는지만 확인한다. // 외부 IP 확보: 공용 API → 실패 시 UPnP 게이트웨이의 외부 IP로 폴백. let externalIp = await detectExternalIpHttp() if (externalIp) { @@ -877,9 +872,9 @@ ipcMain.handle('server:portForward', async (_event, port: number): Promise { @@ -1124,64 +1081,6 @@ function fetchIfconfigCoPortOnce(port: number): Promise { }) } -function removeUpnpMapping(port: number): Promise { - return new Promise((resolve) => { - let settled = false - const done = () => { if (!settled) { settled = true; resolve() } } - let client: ReturnType | null = null - try { - client = natUpnp.createClient() - } catch (err) { - sendLog(t('log.upnpClientFailRemove', { message: (err as Error).message })) - done() - return - } - const timer = setTimeout(() => { - try { client && client.close() } catch {} - sendLog(t('log.upnpRemoveTimeout')) - done() - }, 8000) - client.portUnmapping({ public: port, protocol: 'tcp' }, (err: Error | null) => { - clearTimeout(timer) - try { client && client.close() } catch {} - if (err) sendLog(t('log.upnpRemoveAttempt', { message: err.message })) - else sendLog(t('log.upnpRemoveDone', { port })) - done() - }) - }) -} - -function openPortViaUpnp(port: number): Promise { - return new Promise((resolve, reject) => { - let settled = false - const done = (err?: Error) => { - if (settled) return - settled = true - if (err) reject(err) - else resolve() - } - let client: ReturnType | null = null - try { - client = natUpnp.createClient() - } catch (err) { - done(err as Error) - return - } - const timer = setTimeout(() => { - try { client && client.close() } catch {} - done(new Error(t('errors.upnpTimeout'))) - }, 15000) - client.portMapping( - { public: port, private: port, ttl: 0, description: 'MusicQuiz Server', protocol: 'tcp' }, - (error: Error | null) => { - clearTimeout(timer) - try { client && client.close() } catch {} - done(error || undefined) - } - ) - }) -} - function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)) } diff --git a/src/installer/types.ts b/src/installer/types.ts index 8464792..8a6a4dd 100644 --- a/src/installer/types.ts +++ b/src/installer/types.ts @@ -36,7 +36,8 @@ export interface RamCheckResult { } export interface PortForwardResult { - status: 'preForwarded' | 'upnpOk' | 'upnpFailed' + // 설치기는 포트를 직접 열지 않고 "열려 있는지"만 확인한다. + status: 'open' | 'notOpen' externalIp?: string port: number message?: string