Skip to content

Commit 2b7b728

Browse files
authored
Merge pull request #76 from MT-TEAM-Org/epic/chat
Epic/chat 채팅구현
2 parents 5e332c3 + c6a37f8 commit 2b7b728

33 files changed

+1237
-3
lines changed

build.gradle

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,15 @@ dependencies {
2929
implementation 'org.springframework.boot:spring-boot-starter-web'
3030
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
3131

32+
// CHAT
33+
// web socket
34+
implementation 'org.springframework.boot:spring-boot-starter-websocket'
35+
implementation 'org.webjars:webjars-locator-core'
36+
implementation 'org.webjars:sockjs-client:1.5.1'
37+
implementation 'org.webjars:stomp-websocket:2.3.4'
38+
// kafka
39+
implementation 'org.springframework.kafka:spring-kafka'
40+
3241
compileOnly 'org.projectlombok:lombok'
3342

3443
developmentOnly 'org.springframework.boot:spring-boot-devtools'

docker-compose.yml

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,26 @@ services:
2828
DB_NAME: ${DB_NAME}
2929
DB_PASSWORD: ${DB_PASSWORD}
3030
SLACK_WEBHOOK_URL: ${SLACK_WEBHOOK_URL}
31-
restart: always # 컨테이너가 중단되었을 때 재시작 설정
31+
restart: always # 컨테이너가 중단되었을 때 재시작 설정
32+
33+
zookeeper:
34+
image: confluentinc/cp-zookeeper:latest
35+
container_name: zookeeper
36+
environment:
37+
ZOOKEEPER_CLIENT_PORT: 2181
38+
ports:
39+
- "2181:2181"
40+
41+
kafka:
42+
image: confluentinc/cp-kafka:latest
43+
container_name: kafka
44+
ports:
45+
- "9092:9092"
46+
environment:
47+
KAFKA_BROKER_ID: 1
48+
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
49+
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
50+
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT
51+
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
52+
depends_on:
53+
- zookeeper

src/main/java/org/myteam/server/board/service/CategoryService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ public CategoryResponse delete(long id) {
128128
// 2. 하위 카테고리 존재 여부 확인
129129
if (categoryJpaRepository.existsByParentId(id)) {
130130
log.debug("하위 카테고리 존재하여 삭제가 불가능 합니다. id : {}", id);
131-
throw new PlayHiveException(RESOURCE_CONFLICT);
131+
// throw new PlayHiveException(RESOURCE_CONFLICT);
132132
}
133133

134134
// 3. DTO 생성 (삭제된 정보 반환)
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package org.myteam.server.chat.config;
2+
3+
import lombok.RequiredArgsConstructor;
4+
import lombok.extern.slf4j.Slf4j;
5+
import org.myteam.server.chat.domain.ChatRoom;
6+
import org.myteam.server.chat.repository.ChatRoomRepository;
7+
import org.myteam.server.chat.domain.FilterData;
8+
import org.myteam.server.chat.repository.FilterDataRepository;
9+
import org.springframework.boot.CommandLineRunner;
10+
import org.springframework.stereotype.Component;
11+
12+
@Slf4j
13+
@Component
14+
@RequiredArgsConstructor
15+
public class DataInit implements CommandLineRunner {
16+
17+
private final ChatRoomRepository repository;
18+
private final FilterDataRepository filterDataRepository;
19+
20+
@Override
21+
public void run(String... args) throws Exception {
22+
23+
ChatRoom chatRoom1 = new ChatRoom("맨유 VS 토트넘");
24+
ChatRoom chatRoom2 = new ChatRoom("아스날 VS 맨시티");
25+
ChatRoom chatRoom3 = new ChatRoom("첼시 VS 리버풀");
26+
27+
repository.save(chatRoom1);
28+
repository.save(chatRoom2);
29+
repository.save(chatRoom3);
30+
31+
FilterData filterData1 = new FilterData("맹구");
32+
FilterData filterData2 = new FilterData("닭트넘");
33+
34+
filterDataRepository.save(filterData1);
35+
filterDataRepository.save(filterData2);
36+
37+
log.info("데이터 초기화 완료");
38+
}
39+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package org.myteam.server.chat.config;
2+
3+
4+
import org.apache.kafka.clients.admin.AdminClientConfig;
5+
import org.apache.kafka.clients.consumer.ConsumerConfig;
6+
import org.apache.kafka.clients.producer.ProducerConfig;
7+
import org.apache.kafka.common.serialization.StringDeserializer;
8+
import org.apache.kafka.common.serialization.StringSerializer;
9+
import org.myteam.server.chat.domain.Chat;
10+
import org.springframework.context.annotation.Bean;
11+
import org.springframework.context.annotation.Configuration;
12+
import org.springframework.kafka.annotation.EnableKafka;
13+
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
14+
import org.springframework.kafka.core.*;
15+
import org.springframework.kafka.support.serializer.JsonDeserializer;
16+
import org.springframework.kafka.support.serializer.JsonSerializer;
17+
18+
import java.util.HashMap;
19+
import java.util.Map;
20+
21+
@EnableKafka
22+
@Configuration
23+
public class KafkaConfig {
24+
25+
private static final String BOOTSTRAP_SERVERS = "kafka:9092";
26+
private static final String DEFAULT_GROUP_ID = "chat-group";
27+
28+
@Bean
29+
public KafkaAdmin kafkaAdmin() {
30+
Map<String, Object> configs = new HashMap<>();
31+
configs.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, BOOTSTRAP_SERVERS);
32+
return new KafkaAdmin(configs);
33+
}
34+
35+
/**
36+
* Kafka ProducerFactory를 생성하는 Bean 메서드
37+
*/
38+
@Bean
39+
public ProducerFactory<String, Chat> producerFactory() {
40+
return new DefaultKafkaProducerFactory<>(producerConfigurations());
41+
}
42+
43+
/**
44+
* Kafka Producer 구성을 위한 설정값들을 포함한 맵을 반환하는 메서드
45+
*/
46+
@Bean
47+
public Map<String, Object> producerConfigurations() {
48+
Map<String, Object> producerConfigurations = new HashMap<>();
49+
50+
producerConfigurations.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, BOOTSTRAP_SERVERS);
51+
producerConfigurations.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
52+
producerConfigurations.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class);
53+
producerConfigurations.put(JsonSerializer.ADD_TYPE_INFO_HEADERS, false); // JSON 타입 헤더 제거 (선택사항)
54+
55+
return producerConfigurations;
56+
}
57+
58+
/**
59+
* KafkaTemplate을 생성하는 Bean 메서드
60+
*/
61+
@Bean
62+
public KafkaTemplate<String, Chat> kafkaTemplate() {
63+
return new KafkaTemplate<>(producerFactory());
64+
}
65+
66+
/**
67+
* Kafka ConsumerFactory를 생성하는 Bean 메서드
68+
*/
69+
@Bean
70+
public ConsumerFactory<String, Chat> consumerFactory() {
71+
JsonDeserializer<Chat> deserializer = new JsonDeserializer<>(Chat.class);
72+
deserializer.addTrustedPackages("*"); // 모든 패키지 신뢰 (필요 시 제한적으로 변경)
73+
74+
Map<String, Object> consumerConfigurations = Map.of(
75+
ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, BOOTSTRAP_SERVERS,
76+
ConsumerConfig.GROUP_ID_CONFIG, DEFAULT_GROUP_ID,
77+
ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class,
78+
ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, deserializer,
79+
ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "latest"
80+
);
81+
82+
return new DefaultKafkaConsumerFactory<>(consumerConfigurations, new StringDeserializer(), deserializer);
83+
}
84+
85+
/**
86+
* KafkaListener 컨테이너 팩토리를 생성하는 Bean 메서드
87+
*/
88+
@Bean
89+
public ConcurrentKafkaListenerContainerFactory<String, Chat> kafkaListenerContainerFactory() {
90+
ConcurrentKafkaListenerContainerFactory<String, Chat> factory = new ConcurrentKafkaListenerContainerFactory<>();
91+
factory.setConsumerFactory(consumerFactory());
92+
93+
factory.setConcurrency(3); // 병렬 처리 설정 (기본값 1)
94+
factory.getContainerProperties().setPollTimeout(3000L); // 폴링 시간 설정 (선택사항)
95+
96+
return factory;
97+
}
98+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package org.myteam.server.chat.config;
2+
3+
4+
import lombok.RequiredArgsConstructor;
5+
import org.springframework.context.annotation.Configuration;
6+
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
7+
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
8+
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
9+
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
10+
import org.springframework.web.socket.config.annotation.WebSocketTransportRegistration;
11+
12+
@EnableWebSocketMessageBroker
13+
@Configuration
14+
@RequiredArgsConstructor
15+
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
16+
17+
@Override
18+
public void configureMessageBroker(MessageBrokerRegistry registry) {
19+
registry.setApplicationDestinationPrefixes("/play-hive");
20+
registry.enableSimpleBroker("/room");
21+
}
22+
23+
@Override
24+
public void registerStompEndpoints(StompEndpointRegistry registry) {
25+
registry.addEndpoint("/ws-stomp")
26+
.setAllowedOrigins("http://localhost:3000")
27+
.withSockJS();
28+
registry.addEndpoint("/ws-stomp")
29+
.setAllowedOrigins("http://localhost:3000");
30+
}
31+
32+
// STOMP에서 64KB 이상의 데이터 전송을 못하는 문제 해결
33+
@Override
34+
public void configureWebSocketTransport(WebSocketTransportRegistration registry) {
35+
registry.setMessageSizeLimit(160 * 64 * 1024);
36+
registry.setSendTimeLimit(100 * 10000);
37+
registry.setSendBufferSizeLimit(3 * 512 * 1024);
38+
}
39+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package org.myteam.server.chat.controller;
2+
3+
import lombok.RequiredArgsConstructor;
4+
import org.myteam.server.chat.dto.request.BanRequest;
5+
import org.myteam.server.chat.dto.response.BanResponse;
6+
import org.myteam.server.chat.service.BanService;
7+
import org.myteam.server.global.web.response.ResponseDto;
8+
import org.springframework.http.ResponseEntity;
9+
import org.springframework.web.bind.annotation.*;
10+
11+
import static org.myteam.server.global.web.response.ResponseStatus.SUCCESS;
12+
13+
/**
14+
* Ban 도메인에 대한 HTTP 요청 처리
15+
*/
16+
@RestController
17+
@RequiredArgsConstructor
18+
@RequestMapping("/api/bans")
19+
public class BanController {
20+
21+
private final BanService banService;
22+
23+
/**
24+
* 유저 밴하기
25+
*/
26+
@PostMapping
27+
public ResponseEntity<ResponseDto<BanResponse>> banUser(@RequestBody BanRequest request) {
28+
BanResponse response = banService.banUser(request);
29+
return ResponseEntity.ok(new ResponseDto(
30+
SUCCESS.name(),
31+
"Ban Success",
32+
response
33+
));
34+
}
35+
36+
/**
37+
* 유저 밴 해제
38+
*/
39+
@DeleteMapping("/{username}")
40+
public ResponseEntity<ResponseDto<String>> unbanUser(@PathVariable String username) {
41+
String deleteName = banService.unbanUser(username);
42+
return ResponseEntity.ok(new ResponseDto(
43+
SUCCESS.name(),
44+
"Delete Ban Successfully",
45+
deleteName
46+
));
47+
}
48+
49+
/**
50+
* 특정 유저 밴 정보 조회
51+
*/
52+
@GetMapping("/{username}")
53+
public ResponseEntity<ResponseDto<BanResponse>> getBanByUsername(@PathVariable String username) {
54+
BanResponse response = banService.findBanByUsername(username);
55+
return ResponseEntity.ok(new ResponseDto(
56+
SUCCESS.name(),
57+
"Find Ban Reason Successfully",
58+
response
59+
));
60+
}
61+
}

0 commit comments

Comments
 (0)