├── settings.gradle
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── src
├── test
│ ├── resources
│ │ ├── application.yml
│ │ ├── application-test.yml
│ │ ├── application-oauth.yml
│ │ └── application-common.yml
│ └── java
│ │ └── com
│ │ └── challenge
│ │ └── chat
│ │ ├── ChatApplicationTests.java
│ │ ├── config
│ │ └── JasyptConfigTest.java
│ │ └── domain
│ │ ├── member
│ │ ├── service
│ │ │ └── MemberServiceTest.java
│ │ └── controller
│ │ │ └── MemberControllerTest.java
│ │ └── chat
│ │ └── service
│ │ └── ChatServiceTest.java
└── main
│ ├── resources
│ ├── elastic
│ │ ├── chat-setting.json
│ │ └── chat-mapping.json
│ ├── application.yml
│ ├── static
│ │ └── index.html
│ ├── application-local.yml
│ ├── application-oauth.yml
│ ├── application-common.yml
│ └── application-dev.yml
│ └── java
│ └── com
│ └── challenge
│ └── chat
│ ├── domain
│ ├── member
│ │ ├── constant
│ │ │ ├── SocialType.java
│ │ │ └── MemberRole.java
│ │ ├── dto
│ │ │ ├── request
│ │ │ │ ├── MemberAddRequest.java
│ │ │ │ └── SignupRequest.java
│ │ │ └── MemberDto.java
│ │ ├── repository
│ │ │ ├── MemberFriendRepository.java
│ │ │ └── MemberRepository.java
│ │ ├── entity
│ │ │ ├── MemberFriend.java
│ │ │ └── Member.java
│ │ ├── controller
│ │ │ └── MemberController.java
│ │ └── service
│ │ │ └── MemberService.java
│ └── chat
│ │ ├── entity
│ │ ├── MessageType.java
│ │ ├── TimeStamped.java
│ │ ├── MemberChatRoom.java
│ │ ├── ChatRoom.java
│ │ ├── Chat.java
│ │ └── ChatES.java
│ │ ├── constant
│ │ └── KafkaConstants.java
│ │ ├── dto
│ │ ├── request
│ │ │ ├── ChatRoomAddRequest.java
│ │ │ └── ChatRoomCreateRequest.java
│ │ ├── ChatRoomDto.java
│ │ └── ChatDto.java
│ │ ├── repository
│ │ ├── ChatRoomRepository.java
│ │ ├── ChatSearchRepository.java
│ │ ├── ChatRepository.java
│ │ └── MemberChatRoomRepository.java
│ │ ├── service
│ │ ├── Producer.java
│ │ ├── Consumer.java
│ │ └── ChatService.java
│ │ ├── config
│ │ ├── ProducerConfig.java
│ │ ├── ConsumerConfig.java
│ │ └── RabbitConfig.java
│ │ └── controller
│ │ └── ChatController.java
│ ├── exception
│ ├── dto
│ │ ├── ErrorCode.java
│ │ ├── ErrorResponse.java
│ │ ├── CommonErrorCode.java
│ │ ├── MemberErrorCode.java
│ │ └── ChatErrorCode.java
│ ├── RestApiException.java
│ └── GlobalExceptionHandler.java
│ ├── security
│ ├── oauth
│ │ ├── dto
│ │ │ ├── OAuth2UserInfo.java
│ │ │ ├── GoogleOAuth2UserInfo.java
│ │ │ ├── CustomOAuth2User.java
│ │ │ └── OAuthAttributes.java
│ │ ├── handler
│ │ │ ├── OAuth2LoginFailureHandler.java
│ │ │ └── OAuth2LoginSuccessHandler.java
│ │ └── service
│ │ │ └── CustomOAuth2UserService.java
│ ├── jwt
│ │ ├── util
│ │ │ └── PasswordUtil.java
│ │ ├── service
│ │ │ └── JwtService.java
│ │ └── filter
│ │ │ └── JwtAuthenticationProcessingFilter.java
│ └── login
│ │ ├── handler
│ │ ├── LoginFailureHandler.java
│ │ └── LoginSuccessHandler.java
│ │ ├── service
│ │ └── LoginService.java
│ │ └── filter
│ │ └── CustomJsonUsernamePasswordAuthenticationFilter.java
│ ├── config
│ ├── CustomPrometheusConfig.java
│ ├── ElasticSearchConfig.java
│ ├── JasyptConfig.java
│ ├── WebSocketConfig.java
│ ├── MongoConfig.java
│ └── SecurityConfig.java
│ └── ChatApplication.java
├── appspec.yml
├── scripts
├── stop.sh
└── start.sh
├── HELP.md
├── .github
└── workflows
│ └── main.yml
├── gradlew.bat
├── .gitignore
├── README.md
└── gradlew
/settings.gradle:
--------------------------------------------------------------------------------
1 | rootProject.name = 'chat'
2 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/god-kao-talk/chat-challenge-BE/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/src/test/resources/application.yml:
--------------------------------------------------------------------------------
1 | spring:
2 | profiles:
3 | group:
4 | test:
5 | - common
6 | - oauth
7 |
8 |
--------------------------------------------------------------------------------
/src/main/resources/elastic/chat-setting.json:
--------------------------------------------------------------------------------
1 | {
2 | "analysis": {
3 | "analyzer": {
4 | "korean": {
5 | "type": "nori"
6 | }
7 | }
8 | }
9 | }
--------------------------------------------------------------------------------
/src/main/java/com/challenge/chat/domain/member/constant/SocialType.java:
--------------------------------------------------------------------------------
1 | package com.challenge.chat.domain.member.constant;
2 |
3 | public enum SocialType {
4 | KAKAO, NAVER, GOOGLE
5 | }
6 |
--------------------------------------------------------------------------------
/src/main/java/com/challenge/chat/domain/chat/entity/MessageType.java:
--------------------------------------------------------------------------------
1 | package com.challenge.chat.domain.chat.entity;
2 |
3 | public enum MessageType {
4 | ENTER,
5 | TALK,
6 | LEAVE,
7 | IMAGE
8 | }
9 |
--------------------------------------------------------------------------------
/src/main/resources/application.yml:
--------------------------------------------------------------------------------
1 | spring:
2 | profiles:
3 | group:
4 | local:
5 | - common
6 | - oauth
7 | dev:
8 | - common
9 | - oauth
10 |
11 |
--------------------------------------------------------------------------------
/src/main/resources/static/index.html:
--------------------------------------------------------------------------------
1 | Kakao Login
2 | Google Login
3 | Naver Login
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/src/main/resources/application-local.yml:
--------------------------------------------------------------------------------
1 | spring:
2 | config:
3 | activate:
4 | on-profile: local
5 | datasource:
6 | url: jdbc:h2:mem:db;MODE=MYSQL
7 | username: sa
8 | password: ''
9 | h2:
10 | console:
11 | enabled: true
--------------------------------------------------------------------------------
/src/test/resources/application-test.yml:
--------------------------------------------------------------------------------
1 | spring:
2 | config:
3 | activate:
4 | on-profile: local
5 | datasource:
6 | url: jdbc:h2:mem:db;MODE=MYSQL
7 | username: sa
8 | password: ''
9 | h2:
10 | console:
11 | enabled: true
--------------------------------------------------------------------------------
/src/main/java/com/challenge/chat/exception/dto/ErrorCode.java:
--------------------------------------------------------------------------------
1 | package com.challenge.chat.exception.dto;
2 |
3 | import org.springframework.http.HttpStatus;
4 |
5 | public interface ErrorCode {
6 | String name();
7 | HttpStatus getHttpStatus();
8 | String getMessage();
9 | }
10 |
--------------------------------------------------------------------------------
/src/main/java/com/challenge/chat/domain/member/dto/request/MemberAddRequest.java:
--------------------------------------------------------------------------------
1 | package com.challenge.chat.domain.member.dto.request;
2 |
3 | import lombok.Getter;
4 | import lombok.NoArgsConstructor;
5 |
6 | @Getter
7 | @NoArgsConstructor
8 | public class MemberAddRequest {
9 | private String email;
10 | }
11 |
--------------------------------------------------------------------------------
/src/main/java/com/challenge/chat/domain/chat/constant/KafkaConstants.java:
--------------------------------------------------------------------------------
1 | package com.challenge.chat.domain.chat.constant;
2 |
3 | public class KafkaConstants {
4 | public static final String KAFKA_TOPIC = "kafka-chat";
5 | public static final String GROUP_ID = "foo";
6 | public static final String KAFKA_BROKER = "broker:9092";
7 | }
8 |
--------------------------------------------------------------------------------
/src/main/java/com/challenge/chat/domain/member/constant/MemberRole.java:
--------------------------------------------------------------------------------
1 | package com.challenge.chat.domain.member.constant;
2 |
3 | import lombok.Getter;
4 | import lombok.RequiredArgsConstructor;
5 |
6 | @Getter
7 | @RequiredArgsConstructor
8 | public enum MemberRole {
9 |
10 | GUEST("ROLE_GUEST"), USER("ROLE_USER");
11 |
12 | private final String key;
13 | }
14 |
--------------------------------------------------------------------------------
/src/main/java/com/challenge/chat/exception/dto/ErrorResponse.java:
--------------------------------------------------------------------------------
1 | package com.challenge.chat.exception.dto;
2 |
3 | import lombok.AllArgsConstructor;
4 | import lombok.Getter;
5 |
6 | @Getter
7 | @AllArgsConstructor
8 | public class ErrorResponse {
9 |
10 | private final String errorCode;
11 | private final String status;
12 | private final String message;
13 | }
14 |
--------------------------------------------------------------------------------
/src/main/java/com/challenge/chat/domain/chat/dto/request/ChatRoomAddRequest.java:
--------------------------------------------------------------------------------
1 | package com.challenge.chat.domain.chat.dto.request;
2 |
3 | import lombok.AllArgsConstructor;
4 | import lombok.Getter;
5 | import lombok.NoArgsConstructor;
6 |
7 | @AllArgsConstructor
8 | @NoArgsConstructor
9 | @Getter
10 | public class ChatRoomAddRequest {
11 | private String roomCode;
12 | }
13 |
--------------------------------------------------------------------------------
/src/main/java/com/challenge/chat/domain/chat/dto/request/ChatRoomCreateRequest.java:
--------------------------------------------------------------------------------
1 | package com.challenge.chat.domain.chat.dto.request;
2 |
3 | import lombok.AllArgsConstructor;
4 | import lombok.Getter;
5 | import lombok.NoArgsConstructor;
6 |
7 | @AllArgsConstructor
8 | @NoArgsConstructor
9 | @Getter
10 | public class ChatRoomCreateRequest {
11 | private String roomName;
12 | }
13 |
--------------------------------------------------------------------------------
/src/main/java/com/challenge/chat/exception/RestApiException.java:
--------------------------------------------------------------------------------
1 | package com.challenge.chat.exception;
2 |
3 | import com.challenge.chat.exception.dto.ErrorCode;
4 |
5 | import lombok.Getter;
6 | import lombok.RequiredArgsConstructor;
7 |
8 | @Getter
9 | @RequiredArgsConstructor
10 | public class RestApiException extends RuntimeException {
11 |
12 | private final ErrorCode errorCode;
13 | }
14 |
--------------------------------------------------------------------------------
/src/main/resources/elastic/chat-mapping.json:
--------------------------------------------------------------------------------
1 | {
2 | "properties" : {
3 | "type" : {"type" : "text"},
4 | "nickname" : {"type" : "text"},
5 | "email" : {"type" : "text"},
6 | "roomCode" : {"type" : "text"},
7 | "message" : {"type" : "text", "analyzer" : "korean"},
8 | "createdAt" : {
9 | "type" : "date"
10 | },
11 | "imageUrl" : {"type" : "text"}
12 | }
13 | }
--------------------------------------------------------------------------------
/appspec.yml:
--------------------------------------------------------------------------------
1 | version: 0.0
2 | os: linux
3 |
4 | files:
5 | - source: /
6 | destination: /home/ubuntu/spring-github-action
7 | overwrite: yes
8 | file_exists_behavior: OVERWRITE
9 | permissions:
10 | - object: /
11 | owner: ubuntu
12 | group: ubuntu
13 |
14 | hooks:
15 | AfterInstall:
16 | - location: scripts/stop.sh
17 | timeout: 60
18 | ApplicationStart:
19 | - location: scripts/start.sh
20 | timeout: 60
21 |
--------------------------------------------------------------------------------
/src/main/java/com/challenge/chat/domain/chat/repository/ChatRoomRepository.java:
--------------------------------------------------------------------------------
1 | package com.challenge.chat.domain.chat.repository;
2 |
3 | import com.challenge.chat.domain.chat.entity.ChatRoom;
4 |
5 | import org.springframework.data.jpa.repository.JpaRepository;
6 |
7 | import java.util.Optional;
8 | public interface ChatRoomRepository extends JpaRepository {
9 |
10 | Optional findByRoomCode(String roomCode);
11 | }
12 |
--------------------------------------------------------------------------------
/src/test/java/com/challenge/chat/ChatApplicationTests.java:
--------------------------------------------------------------------------------
1 | // package com.challenge.chat;
2 | //
3 | // import org.junit.jupiter.api.DisplayName;
4 | // import org.junit.jupiter.api.Test;
5 | // import org.springframework.boot.test.context.SpringBootTest;
6 | // import org.springframework.test.context.ActiveProfiles;
7 | //
8 | // @SpringBootTest
9 | // @ActiveProfiles({"test"})
10 | // class ChatApplicationTests {
11 | // @Test
12 | // @DisplayName("통합 테스트 성공")
13 | // void contextLoads() {
14 | // }
15 | // }
16 |
--------------------------------------------------------------------------------
/scripts/stop.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | ROOT_PATH="/home/ubuntu/spring-github-action"
4 | JAR="$ROOT_PATH/application-plain.jar"
5 | STOP_LOG="$ROOT_PATH/stop.log"
6 | CONTAINER="com.challenge.chat.ChatApplication"
7 | SERVICE_PID=$(pgrep -f $CONTAINER) # 실행중인 Spring 서버의 PID
8 |
9 | NOW=$(date +%c)
10 |
11 |
12 | if [ -z "$SERVICE_PID" ]; then
13 | echo " [$NOW] 서비스 NouFound" >> $STOP_LOG
14 | else
15 | echo " [$NOW] [$SERVICE_PID] 서비스 종료 " >> $STOP_LOG
16 | kill -9 "$SERVICE_PID"
17 | # kill -9 $SERVICE_PID # 강제 종료를 하고 싶다면 이 명령어 사용
18 | fi
19 |
--------------------------------------------------------------------------------
/src/main/java/com/challenge/chat/domain/chat/repository/ChatSearchRepository.java:
--------------------------------------------------------------------------------
1 | // package com.challenge.chat.domain.chat.repository;
2 | //
3 | // import java.util.List;
4 | //
5 | // import org.springframework.data.domain.Pageable;
6 | // import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
7 | //
8 | // import com.challenge.chat.domain.chat.entity.ChatES;
9 | //
10 | // public interface ChatSearchRepository extends ElasticsearchRepository {
11 | //
12 | // List findByMessage(String message);
13 | // }
--------------------------------------------------------------------------------
/src/main/java/com/challenge/chat/domain/member/repository/MemberFriendRepository.java:
--------------------------------------------------------------------------------
1 | package com.challenge.chat.domain.member.repository;
2 |
3 | import com.challenge.chat.domain.member.entity.Member;
4 | import com.challenge.chat.domain.member.entity.MemberFriend;
5 |
6 | import org.springframework.data.jpa.repository.JpaRepository;
7 |
8 | import java.util.Optional;
9 |
10 | public interface MemberFriendRepository extends JpaRepository {
11 |
12 | Optional findByMemberAndFriend(Member member, Member friend);
13 | }
14 |
--------------------------------------------------------------------------------
/src/main/java/com/challenge/chat/domain/member/dto/request/SignupRequest.java:
--------------------------------------------------------------------------------
1 | package com.challenge.chat.domain.member.dto.request;
2 |
3 | import javax.validation.constraints.NotBlank;
4 |
5 | import lombok.Getter;
6 | import lombok.NoArgsConstructor;
7 |
8 | @Getter
9 | @NoArgsConstructor
10 | public class SignupRequest {
11 | @NotBlank(message = "Email은 필수 값입니다.")
12 | private String email;
13 |
14 | @NotBlank(message = "Password는 필수 값입니다.")
15 | private String password;
16 |
17 | @NotBlank(message = "Nickname은 필수 값입니다.")
18 | private String nickname;
19 | }
20 |
--------------------------------------------------------------------------------
/src/main/java/com/challenge/chat/exception/dto/CommonErrorCode.java:
--------------------------------------------------------------------------------
1 | package com.challenge.chat.exception.dto;
2 |
3 | import org.springframework.http.HttpStatus;
4 |
5 | import lombok.Getter;
6 | import lombok.RequiredArgsConstructor;
7 |
8 | @Getter
9 | @RequiredArgsConstructor
10 | public enum CommonErrorCode implements ErrorCode {
11 |
12 | INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "내부 서버에서 문제가 발생했습니다"),
13 | INVALID_REQUEST_PARAMETER(HttpStatus.BAD_REQUEST, "유효하지 않은 파라미터 입니다")
14 | ;
15 |
16 | private final HttpStatus httpStatus;
17 | private final String message;
18 | }
19 |
--------------------------------------------------------------------------------
/src/main/java/com/challenge/chat/security/oauth/dto/OAuth2UserInfo.java:
--------------------------------------------------------------------------------
1 | package com.challenge.chat.security.oauth.dto;
2 |
3 | import java.util.Map;
4 |
5 | public abstract class OAuth2UserInfo {
6 |
7 | protected Map attributes;
8 |
9 | public OAuth2UserInfo(Map attributes) {
10 | this.attributes = attributes;
11 | }
12 |
13 | public abstract String getId(); //소셜 식별 값 : 구글 - "sub", 카카오 - "id", 네이버 - "id"
14 |
15 | public abstract String getNickname();
16 |
17 | public abstract String getImageUrl();
18 |
19 | public abstract String getEmail();
20 | }
21 |
--------------------------------------------------------------------------------
/src/main/resources/application-oauth.yml:
--------------------------------------------------------------------------------
1 | spring:
2 | config:
3 | activate:
4 | on-profile: oauth
5 | security:
6 | oauth2:
7 | client:
8 | registration:
9 | google:
10 | client-id: ENC(BqXz+zkPlztuGEnq087JZZvP5Jjy/TGDLCZsFAfqscsPkgcpWACpOMON4Bwg/9cHeYDYKJUB25jGq5t6COe4MtZr3Ra7BBosNYP0TlMyRBxyxzfPEX71gQ==)
11 | client-secret: ENC(O9Lq7E1QZO39aoVW0Z55fctcP/WmrzyVO/LmQByTzU2Ww2Hu+lDq6p3AYAvvcwVs)
12 | scope:
13 | - profile
14 | - email
15 | # redirect-uri: https://www.hhaegg.o-r.kr/login/oauth2/code/google
--------------------------------------------------------------------------------
/src/test/resources/application-oauth.yml:
--------------------------------------------------------------------------------
1 | spring:
2 | config:
3 | activate:
4 | on-profile: oauth
5 | security:
6 | oauth2:
7 | client:
8 | registration:
9 | google:
10 | client-id: ENC(BqXz+zkPlztuGEnq087JZZvP5Jjy/TGDLCZsFAfqscsPkgcpWACpOMON4Bwg/9cHeYDYKJUB25jGq5t6COe4MtZr3Ra7BBosNYP0TlMyRBxyxzfPEX71gQ==)
11 | client-secret: ENC(O9Lq7E1QZO39aoVW0Z55fctcP/WmrzyVO/LmQByTzU2Ww2Hu+lDq6p3AYAvvcwVs)
12 | scope:
13 | - profile
14 | - email
15 | # redirect-uri: https://www.hhaegg.o-r.kr/login/oauth2/code/google
--------------------------------------------------------------------------------
/src/main/java/com/challenge/chat/domain/chat/dto/ChatRoomDto.java:
--------------------------------------------------------------------------------
1 | package com.challenge.chat.domain.chat.dto;
2 |
3 | import com.challenge.chat.domain.chat.entity.ChatRoom;
4 | import lombok.AllArgsConstructor;
5 | import lombok.Getter;
6 | import lombok.NoArgsConstructor;
7 | import lombok.Setter;
8 |
9 | @Getter
10 | @Setter
11 | @NoArgsConstructor
12 | @AllArgsConstructor
13 | public class ChatRoomDto {
14 | private String roomCode;
15 | private String roomName;
16 |
17 | public static ChatRoomDto from(ChatRoom chatRoom) {
18 | return new ChatRoomDto(chatRoom.getRoomCode(), chatRoom.getRoomName());
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/main/resources/application-common.yml:
--------------------------------------------------------------------------------
1 | spring:
2 | config:
3 | activate:
4 | on-profile: common
5 | jpa:
6 | open-in-view: false
7 | jwt:
8 | secretKey: ENC(uvRR3r/GGDxBWH5fuINwi8uQVmhDV9lkuDtiDmEJeKnEMNYLlIZ/lfPYSrpn/LLPUtJflpTdSDxXGd6qRIfWrWmvtdS88f8JI3B1yQQ0VQRsywHyg/wB7w==)
9 | access:
10 | expiration: 604800000 # 1시간(60분) (1000L(ms -> s) * 60L(s -> m) * 60L(m -> h))
11 | header: Authorization
12 | refresh:
13 | expiration: 604800000 # (1000L(ms -> s) * 60L(s -> m) * 60L(m -> h) * 24L(h -> 하루) * 14(2주))
14 | header: Authorization-refresh
15 | logging:
16 | level:
17 | org.springframework.data.elasticsearch.client.WIRE: TRACE
--------------------------------------------------------------------------------
/src/main/java/com/challenge/chat/domain/chat/repository/ChatRepository.java:
--------------------------------------------------------------------------------
1 | package com.challenge.chat.domain.chat.repository;
2 |
3 | import com.challenge.chat.domain.chat.entity.Chat;
4 | import org.springframework.data.mongodb.repository.MongoRepository;
5 |
6 | import java.util.List;
7 | import java.util.Optional;
8 |
9 | public interface ChatRepository extends MongoRepository {
10 | // Spring Data MongoDB -> https://docs.spring.io/spring-data/mongodb/docs/current/reference/html/
11 |
12 | Optional> findByRoomCode(String roomCode);
13 |
14 | Optional> findByRoomCodeAndMessageContaining(String roomCode, String message);
15 | }
16 |
--------------------------------------------------------------------------------
/src/main/java/com/challenge/chat/config/CustomPrometheusConfig.java:
--------------------------------------------------------------------------------
1 | package com.challenge.chat.config;
2 |
3 | import org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer;
4 | import org.springframework.context.annotation.Bean;
5 | import org.springframework.context.annotation.Configuration;
6 |
7 | import io.micrometer.core.instrument.MeterRegistry;
8 | import lombok.extern.slf4j.Slf4j;
9 |
10 | @Configuration
11 | @Slf4j
12 | public class CustomPrometheusConfig {
13 |
14 | @Bean
15 | MeterRegistryCustomizer metricsCommonTags() {
16 | return registry -> registry.config().commonTags("application", "PROMETHEUS-SAMPLE-SERVER");
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/main/java/com/challenge/chat/exception/dto/MemberErrorCode.java:
--------------------------------------------------------------------------------
1 | package com.challenge.chat.exception.dto;
2 |
3 | import org.springframework.http.HttpStatus;
4 |
5 | import lombok.Getter;
6 | import lombok.RequiredArgsConstructor;
7 |
8 | @Getter
9 | @RequiredArgsConstructor
10 | public enum MemberErrorCode implements ErrorCode {
11 |
12 | MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "회원을 찾을 수 없습니다"),
13 | DUPLICATED_EMAIL(HttpStatus.BAD_REQUEST, "이미 존재하는 email 입니다"),
14 | DUPLICATED_MEMBER(HttpStatus.BAD_REQUEST, "이미 추가된 친구 입니다"),
15 | ADDED_FRIEND(HttpStatus.BAD_REQUEST, "이미 추가된 친구 입니다")
16 | ;
17 |
18 | private final HttpStatus httpStatus;
19 | private final String message;
20 | }
21 |
--------------------------------------------------------------------------------
/src/main/java/com/challenge/chat/domain/chat/repository/MemberChatRoomRepository.java:
--------------------------------------------------------------------------------
1 | package com.challenge.chat.domain.chat.repository;
2 |
3 | import com.challenge.chat.domain.chat.entity.ChatRoom;
4 | import com.challenge.chat.domain.chat.entity.MemberChatRoom;
5 | import com.challenge.chat.domain.member.entity.Member;
6 |
7 | import org.springframework.data.jpa.repository.JpaRepository;
8 |
9 | import java.util.List;
10 | import java.util.Optional;
11 |
12 | public interface MemberChatRoomRepository extends JpaRepository {
13 |
14 | Optional> findByMember(Member member);
15 | Optional findByMemberAndRoom(Member member, ChatRoom room);
16 | }
17 |
--------------------------------------------------------------------------------
/src/main/java/com/challenge/chat/exception/dto/ChatErrorCode.java:
--------------------------------------------------------------------------------
1 | package com.challenge.chat.exception.dto;
2 |
3 | import org.springframework.http.HttpStatus;
4 |
5 | import lombok.Getter;
6 | import lombok.RequiredArgsConstructor;
7 |
8 | @Getter
9 | @RequiredArgsConstructor
10 | public enum ChatErrorCode implements ErrorCode {
11 |
12 | CHATROOM_NOT_FOUND(HttpStatus.NOT_FOUND, "채팅방이 존재하지 않습니다"),
13 | KAFKA_PRODUCER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "채팅 전송에 실패했습니다"),
14 | KAFKA_CONSUMER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "채팅 수신에 실패했습니다"),
15 | SOCKET_CONNECTION_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "소켓 통신이 불안정 합니다"),
16 | ;
17 |
18 | private final HttpStatus httpStatus;
19 | private final String message;
20 | }
21 |
--------------------------------------------------------------------------------
/src/main/java/com/challenge/chat/security/oauth/dto/GoogleOAuth2UserInfo.java:
--------------------------------------------------------------------------------
1 | package com.challenge.chat.security.oauth.dto;
2 |
3 | import java.util.Map;
4 |
5 | public class GoogleOAuth2UserInfo extends OAuth2UserInfo {
6 |
7 | public GoogleOAuth2UserInfo(Map attributes) {
8 | super(attributes);
9 | }
10 |
11 | @Override
12 | public String getId() {
13 | return (String) attributes.get("sub");
14 | }
15 |
16 | @Override
17 | public String getNickname() {
18 | return (String) attributes.get("name");
19 | }
20 |
21 | @Override
22 | public String getImageUrl() {
23 | return (String) attributes.get("picture");
24 | }
25 |
26 | @Override
27 | public String getEmail() {
28 | return (String) attributes.get("email");
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/main/java/com/challenge/chat/domain/chat/entity/TimeStamped.java:
--------------------------------------------------------------------------------
1 | package com.challenge.chat.domain.chat.entity;
2 |
3 | import java.time.LocalDateTime;
4 |
5 | import javax.persistence.Column;
6 | import javax.persistence.EntityListeners;
7 | import javax.persistence.MappedSuperclass;
8 |
9 | import org.springframework.data.annotation.CreatedDate;
10 | import org.springframework.data.annotation.LastModifiedDate;
11 | import org.springframework.data.jpa.domain.support.AuditingEntityListener;
12 |
13 | import lombok.Getter;
14 |
15 | @Getter
16 | @MappedSuperclass
17 | @EntityListeners(AuditingEntityListener.class)
18 | public abstract class TimeStamped {
19 |
20 | @CreatedDate
21 | @Column(updatable = false)
22 | private LocalDateTime createdAt;
23 |
24 | @LastModifiedDate
25 | private LocalDateTime modifiedAt;
26 | }
27 |
--------------------------------------------------------------------------------
/src/main/java/com/challenge/chat/domain/member/repository/MemberRepository.java:
--------------------------------------------------------------------------------
1 | package com.challenge.chat.domain.member.repository;
2 |
3 | import com.challenge.chat.domain.member.constant.SocialType;
4 | import com.challenge.chat.domain.member.entity.Member;
5 |
6 | import org.springframework.data.jpa.repository.JpaRepository;
7 |
8 | import java.util.Optional;
9 |
10 | public interface MemberRepository extends JpaRepository {
11 |
12 | Optional findByEmail(String email);
13 |
14 | Optional findByRefreshToken(String refreshToken);
15 |
16 | /**
17 | * 소셜 타입과 소셜의 식별값으로 회원 찾는 메소드
18 | * 정보 제공을 동의한 순간 DB에 저장해야하지만, 아직 추가 정보(사는 도시, 나이 등)를 입력받지 않았으므로
19 | * 유저 객체는 DB에 있지만, 추가 정보가 빠진 상태이다.
20 | * 따라서 추가 정보를 입력받아 회원 가입을 진행할 때 소셜 타입, 식별자로 해당 회원을 찾기 위한 메소드
21 | */
22 | Optional findBySocialTypeAndSocialId(SocialType socialType, String socialId);
23 |
24 | }
25 |
--------------------------------------------------------------------------------
/src/main/java/com/challenge/chat/domain/member/dto/MemberDto.java:
--------------------------------------------------------------------------------
1 | package com.challenge.chat.domain.member.dto;
2 |
3 | import com.challenge.chat.domain.member.entity.Member;
4 | import lombok.Getter;
5 | import lombok.NoArgsConstructor;
6 |
7 | @Getter
8 | @NoArgsConstructor
9 | public class MemberDto {
10 |
11 | private Long id;
12 | private String email;
13 | private String imageUrl;
14 | private String nickname;
15 |
16 | private MemberDto(Long id, String email, String imageUrl, String nickname) {
17 | this.id = id;
18 | this.email = email;
19 | this.imageUrl = imageUrl;
20 | this.nickname = nickname;
21 | }
22 |
23 | public static MemberDto from(Member member) {
24 | return new MemberDto(
25 | member.getId(),
26 | member.getEmail(),
27 | member.getImageUrl(),
28 | member.getNickname()
29 | );
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/scripts/start.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | ROOT_PATH="/home/ubuntu/spring-github-action"
4 | JAR="$ROOT_PATH/application-plain.jar"
5 | #CONTAINER="com.challenge.chat.ChatApplication"
6 | #IMAGE="chat-challenge"
7 | #TAG="latest"
8 |
9 | APP_LOG="$ROOT_PATH/application.log"
10 | ERROR_LOG="$ROOT_PATH/error.log"
11 | START_LOG="$ROOT_PATH/start.log"
12 | PROFILES_ACTIVE="Dspring.profiles.active=dev"
13 |
14 | NOW=$(date +%c)
15 |
16 | echo "[$NOW] $JAR 복사" >> $START_LOG
17 | cp $ROOT_PATH/build/libs/chat-0.0.1-SNAPSHOT.jar $JAR
18 |
19 | # echo "[$NOW] > $JAR 실행" >> $START_LOG
20 | # nohup java -jar -$PROFILES_ACTIVE $JAR > $APP_LOG 2> $ERROR_LOG &
21 |
22 | #echo "[$NOW] JIB 도커 빌드" >> $START_LOG
23 | #cd $ROOT_PATH
24 | #./gradlew jibDockerBuild
25 | #
26 | #echo "[$NOW] > $IMAGE 실행" >> $START_LOG
27 | #docker run -d -p 8080:8080 --name $IMAGE $IMAGE:$TAG
28 | #
29 | #SERVICE_PID=$(pgrep -f $CONTAINER
30 | #echo "[$NOW] > 서비스 PID: $SERVICE_PID" >> $START_LOG
31 |
--------------------------------------------------------------------------------
/src/main/java/com/challenge/chat/security/jwt/util/PasswordUtil.java:
--------------------------------------------------------------------------------
1 | package com.challenge.chat.security.jwt.util;
2 |
3 | import java.util.Random;
4 |
5 | public class PasswordUtil {
6 |
7 | public static String generateRandomPassword() {
8 | int index = 0;
9 | char[] charSet = new char[] {
10 | '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
11 | 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
12 | 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
13 | 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
14 | 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'
15 | }; //배열안의 문자 숫자는 원하는대로
16 |
17 | StringBuffer password = new StringBuffer();
18 | Random random = new Random();
19 |
20 | for (int i = 0; i < 8 ; i++) {
21 | double rd = random.nextDouble();
22 | index = (int) (charSet.length * rd);
23 |
24 | password.append(charSet[index]);
25 | }
26 | System.out.println(password);
27 | return password.toString();
28 | //StringBuffer를 String으로 변환해서 return 하려면 toString()을 사용하면 된다.
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/main/java/com/challenge/chat/security/oauth/handler/OAuth2LoginFailureHandler.java:
--------------------------------------------------------------------------------
1 | package com.challenge.chat.security.oauth.handler;
2 |
3 | import java.io.IOException;
4 |
5 | import javax.servlet.ServletException;
6 | import javax.servlet.http.HttpServletRequest;
7 | import javax.servlet.http.HttpServletResponse;
8 |
9 | import org.springframework.security.core.AuthenticationException;
10 | import org.springframework.security.web.authentication.AuthenticationFailureHandler;
11 | import org.springframework.stereotype.Component;
12 |
13 | import lombok.extern.slf4j.Slf4j;
14 |
15 | @Slf4j
16 | @Component
17 | public class OAuth2LoginFailureHandler implements AuthenticationFailureHandler {
18 | @Override
19 | public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws
20 | IOException,
21 | ServletException {
22 | response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
23 | response.getWriter().write("소셜 로그인 실패! 서버 로그를 확인해주세요.");
24 | log.info("소셜 로그인에 실패했습니다. 에러 메시지 : {}", exception.getMessage());
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/main/java/com/challenge/chat/domain/chat/service/Producer.java:
--------------------------------------------------------------------------------
1 | package com.challenge.chat.domain.chat.service;
2 |
3 | import com.challenge.chat.domain.chat.dto.ChatDto;
4 | import com.challenge.chat.domain.chat.entity.Chat;
5 | import com.challenge.chat.exception.RestApiException;
6 | import com.challenge.chat.exception.dto.ChatErrorCode;
7 |
8 | import lombok.extern.slf4j.Slf4j;
9 | import org.springframework.beans.factory.annotation.Autowired;
10 | import org.springframework.kafka.core.KafkaTemplate;
11 | import org.springframework.stereotype.Component;
12 |
13 | @Component
14 | @Slf4j
15 | public class Producer {
16 |
17 | @Autowired
18 | private KafkaTemplate kafkaTemplate;
19 |
20 | public void send(String topic, ChatDto data) {
21 | log.info("sending data='{}' to topic='{}'", data, topic);
22 | try {
23 | kafkaTemplate.send(topic, data).get(); // send to react clients via websocket (STOMP)
24 | } catch (Exception e) {
25 | throw new RestApiException(ChatErrorCode.KAFKA_PRODUCER_ERROR);
26 | }
27 | }
28 | }
29 |
30 |
--------------------------------------------------------------------------------
/src/test/resources/application-common.yml:
--------------------------------------------------------------------------------
1 | spring:
2 | config:
3 | activate:
4 | on-profile: common
5 | jpa:
6 | properties:
7 | hibernate:
8 | show_sql: true
9 | format_sql: true
10 | hbm2ddl:
11 | auto: update
12 | open-in-view: false
13 |
14 | jwt:
15 | secretKey: ENC(uvRR3r/GGDxBWH5fuINwi8uQVmhDV9lkuDtiDmEJeKnEMNYLlIZ/lfPYSrpn/LLPUtJflpTdSDxXGd6qRIfWrWmvtdS88f8JI3B1yQQ0VQRsywHyg/wB7w==)
16 |
17 | access:
18 | expiration: 3600000 # 1시간(60분) (1000L(ms -> s) * 60L(s -> m) * 60L(m -> h))
19 | header: Authorization
20 |
21 | refresh:
22 | expiration: 1209600000 # (1000L(ms -> s) * 60L(s -> m) * 60L(m -> h) * 24L(h -> 하루) * 14(2주))
23 | header: Authorization-refresh
24 |
25 | cloud:
26 | aws:
27 | s3:
28 | bucket: chatchallengebucket
29 | stack.auto: false
30 | region.static: ENC(0bq5sPO9vq8ID5qNhZiAJw1Kllk8pwUn)
31 | credentials:
32 | accessKey: ENC(DlgOorwSMoUWRgJVRCLT5bMjNYRFF63S3ZSnOEghuU0=)
33 | secretKey: ENC(CMos8FOe37EVh8yf5UYKcSH7zuDzlNCd8gt69yogP8SrBogWqOg0/+vNbJW14Rsw4iyeZxpsjoA=)
34 |
35 |
--------------------------------------------------------------------------------
/src/main/java/com/challenge/chat/domain/chat/service/Consumer.java:
--------------------------------------------------------------------------------
1 | package com.challenge.chat.domain.chat.service;
2 |
3 | import com.challenge.chat.domain.chat.constant.KafkaConstants;
4 | import com.challenge.chat.domain.chat.dto.ChatDto;
5 | import lombok.extern.slf4j.Slf4j;
6 | import org.springframework.beans.factory.annotation.Autowired;
7 | import org.springframework.kafka.annotation.KafkaListener;
8 | import org.springframework.messaging.simp.SimpMessagingTemplate;
9 | import org.springframework.stereotype.Component;
10 |
11 | @Slf4j
12 | @Component
13 | public class Consumer {
14 | /**
15 | * @KafkaLister 어노테이션을 통해 Kafka로부터 메세지를 받을 수 있음
16 | * template.convertAndSend를 통해 WebSocket으로 메시지를 전송
17 | * Message를 작성할 때 경로 잘 보고 import
18 | */
19 | @Autowired
20 | SimpMessagingTemplate msgOperation;
21 |
22 | // @KafkaListener(
23 | // topics = KafkaConstants.KAFKA_TOPIC,
24 | // groupId = KafkaConstants.GROUP_ID
25 | // )
26 | // public void consume(ChatDto chatDto) {
27 | // msgOperation.convertAndSend("/topic/chat/room/" + chatDto.getRoomCode(), chatDto);
28 | // }
29 | }
30 |
--------------------------------------------------------------------------------
/src/main/java/com/challenge/chat/domain/member/entity/MemberFriend.java:
--------------------------------------------------------------------------------
1 | package com.challenge.chat.domain.member.entity;
2 |
3 | import javax.persistence.Entity;
4 | import javax.persistence.GeneratedValue;
5 | import javax.persistence.GenerationType;
6 | import javax.persistence.Id;
7 | import javax.persistence.JoinColumn;
8 | import javax.persistence.ManyToOne;
9 |
10 | import lombok.Getter;
11 | import lombok.NoArgsConstructor;
12 |
13 | @Entity
14 | @Getter
15 | @NoArgsConstructor
16 | public class MemberFriend {
17 |
18 | @Id
19 | @GeneratedValue(strategy = GenerationType.IDENTITY)
20 | private Long id;
21 |
22 | @ManyToOne
23 | @JoinColumn(name = "MEMBER_ID")
24 | private Member member;
25 |
26 | @ManyToOne
27 | @JoinColumn(name = "FRIEND_ID")
28 | private Member friend;
29 |
30 | private MemberFriend(Member member, Member friend) {
31 | this.member = member;
32 | this.friend = friend;
33 | // member.getFriendList().add(this);
34 | }
35 |
36 | public static MemberFriend of(Member member, Member friend) {
37 | return new MemberFriend(member, friend);
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/main/java/com/challenge/chat/config/ElasticSearchConfig.java:
--------------------------------------------------------------------------------
1 | // package com.challenge.chat.config;
2 | //
3 | // import org.elasticsearch.client.RestHighLevelClient;
4 | // import org.springframework.context.annotation.Configuration;
5 | // import org.springframework.data.elasticsearch.client.ClientConfiguration;
6 | // import org.springframework.data.elasticsearch.client.RestClients;
7 | // import org.springframework.data.elasticsearch.config.AbstractElasticsearchConfiguration;
8 | // import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories;
9 | //
10 | // import com.challenge.chat.domain.chat.repository.ChatSearchRepository;
11 | //
12 | // @Configuration
13 | // @EnableElasticsearchRepositories(basePackageClasses = {ChatSearchRepository.class})
14 | // public class ElasticSearchConfig extends AbstractElasticsearchConfiguration {
15 | // @Override
16 | // public RestHighLevelClient elasticsearchClient() {
17 | // // http port 와 통신할 주소
18 | // ClientConfiguration configuration = ClientConfiguration.builder()
19 | // .connectedTo("es:9200")
20 | // .build();
21 | // return RestClients.create(configuration).rest();
22 | // }
23 | // }
24 |
--------------------------------------------------------------------------------
/src/main/java/com/challenge/chat/domain/chat/entity/MemberChatRoom.java:
--------------------------------------------------------------------------------
1 | package com.challenge.chat.domain.chat.entity;
2 |
3 | import javax.persistence.Entity;
4 | import javax.persistence.GeneratedValue;
5 | import javax.persistence.GenerationType;
6 | import javax.persistence.Id;
7 | import javax.persistence.JoinColumn;
8 | import javax.persistence.ManyToOne;
9 |
10 | import com.challenge.chat.domain.member.entity.Member;
11 |
12 | import lombok.Getter;
13 | import lombok.NoArgsConstructor;
14 |
15 | @Entity
16 | @Getter
17 | @NoArgsConstructor
18 | public class MemberChatRoom {
19 | @Id
20 | @GeneratedValue(strategy = GenerationType.IDENTITY)
21 | private Long id;
22 |
23 | @ManyToOne
24 | @JoinColumn(name = "ROOM_ID")
25 | private ChatRoom room;
26 |
27 | @ManyToOne
28 | @JoinColumn(name = "MEMBER_ID")
29 | private Member member;
30 |
31 | private MemberChatRoom(ChatRoom room, Member member) {
32 | this.room = room;
33 | this.member = member;
34 | // room.getMemberList().add(this);
35 | // member.getRoomList().add(this);
36 | }
37 |
38 | public static MemberChatRoom of(ChatRoom room, Member member) {
39 | return new MemberChatRoom(room, member);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/main/java/com/challenge/chat/ChatApplication.java:
--------------------------------------------------------------------------------
1 | package com.challenge.chat;
2 |
3 | import org.springframework.boot.SpringApplication;
4 | import org.springframework.boot.autoconfigure.SpringBootApplication;
5 | import org.springframework.context.annotation.ComponentScan;
6 | import org.springframework.context.annotation.FilterType;
7 | import org.springframework.data.elasticsearch.config.EnableElasticsearchAuditing;
8 | import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
9 | import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
10 | import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;
11 |
12 | import com.challenge.chat.domain.chat.repository.ChatRepository;
13 |
14 | @EnableElasticsearchAuditing
15 | @EnableJpaAuditing
16 | @EnableJpaRepositories(excludeFilters = @ComponentScan.Filter(
17 | type = FilterType.ASSIGNABLE_TYPE,
18 | classes = {ChatRepository.class}))
19 | @EnableMongoRepositories(basePackageClasses = {ChatRepository.class})
20 | @SpringBootApplication
21 | public class ChatApplication {
22 |
23 | public static void main(String[] args) {
24 | SpringApplication.run(ChatApplication.class, args);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/main/java/com/challenge/chat/security/login/handler/LoginFailureHandler.java:
--------------------------------------------------------------------------------
1 | package com.challenge.chat.security.login.handler;
2 |
3 | import java.io.IOException;
4 |
5 | import javax.servlet.http.HttpServletRequest;
6 | import javax.servlet.http.HttpServletResponse;
7 |
8 | import org.springframework.security.core.AuthenticationException;
9 | import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
10 |
11 | import lombok.extern.slf4j.Slf4j;
12 |
13 | /**
14 | * JWT 로그인 실패 시 처리하는 핸들러
15 | * SimpleUrlAuthenticationFailureHandler를 상속받아서 구현
16 | */
17 | @Slf4j
18 | public class LoginFailureHandler extends SimpleUrlAuthenticationFailureHandler {
19 |
20 | @Override
21 | public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
22 | AuthenticationException exception) throws IOException {
23 | response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
24 | response.setCharacterEncoding("UTF-8");
25 | response.setContentType("text/plain;charset=UTF-8");
26 | response.getWriter().write("로그인 실패! 이메일이나 비밀번호를 확인해주세요.");
27 | log.info("로그인에 실패했습니다. 메시지 : {}", exception.getMessage());
28 | }
29 | }
--------------------------------------------------------------------------------
/src/main/java/com/challenge/chat/domain/chat/entity/ChatRoom.java:
--------------------------------------------------------------------------------
1 | package com.challenge.chat.domain.chat.entity;
2 |
3 | import java.util.List;
4 | import java.util.UUID;
5 |
6 | import lombok.Getter;
7 | import lombok.NoArgsConstructor;
8 |
9 | import javax.persistence.CascadeType;
10 | import javax.persistence.Column;
11 | import javax.persistence.Entity;
12 | import javax.persistence.GeneratedValue;
13 | import javax.persistence.GenerationType;
14 | import javax.persistence.Id;
15 | import javax.persistence.OneToMany;
16 |
17 | @Entity
18 | @Getter
19 | @NoArgsConstructor
20 | public class ChatRoom extends TimeStamped {
21 | @Id
22 | @GeneratedValue(strategy = GenerationType.IDENTITY)
23 | @Column(name = "ROOM_ID")
24 | private Long id;
25 |
26 | private String roomCode;
27 |
28 | @Column(nullable = false)
29 | private String roomName;
30 |
31 | @OneToMany(mappedBy = "room", orphanRemoval = true, cascade = CascadeType.ALL)
32 | private List memberList;
33 |
34 | private ChatRoom(String roomName) {
35 | this.roomCode = UUID.randomUUID().toString();
36 | this.roomName = roomName;
37 | }
38 |
39 | public static ChatRoom of(String roomName) {
40 | return new ChatRoom(roomName);
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/main/java/com/challenge/chat/domain/chat/entity/Chat.java:
--------------------------------------------------------------------------------
1 | package com.challenge.chat.domain.chat.entity;
2 |
3 | import lombok.AllArgsConstructor;
4 | import lombok.Getter;
5 | import lombok.NoArgsConstructor;
6 | import lombok.Setter;
7 |
8 | import java.time.Instant;
9 |
10 | import org.springframework.data.mongodb.core.mapping.Document;
11 |
12 | @Getter
13 | @Setter
14 | @Document(collection = "chat")
15 | @NoArgsConstructor
16 | @AllArgsConstructor
17 | public class Chat {
18 | // @Id
19 | // private String id;
20 |
21 | private MessageType type;
22 |
23 | private String nickname;
24 |
25 | private String email;
26 |
27 | private String roomCode;
28 |
29 | private String message;
30 |
31 | // @CreatedDate
32 | private String createdAt;
33 |
34 | private String imageUrl;
35 |
36 | private Chat(MessageType type, String nickname, String email, String roomCode, String message) {
37 | this.type = type;
38 | this.nickname = nickname;
39 | this.email = email;
40 | this.roomCode = roomCode;
41 | this.message = message;
42 | }
43 |
44 | public static Chat of(MessageType type, String nickname, String email, String roomCode, String message) {
45 | return new Chat(type, nickname, email, roomCode, message);
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/test/java/com/challenge/chat/config/JasyptConfigTest.java:
--------------------------------------------------------------------------------
1 | // package com.challenge.chat.config;
2 | //
3 | // import static org.assertj.core.api.Assertions.*;
4 | //
5 | // import org.jasypt.encryption.StringEncryptor;
6 | // import org.junit.jupiter.api.DisplayName;
7 | // import org.junit.jupiter.api.Test;
8 | // import org.springframework.beans.factory.annotation.Autowired;
9 | // import org.springframework.beans.factory.annotation.Qualifier;
10 | // import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
11 | // import org.springframework.boot.test.mock.mockito.MockBean;
12 | // import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext;
13 | //
14 | // @WebMvcTest(JasyptConfig.class)
15 | // @MockBean(JpaMetamodelMappingContext.class)
16 | // class JasyptConfigTest {
17 | //
18 | // @Autowired
19 | // @Qualifier("jasyptStringEncryptor")
20 | // StringEncryptor encryptor;
21 | //
22 | // @Test
23 | // @DisplayName("Jasypt 암복호화 테스트")
24 | // public void jasyptEncryptDecryptTest() {
25 | // String plainText = "TestText";
26 | //
27 | // String encryptedText = encryptor.encrypt(plainText);
28 | // String decryptedText = encryptor.decrypt(encryptedText);
29 | //
30 | // assertThat(plainText).isEqualTo(decryptedText);
31 | // }
32 | // }
--------------------------------------------------------------------------------
/src/main/java/com/challenge/chat/security/oauth/dto/CustomOAuth2User.java:
--------------------------------------------------------------------------------
1 | package com.challenge.chat.security.oauth.dto;
2 |
3 | import java.util.Collection;
4 | import java.util.Map;
5 |
6 | import org.springframework.security.core.GrantedAuthority;
7 | import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
8 |
9 | import com.challenge.chat.domain.member.constant.MemberRole;
10 |
11 | import lombok.Getter;
12 |
13 | /**
14 | * DefaultOAuth2User를 상속하고, email과 role 필드를 추가로 가진다.
15 | */
16 | @Getter
17 | public class CustomOAuth2User extends DefaultOAuth2User {
18 |
19 | private String email;
20 | private MemberRole role;
21 |
22 | /**
23 | * Constructs a {@code DefaultOAuth2User} using the provided parameters.
24 | *
25 | * @param authorities the authorities granted to the user
26 | * @param attributes the attributes about the user
27 | * @param nameAttributeKey the key used to access the user's "name" from
28 | * {@link #getAttributes()}
29 | */
30 | public CustomOAuth2User(Collection extends GrantedAuthority> authorities,
31 | Map attributes, String nameAttributeKey, String email, MemberRole role) {
32 |
33 | super(authorities, attributes, nameAttributeKey);
34 | this.email = email;
35 | this.role = role;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/main/resources/application-dev.yml:
--------------------------------------------------------------------------------
1 | spring:
2 | config:
3 | activate:
4 | on-profile: dev
5 | datasource:
6 | hikari:
7 | connection-timeout: 60000
8 | url: ENC(AtbAmjmXqguvVjtmxT14rzHAGZXtPCNJJ/VUUG7ajPBIGeY/tCv9Dht1wHSnTu4xRD1qFg0jI2J3+4Kky0ZeXYY7CmzGw8RI9/VJuDR4vReoyTkWgVtqEVbd0v2EJNiQRbK+XW1vy3hdZ3wluhgFU7SZZCsAjwM+u2v5S76msntjxsDpjHU7qdrShxqjX3H67+0X59IZjqjr6HPMRjFYhQ==)
9 | username: ENC(nZEFzHuwF4wasrpbc2TcJQ==)
10 | password: ENC(mJyIfhz6z46U9Q4Exy4LQamYMyEBiJiG)
11 | driver-class-name: com.mysql.cj.jdbc.Driver
12 | data:
13 | mongodb:
14 | uri: mongodb+srv://admin:chat1122@thiscode.cpaiaoh.mongodb.net/thiscode?retryWrites=true&w=majority
15 | auto-index-creation: true
16 | jpa:
17 | properties:
18 | hibernate:
19 | show_sql: false
20 | format_sql: false
21 | hbm2ddl:
22 | auto: update
23 | # rabbitmq:
24 | # host: rabbitMQ
25 | # port: 5672
26 | # username: guest
27 | # password: guest
28 |
29 | application:
30 | name: monitoring
31 |
32 | management:
33 | endpoint:
34 | metrics:
35 | enabled: true
36 | prometheus:
37 | enabled: true
38 |
39 | endpoints:
40 | web:
41 | exposure:
42 | include: health, info, metrics, prometheus
43 |
44 | metrics:
45 | tags:
46 | application: ${spring.application.name}
47 |
--------------------------------------------------------------------------------
/src/main/java/com/challenge/chat/config/JasyptConfig.java:
--------------------------------------------------------------------------------
1 | package com.challenge.chat.config;
2 |
3 | import com.ulisesbocchio.jasyptspringboot.annotation.EnableEncryptableProperties;
4 | import org.jasypt.encryption.StringEncryptor;
5 | import org.jasypt.encryption.pbe.PooledPBEStringEncryptor;
6 | import org.jasypt.encryption.pbe.config.SimpleStringPBEConfig;
7 | import org.springframework.beans.factory.annotation.Value;
8 | import org.springframework.context.annotation.Bean;
9 | import org.springframework.context.annotation.Configuration;
10 |
11 | @Configuration
12 | @EnableEncryptableProperties
13 | public class JasyptConfig {
14 |
15 | @Value("${jasypt.password}")
16 | private String password;
17 |
18 | @Bean("jasyptStringEncryptor")
19 | public StringEncryptor stringEncryptor() {
20 | PooledPBEStringEncryptor encryptor = new PooledPBEStringEncryptor();
21 | SimpleStringPBEConfig config = new SimpleStringPBEConfig();
22 | config.setPassword(password);
23 | config.setAlgorithm("PBEWithMD5AndDES");
24 | config.setKeyObtentionIterations("1000");
25 | config.setPoolSize("1");
26 | config.setProviderName("SunJCE");
27 | config.setSaltGeneratorClassName("org.jasypt.salt.RandomSaltGenerator");
28 | config.setIvGeneratorClassName("org.jasypt.iv.NoIvGenerator");
29 | config.setStringOutputType("base64");
30 | encryptor.setConfig(config);
31 | return encryptor;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/main/java/com/challenge/chat/config/WebSocketConfig.java:
--------------------------------------------------------------------------------
1 | package com.challenge.chat.config;
2 |
3 | import org.springframework.context.annotation.Configuration;
4 | import org.springframework.messaging.simp.config.MessageBrokerRegistry;
5 | import org.springframework.util.AntPathMatcher;
6 | import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
7 | import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
8 | import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
9 |
10 | @Configuration
11 | @EnableWebSocketMessageBroker
12 | public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
13 |
14 | @Override
15 | public void configureMessageBroker(MessageBrokerRegistry config) {
16 | // config.setPathMatcher(new AntPathMatcher(".")); // url을 chat/room/3 -> chat.room.3으로 참조하기 위한 설정
17 | config.enableSimpleBroker("/queue", "/topic");
18 | // config.enableStompBrokerRelay("/queue", "/topic", "/exchange", "/amq/queue")
19 | // .setRelayHost("rabbitMQ")
20 | // .setClientLogin("guest")
21 | // .setClientPasscode("guest");
22 | config.setApplicationDestinationPrefixes("/app");
23 | }
24 |
25 | @Override
26 | public void registerStompEndpoints(StompEndpointRegistry registry) {
27 | registry.addEndpoint("/ws-chat")
28 | .setAllowedOriginPatterns("*").withSockJS();
29 | }
30 |
31 | }
--------------------------------------------------------------------------------
/src/main/java/com/challenge/chat/config/MongoConfig.java:
--------------------------------------------------------------------------------
1 | package com.challenge.chat.config;
2 |
3 | import org.springframework.beans.factory.annotation.Autowired;
4 | import org.springframework.context.annotation.Bean;
5 | import org.springframework.context.annotation.Configuration;
6 | import org.springframework.data.mongodb.MongoDatabaseFactory;
7 | import org.springframework.data.mongodb.core.convert.DbRefResolver;
8 | import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver;
9 | import org.springframework.data.mongodb.core.convert.DefaultMongoTypeMapper;
10 | import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
11 | import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
12 |
13 | @Configuration
14 | public class MongoConfig {
15 | // MongoDB 에 "_class" 들어가지 않게 설정
16 | @Autowired
17 | private MongoMappingContext mongoMappingContext;
18 |
19 | @Bean
20 | public MappingMongoConverter mappingMongoConverter(
21 | MongoDatabaseFactory mongoDatabaseFactory,
22 | MongoMappingContext mongoMappingContext
23 | ) {
24 | DbRefResolver dbRefResolver = new DefaultDbRefResolver(mongoDatabaseFactory);
25 | MappingMongoConverter converter = new MappingMongoConverter(dbRefResolver, mongoMappingContext);
26 | converter.setTypeMapper(new DefaultMongoTypeMapper(null));
27 | return converter;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/main/java/com/challenge/chat/security/login/service/LoginService.java:
--------------------------------------------------------------------------------
1 | package com.challenge.chat.security.login.service;
2 |
3 | import org.springframework.security.core.userdetails.UserDetails;
4 | import org.springframework.security.core.userdetails.UserDetailsService;
5 | import org.springframework.security.core.userdetails.UsernameNotFoundException;
6 | import org.springframework.stereotype.Service;
7 |
8 | import com.challenge.chat.domain.member.entity.Member;
9 | import com.challenge.chat.domain.member.repository.MemberRepository;
10 | import com.challenge.chat.exception.RestApiException;
11 | import com.challenge.chat.exception.dto.MemberErrorCode;
12 |
13 | import lombok.RequiredArgsConstructor;
14 | import lombok.extern.slf4j.Slf4j;
15 |
16 | @Slf4j
17 | @Service
18 | @RequiredArgsConstructor
19 | public class LoginService implements UserDetailsService {
20 |
21 | private final MemberRepository memberRepository;
22 |
23 | @Override
24 | public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
25 | Member member = memberRepository.findByEmail(email)
26 | .orElseThrow(() -> new RestApiException(MemberErrorCode.MEMBER_NOT_FOUND));
27 |
28 | log.info("일반 로그인 서비스 로직입니다.");
29 | return org.springframework.security.core.userdetails.User.builder()
30 | .username(member.getEmail())
31 | .password(member.getPassword())
32 | .roles(member.getRole().name())
33 | .build();
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/main/java/com/challenge/chat/domain/member/entity/Member.java:
--------------------------------------------------------------------------------
1 | package com.challenge.chat.domain.member.entity;
2 |
3 | import java.util.List;
4 |
5 | import com.challenge.chat.domain.chat.entity.MemberChatRoom;
6 | import com.challenge.chat.domain.member.constant.MemberRole;
7 | import com.challenge.chat.domain.member.constant.SocialType;
8 | import lombok.*;
9 |
10 | import javax.persistence.CascadeType;
11 | import javax.persistence.Column;
12 | import javax.persistence.Entity;
13 | import javax.persistence.EnumType;
14 | import javax.persistence.Enumerated;
15 | import javax.persistence.GeneratedValue;
16 | import javax.persistence.GenerationType;
17 | import javax.persistence.Id;
18 | import javax.persistence.OneToMany;
19 |
20 | @Entity
21 | @Getter
22 | @NoArgsConstructor
23 | @Builder
24 | @AllArgsConstructor
25 | public class Member {
26 |
27 | @Id
28 | @GeneratedValue(strategy = GenerationType.IDENTITY)
29 | @Column(name = "MEMBER_ID")
30 | private Long id;
31 |
32 | private String email; // 이메일
33 | private String password; // 비밀번호
34 | private String nickname; // 닉네임
35 | private String imageUrl; // 프로필 이미지
36 |
37 | @Enumerated(EnumType.STRING)
38 | private MemberRole role;
39 |
40 | @Enumerated(EnumType.STRING)
41 | private SocialType socialType; // KAKAO, NAVER, GOOGLE
42 |
43 | private String socialId; // 로그인한 소셜 타입의 식별자 값 (일반 로그인인 경우 null)
44 |
45 | private String refreshToken; // 리프레시 토큰
46 |
47 | @OneToMany(mappedBy = "member", orphanRemoval = true, cascade = CascadeType.ALL)
48 | private List roomList;
49 |
50 | @OneToMany(mappedBy = "member", cascade = CascadeType.ALL)
51 | private List friendList;
52 |
53 | public void updateRefreshToken(String updateRefreshToken) {
54 | this.refreshToken = updateRefreshToken;
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/main/java/com/challenge/chat/domain/chat/dto/ChatDto.java:
--------------------------------------------------------------------------------
1 | package com.challenge.chat.domain.chat.dto;
2 |
3 | import com.challenge.chat.domain.chat.entity.Chat;
4 | import com.challenge.chat.domain.chat.entity.MessageType;
5 | import lombok.*;
6 |
7 | import java.time.Instant;
8 |
9 | @Getter
10 | @Setter
11 | @Builder
12 | @AllArgsConstructor
13 | @NoArgsConstructor
14 | public class ChatDto {
15 |
16 | private MessageType type;
17 | private String nickname;
18 | private String email;
19 | private String roomCode;
20 | private String message;
21 | private Instant createdAt;
22 | private String imageUrl;
23 |
24 | public static ChatDto from(Chat chat) {
25 |
26 | Instant instant;
27 | if (chat.getCreatedAt() == null) {
28 | instant = null;
29 | } else {
30 | double timestampValue = Double.parseDouble(chat.getCreatedAt());
31 | long epochSeconds = (long) timestampValue;
32 | instant = Instant.ofEpochSecond(
33 | epochSeconds,
34 | (int) ((timestampValue - epochSeconds) * 1_000_000_000));
35 | }
36 |
37 | return new ChatDto(
38 | chat.getType(),
39 | chat.getNickname(),
40 | chat.getEmail(),
41 | chat.getRoomCode(),
42 | chat.getMessage(),
43 | instant,
44 | chat.getImageUrl()
45 | );
46 | }
47 |
48 | public static Chat toEntity(ChatDto chatDto) {
49 | return Chat.of(
50 | chatDto.getType(),
51 | chatDto.getNickname(),
52 | chatDto.getEmail(),
53 | chatDto.getRoomCode(),
54 | chatDto.getMessage()
55 | );
56 | }
57 |
58 | // public static ChatDto from(ChatES chat) {
59 | // return new ChatDto(
60 | // chat.getType(),
61 | // chat.getNickname(),
62 | // chat.getEmail(),
63 | // chat.getRoomCode(),
64 | // chat.getMessage(),
65 | // Instant.ofEpochMilli(chat.getCreatedAt()),
66 | // chat.getImageUrl()
67 | // );
68 | // }
69 | }
--------------------------------------------------------------------------------
/src/main/java/com/challenge/chat/domain/chat/entity/ChatES.java:
--------------------------------------------------------------------------------
1 | // package com.challenge.chat.domain.chat.entity;
2 | //
3 | // import java.time.Instant;
4 | //
5 | // import javax.persistence.Id;
6 | //
7 | // import org.springframework.data.elasticsearch.annotations.DateFormat;
8 | // import org.springframework.data.elasticsearch.annotations.Document;
9 | // import org.springframework.data.elasticsearch.annotations.Field;
10 | // import org.springframework.data.elasticsearch.annotations.FieldType;
11 | // import org.springframework.data.elasticsearch.annotations.Mapping;
12 | // import org.springframework.data.elasticsearch.annotations.Setting;
13 | //
14 | // import lombok.AccessLevel;
15 | // import lombok.Builder;
16 | // import lombok.Getter;
17 | // import lombok.NoArgsConstructor;
18 | //
19 | // @Mapping(mappingPath = "elastic/chat-mapping.json")
20 | // @Setting(settingPath = "elastic/chat-setting.json")
21 | // @NoArgsConstructor(access = AccessLevel.PROTECTED)
22 | // @Getter
23 | // @Document(indexName = "kafka-chat")
24 | // public class ChatES {
25 | //
26 | // @Id
27 | // private String id;
28 | //
29 | // private MessageType type;
30 | //
31 | // private String nickname;
32 | //
33 | // private String email;
34 | //
35 | // private String roomCode;
36 | //
37 | // private String message;
38 | //
39 | // @Field(type = FieldType.Date, format = {DateFormat.date_hour_minute_second_millis, DateFormat.epoch_millis})
40 | // private long createdAt;
41 | //
42 | // private String imageUrl;
43 | //
44 | //
45 | // // @Builder
46 | // // public ChatES(String id, MessageType type, String nickname, String email, String roomCode, String message,
47 | // // Instant createdAt) {
48 | // // this.id = id;
49 | // // this.type = type;
50 | // // this.nickname = nickname;
51 | // // this.email = email;
52 | // // this.roomCode = roomCode;
53 | // // this.message = message;
54 | // // this.createdAt = createdAt;
55 | // // }
56 | // }
--------------------------------------------------------------------------------
/HELP.md:
--------------------------------------------------------------------------------
1 | # Getting Started
2 |
3 | ### Reference Documentation
4 | For further reference, please consider the following sections:
5 |
6 | * [Official Gradle documentation](https://docs.gradle.org)
7 | * [Spring Boot Gradle Plugin Reference Guide](https://docs.spring.io/spring-boot/docs/3.0.7/gradle-plugin/reference/html/)
8 | * [Create an OCI image](https://docs.spring.io/spring-boot/docs/3.0.7/gradle-plugin/reference/html/#build-image)
9 | * [Spring Web](https://docs.spring.io/spring-boot/docs/3.0.7/reference/htmlsingle/#web)
10 | * [Spring Security](https://docs.spring.io/spring-boot/docs/3.0.7/reference/htmlsingle/#web.security)
11 | * [Spring Data JPA](https://docs.spring.io/spring-boot/docs/3.0.7/reference/htmlsingle/#data.sql.jpa-and-spring-data)
12 | * [OAuth2 Client](https://docs.spring.io/spring-boot/docs/3.0.7/reference/htmlsingle/#web.security.oauth2.client)
13 | * [WebSocket](https://docs.spring.io/spring-boot/docs/3.0.7/reference/htmlsingle/#messaging.websockets)
14 |
15 | ### Guides
16 | The following guides illustrate how to use some features concretely:
17 |
18 | * [Building a RESTful Web Service](https://spring.io/guides/gs/rest-service/)
19 | * [Serving Web Content with Spring MVC](https://spring.io/guides/gs/serving-web-content/)
20 | * [Building REST services with Spring](https://spring.io/guides/tutorials/rest/)
21 | * [Securing a Web Application](https://spring.io/guides/gs/securing-web/)
22 | * [Spring Boot and OAuth2](https://spring.io/guides/tutorials/spring-boot-oauth2/)
23 | * [Authenticating a User with LDAP](https://spring.io/guides/gs/authenticating-ldap/)
24 | * [Accessing Data with JPA](https://spring.io/guides/gs/accessing-data-jpa/)
25 | * [Using WebSocket to build an interactive web application](https://spring.io/guides/gs/messaging-stomp-websocket/)
26 |
27 | ### Additional Links
28 | These additional references should also help you:
29 |
30 | * [Gradle Build Scans – insights for your project's build](https://scans.gradle.com#gradle)
31 |
32 |
--------------------------------------------------------------------------------
/src/main/java/com/challenge/chat/domain/member/controller/MemberController.java:
--------------------------------------------------------------------------------
1 | package com.challenge.chat.domain.member.controller;
2 |
3 | import com.challenge.chat.domain.member.dto.MemberDto;
4 | import com.challenge.chat.domain.member.dto.request.SignupRequest;
5 | import com.challenge.chat.domain.member.dto.request.MemberAddRequest;
6 | import com.challenge.chat.domain.member.service.MemberService;
7 |
8 | import lombok.RequiredArgsConstructor;
9 | import lombok.extern.slf4j.Slf4j;
10 |
11 | import org.springframework.http.HttpStatus;
12 | import org.springframework.http.ResponseEntity;
13 | import org.springframework.security.core.annotation.AuthenticationPrincipal;
14 | import org.springframework.security.core.userdetails.User;
15 | import org.springframework.web.bind.annotation.GetMapping;
16 | import org.springframework.web.bind.annotation.PostMapping;
17 | import org.springframework.web.bind.annotation.RequestBody;
18 | import org.springframework.web.bind.annotation.RestController;
19 |
20 | import java.util.List;
21 |
22 | import javax.validation.Valid;
23 |
24 | @RestController
25 | @Slf4j
26 | @RequiredArgsConstructor
27 | public class MemberController {
28 |
29 | private final MemberService memberService;
30 |
31 | @PostMapping("/users/signup")
32 | public ResponseEntity signup(@RequestBody @Valid final SignupRequest signupRequest) {
33 | memberService.signup(signupRequest);
34 | return ResponseEntity.status(HttpStatus.OK).body("회원가입 성공");
35 | }
36 |
37 | @PostMapping("/users/friend")
38 | public ResponseEntity addFriend(
39 | @AuthenticationPrincipal final User user,
40 | @RequestBody @Valid final MemberAddRequest memberAddRequest) {
41 | memberService.addFriend(user.getUsername(), memberAddRequest.getEmail());
42 | return ResponseEntity.status(HttpStatus.OK).body("친구추가 성공");
43 | }
44 |
45 |
46 | @GetMapping("/users/friend")
47 | public ResponseEntity> getFriendList(@AuthenticationPrincipal final User user) {
48 | return ResponseEntity.status(HttpStatus.OK)
49 | .body(memberService.searchFriendList(user.getUsername()));
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/main/java/com/challenge/chat/domain/chat/config/ProducerConfig.java:
--------------------------------------------------------------------------------
1 | package com.challenge.chat.domain.chat.config;
2 |
3 | import com.challenge.chat.domain.chat.constant.KafkaConstants;
4 | import com.challenge.chat.domain.chat.dto.ChatDto;
5 | import com.challenge.chat.domain.chat.entity.Chat;
6 |
7 | import org.apache.kafka.common.serialization.StringSerializer;
8 | import org.springframework.context.annotation.Bean;
9 | import org.springframework.context.annotation.Configuration;
10 | import org.springframework.kafka.annotation.EnableKafka;
11 | import org.springframework.kafka.core.DefaultKafkaProducerFactory;
12 | import org.springframework.kafka.core.KafkaTemplate;
13 | import org.springframework.kafka.core.ProducerFactory;
14 | import org.springframework.kafka.support.serializer.JsonSerializer;
15 |
16 | import java.util.HashMap;
17 | import java.util.Map;
18 |
19 | @EnableKafka
20 | @Configuration
21 | public class ProducerConfig {
22 | /**
23 | * producer는 TOPIC에 메시지를 작성
24 | * KafkaTemplate을 통해 TOPIC에 메시지를 보낼 수 있음
25 | * BOOTSTRAP_SERVERS_CONFIG는 Kafka가 실행되는 주소를 설정
26 | * KEY_SERIALIZER_CLASS_CONFIG와 VALUE_SERIALIZER_CLASS_CONFIG는 Kafka로 보내는 데이터의 키와 값을 직렬화함
27 | * 문자열을 넘길땐 StringSerializer.class를, JSON 데이터를 넘길 땐 JsonSerializer.class를 적어주면 됨
28 | * properties나 yaml으로 설정할 수도 있고, 아래처럼 @Bean으로 설정해줄 수도 있음
29 | */
30 | @Bean
31 | public ProducerFactory producerFactory() {
32 | return new DefaultKafkaProducerFactory<>(producerConfigurations());
33 | }
34 |
35 | @Bean
36 | public Map producerConfigurations() {
37 | Map configurations = new HashMap<>();
38 | configurations.put(org.apache.kafka.clients.producer.ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, KafkaConstants.KAFKA_BROKER);
39 | configurations.put(org.apache.kafka.clients.producer.ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
40 | configurations.put(org.apache.kafka.clients.producer.ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class);
41 | return configurations;
42 | }
43 |
44 | @Bean
45 | public KafkaTemplate kafkaTemplate() {
46 | return new KafkaTemplate<>(producerFactory());
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/main/java/com/challenge/chat/security/login/handler/LoginSuccessHandler.java:
--------------------------------------------------------------------------------
1 | package com.challenge.chat.security.login.handler;
2 |
3 | import javax.servlet.http.HttpServletRequest;
4 | import javax.servlet.http.HttpServletResponse;
5 |
6 | import org.springframework.beans.factory.annotation.Value;
7 | import org.springframework.security.core.Authentication;
8 | import org.springframework.security.core.userdetails.UserDetails;
9 | import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
10 |
11 | import com.challenge.chat.domain.member.repository.MemberRepository;
12 | import com.challenge.chat.security.jwt.service.JwtService;
13 |
14 | import lombok.RequiredArgsConstructor;
15 | import lombok.extern.slf4j.Slf4j;
16 |
17 | @Slf4j
18 | @RequiredArgsConstructor
19 | public class LoginSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
20 |
21 | private final JwtService jwtService;
22 | private final MemberRepository memberRepository;
23 |
24 | @Value("${jwt.access.expiration}")
25 | private String accessTokenExpiration;
26 |
27 | @Override
28 | public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
29 | Authentication authentication) {
30 | String email = extractUsername(authentication); // 인증 정보에서 Username(email) 추출
31 | String accessToken = jwtService.createAccessToken(email); // JwtService의 createAccessToken을 사용하여 AccessToken 발급
32 | String refreshToken = jwtService.createRefreshToken(); // JwtService의 createRefreshToken을 사용하여 RefreshToken 발급
33 |
34 | jwtService.sendAccessAndRefreshToken(response, accessToken, refreshToken); // 응답 헤더에 AccessToken, RefreshToken 실어서 응답
35 |
36 | // memberRepository.findByEmail(email)
37 | // .ifPresent(user -> {
38 | // user.updateRefreshToken(refreshToken);
39 | // memberRepository.save(user);
40 | // });
41 | log.info("로그인에 성공하였습니다. 이메일 : {}", email);
42 | log.info("로그인에 성공하였습니다. AccessToken : {}", accessToken);
43 | log.info("로그인에 성공하였습니다. RefreshToken : {}", refreshToken);
44 | log.info("발급된 AccessToken 만료 기간 : {}", accessTokenExpiration);
45 | }
46 |
47 | private String extractUsername(Authentication authentication) {
48 | UserDetails userDetails = (UserDetails) authentication.getPrincipal();
49 | return userDetails.getUsername();
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/main/java/com/challenge/chat/security/oauth/dto/OAuthAttributes.java:
--------------------------------------------------------------------------------
1 | package com.challenge.chat.security.oauth.dto;
2 |
3 | import com.challenge.chat.domain.member.constant.MemberRole;
4 | import com.challenge.chat.domain.member.constant.SocialType;
5 | import com.challenge.chat.domain.member.entity.Member;
6 | import com.challenge.chat.security.jwt.util.PasswordUtil;
7 | import lombok.Builder;
8 | import lombok.Getter;
9 |
10 | import java.util.Map;
11 | import java.util.UUID;
12 |
13 | /**
14 | * 각 소셜에서 받아오는 데이터가 다르므로
15 | * 소셜별로 데이터를 받는 데이터를 분기 처리하는 DTO 클래스
16 | */
17 | @Getter
18 | public class OAuthAttributes {
19 |
20 | private String nameAttributeKey; // OAuth2 로그인 진행 시 키가 되는 필드 값, PK와 같은 의미
21 | private OAuth2UserInfo oauth2UserInfo; // 소셜 타입별 로그인 유저 정보(닉네임, 이메일, 프로필 사진 등등)
22 | private PasswordUtil passwordUtil;
23 |
24 | @Builder
25 | public OAuthAttributes(String nameAttributeKey, OAuth2UserInfo oauth2UserInfo) {
26 | this.nameAttributeKey = nameAttributeKey;
27 | this.oauth2UserInfo = oauth2UserInfo;
28 | }
29 |
30 | /**
31 | * SocialType에 맞는 메소드 호출하여 OAuthAttributes 객체 반환
32 | * 파라미터 : userNameAttributeName -> OAuth2 로그인 시 키(PK)가 되는 값 / attributes : OAuth 서비스의 유저 정보들
33 | * 소셜별 of 메소드(ofGoogle, ofKaKao, ofNaver)들은 각각 소셜 로그인 API에서 제공하는
34 | * 회원의 식별값(id), attributes, nameAttributeKey를 저장 후 build
35 | */
36 | public static OAuthAttributes of(SocialType socialType,
37 | String userNameAttributeName, Map attributes) {
38 |
39 | return ofGoogle(userNameAttributeName, attributes);
40 | }
41 |
42 | public static OAuthAttributes ofGoogle(String userNameAttributeName, Map attributes) {
43 | return OAuthAttributes.builder()
44 | .nameAttributeKey(userNameAttributeName)
45 | .oauth2UserInfo(new GoogleOAuth2UserInfo(attributes))
46 | .build();
47 | }
48 |
49 | /**
50 | * of메소드로 OAuthAttributes 객체가 생성되어, 유저 정보들이 담긴 OAuth2UserInfo가 소셜 타입별로 주입된 상태
51 | * OAuth2UserInfo에서 socialId(식별값), nickname, imageUrl을 가져와서 build
52 | * email에는 UUID로 중복 없는 랜덤 값 생성
53 | * role은 GUEST로 설정
54 | */
55 | public Member toEntity(SocialType socialType, OAuth2UserInfo oauth2UserInfo) {
56 | return Member.builder()
57 | .socialType(socialType)
58 | .socialId(oauth2UserInfo.getId())
59 | .email(oauth2UserInfo.getEmail())
60 | .nickname(oauth2UserInfo.getNickname())
61 | .imageUrl(oauth2UserInfo.getImageUrl())
62 | .role(MemberRole.GUEST)
63 | .password(passwordUtil.generateRandomPassword())
64 | .build();
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/main/java/com/challenge/chat/exception/GlobalExceptionHandler.java:
--------------------------------------------------------------------------------
1 | package com.challenge.chat.exception;
2 |
3 | import com.challenge.chat.exception.dto.CommonErrorCode;
4 | import com.challenge.chat.exception.dto.ErrorCode;
5 | import com.challenge.chat.exception.dto.ErrorResponse;
6 | import lombok.extern.slf4j.Slf4j;
7 | import org.springframework.http.ResponseEntity;
8 | import org.springframework.web.bind.annotation.ExceptionHandler;
9 | import org.springframework.web.bind.annotation.RestControllerAdvice;
10 | import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
11 |
12 | @Slf4j
13 | @RestControllerAdvice
14 | public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
15 |
16 | @ExceptionHandler({RestApiException.class})
17 | public ResponseEntity handleRestApiException(final RestApiException exception) {
18 |
19 | log.warn("RestApiException occur: ", exception);
20 |
21 | return this.makeErrorResponseEntity(exception.getErrorCode());
22 | }
23 |
24 | @ExceptionHandler({Exception.class})
25 | public ResponseEntity handleException(final RestApiException exception) {
26 |
27 | log.warn("Exception occur: ", exception);
28 |
29 | return this.makeErrorResponseEntity(CommonErrorCode.INTERNAL_SERVER_ERROR);
30 | }
31 |
32 | // @Override
33 | // protected ResponseEntity