diff --git a/README.md b/README.md
index 75b9f0ab..ee6d97f5 100644
--- a/README.md
+++ b/README.md
@@ -26,11 +26,16 @@
**(프로젝트, 게시글, 팀원을 추천하는 기능을 통해 원활한 프로젝트 진행을 지원합니다)**
+
## Infra Architecture
+
+
+
+
## 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 토큰 인증 프로세스)
+
+
## 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 (컨텐츠 추천 프로세스)
+
+
+
+
## Project Service
사용자가 팀원을 구성하며 팀 프로젝트를 진행하며 공지시항, 일정, 티켓 관리, 평가 등의
@@ -329,18 +362,41 @@ src
- 티켓 관리 기능의 경우 티켓을 칸반보드로 관리하는 기능으로 각 Status별로 나누어 티켓들을 올바른 위치로 이동하게끔 구현
+
+
### API List
- changePositionTicket (tickets/change/{ticketId})
- 티켓 위치 변경 기능을 제공합니다.
+
+
### Sequence Diagram Example (프로젝트 팀원 참가 신청 프로세스)
+
+
### Sequence Diagram Example (티켓 위치 변경 프로세스)
+
+
+
+## 고민 흔적
+
+- [좋은 객체 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())