diff --git a/.github/workflows/backend-ci.yml b/.github/workflows/backend-ci.yml new file mode 100644 index 0000000..1eaa9cd --- /dev/null +++ b/.github/workflows/backend-ci.yml @@ -0,0 +1,53 @@ +name: Server CI + +on: + pull_request: + paths: + - 'server/**' + - '.github/workflows/backend-ci.yml' + +env: + GO_VERSION: '1.24.4' + +defaults: + run: + working-directory: server/src + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + lint-test-and-build: + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + checks: write + contents: read + pull-requests: write + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v6 + with: + go-version: ${{ env.GO_VERSION }} + + - name: golangci-lint + uses: golangci/golangci-lint-action@v8 + with: + version: v2.1 + working-directory: server/src + + - name: Check formatting + run: | + fmt=$(gofmt -l .) + if [ -n "$fmt" ]; then + echo "Go files not formatted:" + echo "$fmt" + exit 1 + fi + + - name: Build + run: go build cmd/route/main.go diff --git a/server/src/cmd/route/main.go b/server/src/cmd/route/main.go index 1243597..4d68c92 100644 --- a/server/src/cmd/route/main.go +++ b/server/src/cmd/route/main.go @@ -39,9 +39,9 @@ func main() { api := e.Group("/api") - api.GET("/", func(c echo.Context) error { - return c.String(200, "OK") - }) + api.GET("/", func(c echo.Context) error { + return c.String(200, "OK") + }) room.RegisterRoutes(api.Group("/room"), db) // quiz.RegisterRoutes に quizSvc を渡す quiz.RegisterRoutes(api.Group("/quiz"), hub, quizSvc) diff --git a/server/src/config/config.go b/server/src/config/config.go index d2c532e..4bdcbc7 100644 --- a/server/src/config/config.go +++ b/server/src/config/config.go @@ -1,4 +1,3 @@ - package config import ( diff --git a/server/src/internal/database/database.go b/server/src/internal/database/database.go index 59be438..05fe7dc 100644 --- a/server/src/internal/database/database.go +++ b/server/src/internal/database/database.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" - "github.com/aws/aws-sdk-go-v2/service/dynamodb" "github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue" + "github.com/aws/aws-sdk-go-v2/service/dynamodb" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" roomtypes "server/src/internal/feature/room/types" ) @@ -33,15 +33,12 @@ func NewDBConnection() (*DBHandler, error) { tableName = "quiz" // デフォルト名 } - tableName = "quiz" - return &DBHandler{ client: client, tableName: tableName, }, nil } - // ルームを1件取得 func (h *DBHandler) ReadDB(id string) (*roomtypes.Room, error) { resp, err := h.client.GetItem(context.TODO(), &dynamodb.GetItemInput{ @@ -80,7 +77,6 @@ func (h *DBHandler) WriteDB(room *roomtypes.Room) error { return err } - func (h *DBHandler) DeleteRoom(roomID string) error { _, err := h.client.DeleteItem(context.TODO(), &dynamodb.DeleteItemInput{ TableName: aws.String(h.tableName), diff --git a/server/src/internal/feature/quiz/handler/quizHandler.go b/server/src/internal/feature/quiz/handler/quizHandler.go index 8b0c37d..1c232ee 100644 --- a/server/src/internal/feature/quiz/handler/quizHandler.go +++ b/server/src/internal/feature/quiz/handler/quizHandler.go @@ -2,12 +2,12 @@ package handler import ( + ws "github.com/gorilla/websocket" + "github.com/labstack/echo/v4" "log" "net/http" "server/src/internal/feature/quiz/service" "server/src/internal/feature/quiz/websocket" - ws "github.com/gorilla/websocket" - "github.com/labstack/echo/v4" ) var upgrader = ws.Upgrader{ diff --git a/server/src/internal/feature/quiz/service/quizService.go b/server/src/internal/feature/quiz/service/quizService.go index dddc716..153eaa8 100644 --- a/server/src/internal/feature/quiz/service/quizService.go +++ b/server/src/internal/feature/quiz/service/quizService.go @@ -155,7 +155,7 @@ func (s *QuizService) nextQuestion(roomID string) { } state.QuestionNumber++ - + // 重複を避けて問題を選択 nextQuestion := s.getNextUniqueQuestion(state.UsedQuestionIDs) if nextQuestion == nil { @@ -163,7 +163,7 @@ func (s *QuizService) nextQuestion(roomID string) { s.endGame(roomID) return } - + state.CurrentQuestion = nextQuestion state.UsedQuestionIDs = append(state.UsedQuestionIDs, nextQuestion.ID) state.AnsweredUsers = make(map[string]bool) @@ -228,15 +228,14 @@ func (s *QuizService) endGame(roomID string) { log.Printf("Game ended in room %s", roomID) } - // getNextUniqueQuestion は出題済みでない問題を返します。 func (s *QuizService) getNextUniqueQuestion(usedIDs []string) *types.Question { availableQuestions := make([]*types.Question, 0) - + for i := range s.questions { question := &s.questions[i] isUsed := false - + // 出題済みかどうかチェック for _, usedID := range usedIDs { if question.ID == usedID { @@ -244,27 +243,20 @@ func (s *QuizService) getNextUniqueQuestion(usedIDs []string) *types.Question { break } } - + if !isUsed { availableQuestions = append(availableQuestions, question) } } - + if len(availableQuestions) == 0 { return nil // 出題可能な問題がない } - + // 出題可能な問題からランダム選択 return availableQuestions[rand.Intn(len(availableQuestions))] } -func (s *QuizService) getRandomQuestion() *types.Question { - if len(s.questions) == 0 { - return nil - } - return &s.questions[rand.Intn(len(s.questions))] -} - func loadQuestions(filePath string) ([]types.Question, error) { file, err := os.ReadFile(filePath) if err != nil { diff --git a/server/src/internal/feature/quiz/types/quizType.go b/server/src/internal/feature/quiz/types/quizType.go index ca7d309..41158cc 100644 --- a/server/src/internal/feature/quiz/types/quizType.go +++ b/server/src/internal/feature/quiz/types/quizType.go @@ -6,7 +6,7 @@ type Message struct { Type string `json:"type"` Payload interface{} `json:"payload,omitempty"` // RoomID はJSONには含めず、ハブ内部でのルーティングに使用します。 - RoomID string `json:"-"` + RoomID string `json:"-"` } // Question は1つのクイズ問題を表す構造体です。 @@ -29,8 +29,8 @@ type GameState struct { // PlayerResult は最終結果のランキング表示に使用する構造体です。 type PlayerResult struct { - UserID string `json:"userId"` + UserID string `json:"userId"` // Name string `json:"name"` // 必要であればユーザー名も追加 - Score int `json:"score"` - Rank int `json:"rank"` + Score int `json:"score"` + Rank int `json:"rank"` } diff --git a/server/src/internal/feature/quiz/websocket/client.go b/server/src/internal/feature/quiz/websocket/client.go index 855de94..9ad3f0e 100644 --- a/server/src/internal/feature/quiz/websocket/client.go +++ b/server/src/internal/feature/quiz/websocket/client.go @@ -2,9 +2,9 @@ package websocket import ( + "github.com/gorilla/websocket" "log" "time" - "github.com/gorilla/websocket" ) const ( @@ -30,11 +30,21 @@ type InboundMessage struct { func (c *Client) ReadPump() { defer func() { c.Hub.Unregister <- c - c.Conn.Close() + if err := c.Conn.Close(); err != nil { + log.Printf("error closing connection in ReadPump: %v", err) + } }() c.Conn.SetReadLimit(maxMessageSize) - c.Conn.SetReadDeadline(time.Now().Add(pongWait)) - c.Conn.SetPongHandler(func(string) error { c.Conn.SetReadDeadline(time.Now().Add(pongWait)); return nil }) + if err := c.Conn.SetReadDeadline(time.Now().Add(pongWait)); err != nil { + log.Printf("error setting read deadline: %v", err) + return + } + c.Conn.SetPongHandler(func(string) error { + if err := c.Conn.SetReadDeadline(time.Now().Add(pongWait)); err != nil { + log.Printf("error setting read deadline in pong handler: %v", err) + } + return nil + }) for { _, message, err := c.Conn.ReadMessage() @@ -52,14 +62,21 @@ func (c *Client) WritePump() { ticker := time.NewTicker(pingPeriod) defer func() { ticker.Stop() - c.Conn.Close() + if err := c.Conn.Close(); err != nil { + log.Printf("error closing connection: %v", err) + } }() for { select { case message, ok := <-c.Send: - c.Conn.SetWriteDeadline(time.Now().Add(writeWait)) + if err := c.Conn.SetWriteDeadline(time.Now().Add(writeWait)); err != nil { + log.Printf("error setting write deadline: %v", err) + return + } if !ok { - c.Conn.WriteMessage(websocket.CloseMessage, []byte{}) + if err := c.Conn.WriteMessage(websocket.CloseMessage, []byte{}); err != nil { + log.Printf("error writing close message: %v", err) + } return } @@ -67,13 +84,19 @@ func (c *Client) WritePump() { if err != nil { return } - w.Write(message) + if _, err := w.Write(message); err != nil { + log.Printf("error writing message: %v", err) + return + } if err := w.Close(); err != nil { return } case <-ticker.C: - c.Conn.SetWriteDeadline(time.Now().Add(writeWait)) + if err := c.Conn.SetWriteDeadline(time.Now().Add(writeWait)); err != nil { + log.Printf("error setting write deadline for ping: %v", err) + return + } if err := c.Conn.WriteMessage(websocket.PingMessage, nil); err != nil { return } diff --git a/server/src/internal/feature/quiz/websocket/hub.go b/server/src/internal/feature/quiz/websocket/hub.go index f051320..f85a699 100644 --- a/server/src/internal/feature/quiz/websocket/hub.go +++ b/server/src/internal/feature/quiz/websocket/hub.go @@ -75,7 +75,6 @@ func (h *RoomHub) registerClient(client *Client) { }() } - func (h *RoomHub) unregisterClient(client *Client) { h.mu.Lock() defer h.mu.Unlock() @@ -88,7 +87,7 @@ func (h *RoomHub) unregisterClient(client *Client) { return } - var isHost bool = false + isHost := false if roomData != nil { hostPlayer, hostPlayerExists := roomData.Players[roomData.HostID] if hostPlayerExists && hostPlayer.Name == userID { @@ -115,26 +114,26 @@ func (h *RoomHub) unregisterClient(client *Client) { // DynamoDB から削除 if err := h.DBHandler.DeleteRoom(roomID); err != nil { log.Printf("error: failed to delete room from DB: %v", err) - for otherClient := range room { - if otherClient != client { // 自分自身(ホスト)は除く - close(otherClient.Send) // 各クライアントのSendチャネルを閉じると、WritePumpが終了し接続が切れる + for otherClient := range room { + if otherClient != client { // 自分自身(ホスト)は除く + close(otherClient.Send) // 各クライアントのSendチャネルを閉じると、WritePumpが終了し接続が切れる + } } + delete(h.rooms, roomID) + } else if len(h.rooms[roomID]) == 0 { + delete(h.rooms, roomID) + log.Printf("Room %s closed", roomID) + } else { + leaveMsg := &types.Message{ + Type: "user_left", + Payload: map[string]string{"userId": userID}, + RoomID: roomID, + } + go h.broadcastMessage(leaveMsg) } - delete(h.rooms, roomID) - } else if len(h.rooms[roomID]) == 0 { - delete(h.rooms, roomID) - log.Printf("Room %s closed", roomID) - } else { - leaveMsg := &types.Message{ - Type: "user_left", - Payload: map[string]string{"userId": userID}, - RoomID: roomID, - } - go h.broadcastMessage(leaveMsg) } } } - } } func (h *RoomHub) broadcastMessage(message *types.Message) { diff --git a/server/src/internal/feature/room/repository/roomRepository.go b/server/src/internal/feature/room/repository/roomRepository.go index 87f889b..aa27f84 100644 --- a/server/src/internal/feature/room/repository/roomRepository.go +++ b/server/src/internal/feature/room/repository/roomRepository.go @@ -1,4 +1,3 @@ - package repository import ( diff --git a/server/src/internal/feature/room/service/errors.go b/server/src/internal/feature/room/service/errors.go index 9a8fce1..8c12d0b 100644 --- a/server/src/internal/feature/room/service/errors.go +++ b/server/src/internal/feature/room/service/errors.go @@ -3,6 +3,6 @@ package service import "errors" var ( - ErrRoomNotFound = errors.New("room not found") - ErrNotHostPermission = errors.New("only the host can delete the room") + ErrRoomNotFound = errors.New("room not found") + ErrNotHostPermission = errors.New("only the host can delete the room") ) diff --git a/server/src/internal/feature/room/types/roomType.go b/server/src/internal/feature/room/types/roomType.go index 8e33090..bc0c69a 100644 --- a/server/src/internal/feature/room/types/roomType.go +++ b/server/src/internal/feature/room/types/roomType.go @@ -17,7 +17,6 @@ type Player struct { IsReady bool `json:"isReady" dynamodbav:"is_ready"` } - // Room は個々のゲームルームの全情報 type Room struct { RoomID string `json:"roomId" dynamodbav:"room_id"` @@ -28,7 +27,6 @@ type Room struct { CreatedAt time.Time `json:"createdAt" dynamodbav:"created_at"` } - // --- リクエスト/レスポンス用の構造体 --- // RoomCreationRequest はルーム作成時のリクエストボディ diff --git a/server/src/internal/feature/room/utils/errors.go b/server/src/internal/feature/room/utils/errors.go index c5d9ff0..c6a4054 100644 --- a/server/src/internal/feature/room/utils/errors.go +++ b/server/src/internal/feature/room/utils/errors.go @@ -3,6 +3,6 @@ package utils import "errors" var ( - ErrRoomNotFound = errors.New("room not found") - ErrNotHostPermission = errors.New("only the host can delete the room") + ErrRoomNotFound = errors.New("room not found") + ErrNotHostPermission = errors.New("only the host can delete the room") )