From f547593c6f031970dbea4e58bae33ef0517d5d3a Mon Sep 17 00:00:00 2001 From: tolelom <98kimsungmin@naver.com> Date: Tue, 24 Feb 2026 23:34:09 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20launcher.exe=EC=99=80=20game.zip=20?= =?UTF-8?q?=EB=B3=84=EB=8F=84=20=EC=97=85=EB=A1=9C=EB=93=9C/=EC=84=9C?= =?UTF-8?q?=EB=B9=99=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - POST /api/download/upload/game - 게임 zip 업로드 - POST /api/download/upload/launcher - launcher.exe 업로드 - GET /api/download/launcher - launcher.exe 서빙 - Info 모델에 LauncherURL, LauncherSize 필드 추가 - Content-Disposition 헤더로 올바른 파일명 설정 Co-Authored-By: Claude Sonnet 4.6 --- internal/download/handler.go | 24 ++++++++++++++++++++ internal/download/model.go | 4 +++- internal/download/service.go | 43 ++++++++++++++++++++++++++++++++++++ routes/routes.go | 4 +++- 4 files changed, 73 insertions(+), 2 deletions(-) diff --git a/internal/download/handler.go b/internal/download/handler.go index 27245e9..bb52267 100644 --- a/internal/download/handler.go +++ b/internal/download/handler.go @@ -45,5 +45,29 @@ func (h *Handler) ServeFile(c *fiber.Ctx) error { if _, err := os.Stat(path); err != nil { return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "파일이 없습니다"}) } + info, _ := h.svc.GetInfo() + filename := "game.zip" + if info != nil && info.FileName != "" { + filename = info.FileName + } + c.Set("Content-Disposition", `attachment; filename="`+filename+`"`) + return c.SendFile(path) +} + +func (h *Handler) UploadLauncher(c *fiber.Ctx) error { + body := c.Request().BodyStream() + info, err := h.svc.UploadLauncher(body, h.baseURL) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "업로드 실패: " + err.Error()}) + } + return c.JSON(info) +} + +func (h *Handler) ServeLauncher(c *fiber.Ctx) error { + path := h.svc.LauncherFilePath() + if _, err := os.Stat(path); err != nil { + return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "파일이 없습니다"}) + } + c.Set("Content-Disposition", `attachment; filename="launcher.exe"`) return c.SendFile(path) } diff --git a/internal/download/model.go b/internal/download/model.go index 8a26cd6..b539bbe 100644 --- a/internal/download/model.go +++ b/internal/download/model.go @@ -15,5 +15,7 @@ type Info struct { Version string `json:"version" gorm:"not null"` FileName string `json:"fileName" gorm:"not null"` FileSize string `json:"fileSize" gorm:"not null"` - FileHash string `json:"fileHash" gorm:"not null;default:''"` + FileHash string `json:"fileHash" gorm:"not null;default:''"` + LauncherURL string `json:"launcherUrl" gorm:"not null;default:''"` + LauncherSize string `json:"launcherSize" gorm:"not null;default:''"` } diff --git a/internal/download/service.go b/internal/download/service.go index f0311ea..5211e65 100644 --- a/internal/download/service.go +++ b/internal/download/service.go @@ -31,6 +31,49 @@ func (s *Service) GameFilePath() string { return filepath.Join(s.gameDir, "game.zip") } +func (s *Service) LauncherFilePath() string { + return filepath.Join(s.gameDir, "launcher.exe") +} + +func (s *Service) UploadLauncher(body io.Reader, baseURL string) (*Info, error) { + if err := os.MkdirAll(s.gameDir, 0755); err != nil { + return nil, fmt.Errorf("디렉토리 생성 실패: %w", err) + } + + finalPath := s.LauncherFilePath() + tmpPath := finalPath + ".tmp" + + f, err := os.Create(tmpPath) + if err != nil { + return nil, fmt.Errorf("파일 생성 실패: %w", err) + } + + n, err := io.Copy(f, body) + f.Close() + if err != nil { + os.Remove(tmpPath) + return nil, fmt.Errorf("파일 저장 실패: %w", err) + } + + if err := os.Rename(tmpPath, finalPath); err != nil { + os.Remove(tmpPath) + return nil, fmt.Errorf("파일 이동 실패: %w", err) + } + + launcherSize := "" + if n > 0 { + launcherSize = fmt.Sprintf("%.1f MB", float64(n)/1024/1024) + } + + info, err := s.repo.GetLatest() + if err != nil { + info = &Info{} + } + info.LauncherURL = baseURL + "/api/download/launcher" + info.LauncherSize = launcherSize + return info, s.repo.Save(info) +} + // Upload streams the body directly to disk, then extracts metadata from the zip. func (s *Service) Upload(filename string, body io.Reader, baseURL string) (*Info, error) { if err := os.MkdirAll(s.gameDir, 0755); err != nil { diff --git a/routes/routes.go b/routes/routes.go index 549475e..85ee43e 100644 --- a/routes/routes.go +++ b/routes/routes.go @@ -39,5 +39,7 @@ func Register( dl := api.Group("/download") dl.Get("/info", dlH.GetInfo) dl.Get("/file", dlH.ServeFile) - dl.Post("/upload", middleware.Auth, middleware.AdminOnly, dlH.Upload) + dl.Get("/launcher", dlH.ServeLauncher) + dl.Post("/upload/game", middleware.Auth, middleware.AdminOnly, dlH.Upload) + dl.Post("/upload/launcher", middleware.Auth, middleware.AdminOnly, dlH.UploadLauncher) }