diff --git a/README.md b/README.md index 75b9f0ab..ee6d97f5 100644 --- a/README.md +++ b/README.md @@ -26,11 +26,16 @@ **(프로젝트, 게시글, 팀원을 추천하는 기능을 통해 원활한 프로젝트 진행을 지원합니다)** +
## Infra Architecture logo + +
+ + ## Skills
@@ -70,18 +75,26 @@
+
+ ## Setup Dev Environment (Local) +> Java 17이 설치되어있다고 가정합니다 +
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을 통해 프론트 로컬 개발환경을 구성합니다.
@@ -89,10 +102,11 @@ 위 로컬 개발환경 구성 순서는 아래와 같이 진행됩니다. 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 실행 ```
@@ -119,9 +133,12 @@ docker pull jonghuni/synergy_be - 프로젝트 신청, 수락, 거절 - 프로젝트 평가 +
## Directory +
+
파일 구조 보기 @@ -288,19 +305,27 @@ src - 로그인(첫 소셜로그인시 자동 회원가입) - 회원 정보 변경 +
+ ### API Lists - login (users/auth/login) - 로그인, 회원가입을 수행합니다. 로그인 성공시 token을 발급하며 이후 요청에 대해서 해당 토큰으로 인증을 진행합니다. - updateMyInfo (users/me/info) - 회원 정보를 변경합니다. +
+ #### Using stack - Spring Boot, Java 11, Spring Data JPA, Mysql, Lombok, Gradle, JWT +
+ ### Sequence Diagram Example (회원 가입, JWT 토큰 인증 프로세스) logo +
+ ## Recommend Service 사용자가 가진 활동들을 바탕으로 사용자에게 알맞는 컨텐츠(게시글, 프로젝트, 유저)를 추천해주는 기능을 제공하는 서비스입니다. @@ -309,6 +334,8 @@ src - 추천 기능이 동작하는 FastAPI 서버를 docker Image화 하여 docker 컨테이너 위에서 실행 (메인서버 또한 Image화 하여 컨테이너위에서 실행) - DB와 메인 서버로부터 데이터와 API 요청을 받아 모델학습 및 추천을 수행 +
+ ### API List - getRecommendProjects (projects/recommend) @@ -318,10 +345,16 @@ src - getSimilarUsers (users/recommend) - 유저 활동을 바탕으로 적합한 유저를 추천합니다. +
+ ### Sequence Diagram Example (컨텐츠 추천 프로세스) +
+ logo +
+ ## Project Service 사용자가 팀원을 구성하며 팀 프로젝트를 진행하며 공지시항, 일정, 티켓 관리, 평가 등의 @@ -329,18 +362,41 @@ src - 티켓 관리 기능의 경우 티켓을 칸반보드로 관리하는 기능으로 각 Status별로 나누어 티켓들을 올바른 위치로 이동하게끔 구현 +
+ ### API List - changePositionTicket (tickets/change/{ticketId}) - 티켓 위치 변경 기능을 제공합니다. +
+ ### Sequence Diagram Example (프로젝트 팀원 참가 신청 프로세스) logo +
+ ### Sequence Diagram Example (티켓 위치 변경 프로세스) logo +
+
+ +## 고민 흔적 + +- [좋은 객체 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) + - 채팅 서비스를 구현하기 위해 아래 사항들을 고려해보았습니다. + - 웹소켓 프로토콜 이해 + - 실시간성 보장
@@ -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 diff --git a/build.gradle b/build.gradle index 37c358f0..43835955 100644 --- a/build.gradle +++ b/build.gradle @@ -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' diff --git a/src/main/java/com/seoultech/synergybe/SynergyBeApplication.java b/src/main/java/com/seoultech/synergybe/SynergyBeApplication.java index 0a8620f5..5286a242 100644 --- a/src/main/java/com/seoultech/synergybe/SynergyBeApplication.java +++ b/src/main/java/com/seoultech/synergybe/SynergyBeApplication.java @@ -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); diff --git a/src/main/java/com/seoultech/synergybe/domain/chat/exception/WebSocketBadRequestException.java b/src/main/java/com/seoultech/synergybe/domain/chat/exception/WebSocketBadRequestException.java new file mode 100644 index 00000000..f6668c52 --- /dev/null +++ b/src/main/java/com/seoultech/synergybe/domain/chat/exception/WebSocketBadRequestException.java @@ -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); + } +} diff --git a/src/main/java/com/seoultech/synergybe/domain/chat/handler/ChatHandler.java b/src/main/java/com/seoultech/synergybe/domain/chat/handler/ChatHandler.java index 345c3357..a31fef00 100644 --- a/src/main/java/com/seoultech/synergybe/domain/chat/handler/ChatHandler.java +++ b/src/main/java/com/seoultech/synergybe/domain/chat/handler/ChatHandler.java @@ -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; @@ -18,37 +22,43 @@ @Slf4j @Component -//@RequiredArgsConstructor public class ChatHandler extends TextWebSocketHandler { -// private static List 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(); @@ -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(); @@ -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 + // 이미지 혹은 영상 처리 + } } @@ -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로 해당하는 채팅방의 채팅세션리스트들을 가져와야하는데 ? // 그 내용을 넣어줄 수 있을까 ? @@ -110,21 +133,18 @@ public void afterConnectionClosed(WebSocketSession session, CloseStatus status) List 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 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) { diff --git a/src/main/java/com/seoultech/synergybe/domain/chat/handler/WebSocketSessionMap.java b/src/main/java/com/seoultech/synergybe/domain/chat/handler/WebSocketSessionMap.java index da40e922..9628f7c5 100644 --- a/src/main/java/com/seoultech/synergybe/domain/chat/handler/WebSocketSessionMap.java +++ b/src/main/java/com/seoultech/synergybe/domain/chat/handler/WebSocketSessionMap.java @@ -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; @@ -24,6 +26,6 @@ public Long getKeyFromSession(WebSocketSession session) { return entry.getKey(); } } - return null; // 세션을 찾지 못한 경우 + throw new WebSocketBadRequestException(ErrorCode.BAD_REQUEST, "채팅방 세션을 찾을 수 없습니다."); } } diff --git a/src/main/java/com/seoultech/synergybe/domain/common/idgenerator/IdPrefix.java b/src/main/java/com/seoultech/synergybe/domain/common/idgenerator/IdPrefix.java index b39057cc..44aefdb5 100644 --- a/src/main/java/com/seoultech/synergybe/domain/common/idgenerator/IdPrefix.java +++ b/src/main/java/com/seoultech/synergybe/domain/common/idgenerator/IdPrefix.java @@ -16,6 +16,8 @@ public enum IdPrefix { PROJECT_USER("project_user"), RATE("rate"), SCHEDULE("schedule"), + PUBLIC_LIBRARY("public_library"), + SMALL_LIBRARY("small_library"), TICKET("ticket"), TICKET_USER("ticket_user"); diff --git a/src/main/java/com/seoultech/synergybe/domain/library/domain/PublicLibrary.java b/src/main/java/com/seoultech/synergybe/domain/library/domain/PublicLibrary.java new file mode 100644 index 00000000..2551f15d --- /dev/null +++ b/src/main/java/com/seoultech/synergybe/domain/library/domain/PublicLibrary.java @@ -0,0 +1,62 @@ +package com.seoultech.synergybe.domain.library.domain; + +import com.seoultech.synergybe.domain.library.vo.LibraryLocation; +import jakarta.persistence.Column; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.geo.Point; + +@Entity +@Getter +@NoArgsConstructor +public class PublicLibrary { + @Id + @Column(name = "library_id") + private String id; + + @Column(name = "name") + private String name; + + @Column(name = "address") + private String address; + + @Column(name = "tel_number") + private String telNumber; + + @Column(name = "homepage_url") + private String homepageUrl; + + @Column(name = "op_time") + private String opTime; + + @Column(name = "close_date") // 정기 휴관일 + private String closeDate; + + @Embedded + private LibraryLocation location; + + @Builder + public PublicLibrary( + String id, + String name, + String address, + String telNumber, + String homepageUrl, + String opTime, + String closeDate, + Point location + ) { + this.id = id; + this.name = name; + this.address = address; + this.telNumber = telNumber; + this.homepageUrl = homepageUrl; + this.opTime = opTime; + this.closeDate = closeDate; + this.location = new LibraryLocation(location); + } +} diff --git a/src/main/java/com/seoultech/synergybe/domain/library/domain/RawPublicLibrary.java b/src/main/java/com/seoultech/synergybe/domain/library/domain/RawPublicLibrary.java new file mode 100644 index 00000000..7f09abeb --- /dev/null +++ b/src/main/java/com/seoultech/synergybe/domain/library/domain/RawPublicLibrary.java @@ -0,0 +1,80 @@ +package com.seoultech.synergybe.domain.library.domain; + +import jakarta.persistence.*; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity(name = "raw_public_library") +@Getter +@NoArgsConstructor +public class RawPublicLibrary { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "public_library_seq") // 도서관 일련번호 + private String publicLibrarySeq; + + @Column(name = "name") // 도서관 명 + private String name; + + @Column(name = "gu_code") // 구 코드 + private String guCode; + + @Column(name = "gu_code_value") // 구 명 + private String guCodeValue; + + @Column(name = "address") // 주소 + private String address; + + @Column(name = "tel_number") // 전화번호 + private String telNumber; + + @Column(name = "hompage_url") // 홈페이지 url + private String hompageUrl; + + @Column(name = "op_time") // 운영시간 + private String opTime; + + @Column(name = "close_date") // 정기 휴관일 + private String closeDate; + + @Column(name = "se_name") // 도서관 구분명 + private String seName; + + @Column(name = "latitude") // 위도 + private Double latitude; + + @Column(name = "longitude") // 경도 + private Double longitude; + + @Builder + public RawPublicLibrary( + String publicLibrarySeq, + String name, + String guCode, + String guCodeValue, + String address, + String telNumber, + String hompageUrl, + String opTime, + String closeDate, + String seName, + Double latitude, + Double longitude + ) { + this.publicLibrarySeq = publicLibrarySeq; + this.name = name; + this.guCode = guCode; + this.guCodeValue = guCodeValue; + this.address = address; + this.telNumber = telNumber; + this.hompageUrl = hompageUrl; + this.opTime = opTime; + this.closeDate = closeDate; + this.seName = seName; + this.latitude = latitude; + this.longitude = longitude; + } +} diff --git a/src/main/java/com/seoultech/synergybe/domain/library/domain/RawSmallLibrary.java b/src/main/java/com/seoultech/synergybe/domain/library/domain/RawSmallLibrary.java new file mode 100644 index 00000000..f7555804 --- /dev/null +++ b/src/main/java/com/seoultech/synergybe/domain/library/domain/RawSmallLibrary.java @@ -0,0 +1,80 @@ +package com.seoultech.synergybe.domain.library.domain; + +import jakarta.persistence.*; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity(name = "raw_small_library") +@Getter +@NoArgsConstructor +public class RawSmallLibrary { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "library_seq") // 도서관 일련번호 + private String smallLibrarySeq; + + @Column(name = "name") // 도서관 명 + private String name; + + @Column(name = "gu_code") // 구 코드 + private String guCode; + + @Column(name = "gu_code_value") // 구 명 + private String guCodeValue; + + @Column(name = "address") // 주소 + private String address; + + @Column(name = "tel_number") // 전화번호 + private String telNumber; + + @Column(name = "hompage_url") // 홈페이지 url + private String hompageUrl; + + @Column(name = "op_time") // 운영시간 + private String opTime; + + @Column(name = "close_date") // 정기 휴관일 + private String closeDate; + + @Column(name = "se_name") // 도서관 구분명 + private String seName; + + @Column(name = "latitude") // 위도 + private Double latitude; + + @Column(name = "longitude") // 경도 + private Double longitude; + + @Builder + public RawSmallLibrary( + String smallLibrarySeq, + String name, + String guCode, + String guCodeValue, + String address, + String telNumber, + String hompageUrl, + String opTime, + String closeDate, + String seName, + Double latitude, + Double longitude + ) { + this.smallLibrarySeq = smallLibrarySeq; + this.name = name; + this.guCode = guCode; + this.guCodeValue = guCodeValue; + this.address = address; + this.telNumber = telNumber; + this.hompageUrl = hompageUrl; + this.opTime = opTime; + this.closeDate = closeDate; + this.seName = seName; + this.latitude = latitude; + this.longitude = longitude; + } +} diff --git a/src/main/java/com/seoultech/synergybe/domain/library/domain/SmallLibrary.java b/src/main/java/com/seoultech/synergybe/domain/library/domain/SmallLibrary.java new file mode 100644 index 00000000..75e72902 --- /dev/null +++ b/src/main/java/com/seoultech/synergybe/domain/library/domain/SmallLibrary.java @@ -0,0 +1,57 @@ +package com.seoultech.synergybe.domain.library.domain; + +import com.seoultech.synergybe.domain.library.vo.LibraryLocation; +import jakarta.persistence.Column; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.geo.Point; + +@Entity +@Getter +@NoArgsConstructor +public class SmallLibrary { + @Id + @Column(name = "library_id") + private String id; + + private String name; + + private String address; + + private String telNumber; + + private String homepageUrl; + + private String opTime; + + @Column(name = "close_date") // 정기 휴관일 + private String closeDate; + + @Embedded + private LibraryLocation location; + + @Builder + public SmallLibrary( + String id, + String name, + String address, + String telNumber, + String homepageUrl, + String opTime, + String closeDate, + Point location + ) { + this.id = id; + this.name = name; + this.address = address; + this.telNumber = telNumber; + this.homepageUrl = homepageUrl; + this.opTime = opTime; + this.closeDate = closeDate; + this.location = new LibraryLocation(location); + } +} diff --git a/src/main/java/com/seoultech/synergybe/domain/library/dto/response/SeoulPublicLibraryInfo.java b/src/main/java/com/seoultech/synergybe/domain/library/dto/response/SeoulPublicLibraryInfo.java new file mode 100644 index 00000000..2e53238e --- /dev/null +++ b/src/main/java/com/seoultech/synergybe/domain/library/dto/response/SeoulPublicLibraryInfo.java @@ -0,0 +1,73 @@ +package com.seoultech.synergybe.domain.library.dto.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.PropertyNamingStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Getter +@NoArgsConstructor +public class SeoulPublicLibraryInfo { + @JsonProperty("list_total_count") + private int listTotalCount; + + @JsonProperty("RESULT") + private Result RESULT; + + @JsonProperty("row") + private List row; + + @Getter + @NoArgsConstructor + public static class Result { + @JsonProperty("CODE") + private String CODE; + + @JsonProperty("MESSAGE") + private String MESSAGE; + } + + @Getter + @NoArgsConstructor + @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) + public static class RawLibrary { + @JsonProperty("LBRRY_SEQ_NO") + private String LBRRY_SEQ_NO; + + @JsonProperty("LBRRY_NAME") + private String LBRRY_NAME; + + @JsonProperty("GU_CODE") + private String GU_CODE; + + @JsonProperty("CODE_VALUE") + private String CODE_VALUE; + + @JsonProperty("ADRES") + private String ADRES; + + @JsonProperty("TEL_NO") + private String TEL_NO; + + @JsonProperty("HMPG_URL") + private String HMPG_URL; + + @JsonProperty("OP_TIME") + private String OP_TIME; + + @JsonProperty("FDRM_CLOSE_DATE") + private String FDRM_CLOSE_DATE; + + @JsonProperty("LBRRY_SE_NAME") + private String LBRRY_SE_NAME; + + @JsonProperty("XCNTS") + private String XCNTS; + + @JsonProperty("YDNTS") + private String YDNTS; + } +} \ No newline at end of file diff --git a/src/main/java/com/seoultech/synergybe/domain/library/dto/response/SeoulPublicLibraryInfoResponse.java b/src/main/java/com/seoultech/synergybe/domain/library/dto/response/SeoulPublicLibraryInfoResponse.java new file mode 100644 index 00000000..d32d1d69 --- /dev/null +++ b/src/main/java/com/seoultech/synergybe/domain/library/dto/response/SeoulPublicLibraryInfoResponse.java @@ -0,0 +1,14 @@ +package com.seoultech.synergybe.domain.library.dto.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +public class SeoulPublicLibraryInfoResponse { + @JsonProperty("SeoulPublicLibraryInfo") + private SeoulPublicLibraryInfo seoulPublicLibraryInfo; +} diff --git a/src/main/java/com/seoultech/synergybe/domain/library/dto/response/SeoulSmallLibraryInfo.java b/src/main/java/com/seoultech/synergybe/domain/library/dto/response/SeoulSmallLibraryInfo.java new file mode 100644 index 00000000..5f4025c4 --- /dev/null +++ b/src/main/java/com/seoultech/synergybe/domain/library/dto/response/SeoulSmallLibraryInfo.java @@ -0,0 +1,73 @@ +package com.seoultech.synergybe.domain.library.dto.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.PropertyNamingStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Getter +@NoArgsConstructor +public class SeoulSmallLibraryInfo { + @JsonProperty("list_total_count") + private int listTotalCount; + + @JsonProperty("RESULT") + private SeoulPublicLibraryInfo.Result RESULT; + + @JsonProperty("row") + private List row; + + @Getter + @NoArgsConstructor + public static class Result { + @JsonProperty("CODE") + private String CODE; + + @JsonProperty("MESSAGE") + private String MESSAGE; + } + + @Getter + @NoArgsConstructor + @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) + public static class RawLibrary { + @JsonProperty("LBRRY_SEQ_NO") + private String LBRRY_SEQ_NO; + + @JsonProperty("LBRRY_NAME") + private String LBRRY_NAME; + + @JsonProperty("GU_CODE") + private String GU_CODE; + + @JsonProperty("CODE_VALUE") + private String CODE_VALUE; + + @JsonProperty("ADRES") + private String ADRES; + + @JsonProperty("TEL_NO") + private String TEL_NO; + + @JsonProperty("HMPG_URL") + private String HMPG_URL; + + @JsonProperty("OP_TIME") + private String OP_TIME; + + @JsonProperty("FDRM_CLOSE_DATE") + private String FDRM_CLOSE_DATE; + + @JsonProperty("LBRRY_SE_NAME") + private String LBRRY_SE_NAME; + + @JsonProperty("XCNTS") + private String XCNTS; + + @JsonProperty("YDNTS") + private String YDNTS; + } +} diff --git a/src/main/java/com/seoultech/synergybe/domain/library/dto/response/SeoulSmallLibraryResponse.java b/src/main/java/com/seoultech/synergybe/domain/library/dto/response/SeoulSmallLibraryResponse.java new file mode 100644 index 00000000..08ef4855 --- /dev/null +++ b/src/main/java/com/seoultech/synergybe/domain/library/dto/response/SeoulSmallLibraryResponse.java @@ -0,0 +1,14 @@ +package com.seoultech.synergybe.domain.library.dto.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +public class SeoulSmallLibraryResponse { + @JsonProperty("SeoulSmallLibraryInfo") + private SeoulSmallLibraryInfo seoulSmallLibraryInfo; +} diff --git a/src/main/java/com/seoultech/synergybe/domain/library/exception/LibraryBadRequestException.java b/src/main/java/com/seoultech/synergybe/domain/library/exception/LibraryBadRequestException.java new file mode 100644 index 00000000..572c6819 --- /dev/null +++ b/src/main/java/com/seoultech/synergybe/domain/library/exception/LibraryBadRequestException.java @@ -0,0 +1,11 @@ +package com.seoultech.synergybe.domain.library.exception; + +import com.seoultech.synergybe.system.exception.BadRequestException; + +import static com.seoultech.synergybe.system.exception.ErrorCode.BAD_REQUEST; + +public class LibraryBadRequestException extends BadRequestException { + public LibraryBadRequestException(String message) { + super(BAD_REQUEST, message); + } +} diff --git a/src/main/java/com/seoultech/synergybe/domain/library/repository/PublicLibraryRepository.java b/src/main/java/com/seoultech/synergybe/domain/library/repository/PublicLibraryRepository.java new file mode 100644 index 00000000..8033925f --- /dev/null +++ b/src/main/java/com/seoultech/synergybe/domain/library/repository/PublicLibraryRepository.java @@ -0,0 +1,7 @@ +package com.seoultech.synergybe.domain.library.repository; + +import com.seoultech.synergybe.domain.library.domain.PublicLibrary; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface PublicLibraryRepository extends JpaRepository { +} diff --git a/src/main/java/com/seoultech/synergybe/domain/library/repository/RawPublicLibraryRepository.java b/src/main/java/com/seoultech/synergybe/domain/library/repository/RawPublicLibraryRepository.java new file mode 100644 index 00000000..fbef4a91 --- /dev/null +++ b/src/main/java/com/seoultech/synergybe/domain/library/repository/RawPublicLibraryRepository.java @@ -0,0 +1,7 @@ +package com.seoultech.synergybe.domain.library.repository; + +import com.seoultech.synergybe.domain.library.domain.RawPublicLibrary; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface RawPublicLibraryRepository extends JpaRepository { +} diff --git a/src/main/java/com/seoultech/synergybe/domain/library/repository/RawSmallLibraryRepository.java b/src/main/java/com/seoultech/synergybe/domain/library/repository/RawSmallLibraryRepository.java new file mode 100644 index 00000000..a0d00b7b --- /dev/null +++ b/src/main/java/com/seoultech/synergybe/domain/library/repository/RawSmallLibraryRepository.java @@ -0,0 +1,7 @@ +package com.seoultech.synergybe.domain.library.repository; + +import com.seoultech.synergybe.domain.library.domain.RawSmallLibrary; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface RawSmallLibraryRepository extends JpaRepository { +} diff --git a/src/main/java/com/seoultech/synergybe/domain/library/repository/SmallLibraryRepository.java b/src/main/java/com/seoultech/synergybe/domain/library/repository/SmallLibraryRepository.java new file mode 100644 index 00000000..e0c1032a --- /dev/null +++ b/src/main/java/com/seoultech/synergybe/domain/library/repository/SmallLibraryRepository.java @@ -0,0 +1,7 @@ +package com.seoultech.synergybe.domain.library.repository; + +import com.seoultech.synergybe.domain.library.domain.SmallLibrary; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface SmallLibraryRepository extends JpaRepository { +} diff --git a/src/main/java/com/seoultech/synergybe/domain/library/scheduler/PublicLibraryScheduler.java b/src/main/java/com/seoultech/synergybe/domain/library/scheduler/PublicLibraryScheduler.java new file mode 100644 index 00000000..f777f445 --- /dev/null +++ b/src/main/java/com/seoultech/synergybe/domain/library/scheduler/PublicLibraryScheduler.java @@ -0,0 +1,47 @@ +package com.seoultech.synergybe.domain.library.scheduler; + +import com.seoultech.synergybe.domain.common.idgenerator.IdGenerator; +import com.seoultech.synergybe.domain.common.idgenerator.IdPrefix; +import com.seoultech.synergybe.domain.library.domain.PublicLibrary; +import com.seoultech.synergybe.domain.library.domain.RawPublicLibrary; +import com.seoultech.synergybe.domain.library.repository.PublicLibraryRepository; +import com.seoultech.synergybe.domain.library.repository.RawPublicLibraryRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.geo.Point; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Slf4j +@Component +@RequiredArgsConstructor +public class PublicLibraryScheduler { + private final PublicLibraryRepository publicLibraryRepository; + private final RawPublicLibraryRepository rawPublicLibraryRepository; + private final IdGenerator idGenerator; + + @Scheduled(cron = "0 0 4 * * 6", zone = "Asia/Seoul") + public void updatePublicLibrary() { + log.info("update Public Library"); + + List rawPublicLibraryList = rawPublicLibraryRepository.findAll(); + + List publicLibraries = rawPublicLibraryList.stream().map( + rawPublicLibrary -> PublicLibrary.builder() + .id(idGenerator.generateId(IdPrefix.PUBLIC_LIBRARY)) + .name(rawPublicLibrary.getName()) + .address(rawPublicLibrary.getAddress()) + .telNumber(rawPublicLibrary.getTelNumber()) + .homepageUrl(rawPublicLibrary.getHompageUrl()) + .opTime(rawPublicLibrary.getOpTime()) + .closeDate(rawPublicLibrary.getCloseDate()) + .location(new Point(rawPublicLibrary.getLatitude(), rawPublicLibrary.getLatitude())) + .build() + ).toList(); + + // 약 120개로 데이터가 크지 않은점을 고려 + publicLibraryRepository.saveAll(publicLibraries); + } +} diff --git a/src/main/java/com/seoultech/synergybe/domain/library/scheduler/RawPublicLibraryScheduler.java b/src/main/java/com/seoultech/synergybe/domain/library/scheduler/RawPublicLibraryScheduler.java new file mode 100644 index 00000000..2092f77d --- /dev/null +++ b/src/main/java/com/seoultech/synergybe/domain/library/scheduler/RawPublicLibraryScheduler.java @@ -0,0 +1,153 @@ +package com.seoultech.synergybe.domain.library.scheduler; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.seoultech.synergybe.domain.library.domain.RawPublicLibrary; +import com.seoultech.synergybe.domain.library.dto.response.SeoulPublicLibraryInfo; +import com.seoultech.synergybe.domain.library.dto.response.SeoulPublicLibraryInfoResponse; +import com.seoultech.synergybe.domain.library.repository.RawPublicLibraryRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.json.JSONException; +import org.json.JSONObject; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.RequestEntity; +import org.springframework.http.ResponseEntity; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; + +import java.util.ArrayList; +import java.util.List; + +@Component +@RequiredArgsConstructor +@Transactional +@Slf4j +public class RawPublicLibraryScheduler { + private final RestTemplate restTemplate; + private final RawPublicLibraryRepository rawPublicLibraryRepository; + + @Value("${library.api.key}") + private String libraryApiKey; + private int dataCount; + private final int BATCH_SIZE = 500; + + + /** + * 크론 스케줄링 + * 첫 번째 필드: 초 (0-59) + * 두 번째 필드: 분 (0-59) + * 세 번째 필드: 시간 (0-23) + * 네 번째 필드: 일 (1-31) + * 다섯 번째 필드: 월 (1-12) + * 여섯 번째 필드: 요일 (0-6, 일요일부터 토요일까지, 일요일=0 또는 7) + * 데이터 총 개수를 가져와서 dataCount에 넣어줍니다. + */ + @Scheduled(cron = "0 0 4 * * 6", zone = "Asia/Seoul") + public void updateRawPublicLibrary() { + log.info("================시작"); + countRawPublicLibraryTotalData(); + savePublicLibraryFromOpenApi(); + log.info("=================끝"); + } + + private void countRawPublicLibraryTotalData() { + UriComponents uriComponents = UriComponentsBuilder + .newInstance() + .scheme("http") + .host("openapi.seoul.go.kr") + .port(8088) + .path("/{libraryAPI}/json/SeoulPublicLibraryInfo/{start}/{end}") + .buildAndExpand(libraryApiKey, 1, 10); + + log.info("uri : " + uriComponents); + RequestEntity requestEntity = RequestEntity.get(uriComponents.toUri()).build(); + ResponseEntity responseEntity = restTemplate.exchange(requestEntity, String.class); + + log.info(responseEntity.getBody()); + try { + dataCount = new JSONObject(responseEntity.getBody()) + .getJSONObject("SeoulPublicLibraryInfo") + .getInt("list_total_count"); + } catch (JSONException jsonException) { + log.error("JSONException {}", jsonException.toString()); + } + + log.info("dataCount : " + dataCount); + } + + public void insertRawPublicLibrary(List libraries) { +// List libraries = getPublicLibraryFromOpenApi(); + + // stream api로 + List rawPublicLibraries = libraries.stream() + .map(library -> RawPublicLibrary.builder() + .publicLibrarySeq(library.getLBRRY_SEQ_NO()) + .name(library.getLBRRY_NAME()) + .guCode(library.getGU_CODE()) + .guCodeValue(library.getCODE_VALUE()) + .address(library.getADRES()) + .telNumber(library.getTEL_NO()) + .hompageUrl(library.getHMPG_URL()) + .opTime(library.getOP_TIME()) + .closeDate(library.getFDRM_CLOSE_DATE()) + .seName(library.getLBRRY_SE_NAME()) + .latitude(Double.valueOf(library.getXCNTS())) + .longitude(Double.valueOf(library.getYDNTS())) + .build()) + .toList(); + + rawPublicLibraryRepository.saveAll(rawPublicLibraries); + + + } + + + private void savePublicLibraryFromOpenApi() { + int start; + List rawLibraries = new ArrayList<>(); + for (start = 1; start <= dataCount; start += BATCH_SIZE) { + + UriComponents uriComponents = UriComponentsBuilder + .newInstance() + .scheme("http") + .host("openapi.seoul.go.kr") + .port(8088) + .path("/{libraryAPI}/json/SeoulPublicLibraryInfo/{start}/{end}") + .buildAndExpand(libraryApiKey, 1, 10); + + RequestEntity requestEntity = RequestEntity.get(uriComponents.toUri()).build(); + ResponseEntity responseEntity = restTemplate.exchange(requestEntity, String.class); + + log.info("RawLibrary ======================"); + log.info("responseENtity : " + responseEntity.getBody()); + + // JSON 문자열을 Java 객체로 변환 + ObjectMapper objectMapper = new ObjectMapper(); + + // 내가 필요한 데이터들만 파싱하기 위해 설정 + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + + try { + SeoulPublicLibraryInfoResponse seoulPublicLibraryInfoResponse = objectMapper.readValue(responseEntity.getBody() , SeoulPublicLibraryInfoResponse.class); + log.info(String.valueOf(seoulPublicLibraryInfoResponse.getSeoulPublicLibraryInfo().getListTotalCount())); + log.info(String.valueOf(seoulPublicLibraryInfoResponse.getSeoulPublicLibraryInfo().getRow().get(0).getHMPG_URL())); + log.info(String.valueOf(seoulPublicLibraryInfoResponse.getSeoulPublicLibraryInfo().getRESULT().getCODE())); + + rawLibraries.addAll(seoulPublicLibraryInfoResponse.getSeoulPublicLibraryInfo().getRow()); + } catch (JsonProcessingException e) { + e.printStackTrace(); + } + + log.info("RawLibrary 변환중 ======================"); + + // batch size로 저장 + insertRawPublicLibrary(rawLibraries); + } + } +} diff --git a/src/main/java/com/seoultech/synergybe/domain/library/scheduler/RawSmallLibraryScheduler.java b/src/main/java/com/seoultech/synergybe/domain/library/scheduler/RawSmallLibraryScheduler.java new file mode 100644 index 00000000..f89e0763 --- /dev/null +++ b/src/main/java/com/seoultech/synergybe/domain/library/scheduler/RawSmallLibraryScheduler.java @@ -0,0 +1,143 @@ +package com.seoultech.synergybe.domain.library.scheduler; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.seoultech.synergybe.domain.library.domain.RawSmallLibrary; +import com.seoultech.synergybe.domain.library.dto.response.SeoulSmallLibraryInfo; +import com.seoultech.synergybe.domain.library.dto.response.SeoulSmallLibraryResponse; +import com.seoultech.synergybe.domain.library.repository.RawSmallLibraryRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.json.JSONException; +import org.json.JSONObject; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.RequestEntity; +import org.springframework.http.ResponseEntity; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; + +import java.util.ArrayList; +import java.util.List; + +@Component +@RequiredArgsConstructor +@Transactional +@Slf4j +public class RawSmallLibraryScheduler { + private final RestTemplate restTemplate; + private final RawSmallLibraryRepository rawSmallLibraryRepository; + + @Value("${library.api.key}") + private String libraryApiKey; + + private int dataCount; + + private final int BATCH_SIZE = 500; + + /** + * 크론 스케줄링 + * 첫 번째 필드: 초 (0-59) + * 두 번째 필드: 분 (0-59) + * 세 번째 필드: 시간 (0-23) + * 네 번째 필드: 일 (1-31) + * 다섯 번째 필드: 월 (1-12) + * 여섯 번째 필드: 요일 (0-6, 일요일부터 토요일까지, 일요일=0 또는 7) + * 데이터 총 개수를 가져와서 dataCount에 넣어줍니다. + */ + @Scheduled(cron = "0 0 4 * * 6", zone = "Asia/Seoul") + public void updateRawSmallLibrary() { + log.info("================시작"); + countRawSmallLibraryTotalData(); + saveSmallLibraryFromOpenApi(); + log.info("=================끝"); + } + + private void countRawSmallLibraryTotalData() { + UriComponents uriComponents = UriComponentsBuilder + .newInstance() + .scheme("http") + .host("openapi.seoul.go.kr") + .port(8088) + .path("/{libraryAPI}/json/SeoulSmallLibraryInfo/{start}/{end}") + .buildAndExpand(libraryApiKey, 1, 10); + + log.info("uri : " + uriComponents); + RequestEntity requestEntity = RequestEntity.get(uriComponents.toUri()).build(); + ResponseEntity responseEntity = restTemplate.exchange(requestEntity, String.class); + log.info(responseEntity.getBody()); + try { + dataCount = new JSONObject(responseEntity.getBody()) + .getJSONObject("SeoulSmallLibraryInfo") + .getInt("list_total_count"); + } catch (JSONException jsonException) { + log.error("JSONException {}", jsonException.toString()); + } + + log.info("dataCount : " + dataCount); + } + + private void insertRawSmallLibrary(List libraries) { +// List libraries = getSmallLibraryFromOpenApi(); + + // stream api + List rawSmallLibraries = libraries.stream() + .map(library -> RawSmallLibrary.builder() + .smallLibrarySeq(library.getLBRRY_SEQ_NO()) + .name(library.getLBRRY_NAME()) + .guCode(library.getGU_CODE()) + .guCodeValue(library.getCODE_VALUE()) + .address(library.getADRES()) + .telNumber(library.getTEL_NO()) + .hompageUrl(library.getHMPG_URL()) + .opTime(library.getOP_TIME()) + .closeDate(library.getFDRM_CLOSE_DATE()) + .seName(library.getLBRRY_SE_NAME()) + .latitude(Double.valueOf(library.getXCNTS())) + .longitude(Double.valueOf(library.getYDNTS())) + .build()) + .toList(); + + rawSmallLibraryRepository.saveAll(rawSmallLibraries); + } + + private void saveSmallLibraryFromOpenApi() { + int start; + List rawLibraries = new ArrayList<>(); + + for (start = 1; start <= dataCount; start += BATCH_SIZE) { + UriComponents uriComponents = UriComponentsBuilder + .newInstance() + .scheme("http") + .host("openapi.seoul.go.kr") + .port(8088) + .path("/{libraryAPI}/json/SeoulSmallLibraryInfo/{start}/{end}") + .buildAndExpand(libraryApiKey, 1, 10); + + RequestEntity requestEntity = RequestEntity.get(uriComponents.toUri()).build(); + ResponseEntity responseEntity = restTemplate.exchange(requestEntity, String.class); + log.info(responseEntity.getBody()); + + ObjectMapper objectMapper = new ObjectMapper(); + + // 내가 필요한 데이터들만 파싱하기 위해 설정 + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + + try { + SeoulSmallLibraryResponse seoulSmallLibraryResponse = objectMapper.readValue(responseEntity.getBody(), SeoulSmallLibraryResponse.class); + + rawLibraries.addAll(seoulSmallLibraryResponse.getSeoulSmallLibraryInfo().getRow()); + } catch (JsonProcessingException e) { + e.printStackTrace(); + } + log.info("RawSmallLibrary 변환중 ====="); + + // batch size 로 저장 + insertRawSmallLibrary(rawLibraries); + } + } +} diff --git a/src/main/java/com/seoultech/synergybe/domain/library/scheduler/SmallLibraryScheduler.java b/src/main/java/com/seoultech/synergybe/domain/library/scheduler/SmallLibraryScheduler.java new file mode 100644 index 00000000..c01b5abf --- /dev/null +++ b/src/main/java/com/seoultech/synergybe/domain/library/scheduler/SmallLibraryScheduler.java @@ -0,0 +1,49 @@ +package com.seoultech.synergybe.domain.library.scheduler; + +import com.seoultech.synergybe.domain.common.idgenerator.IdGenerator; +import com.seoultech.synergybe.domain.common.idgenerator.IdPrefix; +import com.seoultech.synergybe.domain.library.domain.RawSmallLibrary; +import com.seoultech.synergybe.domain.library.domain.SmallLibrary; +import com.seoultech.synergybe.domain.library.repository.RawSmallLibraryRepository; +import com.seoultech.synergybe.domain.library.repository.SmallLibraryRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.geo.Point; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Slf4j +@Component +@RequiredArgsConstructor +public class SmallLibraryScheduler { + private final SmallLibraryRepository smallLibraryRepository; + private final RawSmallLibraryRepository rawSmallLibraryRepository; + private final IdGenerator idGenerator; + + @Scheduled(cron = "0 0 4 * * 6", zone = "Asia/Seoul") + public void updateSmallLibrary() { + log.info("update Small Library"); + + List rawSmallLibraryList = rawSmallLibraryRepository.findAll(); + + List smallLibraries = rawSmallLibraryList.stream().map( + rawSmallLibrary -> SmallLibrary.builder() + .id(idGenerator.generateId(IdPrefix.SMALL_LIBRARY)) + .name(rawSmallLibrary.getName()) + .address(rawSmallLibrary.getAddress()) + .telNumber(rawSmallLibrary.getTelNumber()) + .homepageUrl(rawSmallLibrary.getHompageUrl()) + .opTime(rawSmallLibrary.getOpTime()) + .closeDate(rawSmallLibrary.getCloseDate()) + .location(new Point(rawSmallLibrary.getLatitude(), rawSmallLibrary.getLongitude())) + .build() + ).toList(); + + // 약 1000개로 데이터가 크지 않은점을 고려 + + smallLibraryRepository.saveAll(smallLibraries); + } + +} diff --git a/src/main/java/com/seoultech/synergybe/domain/library/vo/LibraryLocation.java b/src/main/java/com/seoultech/synergybe/domain/library/vo/LibraryLocation.java new file mode 100644 index 00000000..3d96add2 --- /dev/null +++ b/src/main/java/com/seoultech/synergybe/domain/library/vo/LibraryLocation.java @@ -0,0 +1,48 @@ +package com.seoultech.synergybe.domain.library.vo; + +import com.seoultech.synergybe.domain.library.exception.LibraryBadRequestException; +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.geo.Point; + +import java.util.Objects; + +@Getter +@Embeddable +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class LibraryLocation { + @Column(name = "location") + private Point location; + + public LibraryLocation(Point value) { + validateNotNull(value); + this.location = value; + } + + private void validateNotNull(Point value) { + if (value == null) { + throw new LibraryBadRequestException("위치 정보는 필수 항목입니다."); + } + } + + public void updateLocation(Point value) { + validateNotNull(value); + this.location = value; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + LibraryLocation that = (LibraryLocation) o; + return Objects.equals(location, that.location); + } + + @Override + public int hashCode() { + return Objects.hash(location); + } +} diff --git a/src/main/java/com/seoultech/synergybe/domain/user/dto/response/GetUserAccountResponse.java b/src/main/java/com/seoultech/synergybe/domain/user/dto/response/GetUserAccountResponse.java index 1435320f..251cd1eb 100644 --- a/src/main/java/com/seoultech/synergybe/domain/user/dto/response/GetUserAccountResponse.java +++ b/src/main/java/com/seoultech/synergybe/domain/user/dto/response/GetUserAccountResponse.java @@ -5,13 +5,15 @@ @Builder public record GetUserAccountResponse( + String userId, String email, String name, String major, Double temperature ) { @QueryProjection - public GetUserAccountResponse(String email, String name, String major, Double temperature) { + public GetUserAccountResponse(String userId, String email, String name, String major, Double temperature) { + this.userId = userId; this.email = email; this.name = name; this.major = major; diff --git a/src/main/java/com/seoultech/synergybe/domain/user/service/UserService.java b/src/main/java/com/seoultech/synergybe/domain/user/service/UserService.java index 49ffd969..7487f56b 100644 --- a/src/main/java/com/seoultech/synergybe/domain/user/service/UserService.java +++ b/src/main/java/com/seoultech/synergybe/domain/user/service/UserService.java @@ -79,6 +79,7 @@ public GetUserAccountResponse getUserInfo(String userId) { User user = getUser(userId); return GetUserAccountResponse.builder() + .userId(user.getId()) .email(user.getEmail().getEmail()) .major(user.getMajor().getMajor()) .name(user.getName().getName())