Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat : 채팅방과 관련된 서비스 구현을 진행합니다. #91

Merged
merged 12 commits into from
Oct 1, 2024
3 changes: 3 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ output:
# This file contains only configs which differ from defaults.
# All possible options can be found here https://github.com/golangci/golangci-lint/blob/master/.golangci.reference.yml
linters-settings:
gosec:
excludes:
- G115
cyclop:
# The maximal code complexity to report.
# Default: 10
Expand Down
40 changes: 40 additions & 0 deletions api/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,43 @@ func ParsePaginationQueries(c echo.Context, defaultPage, defaultLimit int) (page

return page, size, nil
}

func ParseCursorPaginationQueries(c echo.Context, defaultLimit int) (prev, next, limit int, err *AppError) {
prevQuery := c.QueryParam("prev")
nextQuery := c.QueryParam("next")
sizeQuery := c.QueryParam("size")

if prevQuery == "" && nextQuery == "" {
return 0, 0, 0, ErrInvalidPagination(errors.New("expected either prev or next query"))
}

if prevQuery != "" {
var atoiError error
prev, atoiError = strconv.Atoi(prevQuery)
if atoiError != nil || prev <= 0 {
return 0, 0, 0, ErrInvalidPagination(errors.New("expected integer value bigger than 0 for query: prev"))
}
}

if nextQuery != "" {
var atoiError error
next, atoiError = strconv.Atoi(nextQuery)
if atoiError != nil || next <= 0 {
return 0, 0, 0, ErrInvalidPagination(errors.New("expected integer value bigger than 0 for query: next"))
}
}
Comment on lines +103 to +117
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prev, next는 optional string 이어야 해요. 지금은 optional integer 군요

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

맞아요 id값 uuid으로 마이그레이션 진행될 경우 변경할 예정입니다.

현재는, integer 값으로 설정되어있어 임시로 처리한 상황입니다

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

chat 쪽은 마이그레이션 대상이 아니었긴 한데... 한번에 해도 괜찮을 것 같긴 하네요
그럼 한번에 epic 브랜치에서 리베이스해서 다시 마이그레이션 PR 업데이트해볼게요


if sizeQuery != "" {
var atoiError error
limit, atoiError = strconv.Atoi(sizeQuery)
if atoiError != nil || limit <= 0 {
return 0, 0, 0, ErrInvalidPagination(errors.New("expected integer value bigger than 0 for query: size"))
}
}

if limit == 0 {
limit = defaultLimit
}

return prev, next, limit, nil
}
219 changes: 167 additions & 52 deletions cmd/server/handler/chat_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,81 +3,196 @@ package handler
import (
"net/http"

"github.com/gorilla/websocket"
"github.com/labstack/echo/v4"

"github.com/pet-sitter/pets-next-door-api/internal/chat"
"github.com/pet-sitter/pets-next-door-api/internal/domain/user"
pnd "github.com/pet-sitter/pets-next-door-api/api"
domain "github.com/pet-sitter/pets-next-door-api/internal/domain/chat"
"github.com/pet-sitter/pets-next-door-api/internal/service"
)

type ChatHandler struct {
wsServer *chat.WebSocketServer
upgrader websocket.Upgrader
stateManager *chat.StateManager
authService service.AuthService
chatService service.ChatService
}

var upgrader = websocket.Upgrader{
// 사이즈 단위: byte
// 영어 기준: 2048 글자
// 한국어(글자당 3바이트) 기준: 약 682 글자
// 영어 + 한국어 기준(글자당 2바이트로 가정): 약 1024 글자
ReadBufferSize: 2048,
WriteBufferSize: 2048,
authService service.AuthService
chatService service.ChatService
}

func NewChatController(
wsServer *chat.WebSocketServer,
stateManager chat.StateManager,
func NewChatHandler(
authService service.AuthService,
chatService service.ChatService,
) *ChatHandler {
return &ChatHandler{
wsServer: wsServer,
upgrader: upgrader,
stateManager: &stateManager,
authService: authService,
chatService: chatService,
authService: authService,
chatService: chatService,
}
}

func (h *ChatHandler) ServerWebsocket(
c echo.Context, w http.ResponseWriter, r *http.Request,
) error {
// FindRoomByID godoc
// @Summary 채팅방을 조회합니다.
// @Description
// @Tags chat
// @Accept json
// @Produce json
// @Param roomID path int true "채팅방 ID"
// @Security FirebaseAuth
// @Success 200 {object} domain.RoomSimpleInfo
// @Router /chat/rooms/{roomID} [get]
func (h ChatHandler) FindRoomByID(c echo.Context) error {
foundUser, err := h.authService.VerifyAuthAndGetUser(c.Request().Context(), c.Request().Header.Get("Authorization"))
if err != nil {
return c.JSON(err.StatusCode, err)
}

conn, err2 := upgrader.Upgrade(w, r, nil)
if err2 != nil {
return c.JSON(http.StatusInternalServerError, map[string]interface{}{
"error": err2.Error(),
})
roomID, err := pnd.ParseIDFromPath(c, "roomID")
if err != nil {
return c.JSON(err.StatusCode, err)
}

res, err := h.chatService.FindChatRoomByUIDAndRoomID(c.Request().Context(), foundUser.FirebaseUID, int64(*roomID))
if err != nil {
return c.JSON(err.StatusCode, err)
}

return c.JSON(http.StatusOK, res)
}

// CreateRoom godoc
// @Summary 채팅방을 생성합니다.
// @Description
// @Tags chat
// @Accept json
// @Produce json
// @Param request body domain.CreateRoomRequest true "채팅방 생성 요청"
// @Security FirebaseAuth
// @Success 201 {object} domain.RoomSimpleInfo
// @Router /chat/rooms [post]
func (h ChatHandler) CreateRoom(c echo.Context) error {
var createRoomRequest domain.CreateRoomRequest

if err := pnd.ParseBody(c, &createRoomRequest); err != nil {
return c.JSON(err.StatusCode, err)
}

res, err := h.chatService.CreateRoom(
c.Request().Context(),
createRoomRequest.RoomName,
createRoomRequest.RoomType,
createRoomRequest.JoinUserIDs,
)
if err != nil {
return c.JSON(err.StatusCode, err)
}

return c.JSON(http.StatusCreated, res)
}

// JoinChatRoom godoc
// @Summary 채팅방에 참가합니다.
// @Description 채팅방에 참가합니다.
// @Tags chat
// @Accept json
// @Produce json
// @Param roomID path int true "채팅방 ID"
// @Security FirebaseAuth
// @Success 200 {object} domain.JoinRoomsView
// @Router /chat/rooms/{roomID}/join [post]
func (h ChatHandler) JoinChatRoom(c echo.Context) error {
foundUser, err := h.authService.VerifyAuthAndGetUser(c.Request().Context(), c.Request().Header.Get("Authorization"))
if err != nil {
return c.JSON(err.StatusCode, err)
}

client := h.initializeOrUpdateClient(conn, foundUser)
roomID, err := pnd.ParseIDFromPath(c, "roomID")
if err != nil {
return c.JSON(err.StatusCode, err)
}

// 클라이언트의 메시지를 읽고 쓰는 데 사용되는 고루틴을 시작 (비동기)
go client.HandleWrite()
go client.HandleRead(*h.stateManager, &h.chatService)
res, err := h.chatService.JoinRoom(c.Request().Context(), int64(*roomID), foundUser.FirebaseUID)
if err != nil {
return c.JSON(err.StatusCode, err)
}

return nil
return c.JSON(http.StatusOK, res)
}

// 클라이언트를 초기화하거나 기존 클라이언트를 업데이트하는 함수
func (h *ChatHandler) initializeOrUpdateClient(
conn *websocket.Conn, userData *user.InternalView,
) *chat.Client {
client := h.wsServer.StateManager.FindClientByUID(userData.FirebaseUID)
if client == nil {
client = chat.NewClient(conn, userData.Nickname, userData.FirebaseUID)
h.wsServer.StateManager.RegisterClient(client)
} else {
// 기존 클라이언트가 있는 경우 연결을 업데이트
client.UpdateConn(conn)
}
return client
// LeaveChatRoom godoc
// @Summary 채팅방을 나갑니다.
// @Description 채팅방을 나갑니다.
// @Tags chat
// @Accept json
// @Produce json
// @Param roomID path int true "채팅방 ID"
// @Security FirebaseAuth
// @Success 200
// @Router /chat/rooms/{roomID}/leave [post]
func (h ChatHandler) LeaveChatRoom(c echo.Context) error {
foundUser, err := h.authService.VerifyAuthAndGetUser(c.Request().Context(), c.Request().Header.Get("Authorization"))
if err != nil {
return c.JSON(err.StatusCode, err)
}

roomID, err := pnd.ParseIDFromPath(c, "roomID")
if err != nil {
return c.JSON(err.StatusCode, err)
}

res := h.chatService.LeaveRoom(c.Request().Context(), int64(*roomID), foundUser.FirebaseUID)
return c.JSON(http.StatusOK, res)
}

// FindAllRooms godoc
// @Summary 사용자의 채팅방 목록을 조회합니다.
// @Description 사용자의 채팅방 목록을 조회합니다.
// @Tags chat
// @Accept json
// @Produce json
// @Security FirebaseAuth
// @Success 200 {object} []domain.JoinRoomsView
// @Router /chat/rooms [get]
func (h ChatHandler) FindAllRooms(c echo.Context) error {
foundUser, err := h.authService.VerifyAuthAndGetUser(c.Request().Context(), c.Request().Header.Get("Authorization"))
if err != nil {
return c.JSON(err.StatusCode, err)
}

rooms, err := h.chatService.FindAllByUserUID(c.Request().Context(), foundUser.FirebaseUID)
if err != nil {
return c.JSON(err.StatusCode, err)
}
return c.JSON(http.StatusOK, rooms)
}

// FindMessagesByRoomID godoc
// @Summary 채팅방의 메시지 목록을 조회합니다.
// @Description 채팅방의 메시지 목록을 조회합니다.
// @Tags chat
// @Accept json
// @Produce json
// @Param roomID path int true "채팅방 ID"
// @Param prev query int false "이전 페이지"
// @Param next query int false "다음 페이지"
// @Param size query int false "페이지 사이즈" default(30)
// @Security FirebaseAuth
// @Success 200 {object} domain.MessageCursorView
// @Router /chat/rooms/{roomID}/messages [get]
func (h ChatHandler) FindMessagesByRoomID(c echo.Context) error {
roomID, err := pnd.ParseIDFromPath(c, "roomID")
if err != nil {
return c.JSON(err.StatusCode, err)
}

prev, next, limit, appError := pnd.ParseCursorPaginationQueries(c, 30)
if appError != nil {
return c.JSON(appError.StatusCode, appError)
}

res, err := h.chatService.FindChatRoomMessagesByRoomID(
c.Request().Context(),
int64(*roomID),
int64(prev),
int64(next),
int64(limit),
)
if err != nil {
return c.JSON(err.StatusCode, err)
}

return c.JSON(http.StatusOK, res)
}
18 changes: 9 additions & 9 deletions cmd/server/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ func NewRouter(app *firebaseinfra.FirebaseApp) (*echo.Echo, error) {
breedService := service.NewBreedService(db)
sosPostService := service.NewSOSPostService(db)
conditionService := service.NewSOSConditionService(db)
// chatService := service.NewChatService(db)
chatService := service.NewChatService(db)

// Initialize handlers
authHandler := handler.NewAuthHandler(authService, kakaoinfra.NewKakaoDefaultClient())
Expand All @@ -64,6 +64,7 @@ func NewRouter(app *firebaseinfra.FirebaseApp) (*echo.Echo, error) {
breedHandler := handler.NewBreedHandler(*breedService)
sosPostHandler := handler.NewSOSPostHandler(*sosPostService, authService)
conditionHandler := handler.NewConditionHandler(*conditionService)
chatHandler := handler.NewChatHandler(authService, *chatService)

// // InMemoryStateManager는 클라이언트와 채팅방의 상태를 메모리에 저장하고 관리합니다.
// // 이 메서드는 단순하고 빠르며 테스트 목적으로 적합합니다.
Expand All @@ -72,7 +73,7 @@ func NewRouter(app *firebaseinfra.FirebaseApp) (*echo.Echo, error) {
// wsServer := chat.NewWebSocketServer(stateManager)
// go wsServer.Run()
// chat.InitializeWebSocketServer(ctx, wsServer, chatService)
// chatHandler := handler.NewChatController(wsServer, stateManager, authService, *chatService)
// chatHandler := handler.NewChatHandler(wsServer, stateManager, authService, *chatService)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기 지워도 될 듯합니다

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NewChatHandler 말씀하시는거 맞을까요?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 주석이기도 하고 이미 chatHandler 생성을 위에서 하셔서 여긴 필요 없을 것 같아요!


// RegisterChan middlewares
logger := zerolog.New(os.Stdout)
Expand Down Expand Up @@ -142,13 +143,6 @@ func NewRouter(app *firebaseinfra.FirebaseApp) (*echo.Echo, error) {
postAPIGroup.GET("/sos/conditions", conditionHandler.FindConditions)
}

// chatAPIGroup := apiRouteGroup.Group("/chat")
// {
// chatAPIGroup.GET("/ws", func(c echo.Context) error {
// return chatHandler.ServerWebsocket(c, c.Response().Writer, c.Request())
// })
// }

upgrader := wschat.NewDefaultUpgrader()
wsServerV2 := wschat.NewWSServer(upgrader, authService, *mediaService)

Expand All @@ -157,6 +151,12 @@ func NewRouter(app *firebaseinfra.FirebaseApp) (*echo.Echo, error) {
chatAPIGroup := apiRouteGroup.Group("/chat")
{
chatAPIGroup.GET("/ws", wsServerV2.HandleConnections)
chatAPIGroup.POST("/rooms", chatHandler.CreateRoom)
chatAPIGroup.PUT("/rooms/:roomID/join", chatHandler.JoinChatRoom)
chatAPIGroup.PUT("/rooms/:roomID/leave", chatHandler.LeaveChatRoom)
chatAPIGroup.GET("/rooms/:roomID", chatHandler.FindRoomByID)
chatAPIGroup.GET("/rooms", chatHandler.FindAllRooms)
chatAPIGroup.GET("/rooms/:roomID/messages", chatHandler.FindMessagesByRoomID)
}

return e, nil
Expand Down
Loading