diff --git a/.gitignore b/.gitignore index 5ca2add7..1d0b1312 100644 --- a/.gitignore +++ b/.gitignore @@ -39,4 +39,10 @@ out/ ### Custom ### db_dev.mv.db db_dev.trace.db -.env \ No newline at end of file +.env + +### Terraform ### +/infra/terraform/.terraform +/infra/terraform/.terraform.lock.hcl +/infra/terraform/terraform.tfstate +/infra/terraform/terraform.tfstate.backup \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 7192cc2d..aeefb0b6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -27,6 +27,7 @@ repositories { dependencies { 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-security") testImplementation("org.springframework.security:spring-security-test") compileOnly("org.projectlombok:lombok") diff --git a/infra/terraform/main.tf b/infra/terraform/main.tf new file mode 100644 index 00000000..647ac23e --- /dev/null +++ b/infra/terraform/main.tf @@ -0,0 +1,121 @@ +terraform { + // aws 라이브러리 불러옴 + required_providers { + aws = { + source = "hashicorp/aws" + } + } +} + +# 디폴드 리전 설정 +provider "aws" { + region = "ap-northeast-2" +} + +# VPC_1 +resource "aws_vpc" "vpc_1" { + cidr_block = "10.0.0.0/16" + enable_dns_support = true + enable_dns_hostnames = true + + tags = { + Name = "team5-vpc-1" + } +} + +# 퍼블릭 서브넷 (Subnet_1) +resource "aws_subnet" "subnet_1" { + vpc_id = aws_vpc.vpc_1.id + cidr_block = "10.0.1.0/24" + availability_zone = "ap-northeast-2a" + map_public_ip_on_launch = true # 퍼블릭 IP 자동 할당 + + tags = { + Name = "team5-subnet-1-public" + } +} + +# 프라이빗 서브넷 (Subnet_2) +resource "aws_subnet" "subnet_2" { + vpc_id = aws_vpc.vpc_1.id + cidr_block = "10.0.2.0/24" + availability_zone = "ap-northeast-2b" + + tags = { + Name = "team5-subnet-2-private" + } +} + +# 인터넷 게이트 웨이 +resource "aws_internet_gateway" "igw_1" { + vpc_id = aws_vpc.vpc_1.id + + tags = { + Name = "team5-igw-1" + } +} + +# 라우팅 테이블 +resource "aws_route_table" "rt_1" { + vpc_id = aws_vpc.vpc_1.id + + # 모든 트래픽에 대해 인터넷 게이트웨이로 보냄 + route { + cidr_block = "0.0.0.0/0" + gateway_id = aws_internet_gateway.igw_1.id + } + + tags = { + Name = "team5-rt-1" + } +} + +resource "aws_route_table_association" "association_1" { + # 연결할 서브넷 + subnet_id = aws_subnet.subnet_1.id + + # 연결할 라우트 테이블 지정 + route_table_id = aws_route_table.rt_1.id +} + +resource "aws_route_table_association" "association_2" { + # 연결할 서브넷 + subnet_id = aws_subnet.subnet_2.id + + # 연결할 라우트 테이블 지정 + route_table_id = aws_route_table.rt_1.id +} + +resource "aws_security_group" "sg_1" { + name = "team5-sg-1" + description = "Allow SSH and HTTP" + vpc_id = aws_vpc.vpc_1.id + + ingress { + from_port = 0 + to_port = 0 + protocol = "all" # 모든 프로토콜 + cidr_blocks = ["0.0.0.0/0"] # 모든 IP 허용 + } + + egress { + from_port = 0 + to_port = 0 + protocol = "all" # 모든 프로토콜 + cidr_blocks = ["0.0.0.0/0"] # 모든 IP 허용 + } +} + +resource "aws_instance" "ec2_1" { + ami = "ami-077ad873396d76f6a" + instance_type = "t2.micro" + + subnet_id = aws_subnet.subnet_1.id + vpc_security_group_ids = [aws_security_group.sg_1.id] + + associate_public_ip_address = true + + tags = { + Name = "team5-ec2-1" + } +} \ No newline at end of file diff --git a/src/main/java/com/back/domain/websocket/config/WebSocketConfig.java b/src/main/java/com/back/domain/websocket/config/WebSocketConfig.java new file mode 100644 index 00000000..af0cf877 --- /dev/null +++ b/src/main/java/com/back/domain/websocket/config/WebSocketConfig.java @@ -0,0 +1,36 @@ +package com.back.domain.websocket.config; + +import org.springframework.context.annotation.Configuration; +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; + +@Configuration +@EnableWebSocketMessageBroker +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + + /** + * 메시지 브로커 설정 + * - /topic: 1:N 브로드캐스트 (방 채팅) + * - /queue: 1:1 메시지 (개인 DM) + * - /app: 클라이언트에서 서버로 메시지 전송 시 prefix + */ + @Override + public void configureMessageBroker(MessageBrokerRegistry config) { + config.enableSimpleBroker("/topic", "/queue"); + config.setApplicationDestinationPrefixes("/app"); + config.setUserDestinationPrefix("/user"); + } + + /** + * STOMP 엔드포인트 등록 + * 클라이언트가 WebSocket 연결을 위해 사용할 엔드포인트 + */ + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry.addEndpoint("/ws") + .setAllowedOriginPatterns("*") // 모든 도메인 허용 (개발용) + .withSockJS(); // SockJS 사용 + } +} diff --git a/src/main/java/com/back/domain/websocket/controller/WebSocketTestController.java b/src/main/java/com/back/domain/websocket/controller/WebSocketTestController.java new file mode 100644 index 00000000..4e1d8539 --- /dev/null +++ b/src/main/java/com/back/domain/websocket/controller/WebSocketTestController.java @@ -0,0 +1,65 @@ +package com.back.domain.websocket.controller; + +import com.back.global.common.dto.RsData; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; + +@Slf4j +@RestController +@RequestMapping("/api/websocket") +public class WebSocketTestController { // WebSocket 기능 테스트용 REST 컨트롤러 + + // WebSocket 서버 상태 확인 + @GetMapping("/health") + public ResponseEntity>> healthCheck() { + log.info("WebSocket 헬스체크 요청"); + + Map data = new HashMap<>(); + data.put("service", "WebSocket"); + data.put("status", "running"); + data.put("timestamp", LocalDateTime.now()); + data.put("endpoints", Map.of( + "websocket", "/ws", + "chat", "/app/rooms/{roomId}/chat", + "join", "/app/rooms/{roomId}/join", + "leave", "/app/rooms/{roomId}/leave" + )); + + return ResponseEntity + .status(HttpStatus.OK) + .body(RsData.success("WebSocket 서비스가 정상 동작중입니다.", data)); + } + + // WebSocket 연결 정보 제공 + @GetMapping("/info") + public ResponseEntity>> getConnectionInfo() { + log.info("WebSocket 연결 정보 요청"); + + Map connectionInfo = new HashMap<>(); + connectionInfo.put("websocketUrl", "/ws"); + connectionInfo.put("sockjsSupport", true); + connectionInfo.put("stompVersion", "1.2"); + connectionInfo.put("subscribeTopics", Map.of( + "roomChat", "/topic/rooms/{roomId}/chat", + "privateMessage", "/user/queue/messages", + "notifications", "/user/queue/notifications" + )); + connectionInfo.put("sendDestinations", Map.of( + "roomChat", "/app/rooms/{roomId}/chat", + "joinRoom", "/app/rooms/{roomId}/join", + "leaveRoom", "/app/rooms/{roomId}/leave" + )); + + return ResponseEntity + .status(HttpStatus.OK) + .body(RsData.success("WebSocket 연결 정보", connectionInfo)); + } +} \ No newline at end of file diff --git a/src/main/java/com/back/global/entity/BaseEntity.java b/src/main/java/com/back/global/entity/BaseEntity.java index 935cef49..772d8e1b 100644 --- a/src/main/java/com/back/global/entity/BaseEntity.java +++ b/src/main/java/com/back/global/entity/BaseEntity.java @@ -16,7 +16,7 @@ public abstract class BaseEntity { private Long id; @CreatedDate - private LocalDateTime createAt; + private LocalDateTime createdAt; @LastModifiedDate private LocalDateTime updatedAt;