feat: 오프라인 모드 + 다운로드 UX + 언인스톨 개선

오프라인 모드:
- 서버 미응답 시 설치된 게임 직접 실행 옵션

다운로드 UX:
- 속도(MB/s) + 남은 시간 표시 (초/분)

언인스톨:
- 게임 데이터 삭제 여부 사용자 선택
- --version 플래그 추가

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-18 10:51:18 +09:00
parent 66dc8a14de
commit a8d9ab9d36

56
main.go
View File

@@ -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)