From a8d9ab9d369586d2707e053fdc9a81327f10d32e Mon Sep 17 00:00:00 2001 From: tolelom <98kimsungmin@naver.com> Date: Wed, 18 Mar 2026 10:51:18 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=98=A4=ED=94=84=EB=9D=BC=EC=9D=B8=20?= =?UTF-8?q?=EB=AA=A8=EB=93=9C=20+=20=EB=8B=A4=EC=9A=B4=EB=A1=9C=EB=93=9C?= =?UTF-8?q?=20UX=20+=20=EC=96=B8=EC=9D=B8=EC=8A=A4=ED=86=A8=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 오프라인 모드: - 서버 미응답 시 설치된 게임 직접 실행 옵션 다운로드 UX: - 속도(MB/s) + 남은 시간 표시 (초/분) 언인스톨: - 게임 데이터 삭제 여부 사용자 선택 - --version 플래그 추가 Co-Authored-By: Claude Opus 4.6 (1M context) --- main.go | 56 +++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 51 insertions(+), 5 deletions(-) diff --git a/main.go b/main.go index 55028e2..24bdba5 100644 --- a/main.go +++ b/main.go @@ -515,6 +515,10 @@ func doDownload(downloadURL, destDir string) error { buf := make([]byte, 32*1024) + var lastSpeedUpdate time.Time + var lastBytes int64 + var speedBytesPerSec float64 + for { if downloadCancelled.Load() { tmpFile.Close() @@ -533,12 +537,34 @@ func doDownload(downloadURL, destDir string) error { os.Remove(tmpPath) return fmt.Errorf("다운로드 크기가 제한을 초과했습니다") } + + now := time.Now() + if now.Sub(lastSpeedUpdate) >= 500*time.Millisecond { + elapsed := now.Sub(lastSpeedUpdate).Seconds() + if elapsed > 0 { + speedBytesPerSec = float64(downloaded-lastBytes) / elapsed + } + lastBytes = downloaded + lastSpeedUpdate = now + } + if total > 0 { pct := int(downloaded * 100 / total) if pct > 100 { pct = 100 } - setProgress(fmt.Sprintf("다운로드 중... %d%%", pct), pct) + + speedMB := speedBytesPerSec / 1024 / 1024 + text := fmt.Sprintf("다운로드 중... %d%% (%.1f MB/s)", pct, speedMB) + if speedBytesPerSec > 0 { + remaining := float64(total-downloaded) / speedBytesPerSec + if remaining < 60 { + text = fmt.Sprintf("다운로드 중... %d%% (%.1f MB/s, %d초 남음)", pct, speedMB, int(remaining)) + } else { + text = fmt.Sprintf("다운로드 중... %d%% (%.1f MB/s, %d분 남음)", pct, speedMB, int(remaining/60)) + } + } + setProgress(text, pct) } } if err == io.EOF { @@ -853,10 +879,6 @@ func uninstall() error { return err } } - // 설치 디렉토리 삭제 (자기 자신은 실행 중이라 삭제 실패할 수 있음 — 무시) - if dir, err := installDir(); err == nil { - os.RemoveAll(dir) - } return nil } @@ -1049,6 +1071,20 @@ func handleURI(rawURI string) error { serverInfo, err := fetchServerInfo() if err != nil { + // 오프라인 모드: 게임이 이미 설치되어 있으면 직접 실행 + if _, statErr := os.Stat(gamePath); statErr == nil { + ret := msgBox("One of the plans", "서버에 연결할 수 없습니다.\n설치된 게임을 실행하시겠습니까?\n(업데이트 확인 불가)", mbYesNo|mbQ) + if ret == idYes { + cmd := exec.Command(gamePath) + cmd.Dir = gameDir + cmd.Env = append(os.Environ(), "A301_TOKEN="+token) + if err := cmd.Start(); err != nil { + return fmt.Errorf("게임 실행 실패: %w", err) + } + return nil + } + return fmt.Errorf("사용자가 취소했습니다") + } return fmt.Errorf("버전 확인 실패: %w", err) } @@ -1129,12 +1165,22 @@ func main() { msgBox("One of the plans 런처", "a301:// 프로토콜이 등록되었습니다.", mbOK|mbInfo) case arg == "uninstall": + ret := msgBox("One of the plans 런처", "게임 데이터도 함께 삭제하시겠습니까?", mbYesNo|mbQ) + deleteData := ret == idYes if err := uninstall(); err != nil { msgBox("One of the plans 런처 - 오류", fmt.Sprintf("제거 실패:\n%v", err), mbOK|mbError) os.Exit(1) } + if deleteData { + if dir, err := installDir(); err == nil { + os.RemoveAll(dir) + } + } msgBox("One of the plans 런처", "a301:// 프로토콜이 제거되었습니다.", mbOK|mbInfo) + case arg == "--version" || arg == "version": + msgBox("One of the plans 런처", "버전: 1.0.0", mbOK|mbInfo) + case strings.HasPrefix(arg, protocolName+"://"): if err := handleURI(arg); err != nil { msgBox("One of the plans 런처 - 오류", fmt.Sprintf("실행 실패:\n%v", err), mbOK|mbError)