Skip to content

Commit

Permalink
Merge pull request #92 from TeamSynergyy/feature/queryDsl
Browse files Browse the repository at this point in the history
#79 공공 도서관 현황 스케줄러 적용
  • Loading branch information
rivkode authored May 10, 2024
2 parents 0a03012 + b42c3c5 commit de12add
Show file tree
Hide file tree
Showing 27 changed files with 1,067 additions and 58 deletions.
85 changes: 65 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,16 @@

**(프로젝트, 게시글, 팀원을 추천하는 기능을 통해 원활한 프로젝트 진행을 지원합니다)**

<br/>

## Infra Architecture

<img src="./public/infra_architecture.png" alt="logo" width="80%" />


<br/>


## Skills

<div align="center">
Expand Down Expand Up @@ -70,29 +75,38 @@

</div>

<br>

## Setup Dev Environment (Local)

> Java 17이 설치되어있다고 가정합니다
<br>

1. synergy_be 프로젝트를 `git clone` 명령어를 통해 클론 받습니다.
- git clone https://github.com/TeamSynergyy/synergy_be.git
2. app_network 이름의 네트워크를 `docker network create app_network` 명령어로 생성합니다.
3. `docker-compose.yml` 파일을 루트 디렉토리에 생성합니다.
2. 클론받은 스프링 프로젝트 실행파일 (*.jar) 을 생성합니다.
- 루트 위치에서 권한 부여를 위해 `chmod +x gradlew` 명령어 실행
- `./gradlew build -x test` 명령어 실행
3. app_network 이름의 네트워크를 `docker network create app_network` 명령어로 생성합니다.
4. `docker-compose.yml` 파일을 루트 디렉토리에 생성합니다.
- `docker-compose.yml` 파일은 보안상 개인적으로 전달합니다.
4. `docker-compose build` 명령어로 docker 이미지를 생성합니다.
5. `docker-compose up -d` 명령어로 docker-compose 를 통해 docker 이미지를 실행 (컨테이너화) 합니다.
6. host 는 `localhost` 이며 `localhost` url을 통해 프론트 로컬 개발환경을 구성합니다.
- mac일 경우 docker-compose.yml의 mysql, mongo 에 `platform: linux/amd64` 추가 필요
5. `docker-compose build` 명령어로 docker 이미지를 생성합니다.
6. `docker-compose up -d` 명령어로 docker-compose 를 통해 docker 이미지를 실행 (컨테이너화) 합니다.
7. host 는 `localhost` 이며 `localhost` url을 통해 프론트 로컬 개발환경을 구성합니다.

<br>

```
위 로컬 개발환경 구성 순서는 아래와 같이 진행됩니다.
1. 프로젝트 clone
2. docker network 생성
3. docker-compose.yml 파일 생성
4. docker image 빌드
5. docker image 실행
2. 프로젝트 빌드
3. docker network 생성
4. docker-compose.yml 파일 생성
5. docker image 빌드
6. docker image 실행
```

<br>
Expand All @@ -119,9 +133,12 @@ docker pull jonghuni/synergy_be
- 프로젝트 신청, 수락, 거절
- 프로젝트 평가

<br/>

## Directory

<br/>

<details>
<summary> 파일 구조 보기 </summary>

Expand Down Expand Up @@ -288,19 +305,27 @@ src
- 로그인(첫 소셜로그인시 자동 회원가입)
- 회원 정보 변경

<br/>

### API Lists
- login (users/auth/login)
- 로그인, 회원가입을 수행합니다. 로그인 성공시 token을 발급하며 이후 요청에 대해서 해당 토큰으로 인증을 진행합니다.
- updateMyInfo (users/me/info)
- 회원 정보를 변경합니다.

<br/>

#### Using stack
- Spring Boot, Java 11, Spring Data JPA, Mysql, Lombok, Gradle, JWT

<br/>

### Sequence Diagram Example (회원 가입, JWT 토큰 인증 프로세스)

<img src="./public/socialLogin.jpg" alt="logo" width="80%" />

<br/>

## Recommend Service

사용자가 가진 활동들을 바탕으로 사용자에게 알맞는 컨텐츠(게시글, 프로젝트, 유저)를 추천해주는 기능을 제공하는 서비스입니다.
Expand All @@ -309,6 +334,8 @@ src
- 추천 기능이 동작하는 FastAPI 서버를 docker Image화 하여 docker 컨테이너 위에서 실행 (메인서버 또한 Image화 하여 컨테이너위에서 실행)
- DB와 메인 서버로부터 데이터와 API 요청을 받아 모델학습 및 추천을 수행

<br/>

### API List

- getRecommendProjects (projects/recommend)
Expand All @@ -318,29 +345,58 @@ src
- getSimilarUsers (users/recommend)
- 유저 활동을 바탕으로 적합한 유저를 추천합니다.

<br/>

### Sequence Diagram Example (컨텐츠 추천 프로세스)

<br/>

<img src="./public/recommend.png" alt="logo" width="80%" />

<br/>

## Project Service

사용자가 팀원을 구성하며 팀 프로젝트를 진행하며 공지시항, 일정, 티켓 관리, 평가 등의
기능을 제공하는 서비스입니다.

- 티켓 관리 기능의 경우 티켓을 칸반보드로 관리하는 기능으로 각 Status별로 나누어 티켓들을 올바른 위치로 이동하게끔 구현

<br/>

### API List
- changePositionTicket (tickets/change/{ticketId})
- 티켓 위치 변경 기능을 제공합니다.

<br/>

### Sequence Diagram Example (프로젝트 팀원 참가 신청 프로세스)

<img src="./public/apply.png" alt="logo" width="80%" />

<br/>

### Sequence Diagram Example (티켓 위치 변경 프로세스)

<img src="./public/ticket.png" alt="logo" width="80%" />

<br/>
<br/>

## 고민 흔적

- [좋은 객체 ID 만들기 블로그 - click](https://velog.io/@rivkode/ID-%EC%83%9D%EC%84%B1%EA%B8%B0-%EA%B5%AC%ED%98%84%EC%9D%84-%ED%95%B4%EB%B3%B4%EC%95%84%EC%9A%94-2%ED%83%84)
- 좋은 객체 ID를 만들기 위해 아래 4가지 사항을 고려하여 만들기 위해 노력하였습니다.
- 고유성
- 식별 가능성
- 보안성
- 생성 시간순 정렬
- 관련 PR
- [ID 생성기 구현](https://github.com/TeamSynergyy/synergy_be/pull/70)
- [WebSocket 을 이해하며 채팅서비스를 구현해보아요 | MySQL, MongoDB](https://velog.io/@rivkode/WebSocket-%EC%9D%84-%EC%9D%B4%ED%95%B4%ED%95%98%EB%A9%B0-%EC%B1%84%ED%8C%85%EC%84%9C%EB%B9%84%EC%8A%A4%EB%A5%BC-%EA%B5%AC%ED%98%84%ED%95%B4%EB%B3%B4%EC%95%84%EC%9A%94-MySQL-MongoDB)
- 채팅 서비스를 구현하기 위해 아래 사항들을 고려해보았습니다.
- 웹소켓 프로토콜 이해
- 실시간성 보장

<br/>

Expand Down Expand Up @@ -376,17 +432,6 @@ https://github.com/TeamSynergyy/synergy_be/assets/109144975/46ca7bdf-9372-49b0-9
https://github.com/TeamSynergyy/synergy_be/assets/109144975/ada92a51-bc5e-41a8-b0aa-cfef4a11d66b


## 고민 흔적

- [좋은 객체 ID 만들기 블로그 - click](https://velog.io/@rivkode/ID-%EC%83%9D%EC%84%B1%EA%B8%B0-%EA%B5%AC%ED%98%84%EC%9D%84-%ED%95%B4%EB%B3%B4%EC%95%84%EC%9A%94-2%ED%83%84)
- 좋은 객체 ID를 만들기 위해 아래 4가지 사항을 고려하여 만들기 위해 노력하였습니다.
- 고유성
- 식별 가능성
- 보안성
- 생성 시간순 정렬
- 관련 PR
- [ID 생성기 구현](https://github.com/TeamSynergyy/synergy_be/pull/70)



## 발표 PPT
Expand Down
7 changes: 2 additions & 5 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,10 @@ dependencies {
// implementation 'org.springframework.boot:spring-boot-starter-data-redis'

//security
// implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'org.springframework.boot:spring-boot-starter-security'

// 좌표 저장
// implementation 'org.hibernate', name: 'hibernate-spatial'

annotationProcessor "org.springframework.boot:spring-boot-configuration-processor"
// json
implementation 'org.json:json:20231013'

//lombok
compileOnly 'org.projectlombok:lombok'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,10 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling
public class SynergyBeApplication {
public static void main(String[] args) {
SpringApplication.run(SynergyBeApplication.class, args);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.seoultech.synergybe.domain.chat.exception;

import com.seoultech.synergybe.system.exception.BadRequestException;
import com.seoultech.synergybe.system.exception.ErrorCode;

public class WebSocketBadRequestException extends BadRequestException {
public WebSocketBadRequestException(ErrorCode errorCode, String message) {
super(errorCode, message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.seoultech.synergybe.domain.chat.domain.ChatType;
import com.seoultech.synergybe.domain.chat.dto.request.ChatMessageRequest;
import com.seoultech.synergybe.domain.chat.exception.WebSocketBadRequestException;
import com.seoultech.synergybe.domain.chat.service.ChatMessageService;
import com.seoultech.synergybe.domain.user.User;
import com.seoultech.synergybe.domain.user.service.UserService;
import com.seoultech.synergybe.system.exception.ErrorCode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
Expand All @@ -18,37 +22,43 @@

@Slf4j
@Component
//@RequiredArgsConstructor
public class ChatHandler extends TextWebSocketHandler {
// private static List<WebSocketSession> webSocketSessions = new ArrayList<>();
private WebSocketSessionMap webSocketSessionMap; // 채팅방별 세션리스트 모음
private ObjectMapper objectMapper;
private ChatMessageService chatMessageService;
private UserService userService;



// 채팅 핸들러 생성
public ChatHandler(ObjectMapper objectMapper, ChatMessageService chatMessageService) {
public ChatHandler(ObjectMapper objectMapper, ChatMessageService chatMessageService, UserService userService) {
this.webSocketSessionMap = new WebSocketSessionMap();
this.objectMapper = objectMapper;
this.chatMessageService = chatMessageService;
this.userService = userService;
}

@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
// webSocketSessions.add(session);
Long chatRoomId = webSocketSessionMap.getKeyFromSession(session);

// 여기서 request를 어떻게 알지 ?
if (chatRoomId == null) {
// 채팅방 생성
// chatRoomService.createRoom();
}


// log.info("session add : " + session.getId());
}

// todo
// 좀 더 책임을 나누자

/**
* 전체 로직
* - TextMessage에서 payLoad를 가져옴
* - payLoad 값을 통해 objectMapper를 사용하여 ChatMessageRequest Dto 로 변환
* - payLoad에 담긴 userId를 통해 user 검증
* - payLoad에 담긴 chatRoomId를 통해 websocketList 탐색
* - 만약 없으면 생성
* - chatType이 ENTER일 경우 session 에 add
* - chatType이 TEXT일 경우 sendMessage(), saveMessage() 호출
* - websocket connection을 close할 경우 해당 session을 websocket List에서 remove
* @param session
* @param message
* @throws Exception
*/
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
String payLoad = message.getPayload();
Expand All @@ -58,6 +68,13 @@ protected void handleTextMessage(WebSocketSession session, TextMessage message)
ChatMessageRequest chatMessageRequest = objectMapper.readValue(payLoad, ChatMessageRequest.class);
log.info("session {}", chatMessageRequest.toString());

// 유저 검증
User user = userService.getUser(chatMessageRequest.userId());
if (user == null) {
throw new WebSocketBadRequestException(ErrorCode.BAD_REQUEST, "유효하지 않은 유저의 메세지 요청입니다.");
}


// payload에 chatroomId 가져옴
Long chatRoomId = chatMessageRequest.chatRoomId();

Expand All @@ -80,18 +97,26 @@ protected void handleTextMessage(WebSocketSession session, TextMessage message)

// 만약 입장하는 경우라면 (Type이 Enter 라면)
if (chatMessageRequest.chatType().equals(ChatType.ENTER)) {
// afterConnectionEstablished -> 이 메소들 사용해서 session add는 안되는지 다시 고민
// afterConnectionEstablished(session);
log.info("before add session List size : {}", webSocketSessionList.getWebSocketSessions().size());
// 현재 들어온 세션을 해당 채팅방 세션리스트에 추가
webSocketSessionList.getWebSocketSessions().add(session);
log.info("session add / session Id : {}", session.getId());
}
log.info("session add | session Id : {}", session.getId());

} else if (chatMessageRequest.chatType().equals(ChatType.TEXT)) {
log.info("websocket Session List size : {}",webSocketSessionList.getWebSocketSessions().size());

// 만약 텍스트를 보낸다면
if (chatMessageRequest.chatType().equals(ChatType.TEXT)) {
// 채팅 전송
sendAndSaveMessageToChatRoom(chatMessageRequest, webSocketSessionList);
}
sendMessageToChatRoom(chatMessageRequest, webSocketSessionList);

// 한사람에 대해서만 저장을 해야함
saveMessage(chatMessageRequest);

} else if (chatMessageRequest.chatType().equals(ChatType.IMAGE)) {
// todo
// 이미지 혹은 영상 처리
}
}


Expand All @@ -100,8 +125,6 @@ protected void handleTextMessage(WebSocketSession session, TextMessage message)
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
Long chatRoomId = webSocketSessionMap.getKeyFromSession(session);



// 얘가 호출되니까 여기서 removeClosedSession을 호출해야지
// 그런데 roomId로 해당하는 채팅방의 채팅세션리스트들을 가져와야하는데 ?
// 그 내용을 넣어줄 수 있을까 ?
Expand All @@ -110,21 +133,18 @@ public void afterConnectionClosed(WebSocketSession session, CloseStatus status)
List<WebSocketSession> webSocketSessions = getSessionListByChatRoomId(chatRoomId);
webSocketSessions.remove(session);

log.info("session remove / session Id : {}", session.getId());
log.info("session remove | session Id : {}", session.getId());
log.info("after connection closed session List size : {}",webSocketSessions.size());
}

private List<WebSocketSession> getSessionListByChatRoomId(Long chatRoomId) {
return webSocketSessionMap.getWebsocketListHashMap().get(chatRoomId).getWebSocketSessions();
}

private void sendAndSaveMessageToChatRoom(ChatMessageRequest chatMessageRequest, WebSocketSessionList webSocketSessionList) {
private void sendMessageToChatRoom(ChatMessageRequest chatMessageRequest, WebSocketSessionList webSocketSessionList) {
for (WebSocketSession session : webSocketSessionList.getWebSocketSessions()) {
sendMessage(session, chatMessageRequest.message());
saveMessage(chatMessageRequest);
}

// webSocketSessionList.getWebSocketSessions().parallelStream().forEach(sess -> sendMessage(sess, chatMessageRequest.message()));

}

private void saveMessage(ChatMessageRequest chatMessageRequest) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.seoultech.synergybe.domain.chat.handler;

import com.seoultech.synergybe.domain.chat.exception.WebSocketBadRequestException;
import com.seoultech.synergybe.system.exception.ErrorCode;
import lombok.Getter;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketSession;
Expand All @@ -24,6 +26,6 @@ public Long getKeyFromSession(WebSocketSession session) {
return entry.getKey();
}
}
return null; // 세션을 찾지 못한 경우
throw new WebSocketBadRequestException(ErrorCode.BAD_REQUEST, "채팅방 세션을 찾을 수 없습니다.");
}
}
Loading

0 comments on commit de12add

Please sign in to comment.