Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions .github/workflows/cd-prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ jobs:
with:
context: .
file: Docker/Dockerfile
platforms: linux/amd64
push: true
tags: |
${{ secrets.DOCKERHUB_USERNAME }}/realmatch-backend:prod
Expand All @@ -48,16 +49,15 @@ jobs:
key: ${{ secrets.PROD_SERVER_SSH_KEY }}
script: |
cd /home/ubuntu/realmatch
git pull origin main

docker compose pull
docker compose up -d
docker-compose pull
docker-compose up -d

echo "Waiting for app to start..."
sleep 10
if ! docker compose ps | grep "Up"; then

if ! docker-compose ps | grep "Up"; then
echo "❌ Container is not running"
docker compose logs
docker-compose logs
exit 1
fi
fi
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-websocket'
implementation 'org.springframework.boot:spring-boot-starter-validation'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
Expand Down
7 changes: 3 additions & 4 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,11 @@ services:
- "6379:6379"

app:
build:
context: .
dockerfile: Docker/Dockerfile
image: ${DOCKERHUB_USERNAME}/realmatch-backend:prod
container_name: spring_app
restart: always
ports:
- "6000:6000"
- "8080:6000"
depends_on:
- db
- redis
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.example.RealMatch.chat.presentation.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;

import com.example.RealMatch.chat.presentation.conversion.MessageCursor;
import com.example.RealMatch.chat.presentation.conversion.RoomCursor;

/**
* cursor 요청 파라미터를 RoomCursor/MessageCursor로 바인딩하는 설정
*/
@Configuration
public class ChatCursorConverterConfig {

@Bean
public Converter<String, RoomCursor> roomCursorConverter() {
return RoomCursor::decode;
}

@Bean
public Converter<String, MessageCursor> messageCursorConverter() {
return MessageCursor::decode;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.example.RealMatch.chat.presentation.config;

import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.lang.NonNull;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.server.HandshakeHandler;

@Configuration
@EnableWebSocketMessageBroker
public class ChatWebSocketConfig implements WebSocketMessageBrokerConfigurer {

@Value("${cors.allowed-origin}")
private String allowedOrigin;
private final ObjectProvider<HandshakeHandler> handshakeHandlerProvider;

public ChatWebSocketConfig(ObjectProvider<HandshakeHandler> handshakeHandlerProvider) {
this.handshakeHandlerProvider = handshakeHandlerProvider;
}

@Override
public void registerStompEndpoints(@NonNull StompEndpointRegistry registry) {
var registration = registry.addEndpoint("/ws/chat")
.setAllowedOrigins(allowedOrigin);
HandshakeHandler handshakeHandler = handshakeHandlerProvider.getIfAvailable();
if (handshakeHandler != null) {
registration.setHandshakeHandler(handshakeHandler);
}
}

@Override
public void configureMessageBroker(@NonNull MessageBrokerRegistry registry) {
registry.setApplicationDestinationPrefixes("/app");
registry.setUserDestinationPrefix("/user");
registry.enableSimpleBroker("/topic", "/queue");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package com.example.RealMatch.chat.presentation.controller;

import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import com.example.RealMatch.chat.presentation.controller.fixture.ChatFixtureFactory;
import com.example.RealMatch.chat.presentation.conversion.MessageCursor;
import com.example.RealMatch.chat.presentation.conversion.RoomCursor;
import com.example.RealMatch.chat.presentation.dto.enums.ChatRoomFilterStatus;
import com.example.RealMatch.chat.presentation.dto.enums.ChatRoomSort;
import com.example.RealMatch.chat.presentation.dto.enums.ChatRoomTab;
import com.example.RealMatch.chat.presentation.dto.request.ChatAttachmentUploadRequest;
import com.example.RealMatch.chat.presentation.dto.request.ChatRoomCreateRequest;
import com.example.RealMatch.chat.presentation.dto.response.ChatAttachmentUploadResponse;
import com.example.RealMatch.chat.presentation.dto.response.ChatMessageListResponse;
import com.example.RealMatch.chat.presentation.dto.response.ChatRoomCreateResponse;
import com.example.RealMatch.chat.presentation.dto.response.ChatRoomDetailResponse;
import com.example.RealMatch.chat.presentation.dto.response.ChatRoomListResponse;
import com.example.RealMatch.chat.presentation.swagger.ChatSwagger;
import com.example.RealMatch.global.config.jwt.CustomUserDetails;
import com.example.RealMatch.global.presentation.CustomResponse;

import jakarta.validation.Valid;

@RestController
@RequestMapping("/api/chat")
public class ChatController implements ChatSwagger {

@PostMapping("/rooms")
public CustomResponse<ChatRoomCreateResponse> createOrGetRoom(
@AuthenticationPrincipal CustomUserDetails user,
@Valid @RequestBody ChatRoomCreateRequest request
) {
return CustomResponse.ok(ChatFixtureFactory.sampleRoomCreateResponse());
}

@GetMapping("/rooms")
public CustomResponse<ChatRoomListResponse> getRoomList(
@AuthenticationPrincipal CustomUserDetails user,
@RequestParam(required = false) ChatRoomTab tab,
@RequestParam(name = "status", required = false) ChatRoomFilterStatus filterStatus,
@RequestParam(required = false) ChatRoomSort sort,
@RequestParam(name = "cursor", required = false) RoomCursor roomCursor,
@RequestParam(defaultValue = "20") int size
) {
return CustomResponse.ok(ChatFixtureFactory.sampleRoomListResponse());
}

@GetMapping("/rooms/{roomId}")
public CustomResponse<ChatRoomDetailResponse> getRoomDetail(
@AuthenticationPrincipal CustomUserDetails user,
@PathVariable Long roomId
) {
return CustomResponse.ok(ChatFixtureFactory.sampleRoomDetailResponse(roomId));
}

@GetMapping("/rooms/{roomId}/messages")
public CustomResponse<ChatMessageListResponse> getMessages(
@AuthenticationPrincipal CustomUserDetails user,
@PathVariable Long roomId,
@RequestParam(name = "cursor", required = false) MessageCursor messageCursor,
@RequestParam(defaultValue = "20") int size
) {
return CustomResponse.ok(ChatFixtureFactory.sampleMessageListResponse(roomId));
}

@PostMapping("/attachments")
public CustomResponse<ChatAttachmentUploadResponse> uploadAttachment(
@AuthenticationPrincipal CustomUserDetails user,
@Valid @RequestPart("request") ChatAttachmentUploadRequest request,
@RequestPart("file") MultipartFile file
) {
return CustomResponse.ok(ChatFixtureFactory.sampleAttachmentUploadResponse(request, file));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.example.RealMatch.chat.presentation.controller;

import java.util.Objects;

import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.messaging.simp.annotation.SendToUser;
import org.springframework.stereotype.Controller;

import com.example.RealMatch.chat.presentation.controller.fixture.ChatSocketFixtureFactory;
import com.example.RealMatch.chat.presentation.dto.websocket.ChatMessageCreatedEvent;
import com.example.RealMatch.chat.presentation.dto.websocket.ChatSendMessageAck;
import com.example.RealMatch.chat.presentation.dto.websocket.ChatSendMessageCommand;

import jakarta.validation.Valid;

@Controller
public class ChatSocketController {

private final SimpMessagingTemplate messagingTemplate;

public ChatSocketController(SimpMessagingTemplate messagingTemplate) {
this.messagingTemplate = messagingTemplate;
}

@MessageMapping("/chat.send")
@SendToUser("/queue/chat.ack")
public ChatSendMessageAck sendMessage(@Valid @Payload ChatSendMessageCommand command) {
ChatMessageCreatedEvent event = ChatSocketFixtureFactory.sampleMessageCreatedEvent(command);
messagingTemplate.convertAndSend("/topic/rooms/" + command.roomId(), Objects.requireNonNull(event));
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

Objects.requireNonNull(event) 호출은 불필요해 보입니다. ChatSocketFixtureFactory.sampleMessageCreatedEvent(command) 메소드는 항상 새로운 non-null 객체를 반환하므로, 이 null 체크는 현재로서는 아무런 효과가 없습니다. 코드를 더 명확하게 만들기 위해 이 부분을 제거하는 것이 좋습니다. messagingTemplate.convertAndSend 메소드는 event 객체를 직접 처리할 수 있습니다. (참고: ChatSocketFixtureFactory와 같은 테스트 픽스처는 초기 개발 단계에서 임시로 사용될 수 있지만, 기능이 완료되기 전에 실제 서비스 로직으로 대체되어야 합니다.)

Suggested change
messagingTemplate.convertAndSend("/topic/rooms/" + command.roomId(), Objects.requireNonNull(event));
messagingTemplate.convertAndSend("/topic/rooms/" + command.roomId(), event);
References
  1. Test fixtures can be temporarily used in the main source set (src/main/java) to provide sample responses for top-down contract validation during early development stages. This code must be replaced with actual service logic before the feature is considered complete.

return ChatSocketFixtureFactory.sampleAck(command, event.message().messageId());
}
}
Loading
Loading