diff --git a/Dockerfile b/Dockerfile index ccbf8350..02873133 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,3 +1,3 @@ -FROM openjdk:17 +FROM amazoncorretto:17-alpine COPY build/libs/team-c-back-0.0.1-SNAPSHOT.jar app.jar ENTRYPOINT ["java", "-jar", "app.jar"] diff --git a/build.gradle b/build.gradle index 730d61b0..561fdc63 100644 --- a/build.gradle +++ b/build.gradle @@ -123,6 +123,13 @@ dependencies { // elasticsearch // implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch' + + // 크롤링 + implementation 'org.jsoup:jsoup:1.17.2' + + // Apple App Store Server Library + implementation 'com.apple.itunes.storekit:app-store-server-library:3.6.0' + } dependencyManagement { diff --git a/src/main/java/devkor/com/teamcback/domain/ble/controller/AdminBLEController.java b/src/main/java/devkor/com/teamcback/domain/ble/controller/AdminBLEController.java index 8475a12c..bea0226c 100644 --- a/src/main/java/devkor/com/teamcback/domain/ble/controller/AdminBLEController.java +++ b/src/main/java/devkor/com/teamcback/domain/ble/controller/AdminBLEController.java @@ -1,8 +1,10 @@ package devkor.com.teamcback.domain.ble.controller; -import devkor.com.teamcback.domain.ble.dto.request.CreateBLEReq; -import devkor.com.teamcback.domain.ble.dto.response.CreateBLERes; -import devkor.com.teamcback.domain.ble.entity.BLEDevice; +import devkor.com.teamcback.domain.ble.dto.request.CreateBLEDeviceReq; +import devkor.com.teamcback.domain.ble.dto.request.ModifyBLEDeviceReq; +import devkor.com.teamcback.domain.ble.dto.response.CreateBLEDeviceRes; +import devkor.com.teamcback.domain.ble.dto.response.DeleteBLEDeviceRes; +import devkor.com.teamcback.domain.ble.dto.response.ModifyBLEDeviceRes; import devkor.com.teamcback.domain.ble.service.AdminBLEService; import devkor.com.teamcback.global.response.CommonResponse; import io.swagger.v3.oas.annotations.Operation; @@ -13,10 +15,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; @RestController @RequiredArgsConstructor @@ -34,8 +33,38 @@ public class AdminBLEController { @ApiResponse(responseCode = "401", description = "권한이 없습니다.", content = @Content(schema = @Schema(implementation = CommonResponse.class))), }) - public CommonResponse createBLE( - @Parameter(description = "BLE장비 생성 요청 dto") @Valid @RequestBody CreateBLEReq createBLEReq) { - return CommonResponse.success(adminBLEService.CreateBLEDevice(createBLEReq)); + public CommonResponse createBLE( + @Parameter(description = "BLE장비 생성 요청 dto") @Valid @RequestBody CreateBLEDeviceReq createBLEDeviceReq) { + return CommonResponse.success(adminBLEService.createBLEDevice(createBLEDeviceReq)); + } + + @PutMapping + @Operation(summary = "BLE장비 수정", + description = "BLE장비 수정") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "정상 처리 되었습니다."), + @ApiResponse(responseCode = "404", description = "장비를 찾을 수 없습니다.", + content = @Content(schema = @Schema(implementation = CommonResponse.class))), + @ApiResponse(responseCode = "401", description = "권한이 없습니다.", + content = @Content(schema = @Schema(implementation = CommonResponse.class))), + }) + public CommonResponse modifyBLE( + @Parameter(description = "BLE장비 수정 요청 dto") @Valid @RequestBody ModifyBLEDeviceReq modifyBLEDeviceReq) { + return CommonResponse.success(adminBLEService.modifyBLEDevice(modifyBLEDeviceReq)); + } + + @DeleteMapping("/{bleId}") + @Operation(summary = "BLE장비 삭제", + description = "BLE장비 삭제") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "정상 처리 되었습니다."), + @ApiResponse(responseCode = "404", description = "장비를 찾을 수 없습니다.", + content = @Content(schema = @Schema(implementation = CommonResponse.class))), + @ApiResponse(responseCode = "401", description = "권한이 없습니다.", + content = @Content(schema = @Schema(implementation = CommonResponse.class))), + }) + public CommonResponse deleteBLE( + @Parameter(description = "BLE장비 삭제 요청 dto") @PathVariable Long bleId) { + return CommonResponse.success(adminBLEService.deleteBLEDevice(bleId)); } } diff --git a/src/main/java/devkor/com/teamcback/domain/ble/controller/BLEController.java b/src/main/java/devkor/com/teamcback/domain/ble/controller/BLEController.java index db1d3dbb..70572a37 100644 --- a/src/main/java/devkor/com/teamcback/domain/ble/controller/BLEController.java +++ b/src/main/java/devkor/com/teamcback/domain/ble/controller/BLEController.java @@ -1,6 +1,7 @@ package devkor.com.teamcback.domain.ble.controller; import devkor.com.teamcback.domain.ble.dto.request.UpdateBLEReq; +import devkor.com.teamcback.domain.ble.dto.response.BLETimePatternRes; import devkor.com.teamcback.domain.ble.dto.response.GetBLERes; import devkor.com.teamcback.domain.ble.dto.response.UpdateBLERes; import devkor.com.teamcback.domain.ble.service.BLEService; @@ -41,8 +42,25 @@ public CommonResponse getBLE( } @PutMapping + @Operation(summary = "ble 기기에서 전송하는 데이터 통한 데이터 축적", + description = "ble 기기에서 전송하는 데이터 통한 데이터 축적") public CommonResponse updateBLE( @Valid @RequestBody UpdateBLEReq updateBLEReq){ - return CommonResponse.success(bleService.updateBLEDevice(updateBLEReq)); + return CommonResponse.success(bleService.updateBLE(updateBLEReq)); } + + @GetMapping("/pattern") + @Operation(summary = "BLE 요일/시간대별 평균 인원 조회", + description = "최근 1달 동안 특정 placeId에 대해 요일별 7/10/13/16/19/22시 기준 평균 인원 수를 반환") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "정상 처리 되었습니다."), + @ApiResponse(responseCode = "404", description = "장소 또는 장비를 찾을 수 없습니다.", + content = @Content(schema = @Schema(implementation = CommonResponse.class))), + }) + public CommonResponse getBLETimePattern( + @Parameter(name="placeId", description = "BLE 정보를 얻고자 하는 placeId") + @RequestParam Long placeId) { + return CommonResponse.success(bleService.getBLETimePattern(placeId)); + } + } diff --git a/src/main/java/devkor/com/teamcback/domain/ble/dto/request/CreateBLEReq.java b/src/main/java/devkor/com/teamcback/domain/ble/dto/request/CreateBLEDeviceReq.java similarity index 64% rename from src/main/java/devkor/com/teamcback/domain/ble/dto/request/CreateBLEReq.java rename to src/main/java/devkor/com/teamcback/domain/ble/dto/request/CreateBLEDeviceReq.java index 72266bb5..21dc965d 100644 --- a/src/main/java/devkor/com/teamcback/domain/ble/dto/request/CreateBLEReq.java +++ b/src/main/java/devkor/com/teamcback/domain/ble/dto/request/CreateBLEDeviceReq.java @@ -1,21 +1,21 @@ package devkor.com.teamcback.domain.ble.dto.request; -import devkor.com.teamcback.domain.ble.entity.BLEstatus; -import devkor.com.teamcback.domain.place.entity.Place; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; import lombok.Setter; -import java.time.LocalTime; - @Schema(description = "BLEdevice 생성 정보") @Getter @Setter -public class CreateBLEReq { +public class CreateBLEDeviceReq { @Schema(description = "라운지 설치된 기기명", example = "woodang_1f_lounge") private String deviceName; @Schema(description ="라운지 place") private Long placeId; @Schema(description = "라운지별 최대정원", example = "20") private int capacity; + @Schema(description = "위치별 기본 시간별 카운팅 횟수", example = "30") + private int defaultCount; + @Schema(description = "위치별 기본 시간별 평균 기기 신호 횟수", example = "100") + private int ratio; } diff --git a/src/main/java/devkor/com/teamcback/domain/ble/dto/request/ModifyBLEDeviceReq.java b/src/main/java/devkor/com/teamcback/domain/ble/dto/request/ModifyBLEDeviceReq.java new file mode 100644 index 00000000..270654ef --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/ble/dto/request/ModifyBLEDeviceReq.java @@ -0,0 +1,23 @@ +package devkor.com.teamcback.domain.ble.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +@Schema(description = "BLEdevice 생성 정보") +@Getter +@Setter +public class ModifyBLEDeviceReq { + @Schema(description = "수정하고자 하는 BLEDevice ID", example = "1") + private Long id; + @Schema(description = "라운지 설치된 기기명", example = "woodang_1f_lounge") + private String deviceName; + @Schema(description ="라운지 place") + private Long placeId; + @Schema(description = "라운지별 최대정원", example = "20") + private int capacity; + @Schema(description = "위치별 기본 시간별 카운팅 횟수", example = "30") + private int defaultCount; + @Schema(description = "위치별 기본 시간별 평균 기기 신호 횟수", example = "100") + private int ratio; +} diff --git a/src/main/java/devkor/com/teamcback/domain/ble/dto/request/UpdateBLEReq.java b/src/main/java/devkor/com/teamcback/domain/ble/dto/request/UpdateBLEReq.java index 4af66dc7..3a5fd858 100644 --- a/src/main/java/devkor/com/teamcback/domain/ble/dto/request/UpdateBLEReq.java +++ b/src/main/java/devkor/com/teamcback/domain/ble/dto/request/UpdateBLEReq.java @@ -12,9 +12,9 @@ @Getter @Setter public class UpdateBLEReq { - @Schema(description = "라운지 설치된 기기명", example = "woodang_1f_lounge") + @Schema(description = "라운지 설치된 기기명", example = "SKFutureHall_5f_lounge_517") private String deviceName; - @Schema(description = "최근 감지 인원", example = "10") + @Schema(description = "최근 감지 신호 개수", example = "10") private int lastCount; @Schema(description = "최근 신호 전송 시간") @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") diff --git a/src/main/java/devkor/com/teamcback/domain/ble/dto/response/BLETimePatternRes.java b/src/main/java/devkor/com/teamcback/domain/ble/dto/response/BLETimePatternRes.java new file mode 100644 index 00000000..6cfab7c2 --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/ble/dto/response/BLETimePatternRes.java @@ -0,0 +1,18 @@ +package devkor.com.teamcback.domain.ble.dto.response; + +import lombok.Getter; + +@Getter +public class BLETimePatternRes { + private Long placeId; // 요청 placeId + private int[] hours; // {7,10,13,16,19,21,24} + private int[] dayOfWeeks; // {1,2,3,4,5,6,7} (java.time.DayOfWeek 값) + private int[][] averages; // [dayIndex][timeIndex] 형태, 각 원소는 반올림된 int + + public BLETimePatternRes(Long placeId, int[] timeSlots, int[] dayOfWeeks, int[][] averages) { + this.placeId = placeId; + this.hours = timeSlots; + this.dayOfWeeks = dayOfWeeks; + this.averages = averages; + } +} diff --git a/src/main/java/devkor/com/teamcback/domain/ble/dto/response/CreateBLERes.java b/src/main/java/devkor/com/teamcback/domain/ble/dto/response/CreateBLEDeviceRes.java similarity index 78% rename from src/main/java/devkor/com/teamcback/domain/ble/dto/response/CreateBLERes.java rename to src/main/java/devkor/com/teamcback/domain/ble/dto/response/CreateBLEDeviceRes.java index 68577888..1e112043 100644 --- a/src/main/java/devkor/com/teamcback/domain/ble/dto/response/CreateBLERes.java +++ b/src/main/java/devkor/com/teamcback/domain/ble/dto/response/CreateBLEDeviceRes.java @@ -6,10 +6,10 @@ @Schema(description = "BLEdevice 생성 정보") @Getter -public class CreateBLERes { +public class CreateBLEDeviceRes { private Long id; - public CreateBLERes(BLEDevice bleDevice) { + public CreateBLEDeviceRes(BLEDevice bleDevice) { this.id = bleDevice.getId(); } } diff --git a/src/main/java/devkor/com/teamcback/domain/ble/dto/response/DeleteBLEDeviceRes.java b/src/main/java/devkor/com/teamcback/domain/ble/dto/response/DeleteBLEDeviceRes.java new file mode 100644 index 00000000..0d02b8c6 --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/ble/dto/response/DeleteBLEDeviceRes.java @@ -0,0 +1,9 @@ +package devkor.com.teamcback.domain.ble.dto.response; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "BLEDevice 삭제 응답 dto") +@JsonIgnoreProperties +public class DeleteBLEDeviceRes { +} diff --git a/src/main/java/devkor/com/teamcback/domain/ble/dto/response/GetBLERes.java b/src/main/java/devkor/com/teamcback/domain/ble/dto/response/GetBLERes.java index 716ea1b0..c6f29335 100644 --- a/src/main/java/devkor/com/teamcback/domain/ble/dto/response/GetBLERes.java +++ b/src/main/java/devkor/com/teamcback/domain/ble/dto/response/GetBLERes.java @@ -17,12 +17,12 @@ public class GetBLERes { private int lastStatus; private LocalDateTime lastTime; - public GetBLERes(BLEDevice device, BLEData data, BLEstatus status) { + public GetBLERes(BLEDevice device, BLEData data, BLEstatus status, int people) { this.id = device.getId(); this.deviceName = device.getDeviceName(); this.placeId = device.getPlace().getId(); this.capacity = device.getCapacity(); - this.lastCount = data.getLastCount(); + this.lastCount = people; this.lastStatus = status.getCode(); this.lastTime = data.getLastTime(); } diff --git a/src/main/java/devkor/com/teamcback/domain/ble/dto/response/ModifyBLEDeviceRes.java b/src/main/java/devkor/com/teamcback/domain/ble/dto/response/ModifyBLEDeviceRes.java new file mode 100644 index 00000000..d7652b2e --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/ble/dto/response/ModifyBLEDeviceRes.java @@ -0,0 +1,9 @@ +package devkor.com.teamcback.domain.ble.dto.response; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "BLEDevice 수정 응답 dto") +@JsonIgnoreProperties +public class ModifyBLEDeviceRes { +} diff --git a/src/main/java/devkor/com/teamcback/domain/ble/dto/response/UpdateBLERes.java b/src/main/java/devkor/com/teamcback/domain/ble/dto/response/UpdateBLERes.java index f6d515b3..994bab38 100644 --- a/src/main/java/devkor/com/teamcback/domain/ble/dto/response/UpdateBLERes.java +++ b/src/main/java/devkor/com/teamcback/domain/ble/dto/response/UpdateBLERes.java @@ -11,7 +11,7 @@ public class UpdateBLERes { @Schema(description = "라운지 설치된 기기명", example = "woodang_1f_lounge") private Long deviceId; - @Schema(description = "최근 감지 인원", example = "10") + @Schema(description = "최근 감지 인원(10의 자리 반올림)", example = "10") private int lastCount; @Schema(description = "최근 Status", example = "AVAILABLE") private BLEstatus lastStatus; diff --git a/src/main/java/devkor/com/teamcback/domain/ble/entity/BLEDevice.java b/src/main/java/devkor/com/teamcback/domain/ble/entity/BLEDevice.java index e5d7dca6..315334a3 100644 --- a/src/main/java/devkor/com/teamcback/domain/ble/entity/BLEDevice.java +++ b/src/main/java/devkor/com/teamcback/domain/ble/entity/BLEDevice.java @@ -1,7 +1,8 @@ package devkor.com.teamcback.domain.ble.entity; -import devkor.com.teamcback.domain.ble.dto.request.UpdateBLEReq; +import devkor.com.teamcback.domain.ble.dto.request.CreateBLEDeviceReq; +import devkor.com.teamcback.domain.ble.dto.request.ModifyBLEDeviceReq; import devkor.com.teamcback.domain.common.entity.BaseEntity; import devkor.com.teamcback.domain.place.entity.Place; import jakarta.persistence.*; @@ -9,8 +10,6 @@ import lombok.NoArgsConstructor; import lombok.Setter; -import java.time.LocalDateTime; - @Entity @Table(name="tb_ble_device") @Getter @@ -31,10 +30,26 @@ public class BLEDevice extends BaseEntity { @Column private int capacity; - public BLEDevice(String deviceName, Place place, int capacity) { - this.deviceName = deviceName; + @Column(name="default_count") + private double defaultCount; + + @Column + private double ratio; + + public BLEDevice(CreateBLEDeviceReq req, Place place) { + this.deviceName = req.getDeviceName(); + this.place = place; + this.capacity = req.getCapacity(); + this.defaultCount = req.getDefaultCount(); + this.ratio = req.getRatio(); + } + + public void update(ModifyBLEDeviceReq req, Place place){ + this.deviceName = req.getDeviceName(); this.place = place; - this.capacity = capacity; + this.capacity = req.getCapacity(); + this.defaultCount = req.getDefaultCount(); + this.ratio = req.getRatio(); } } diff --git a/src/main/java/devkor/com/teamcback/domain/ble/repository/BLEDataRepository.java b/src/main/java/devkor/com/teamcback/domain/ble/repository/BLEDataRepository.java index d03d497f..5e3c6854 100644 --- a/src/main/java/devkor/com/teamcback/domain/ble/repository/BLEDataRepository.java +++ b/src/main/java/devkor/com/teamcback/domain/ble/repository/BLEDataRepository.java @@ -5,8 +5,11 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.repository.CrudRepository; +import java.time.LocalDateTime; +import java.util.List; import java.util.Optional; public interface BLEDataRepository extends JpaRepository { Optional findTopByDeviceOrderByLastTimeDesc(BLEDevice device); + List findAllByDeviceAndLastTimeBetweenOrderByLastTimeAsc(BLEDevice device, LocalDateTime start, LocalDateTime end); } diff --git a/src/main/java/devkor/com/teamcback/domain/ble/repository/BLEDeviceRepository.java b/src/main/java/devkor/com/teamcback/domain/ble/repository/BLEDeviceRepository.java index 64f254e8..02c50593 100644 --- a/src/main/java/devkor/com/teamcback/domain/ble/repository/BLEDeviceRepository.java +++ b/src/main/java/devkor/com/teamcback/domain/ble/repository/BLEDeviceRepository.java @@ -7,7 +7,8 @@ import java.util.List; public interface BLEDeviceRepository extends JpaRepository { - List findByDeviceName(String deviceName); - List findByPlace(Place place); + BLEDevice findByDeviceName(String deviceName); + BLEDevice findByPlace(Place place); boolean existsByDeviceName(String deviceName); + boolean existsByPlace(Place place); } diff --git a/src/main/java/devkor/com/teamcback/domain/ble/service/AdminBLEService.java b/src/main/java/devkor/com/teamcback/domain/ble/service/AdminBLEService.java index 4ecc8885..b7f34f06 100644 --- a/src/main/java/devkor/com/teamcback/domain/ble/service/AdminBLEService.java +++ b/src/main/java/devkor/com/teamcback/domain/ble/service/AdminBLEService.java @@ -1,7 +1,10 @@ package devkor.com.teamcback.domain.ble.service; -import devkor.com.teamcback.domain.ble.dto.request.CreateBLEReq; -import devkor.com.teamcback.domain.ble.dto.response.CreateBLERes; +import devkor.com.teamcback.domain.ble.dto.request.CreateBLEDeviceReq; +import devkor.com.teamcback.domain.ble.dto.request.ModifyBLEDeviceReq; +import devkor.com.teamcback.domain.ble.dto.response.CreateBLEDeviceRes; +import devkor.com.teamcback.domain.ble.dto.response.DeleteBLEDeviceRes; +import devkor.com.teamcback.domain.ble.dto.response.ModifyBLEDeviceRes; import devkor.com.teamcback.domain.ble.entity.BLEDevice; import devkor.com.teamcback.domain.ble.repository.BLEDeviceRepository; import devkor.com.teamcback.domain.place.entity.Place; @@ -12,7 +15,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import static devkor.com.teamcback.global.response.ResultCode.NOT_FOUND_PLACE; +import java.util.Objects; @Service @RequiredArgsConstructor @@ -20,11 +23,32 @@ public class AdminBLEService { private final BLEDeviceRepository bleDeviceRepository; private final PlaceRepository placeRepository; + //bledevice 생성 @Transactional - public CreateBLERes CreateBLEDevice(CreateBLEReq createBLEReq) { - Place place = placeRepository.findById(createBLEReq.getPlaceId()).orElseThrow(() -> new GlobalException(NOT_FOUND_PLACE)); - if (bleDeviceRepository.existsByDeviceName(createBLEReq.getDeviceName())) throw new GlobalException(ResultCode.EXISTING_DEVICE_NAME); - BLEDevice bleDevice = bleDeviceRepository.save(new BLEDevice(createBLEReq.getDeviceName(), place, createBLEReq.getCapacity())); - return new CreateBLERes(bleDevice); + public CreateBLEDeviceRes createBLEDevice(CreateBLEDeviceReq createBLEDeviceReq) { + Place place = placeRepository.findById(createBLEDeviceReq.getPlaceId()).orElseThrow(() -> new GlobalException(ResultCode.NOT_FOUND_PLACE)); + if (bleDeviceRepository.existsByDeviceName(createBLEDeviceReq.getDeviceName())) throw new GlobalException(ResultCode.EXISTING_DEVICE_NAME); + if (bleDeviceRepository.existsByPlace(place)) throw new GlobalException(ResultCode.EXISTING_PLACE_FOR_DEVICE); + BLEDevice bleDevice = bleDeviceRepository.save(new BLEDevice(createBLEDeviceReq, place)); + return new CreateBLEDeviceRes(bleDevice); + } + + //bledevice 수정 + @Transactional + public ModifyBLEDeviceRes modifyBLEDevice(ModifyBLEDeviceReq modifyBLEReq){ + Place place = placeRepository.findById(modifyBLEReq.getPlaceId()).orElseThrow(() -> new GlobalException(ResultCode.NOT_FOUND_PLACE)); + BLEDevice bleDevice = bleDeviceRepository.findById(modifyBLEReq.getId()).orElseThrow(() -> new GlobalException(ResultCode.NOT_FOUND_DEVICE)); + if (bleDeviceRepository.existsByDeviceName(modifyBLEReq.getDeviceName()) && !Objects.equals(modifyBLEReq.getDeviceName(), bleDevice.getDeviceName())) throw new GlobalException(ResultCode.EXISTING_DEVICE_NAME); + if (bleDeviceRepository.existsByPlace(place) && !Objects.equals(modifyBLEReq.getPlaceId(), bleDevice.getPlace().getId())) throw new GlobalException(ResultCode.EXISTING_PLACE_FOR_DEVICE); + bleDevice.update(modifyBLEReq, place); + return new ModifyBLEDeviceRes(); + } + + //bledevice 삭제 + @Transactional + public DeleteBLEDeviceRes deleteBLEDevice(Long bleId) { + BLEDevice bleDevice = bleDeviceRepository.findById(bleId).orElseThrow(() -> new GlobalException(ResultCode.NOT_FOUND_DEVICE)); + bleDeviceRepository.delete(bleDevice); + return new DeleteBLEDeviceRes(); } } diff --git a/src/main/java/devkor/com/teamcback/domain/ble/service/BLEService.java b/src/main/java/devkor/com/teamcback/domain/ble/service/BLEService.java index a1bbc85c..98104f22 100644 --- a/src/main/java/devkor/com/teamcback/domain/ble/service/BLEService.java +++ b/src/main/java/devkor/com/teamcback/domain/ble/service/BLEService.java @@ -2,6 +2,7 @@ import devkor.com.teamcback.domain.ble.dto.request.UpdateBLEReq; +import devkor.com.teamcback.domain.ble.dto.response.BLETimePatternRes; import devkor.com.teamcback.domain.ble.dto.response.GetBLERes; import devkor.com.teamcback.domain.ble.dto.response.UpdateBLERes; import devkor.com.teamcback.domain.ble.entity.BLEData; @@ -32,15 +33,20 @@ public class BLEService { private final BLEDataRepository bleDataRepository; private final PlaceRepository placeRepository; + // 평균 구해올 시간대 라벨 + private static final int[] TIME_SLOTS = {7, 10, 13, 16, 19, 22}; + //요일 라벨 (1=월요일, 7=일요일) + private static final int[] DAY_OF_WEEKS = {1, 2, 3, 4, 5, 6, 7}; + @Transactional - public UpdateBLERes updateBLEDevice(UpdateBLEReq updateBLEReq) { - List deviceList = bledeviceRepository.findByDeviceName(updateBLEReq.getDeviceName()); - if (deviceList.isEmpty()) throw new GlobalException(ResultCode.NOT_FOUND_DEVICE_NAME); - BLEDevice bleDevice = deviceList.get(0); - double ratio = (double) updateBLEReq.getLastCount() /bleDevice.getCapacity(); + public UpdateBLERes updateBLE(UpdateBLEReq updateBLEReq) { + BLEDevice bleDevice = bledeviceRepository.findByDeviceName(updateBLEReq.getDeviceName()); + int capacity = bleDevice.getCapacity(); + int people = getBlEPeople(updateBLEReq.getLastCount(), bleDevice); + double final_ratio = (double) people / capacity; BLEstatus status; - if (ratio < 0.3) status = BLEstatus.VACANT; - else if (ratio < 0.7) status = BLEstatus.AVAILABLE; + if (final_ratio < 0.3) status = BLEstatus.VACANT; + else if (final_ratio < 0.7) status = BLEstatus.AVAILABLE; else status = BLEstatus.CROWDED; BLEData bleData = new BLEData(); @@ -53,13 +59,23 @@ public UpdateBLERes updateBLEDevice(UpdateBLEReq updateBLEReq) { return new UpdateBLERes(bleData); } + private int getBlEPeople(int lastCount, BLEDevice bleDevice) { + if (bleDevice == null) throw new GlobalException(ResultCode.NOT_FOUND_DEVICE_NAME); + double ratio = bleDevice.getRatio(); + double defaultCount = bleDevice.getDefaultCount(); + return calculate_people(lastCount, ratio, defaultCount); + } + + //count, ratio, defaultcount로 예측 인원 계산(int) + private int calculate_people(int count, double ratio, double defaultCount) { + return (int) Math.round(count * ratio + defaultCount); + } + @Transactional(readOnly = true) public GetBLERes getBLE(Long placeId) { - LocalDateTime now = LocalDateTime.now(); Place place = placeRepository.findById(placeId).orElseThrow(() -> new GlobalException(NOT_FOUND_PLACE)); - List devices = bledeviceRepository.findByPlace(place); - if (devices.isEmpty()) throw new GlobalException(ResultCode.NOT_FOUND_DEVICE); - BLEDevice device = devices.get(0); + BLEDevice device = bledeviceRepository.findByPlace(place); + if (device == null) throw new GlobalException(ResultCode.NOT_FOUND_DEVICE); BLEData latest = bleDataRepository.findTopByDeviceOrderByLastTimeDesc(device).orElseThrow(() -> new GlobalException(ResultCode.NO_DATA_FOR_DEVICE)); BLEstatus status; if (latest.getLastTime() == null || @@ -67,7 +83,83 @@ public GetBLERes getBLE(Long placeId) { status = BLEstatus.FAILURE; } else status = latest.getLastStatus(); - return new GetBLERes(device, latest, status); + // 사람 수를 예측 후 10의 배수로 리턴 + int people = getBlEPeople(latest.getLastCount(), device); + people = (int) Math.round(people / 10.0) * 10; + return new GetBLERes(device, latest, status, people); + } + + @Transactional(readOnly = true) + public BLETimePatternRes getBLETimePattern(Long placeId) { + //place, device 조회 + Place place = placeRepository.findById(placeId).orElseThrow(() -> new GlobalException(NOT_FOUND_PLACE)); + BLEDevice device = bledeviceRepository.findByPlace(place); + if (device == null) throw new GlobalException(ResultCode.NOT_FOUND_DEVICE); + double ratio = device.getRatio(); + double defaultCount = device.getDefaultCount(); + //기간 설정: 1달 + LocalDateTime end = LocalDateTime.now(); + LocalDateTime start = end.minusMonths(1); + //1달치 bledata 가져오기 + List dataList = bleDataRepository.findAllByDeviceAndLastTimeBetweenOrderByLastTimeAsc(device, start, end); + //계산 위한 배열 준비 + // dayIndex: 0~6 (월~일), timeIndex: 0~6 (7,10,13,16,19,22) + long[][] sum = new long[DAY_OF_WEEKS.length][TIME_SLOTS.length]; + long[][] count = new long[DAY_OF_WEEKS.length][TIME_SLOTS.length]; + // 시간대를 "가장 가까운 target 시간"으로 매핑하기 위해 분 단위로 처리 + int[] slotMinutes = new int[TIME_SLOTS.length]; + for (int i = 0; i < TIME_SLOTS.length; i++) { + slotMinutes[i] = TIME_SLOTS[i] * 60; + } + // BLEData를 요일/시간대 bucket에 넣어서 합/개수 집계 + for (BLEData d : dataList) { + if (d.getLastTime() == null) continue; + if (d.getLastCount() == null) continue; + LocalDateTime t = d.getLastTime(); + // 요일 index (0~6) : DayOfWeek.getValue() 는 1~7(MON~SUN) + int dayValue = t.getDayOfWeek().getValue(); // 1~7 + int dayIndex = dayValue - 1; + // 분 단위 시간 (0~1439) + int minuteOfDay = t.getHour() * 60 + t.getMinute(); + // 이 데이터가 들어갈 slot 찾기 (전후 30분 이내만 허용) + int bestSlotIndex = -1; + int bestDiff = Integer.MAX_VALUE; + + for (int i = 0; i < slotMinutes.length; i++) { + int diff = Math.abs(minuteOfDay - slotMinutes[i]); + + // 전후 30분(= 60분 window) 안에 들어오는 경우만 고려 + if (diff <= 30 && diff < bestDiff) { + bestDiff = diff; + bestSlotIndex = i; + } + } + // 어떤 slot에도 해당되지 않으면 이 데이터는 버림 + if (bestSlotIndex == -1) { + continue; + } + // 합/개수 축적 + sum[dayIndex][bestSlotIndex] += calculate_people(d.getLastCount(), ratio, defaultCount); + count[dayIndex][bestSlotIndex] += 1; + } + // 평균 계산 (반올림 후 int) + int[][] averages = new int[DAY_OF_WEEKS.length][TIME_SLOTS.length]; + for (int di = 0; di < DAY_OF_WEEKS.length; di++) { + for (int ti = 0; ti < TIME_SLOTS.length; ti++) { + if (count[di][ti] == 0L) { + averages[di][ti] = 0; // 데이터 없으면 0 + } else { + double avg = (double) sum[di][ti] / (double) count[di][ti]; + averages[di][ti] = (int) Math.round(avg); + } + } + } + return new BLETimePatternRes( + placeId, + TIME_SLOTS, + DAY_OF_WEEKS, + averages + ); } } diff --git a/src/main/java/devkor/com/teamcback/domain/operatingtime/scheduler/OperatingScheduler.java b/src/main/java/devkor/com/teamcback/domain/operatingtime/scheduler/OperatingScheduler.java index b5ffead9..a9c17112 100644 --- a/src/main/java/devkor/com/teamcback/domain/operatingtime/scheduler/OperatingScheduler.java +++ b/src/main/java/devkor/com/teamcback/domain/operatingtime/scheduler/OperatingScheduler.java @@ -30,7 +30,7 @@ public class OperatingScheduler { private static Boolean isEvenWeek = null; @Scheduled(cron = "0 0 0 * * *") // 매일 자정마다 - @EventListener(ApplicationReadyEvent.class) + // @EventListener(ApplicationReadyEvent.class) public void updateOperatingTime() { setState(); log.info("운영 시간 업데이트"); diff --git a/src/main/java/devkor/com/teamcback/domain/operatingtime/service/OperatingService.java b/src/main/java/devkor/com/teamcback/domain/operatingtime/service/OperatingService.java index 06820301..5dd84d0e 100644 --- a/src/main/java/devkor/com/teamcback/domain/operatingtime/service/OperatingService.java +++ b/src/main/java/devkor/com/teamcback/domain/operatingtime/service/OperatingService.java @@ -146,6 +146,7 @@ private String findPlaceOperatingTime(Place place, DayOfWeek dayOfWeek) { // 장소의 운영 시간이 시간 형식이 아니면 건물의 운영 시간을 따라감 if(!isTimeRangePattern(operatingTime)) { + if(place.getBuilding() == null) return DEFAULT_OPERATING_TIME; operatingTime = place.getBuilding().getOperatingTime(); } @@ -327,10 +328,10 @@ public void updatePlaceOperatingTime() { for(Place place : places) { if(!placesWithCondition.contains(place)) { // 조건이 없는 장소는 건물의 운영 여부 및 운영 시간과 동일하도록 세팅 - place.setSundayOperatingTime(place.getBuilding().getSundayOperatingTime()); - place.setSaturdayOperatingTime(place.getBuilding().getSaturdayOperatingTime()); - place.setWeekdayOperatingTime(place.getBuilding().getWeekdayOperatingTime()); - place.setOperating(place.getBuilding().isOperating()); + place.setSundayOperatingTime(place.getBuilding() == null ? DEFAULT_OPERATING_TIME : place.getBuilding().getSundayOperatingTime()); + place.setSaturdayOperatingTime(place.getBuilding() == null ? DEFAULT_OPERATING_TIME : place.getBuilding().getSaturdayOperatingTime()); + place.setWeekdayOperatingTime(place.getBuilding() == null ? DEFAULT_OPERATING_TIME : place.getBuilding().getWeekdayOperatingTime()); + place.setOperating(place.getBuilding() == null ? true : place.getBuilding().isOperating()); } } } diff --git a/src/main/java/devkor/com/teamcback/domain/place/controller/CafeteriaMenuController.java b/src/main/java/devkor/com/teamcback/domain/place/controller/CafeteriaMenuController.java new file mode 100644 index 00000000..38bef201 --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/place/controller/CafeteriaMenuController.java @@ -0,0 +1,42 @@ +package devkor.com.teamcback.domain.place.controller; + +import devkor.com.teamcback.domain.place.dto.response.GetCafeteriaMenuListRes; +import devkor.com.teamcback.domain.place.service.CafeteriaMenuService; +import devkor.com.teamcback.global.response.CommonResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.time.LocalDate; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/places/cafeterias") +public class CafeteriaMenuController { + + private final CafeteriaMenuService cafeteriaMenuService; + + @Operation(summary = "건물 id와 날짜로 학식 메뉴 검색", + description = "건물 id와 날짜로 학식 메뉴 검색") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "정상 처리 되었습니다."), + @ApiResponse(responseCode = "401", description = "권한이 없습니다.", + content = @Content(schema = @Schema(implementation = CommonResponse.class))), + }) + @GetMapping("/menus") + public CommonResponse getCafeteriaMenu( + @Parameter(name = "placeId", description = "장소 ID") @RequestParam Long placeId, + @Parameter(name = "startDate", description = "2025-12-25 형식의 요청 시작 날짜") @RequestParam LocalDate startDate, + @Parameter(name = "endDate", description = "2025-12-25 형식의 요청 마지막 날짜") @RequestParam LocalDate endDate) { + + return CommonResponse.success(cafeteriaMenuService.getCafeteriaMenu(placeId, startDate, endDate)); + } +} diff --git a/src/main/java/devkor/com/teamcback/domain/place/dto/response/GetCafeteriaMenuListRes.java b/src/main/java/devkor/com/teamcback/domain/place/dto/response/GetCafeteriaMenuListRes.java new file mode 100644 index 00000000..55ff8aa4 --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/place/dto/response/GetCafeteriaMenuListRes.java @@ -0,0 +1,26 @@ +package devkor.com.teamcback.domain.place.dto.response; + +import lombok.Getter; + +import java.time.LocalDate; +import java.util.HashMap; +import java.util.Map; + +@Getter +public class GetCafeteriaMenuListRes { + private Long placeId; + private String placeName; + private String address; + private String operatingTime; + private String contact; + private Map> menus = new HashMap<>(); + + public GetCafeteriaMenuListRes(Long placeId, String name, String address, String operatingTime, String contact, Map> menus) { + this.placeId = placeId; + this.placeName = name; + this.address = address; + this.operatingTime = operatingTime; + this.contact = contact; + this.menus = menus; + } +} diff --git a/src/main/java/devkor/com/teamcback/domain/place/entity/CafeteriaMenu.java b/src/main/java/devkor/com/teamcback/domain/place/entity/CafeteriaMenu.java new file mode 100644 index 00000000..1ad5fe4c --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/place/entity/CafeteriaMenu.java @@ -0,0 +1,52 @@ +package devkor.com.teamcback.domain.place.entity; + +import devkor.com.teamcback.domain.common.entity.BaseEntity; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.time.LocalDate; + +@Entity +@Getter +@Setter +@Table(name = "tb_cafeteria_menu") +@NoArgsConstructor +public class CafeteriaMenu extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private LocalDate date; + + @Column(nullable = false) // 식단 구분 - 조식/중식/석식 + private String kind; + + @Getter + @Column(nullable = false, length = 500) // 식단 내용 + private String menu; + + @Column(nullable = false) // 상세 정보 + private Long placeId; + + public CafeteriaMenu(LocalDate date, String kind, String menu, Long placeId) { + this.date = date; + this.kind = kind; + this.menu = menu; + this.placeId = placeId; + } + + @Override + public String toString() { + return "CafeteriaMenu{" + + "id=" + id + + ", date=" + date + + ", kind='" + kind + '\'' + + ", menu='" + menu + '\'' + + ", placeId=" + placeId + + '}'; + } +} + diff --git a/src/main/java/devkor/com/teamcback/domain/place/entity/Place.java b/src/main/java/devkor/com/teamcback/domain/place/entity/Place.java index 89e16eab..0d2e8c41 100644 --- a/src/main/java/devkor/com/teamcback/domain/place/entity/Place.java +++ b/src/main/java/devkor/com/teamcback/domain/place/entity/Place.java @@ -70,6 +70,9 @@ public class Place extends BaseEntity { @Column(nullable = false) private Integer starNum = 0; + @Column + private String contact; // 연락처 등 + @ManyToOne @JoinColumn(name = "building_id") private Building building; diff --git a/src/main/java/devkor/com/teamcback/domain/place/repository/CafeteriaMenuRepository.java b/src/main/java/devkor/com/teamcback/domain/place/repository/CafeteriaMenuRepository.java new file mode 100644 index 00000000..1e35628f --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/place/repository/CafeteriaMenuRepository.java @@ -0,0 +1,13 @@ +package devkor.com.teamcback.domain.place.repository; + +import devkor.com.teamcback.domain.place.entity.CafeteriaMenu; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.time.LocalDate; +import java.util.List; + +public interface CafeteriaMenuRepository extends JpaRepository { + CafeteriaMenu findByDateAndKindAndPlaceId(LocalDate date, String kind, Long placeId); + + List findByDateAndPlaceId(LocalDate date, Long placeId); +} diff --git a/src/main/java/devkor/com/teamcback/domain/place/scheduler/CafeteriaMenuScheduler.java b/src/main/java/devkor/com/teamcback/domain/place/scheduler/CafeteriaMenuScheduler.java new file mode 100644 index 00000000..3133f075 --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/place/scheduler/CafeteriaMenuScheduler.java @@ -0,0 +1,48 @@ +package devkor.com.teamcback.domain.place.scheduler; + +import devkor.com.teamcback.domain.place.service.CafeteriaMenuService; +import devkor.com.teamcback.global.redis.RedisLockUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +@Slf4j(topic = "Cafeteria Menu Scheduler") +@Component +@RequiredArgsConstructor +public class CafeteriaMenuScheduler { + + private final CafeteriaMenuService cafeteriaMenuService; + private final RedisLockUtil redisLockUtil; + + // @EventListener(ApplicationReadyEvent.class) + @Scheduled(cron = "0 10 0 * * *") // 매일 자정 10분마다 + public void updateMenus() { + redisLockUtil.executeWithLock("menu_lock", 1, 300, () -> { + + System.out.println("--- 고려대학교 학식메뉴 스크래핑 시작 ---"); + + // 수당삼양패컬티하우스 송림 + cafeteriaMenuService.scrapeMenu(503, 9757L); + // 자연계 학생식당 + cafeteriaMenuService.scrapeMenu(504, 3103L); + // 자연계 교직원 식당 + cafeteriaMenuService.scrapeMenu(504, 2490L); + // 안암학사 식당 + cafeteriaMenuService.scrapeMenu(505, 3654L); + // 산학관 식당 + cafeteriaMenuService.scrapeMenu(506, 3020L); + // 교우회관 학생식당 + cafeteriaMenuService.scrapeMenu(507, 7705L); + // 학생회관 학생식당 + cafeteriaMenuService.scrapeMenu(508, 9758L); + + System.out.println("------------------종료-------------------"); + return null; + }); + + } +} + diff --git a/src/main/java/devkor/com/teamcback/domain/place/service/CafeteriaMenuService.java b/src/main/java/devkor/com/teamcback/domain/place/service/CafeteriaMenuService.java new file mode 100644 index 00000000..cee50ada --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/place/service/CafeteriaMenuService.java @@ -0,0 +1,230 @@ +package devkor.com.teamcback.domain.place.service; + +import devkor.com.teamcback.domain.building.entity.Building; +import devkor.com.teamcback.domain.building.repository.BuildingRepository; +import devkor.com.teamcback.domain.place.dto.response.GetCafeteriaMenuListRes; +import devkor.com.teamcback.domain.place.entity.CafeteriaMenu; +import devkor.com.teamcback.domain.place.entity.Place; +import devkor.com.teamcback.domain.place.repository.CafeteriaMenuRepository; +import devkor.com.teamcback.domain.place.repository.PlaceRepository; +import devkor.com.teamcback.global.exception.exception.GlobalException; +import devkor.com.teamcback.global.response.ResultCode; +import lombok.RequiredArgsConstructor; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.io.IOException; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +@Service +@RequiredArgsConstructor +public class CafeteriaMenuService { + + // 자연계 학생식당 메뉴 URL + private static final String URL1 = "https://www.korea.ac.kr/ko/"; + private static final String URL2 = "/subview.do"; + // HTML 테이블을 선택하는 CSS 선택자 + private static final String TABLE_SELECTOR = ".table_1 table"; + // 식단 메뉴가 없을 때 문구 + private static final String NO_MENU_INFO = "등록된 식단내용이(가) 없습니다."; + + private final CafeteriaMenuRepository cafeteriaMenuRepository; + private final PlaceRepository placeRepository; + + /** + * 학생식당 메뉴 조회 + */ + @Transactional(readOnly = true) + public GetCafeteriaMenuListRes getCafeteriaMenu(Long placeId, LocalDate startDate, LocalDate endDate) { + Place place = placeRepository.findById(placeId).orElseThrow(() -> new GlobalException(ResultCode.NOT_FOUND_PLACE)); + + String address = place.getBuilding() == null ? "" : place.getBuilding().getName() + " "; + address += place.getFloor() < 0 ? "B" : ""; + address += (int)Math.abs(place.getFloor()) + "층"; + + Map> menuMap = new HashMap<>(); + + startDate + .datesUntil(endDate) + .forEach(date -> { + List cafeteriaMenuList = cafeteriaMenuRepository.findByDateAndPlaceId(date, placeId); + + Map menuByKind = new HashMap<>(); + for (CafeteriaMenu cafeteriaMenu : cafeteriaMenuList) { + menuByKind.put(cafeteriaMenu.getKind(), cafeteriaMenu.getMenu()); + } + + menuMap.put(date, menuByKind); + }); + + GetCafeteriaMenuListRes res = new GetCafeteriaMenuListRes(placeId, place.getName(), address, place.getDetail(), place.getContact(), menuMap); + + return res; + } + + /** + * 웹 페이지에서 식단 정보를 스크래핑하고 리스트로 반환합니다. + */ + @Transactional + public void scrapeMenu(int page, Long placeId) { + // 식당 설명 초기화 + Place place = placeRepository.findById(placeId).orElseThrow(() -> new GlobalException(ResultCode.NOT_FOUND_PLACE)); + place.setDescription(""); + + // 식당 설명(메뉴) 수정 여부 + boolean updated = false; + + // 문자열 형식에 맞는 포맷터(날짜 문자열 -> Date) + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd"); + + Document doc; + + try { + // 1. Jsoup을 사용하여 HTML 문서 가져오기 (requests + BeautifulSoup 역할) + doc = Jsoup.connect(URL1 + page + URL2) + .userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64)") // User-Agent 설정 + .timeout(5000) // 5초 타임아웃 + .get(); + + // 2. 지정된 선택자를 이용해 테이블 찾기 + Element mealTable = doc.selectFirst(TABLE_SELECTOR); + + if (mealTable == null) { + System.err.println("오류: 지정된 선택자 '" + TABLE_SELECTOR + "'로 테이블을 찾을 수 없습니다."); + return; + } + + // 3. 테이블의 순수한 텍스트 내용 추출 + // Jsoup의 text()는 태그를 제거하고 공백을 정리합니다. + String mealTableText = mealTable.text(); + + // 4. 공백 정규화 및 정리 + String cleanedHtmlContent = mealTableText.replaceAll("[ \t]+", " "); + cleanedHtmlContent = cleanedHtmlContent.trim(); + + // 5. 데이터를 날짜-요일 패턴을 기준으로 분리하여 블록화 + // 패턴: (YYYY.MM.DD (요일)) + Pattern dateDayPattern = Pattern.compile("(\\d{4}\\.\\d{2}\\.\\d{2})\\s*"); + Matcher dateDayMatcher = dateDayPattern.matcher(cleanedHtmlContent); + + List blocks = new ArrayList<>(); + int lastEnd = 0; + + // 날짜(요일) 블록을 기준으로 텍스트를 나눕니다. + while (dateDayMatcher.find()) { + // 이전 블록 내용을 추가합니다. + if (dateDayMatcher.start() > lastEnd) { + blocks.add(cleanedHtmlContent.substring(lastEnd, dateDayMatcher.start()).trim()); + } + // 현재 날짜(요일) 블록을 추가합니다. + blocks.add(dateDayMatcher.group().trim()); + lastEnd = dateDayMatcher.end(); + } + // 마지막 블록의 나머지 내용을 추가합니다. + if (lastEnd < cleanedHtmlContent.length()) { + blocks.add(cleanedHtmlContent.substring(lastEnd).trim()); + } + + String currentDate = null; + + // 6. 분리된 블록을 순회하며 식단 데이터 추출 + // 패턴: (조식|중식|석식) (.+?) - 내용물은 하이픈(-) 기준으로 분리됩니다. + Pattern menuItemsPattern = Pattern.compile( + "(조식|중식|석식|식사|요리|파스타/스테이크코스|천원의밥상)\\s*(.*?)(?=\\s*(조식|중식|석식|식사|요리|파스타/스테이크코스|천원의밥상)\\s*|$)", + Pattern.DOTALL + ); + + for (String block : blocks) { + if (block.isEmpty()) { + continue; + } + + // 현재 블록이 날짜 패턴을 포함하는 경우 + Matcher dateMatch = Pattern.compile("(\\d{4}\\.\\d{2}\\.\\d{2})").matcher(block); + if (dateMatch.find()) { + currentDate = dateMatch.group(1); + continue; // 메뉴가 포함된 다음 블록을 위해 건너뜁니다. + } + + // 현재 블록이 메뉴 내용이면 + if (currentDate != null) { + LocalDate date = LocalDate.parse(currentDate, formatter); + + Matcher menuMatcher = menuItemsPattern.matcher(block); + + while (menuMatcher.find()) { + String kind = menuMatcher.group(1).trim(); + String content = menuMatcher.group(2); + + // & 와 잔여 텍스트 정리 + content = content.replace("&", "&"); + content = content.replaceAll("요일 식단구분 식단제목 식단내용 기타정보", ""); // 불필요한 헤더 제거 + content = content.replaceAll("[-]+$", "").trim(); // 끝의 '-'와 공백 제거 + content = content.replaceAll("\\s+", " ").trim(); // 여러 공백을 단일 공백으로 정리 + + if (!content.isEmpty()) { + + // 애기능 - 학생식당 + if(placeId == 3103) { + if(!content.contains("[학생식당]")) content = NO_MENU_INFO; + else content = content.substring(content.lastIndexOf("[학생식당]") + "[학생식당]".length(), content.contains("[교직원식당]") && content.lastIndexOf("[교직원식당]") > content.lastIndexOf("[학생식당]")? content.lastIndexOf("[교직원식당]") : content.length()).trim(); + } + // 애기능 - 교직원식당 + else if(placeId == 2490) { + if(!content.contains("[교직원식당]")) content = NO_MENU_INFO; + else content = content.substring(content.lastIndexOf("[교직원식당]") + "[교직원식당]".length()).trim(); + } + + CafeteriaMenu savedMenu = cafeteriaMenuRepository.findByDateAndKindAndPlaceId(date, kind, placeId); + + // 메뉴가 존재하는 경우 + if(!content.equals(NO_MENU_INFO)) { + + // 메뉴가 변경된 경우 + if (savedMenu == null || !savedMenu.getMenu().equals(content)) { + // 학식 메뉴 저장 + cafeteriaMenuRepository.save(new CafeteriaMenu(date, kind, content, placeId)); + } + + // 당일에 해당하는 경우 식당 설명 수정 + if(date.equals(LocalDate.now())) { + if(!updated) { + place.setDescription(kind + " - " + content); + updated = true; + } + else place.setDescription(place.getDescription() + "\n" + kind + " - " + content); + } + + } + } + } + // 해당 날짜의 모든 메뉴를 추출했으므로 날짜를 초기화 + currentDate = null; + } + } + + // 식당 설명 없으면 메뉴 정보 없다고 표시 + if(place.getDescription().isEmpty()) { + place.setDescription(NO_MENU_INFO); + } + + } catch (IOException e) { + // 웹 접속 관련 오류 처리 (404, 네트워크 문제 등) + System.err.println("웹 페이지 접속 오류 (IOException): " + e.getMessage()); + } catch (Exception e) { + // 그 외 예상치 못한 오류 처리 + System.err.println("스크래핑 중 예상치 못한 오류 발생: " + e.getMessage()); + e.printStackTrace(); + } + } +} diff --git a/src/main/java/devkor/com/teamcback/domain/routes/dto/response/DijkstraRes.java b/src/main/java/devkor/com/teamcback/domain/routes/dto/response/DijkstraRes.java index 6914ce9e..2ea3488c 100644 --- a/src/main/java/devkor/com/teamcback/domain/routes/dto/response/DijkstraRes.java +++ b/src/main/java/devkor/com/teamcback/domain/routes/dto/response/DijkstraRes.java @@ -3,8 +3,10 @@ import devkor.com.teamcback.domain.routes.entity.Node; import java.util.List; import lombok.Getter; +import lombok.Setter; @Getter +@Setter public class DijkstraRes { private Long distance; private List path; diff --git a/src/main/java/devkor/com/teamcback/domain/routes/dto/response/GetGraphRes.java b/src/main/java/devkor/com/teamcback/domain/routes/dto/response/GetGraphRes.java index 1df9d5c4..a6a5faa0 100644 --- a/src/main/java/devkor/com/teamcback/domain/routes/dto/response/GetGraphRes.java +++ b/src/main/java/devkor/com/teamcback/domain/routes/dto/response/GetGraphRes.java @@ -5,8 +5,10 @@ import java.util.List; import java.util.Map; import lombok.Getter; +import lombok.Setter; @Getter +@Setter public class GetGraphRes { private List graphNode; private Map> graphEdge; diff --git a/src/main/java/devkor/com/teamcback/domain/routes/entity/ShuttleTime.java b/src/main/java/devkor/com/teamcback/domain/routes/entity/ShuttleTime.java new file mode 100644 index 00000000..18c0b7a9 --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/routes/entity/ShuttleTime.java @@ -0,0 +1,25 @@ +package devkor.com.teamcback.domain.routes.entity; + +import devkor.com.teamcback.domain.common.entity.BaseEntity; +import devkor.com.teamcback.domain.place.entity.Place; +import jakarta.persistence.*; +import lombok.Getter; + +import java.time.LocalTime; + +@Entity +@Getter +@Table(name = "tb_bus_timetable") +public class ShuttleTime extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "placeId") + private Place place; + + private LocalTime time; + + private boolean summerSession; +} diff --git a/src/main/java/devkor/com/teamcback/domain/routes/repository/ShuttleTimeRepository.java b/src/main/java/devkor/com/teamcback/domain/routes/repository/ShuttleTimeRepository.java new file mode 100644 index 00000000..107afcfa --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/routes/repository/ShuttleTimeRepository.java @@ -0,0 +1,14 @@ +package devkor.com.teamcback.domain.routes.repository; + +import devkor.com.teamcback.domain.place.entity.Place; +import devkor.com.teamcback.domain.routes.entity.ShuttleTime; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface ShuttleTimeRepository extends JpaRepository { + List findAllByPlaceAndSummerSession(Place place, boolean isSummerSession, Sort sort); + + List findAllBySummerSession(boolean isSummerSession, Sort sort); +} diff --git a/src/main/java/devkor/com/teamcback/domain/routes/service/RouteService.java b/src/main/java/devkor/com/teamcback/domain/routes/service/RouteService.java index 6f41a41c..e9e44e2b 100644 --- a/src/main/java/devkor/com/teamcback/domain/routes/service/RouteService.java +++ b/src/main/java/devkor/com/teamcback/domain/routes/service/RouteService.java @@ -4,6 +4,7 @@ import devkor.com.teamcback.domain.building.repository.BuildingRepository; import devkor.com.teamcback.domain.building.repository.ConnectedBuildingRepository; import devkor.com.teamcback.domain.place.entity.Place; +import devkor.com.teamcback.domain.place.entity.PlaceType; import devkor.com.teamcback.domain.place.repository.PlaceRepository; import devkor.com.teamcback.domain.routes.dto.response.DijkstraRes; import devkor.com.teamcback.domain.routes.dto.response.GetGraphRes; @@ -12,17 +13,23 @@ import devkor.com.teamcback.domain.routes.entity.*; import devkor.com.teamcback.domain.routes.repository.CheckpointRepository; import devkor.com.teamcback.domain.routes.repository.NodeRepository; +import devkor.com.teamcback.domain.routes.repository.ShuttleTimeRepository; +import devkor.com.teamcback.domain.search.util.HangeulUtils; import devkor.com.teamcback.global.exception.exception.AdminException; import devkor.com.teamcback.global.exception.exception.GlobalException; import devkor.com.teamcback.global.logging.LogUtil; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.time.Duration; +import java.time.LocalDate; +import java.time.LocalTime; import java.util.*; -import static devkor.com.teamcback.domain.routes.entity.Conditions.BARRIERFREE; +import static devkor.com.teamcback.domain.routes.entity.Conditions.*; import static devkor.com.teamcback.global.response.ResultCode.*; @Slf4j @@ -35,6 +42,7 @@ public class RouteService { private final CheckpointRepository checkpointRepository; private final ConnectedBuildingRepository connectedBuildingRepository; private final LogUtil logUtil; + private final ShuttleTimeRepository shuttleTimeRepository; private static final long OUTDOOR_ID = 0L; private static final String SEPARATOR = ","; @@ -43,18 +51,22 @@ public class RouteService { private static final Double MAX_OUTDOOR_DISTANCE = 0.003; private static final Double MIN_OUTDOOR_DISTANCE = 0.0001; private static final Double INDOOR_ROUTE_WEIGHT = 0.3; + private static final Double BETWEEN_WEIGHT = 0.0005; + + //테스트용 TESTTIME + private static final LocalTime TEST_TIME = LocalTime.of(14, 20, 0); /** * 메인 경로탐색 메서드 */ @Transactional(readOnly = true) public List findRoute(LocationType startType, Long startId, Double startLat, Double startLong, - LocationType endType, Long endId, Double endLat, Double endLong, List conditions){ + LocationType endType, Long endId, Double endLat, Double endLong, List conditions) { - if (conditions == null){ + if (conditions == null) { conditions = new ArrayList<>(); } - // 출발, 도착 노드 검색 + conditions.add(OPERATING); Node startNode = getNodeByType(startType, startId, startLat, startLong); Node endNode = getNodeByType(endType, endId, endLat, endLong); @@ -65,13 +77,13 @@ public List findRoute(LocationType startType, Long startId, Double List routeRes = new ArrayList<>(); // 연결된 건물 찾기 - List buildingList = getBuildingsForRoute(startNode, endNode); + HashSet buildingList = getBuildingsForRoute(startNode, endNode, conditions); // 경로를 하나만 반환하는 경우 GetGraphRes graphRes = getGraph(buildingList, startNode, endNode, conditions); DijkstraRes route = dijkstra(graphRes, startNode, endNode); - routeRes.add(buildRouteResponse(route, startType == LocationType.BUILDING, endType == LocationType.BUILDING)); + routeRes.add(buildRouteResponse(route, startType == LocationType.BUILDING, endType == LocationType.BUILDING, conditions)); // 베리어프리만 추가로 적용하는 경우(임시) // GetGraphRes graphRes2 = getGraph(buildingList, startNode, endNode, List.of(BARRIERFREE)); @@ -81,9 +93,9 @@ public List findRoute(LocationType startType, Long startId, Double // 로그 저장 Building startBuilding = null, endBuilding = null; - Place startPlace= null, endPlace = null; + Place startPlace = null, endPlace = null; - if(checkType(startType, endType)) { + if (checkType(startType, endType)) { if (startType == LocationType.BUILDING) startBuilding = findBuilding(startId); else if (startType == LocationType.PLACE) startPlace = findPlace(startId); @@ -127,7 +139,7 @@ private void checkLocationError(Node startNode, Node endNode, LocationType start } // 3. 야외에서 너무 가까운 경우 - if (startNode.getBuilding().getId().equals(OUTDOOR_ID) && endNode.getBuilding().getId().equals(OUTDOOR_ID)){ + if (startNode.getBuilding().getId().equals(OUTDOOR_ID) && endNode.getBuilding().getId().equals(OUTDOOR_ID)) { if (getEuclidDistance(startNode.getLatitude(), startNode.getLongitude(), endNode.getLatitude(), endNode.getLongitude()) < MIN_OUTDOOR_DISTANCE) { throw new GlobalException(COORDINATES_TOO_NEAR); } @@ -150,22 +162,22 @@ private Node getNodeByType(LocationType type, Long id, Double latitude, Double l /** * 주어진 좌표값에서 가장 가까운 외부 노드 찾기 */ - private Node findNearestNode(Double latitude, Double longitude){ + private Node findNearestNode(Double latitude, Double longitude) { List graphNode = new ArrayList<>(findAllNode(findBuilding(OUTDOOR_ID))); double shortestDistance = INIT_OUTDOOR_DISTANCE; Node nearestNode = null; - for (Node node: graphNode){ + for (Node node : graphNode) { double thisDistance = getEuclidDistance(latitude, longitude, node.getLatitude(), node.getLongitude()); - if (nearestNode == null || shortestDistance > thisDistance){ + if (nearestNode == null || shortestDistance > thisDistance) { nearestNode = node; shortestDistance = thisDistance; } } - if(nearestNode == null) { + if (nearestNode == null) { throw new GlobalException(NOT_FOUND_NODE); } - if (getEuclidDistance(latitude, longitude, nearestNode.getLatitude(), nearestNode.getLongitude()) > MAX_OUTDOOR_DISTANCE){ + if (getEuclidDistance(latitude, longitude, nearestNode.getLatitude(), nearestNode.getLongitude()) > MAX_OUTDOOR_DISTANCE) { throw new GlobalException(COORDINATES_TOO_FAR); } return nearestNode; @@ -178,14 +190,24 @@ private double getEuclidDistance(double startX, double startY, double endX, doub /** * 탐색 알고리즘의 효율성을 위해 이동할 만한 건물들만 추리는 메서드 */ - private List getBuildingsForRoute(Node startNode, Node endNode) { - List buildingList = new ArrayList<>(); + private HashSet getBuildingsForRoute(Node startNode, Node endNode, List conditions) { + HashSet buildingList = new HashSet<>(); buildingList.add(findBuilding(OUTDOOR_ID)); // 외부 경로 추가 - if(!buildingList.contains(startNode.getBuilding())) buildingList.add(startNode.getBuilding()); - if(!buildingList.contains(endNode.getBuilding())) buildingList.add(endNode.getBuilding()); + buildingList.add(startNode.getBuilding()); + buildingList.add(endNode.getBuilding()); + if (conditions.contains(INNERROUTE)) { + Node startSearchNode = startNode; + Node endSearchNode = endNode; + if (!startNode.getBuilding().getId().equals(OUTDOOR_ID)) + startSearchNode = startNode.getBuilding().getNode(); + if (!endNode.getBuilding().getId().equals(OUTDOOR_ID)) endSearchNode = endNode.getBuilding().getNode(); - addConnectedBuildings(startNode.getBuilding(), buildingList); - addConnectedBuildings(endNode.getBuilding(), buildingList); + addInBetweenBuildings(startSearchNode, endSearchNode, buildingList); + } + HashSet buildingListCpy = new HashSet<>(buildingList); + for (Building building : buildingListCpy) { + addConnectedBuildings(building, buildingList); + } return buildingList; } @@ -194,7 +216,7 @@ private List getBuildingsForRoute(Node startNode, Node endNode) { * 출발/도착지에 연결된 건물들이 있는 경우 buildingList에 추가하는 메서드 * 연쇄적으로 연결된 건물들도 반영하도록(ex: 엘포관-백기-중지-SK미래관...) 수정 */ - private void addConnectedBuildings(Building startBuilding, List buildingList) { + private void addConnectedBuildings(Building startBuilding, HashSet buildingList) { Queue queue = new LinkedList<>(); Set visited = new HashSet<>(); @@ -216,33 +238,62 @@ private void addConnectedBuildings(Building startBuilding, List buildi } } + /** + * 실내우선 경로 전용 + * 시작 도착지역 기준 사이 건물 추가 메서드 + */ + private void addInBetweenBuildings(Node startNode, Node endNode, HashSet buildingList) { + Vector2D startPoint = new Vector2D(startNode.getLatitude(), startNode.getLongitude()); + Vector2D endPoint = new Vector2D(endNode.getLatitude(), endNode.getLongitude()); + List allBuildings = buildingRepository.findAll(); + Vector2D startToEnd = endPoint.subtract(startPoint); + for (Building building : allBuildings) { + if (building.getNode() != null) { + Vector2D buildingPoint = new Vector2D(building.getNode().getLatitude(), building.getNode().getLongitude()); + Vector2D startToBuilding = buildingPoint.subtract(startPoint); + double t = startToBuilding.dot(startToEnd) / startToEnd.normSquared(); + + if (t > 0 && t < 1) { + double distance = Math.sqrt(startToBuilding.normSquared() - t * t * startToEnd.normSquared()); + if (distance < BETWEEN_WEIGHT) buildingList.add(building); + } + } + } + + + } + /** * 그래프 요소 찾기(node, edge 묶음) * 노드 테이블의 String 인접 노드와 거리를 그래프로 변환 */ - private GetGraphRes getGraph(List buildingList, Node startNode, Node endNode, List conditions){ + private GetGraphRes getGraph(HashSet buildingList, Node startNode, Node endNode, List conditions) { List graphNode = new ArrayList<>(); Map> graphEdge = new HashMap<>(); // 조건에 따른 노드 검색 - for (Building building : buildingList){ + for (Building building : buildingList) { + if (conditions.contains(OPERATING)) { + if (!building.isOperating()) continue; + } graphNode.addAll(findNodeWithConditions(building, conditions)); } - if (!graphNode.contains(startNode)){ + if (!graphNode.contains(startNode)) { graphNode.add(startNode); } - if (!graphNode.contains(endNode)){ + if (!graphNode.contains(endNode)) { graphNode.add(endNode); } - for (Node node: graphNode){ + for (Node node : graphNode) { String rawAdjacentNode = node.getAdjacentNode(); String rawDistance = node.getDistance(); - if (rawAdjacentNode == null || rawDistance == null || rawAdjacentNode.isEmpty() || rawDistance.isEmpty()) continue; + if (rawAdjacentNode == null || rawDistance == null || rawAdjacentNode.isEmpty() || rawDistance.isEmpty()) + continue; Long[] nextNodeId; Long[] distance; @@ -258,7 +309,7 @@ private GetGraphRes getGraph(List buildingList, Node startNode, Node e throw new AdminException(INCORRECT_NODE_DATA, "노드" + node.getId() + "의 인접 노드와 거리 개수가 다릅니다."); } - if(!graphEdge.containsKey(node.getId())) { + if (!graphEdge.containsKey(node.getId())) { graphEdge.put(node.getId(), new ArrayList<>()); } for (int i = 0; i < nextNodeId.length; i++) { @@ -274,13 +325,12 @@ private GetGraphRes getGraph(List buildingList, Node startNode, Node e * 조건에 맞는 노드만 포함 * 추후 이 메서드와 getGraph 수정하여 operating 여부, 실내 탐색 필요. */ - private List findNodeWithConditions(Building building, List conditions){ + private List findNodeWithConditions(Building building, List conditions) { List nodeTypes = new ArrayList<>(Arrays.asList(NodeType.NORMAL, NodeType.STAIR, NodeType.ELEVATOR, NodeType.ENTRANCE, NodeType.CHECKPOINT)); - if (conditions != null){ - if (conditions.contains(BARRIERFREE)){ + if (conditions != null) { + if (conditions.contains(BARRIERFREE)) { nodeTypes.remove(NodeType.STAIR); - } - else if (conditions.contains(Conditions.SHUTTLE)){ + } else if (conditions.contains(Conditions.SHUTTLE)) { nodeTypes.add(NodeType.SHUTTLE); } } @@ -294,6 +344,7 @@ private DijkstraRes dijkstra(GetGraphRes graphRes, Node startNode, Node endNode) List nodes = graphRes.getGraphNode(); Map> edges = graphRes.getGraphEdge(); Map distances = new HashMap<>(); + Map weights = new HashMap<>(); Map previousNodes = new HashMap<>(); PriorityQueue priorityQueue = new PriorityQueue<>(); Set visitedNodes = new HashSet<>(); @@ -302,9 +353,11 @@ private DijkstraRes dijkstra(GetGraphRes graphRes, Node startNode, Node endNode) for (Node node : nodes) { if (node.equals(startNode)) { distances.put(node.getId(), 0L); + weights.put(node.getId(), 0L); priorityQueue.add(new NodeDistancePair(node.getId(), 0L)); } else { distances.put(node.getId(), INF); + weights.put(node.getId(), INF); } previousNodes.put(node.getId(), null); } @@ -323,30 +376,28 @@ private DijkstraRes dijkstra(GetGraphRes graphRes, Node startNode, Node endNode) for (Edge edge : edges.get(currentNode)) { Long neighbor = edge.getEndNode(); Long currentDistance = distances.get(currentNode); - if (currentDistance == null) continue; + Long currentWeight = weights.get(currentNode); + if (currentWeight == null) continue; + + Long newWeight = currentWeight + edge.getWeight(); //weight 기반 탐색으로 수정 + Long newDistance = currentDistance + edge.getDistance(); + Long neighborWeight = weights.get(neighbor); - Long newDist = currentDistance + edge.getWeight(); //weight 기반 탐색으로 수정 - Long neighborDist = distances.get(neighbor); - if (neighborDist == null || newDist < neighborDist) { - distances.put(neighbor, newDist); + if (neighborWeight == null || newWeight < neighborWeight && newWeight < INF) { + weights.put(neighbor, newWeight); + distances.put(neighbor, newDistance); previousNodes.put(neighbor, currentNode); - priorityQueue.add(new NodeDistancePair(neighbor, newDist)); + priorityQueue.add(new NodeDistancePair(neighbor, newWeight)); } } } //path 생성 List path = new ArrayList<>(); - Node pathPrevNode = null; - Long finalDistance = 0L; + Long finalDistance = distances.get(endNode.getId()); for (Long at = endNode.getId(); at != null; at = previousNodes.get(at)) { Node node = nodeRepository.findById(at).orElseThrow(() -> new GlobalException(NOT_FOUND_ROUTE)); - if (pathPrevNode != null) { - Edge edge = findEdge(edges, node.getId(), pathPrevNode.getId()); - finalDistance += edge.getDistance(); - } path.add(node); - pathPrevNode = node; } Collections.reverse(path); @@ -357,34 +408,30 @@ private DijkstraRes dijkstra(GetGraphRes graphRes, Node startNode, Node endNode) return new DijkstraRes(finalDistance, path); } - private Edge findEdge(Map> edges, Long from, Long to) { - return edges.get(from).stream() - .filter(edge -> edge.getEndNode().equals(to)) - .findFirst() - .orElse(null); - } /** * 경로를 분할하고 응답 형식에 맞게 변환 */ - private GetRouteRes buildRouteResponse(DijkstraRes route, boolean isStartBuilding, boolean isEndBuilding) { + private GetRouteRes buildRouteResponse(DijkstraRes route, boolean isStartBuilding, boolean isEndBuilding, List conditions) { // if (route.getPath().isEmpty()) return new GetRouteRes(1);//경로 미탐색 막기 위해 임의로 추가 - + List busTimeStamps = null; + int busIdx = 0; + if (conditions.contains(SHUTTLE)) busTimeStamps = modifyRoute(route); Long duration = route.getDistance(); List> path = cutRoute(route.getPath()); // 분할된 경로 //시작, 끝이 건물인 경우 해당 노드 지우기 if (isStartBuilding) { // 첫번째 path의 길이에 따라 삭제 다르게 하기 - if(path.get(0).size() > 1) { + if (path.get(0).size() > 1) { path.get(0).remove(0); } else { path.remove(0); } } - if (isEndBuilding && path.get(path.size()-1).size() != 1) { - path.get(path.size()-1).remove(path.get(path.size()-1).size()-1); + if (isEndBuilding && path.get(path.size() - 1).size() != 1) { + path.get(path.size() - 1).remove(path.get(path.size() - 1).size() - 1); } List totalRoute = new ArrayList<>(); @@ -401,10 +448,18 @@ private GetRouteRes buildRouteResponse(DijkstraRes route, boolean isStartBuildin : new PartialRouteRes(thisPath.get(0).getBuilding().getId(), thisPath.get(0).getFloor(), partialRoute); // 실내 경로 // 부분 경로의 마지막 노드인 경우 설명 추가 + //route 경로 설명을 추가하며, 셔틀경로가 있는 경우 waiting time 더하기 if (i + 1 == path.size()) { - partialRouteRes.setInfo(makeInfo(thisPath.get(thisPath.size() - 1), null)); + partialRouteRes.setInfo(makeInfo(thisPath.get(thisPath.size() - 1), null, null)); } else { - partialRouteRes.setInfo(makeInfo(thisPath.get(thisPath.size() - 1), path.get(i + 1).get(0))); + if (busTimeStamps == null) { + partialRouteRes.setInfo(makeInfo(thisPath.get(thisPath.size() - 1), path.get(i + 1).get(0), null)); + } + else{ + String info = makeInfo(thisPath.get(thisPath.size() - 1), path.get(i + 1).get(0), busTimeStamps.get(busIdx)); + if (info.contains("탑승하세요") && busIdx < busTimeStamps.size() - 1) busIdx++; + partialRouteRes.setInfo(info); + } } totalRoute.add(partialRouteRes); @@ -423,16 +478,19 @@ private List> cutRoute(List route) { List partialRoute = new ArrayList<>(); int count = 0; Node thisNode, nextNode; + List busStops = placeRepository.findAllByType(PlaceType.SHUTTLE_BUS); while (count < route.size() - 1) { thisNode = route.get(count); nextNode = route.get(count + 1); - // 새로운 건물로 이동할 때 & 체크포인트일때 & 외부에서 새로운 건물로 들어갈 때(입구 분리) 경로분할 + // 새로운 건물로 이동할 때 & 체크포인트일때 & 외부에서 새로운 건물로 들어갈 때(입구 분리) & 셔틀버스 경로로 출입할때 경로분할 if ((!thisNode.getBuilding().equals(nextNode.getBuilding()) && thisNode.getBuilding().getId() != OUTDOOR_ID) || (thisNode.getType() != nextNode.getType() && thisNode.getType() == NodeType.CHECKPOINT) - || (thisNode.getBuilding().getId() == OUTDOOR_ID && nextNode.getType() == NodeType.ENTRANCE)) { + || (thisNode.getBuilding().getId() == OUTDOOR_ID && nextNode.getType() == NodeType.ENTRANCE) + || (isBusStop(busStops, thisNode) && nextNode.getType() == NodeType.SHUTTLE) + || (thisNode.getType() != nextNode.getType() && thisNode.getType() == NodeType.SHUTTLE)) { partialRoute.add(thisNode); returnRoute.add(new ArrayList<>(partialRoute)); partialRoute.clear(); @@ -443,19 +501,17 @@ else if (!Objects.equals(thisNode.getFloor(), nextNode.getFloor())) { partialRoute.add(thisNode); // 계단/엘리베이터를 통한 연속적인 층 이동을 감지하여 중간 층을 생략 - while (count < route.size() - 1 && !Objects.equals(thisNode.getFloor(), nextNode.getFloor()) && thisNode.getBuilding().equals(nextNode.getBuilding())) { - thisNode = route.get(count); - nextNode = route.get(count + 1); + while (!Objects.equals(thisNode.getFloor(), nextNode.getFloor()) && thisNode.getBuilding().equals(nextNode.getBuilding())) { count++; + thisNode = route.get(count); + nextNode = route.get(count+1); } returnRoute.add(new ArrayList<>(partialRoute)); partialRoute.clear(); // 끝 층의 시작 노드를 새 경로로 추가 - partialRoute.add(thisNode); count--; - } - else { + } else { partialRoute.add(thisNode); } @@ -492,51 +548,74 @@ private List> convertNodesToCoordinates(List nodes, boolean i * 나눠진 기준으로 두 노드를 받아 비교하여 간단한 설명 생성 * nextNode에 null이 들어오면 경로 안내가 끝난 상황이라고 판단 */ - private String makeInfo(Node prevNode, Node nextNode){ + private String makeInfo(Node prevNode, Node nextNode, LocalTime timeStamp) { + List busStops = placeRepository.findAllByType(PlaceType.SHUTTLE_BUS); if (nextNode == null) return "도착"; - if (prevNode.getType() == NodeType.CHECKPOINT){ + if (prevNode.getType() == NodeType.CHECKPOINT) { String checkpointName = findCheckpoint(prevNode).getName(); - return checkpointName + "(으)로 이동하세요."; + return makeString(checkpointName); + } + //shuttle버스 탑승하는 경우 + if (isBusStop(busStops, prevNode) && nextNode.getType() == NodeType.SHUTTLE) { + if (timeStamp == null) { + return placeRepository.findByNode(prevNode).getDetail() + "에서 셔틀버스에 탑승하세요."; + } else { + return placeRepository.findByNode(prevNode).getDetail() + "에서 셔틀버스에 탑승하세요. (" + timeStamp.getHour() + "시 " + timeStamp.getMinute() + "분 버스)"; + } + } + + //셔틀버스 내리는 경우 + if (prevNode.getType() != nextNode.getType() && prevNode.getType() == NodeType.SHUTTLE) { + return placeRepository.findByNode(nextNode).getDetail() + "에서 셔틀버스에서 내리세요."; } Building prevNodeBuilding = prevNode.getBuilding(); Building nextNodeBuilding = nextNode.getBuilding(); - // 건물 외부에서 내부로 들어가는 경우: prevNodeBuilding과 nextNodeBuilding이 같음(id=0L) // 미리 출입구에서 한 번 추가적으로 끊기 때문. - if (Objects.equals(prevNodeBuilding, nextNodeBuilding) && prevNodeBuilding.getId() == OUTDOOR_ID){ + if (Objects.equals(prevNodeBuilding, nextNodeBuilding) && prevNodeBuilding.getId() == OUTDOOR_ID) { Building enteringBuilding = findLinkedBuilding(nextNode); - return enteringBuilding.getName() + "(으)로 이동하세요."; + return makeString(enteringBuilding.getName()); } int nextNodeFloor = nextNode.getFloor().intValue(); String floor = nextNodeFloor >= 0 ? Integer.toString(nextNodeFloor) : "B" + Math.abs(nextNodeFloor); - // 그 외 건물이 같은 경우는 층 이동의 경우밖에 없음 - if (Objects.equals(prevNodeBuilding, nextNodeBuilding)){ + if (Objects.equals(prevNodeBuilding, nextNodeBuilding)) { return floor + "층으로 이동하세요."; } - // 끊긴 출입구 기준으로 바깥에서 안으로 들어가는 경우 - if (prevNodeBuilding.getId() == OUTDOOR_ID){ + if (prevNodeBuilding.getId() == OUTDOOR_ID) { return nextNodeBuilding.getName() + " " + floor + "층 출입구로 들어가세요."; } // 안에서 바깥으로 나가는 경우 if (nextNodeBuilding.getId() == OUTDOOR_ID) return "출입구를 통해 밖으로 나가세요"; - // 건물 사이를 바로 이동하는 경우 return "출입구를 통해 " + nextNodeBuilding.getName() + " " + floor + "층으로 이동하세요."; } + + /** + * makeinfo에서 활용하는 로/으로 구분 메서드 + */ + private String makeString(String name) { + String decomposedName = HangeulUtils.decomposeHangulString(name); + if (decomposedName.endsWith("ㄹ") || !HangeulUtils.isConsonantOnly(decomposedName.substring(decomposedName.length() - 1))) { + return name + "로 이동하세요."; + } else { + return name + "으로 이동하세요."; + } + } + /** * 노드에 연결된 건물을 찾는 메서드(들어오는 예상 노드는 ENTRANCE). * 건물 내부가 완료되지 않아 / 가장 가까운 출입구가 출입금지라 entrance와 연결된 내부 노드가 없을 수도 있는데, 이 경우 건물 노드로 찾는다 */ - private Building findLinkedBuilding(Node node){ + private Building findLinkedBuilding(Node node) { Long[] adjacentNodeIds = convertStringToArray(node.getAdjacentNode()); return buildingRepository.findByNodeIdIn(adjacentNodeIds) - .orElseThrow(() -> new AdminException(INCORRECT_NODE_DATA,node.getId() + "번 노드에 연결된 건물이 없습니다")); + .orElseThrow(() -> new AdminException(INCORRECT_NODE_DATA, node.getId() + "번 노드에 연결된 건물이 없습니다")); } /** @@ -551,6 +630,117 @@ private Long[] convertStringToArray(String str) throws NumberFormatException { return arr; } + //셔틀버스용 메서드들 + + /** + * 특정 place들이 전부 bus stop인지 확인하는 메서드 + */ + private boolean isBusStop(List busStops, Node node) { + for (Place busStop : busStops) { + if (busStop.getNode() == node) return true; + } + return false; + } + + /** + * (대략적으로) 학기중이면 0, 여름방학이면 1, 겨울방학이면 2 리턴 메서드 + */ + private int summerSession() { + int now = LocalDate.now().getMonthValue(); + return switch (now) { + case 3, 4, 5, 6, 9, 10, 11, 12 -> 0; + case 7, 8 -> 1; + default -> 2; + }; + } + + /** + * 특정 노드(반드시 셔틀버스 정류장)에서 현재 시간 + distance기준으로 몇 분 버스에 타야 하는지 리턴하는 메서드 + */ + private LocalTime calculateBusTime(Long distance, Node node, LocalTime currentTime) { + boolean isSummerSession = summerSession() != 0; + //테스트용 summersession 코드 + //boolean isSummerSession = false; + LocalTime startTime = currentTime.plusSeconds(distance); + Place busStop = placeRepository.findByNode(node); + List busStopSchedule = shuttleTimeRepository.findAllByPlaceAndSummerSession(busStop, isSummerSession, Sort.by(Sort.Direction.ASC, "time")); + for (ShuttleTime shuttleTime : busStopSchedule) { + LocalTime thisTime = shuttleTime.getTime(); + if (startTime.isBefore(thisTime)) { + return thisTime; + } + } + return null; + } + + /** + * 현재 버스 운영 시간인지 리턴 + */ + private boolean isBusTime() { + int semester = summerSession(); + boolean isSemester; + if (semester == 2) return false; + else isSemester = (semester == 0); + LocalTime now = LocalTime.now(); + //테스트용 now 설정 + //LocalTime now = TEST_TIME; + List busStopSchedule = shuttleTimeRepository.findAllBySummerSession(isSemester, Sort.by(Sort.Direction.ASC, "time")); + LocalTime firstTime = busStopSchedule.get(0).getTime(); + LocalTime lastTime = busStopSchedule.get(busStopSchedule.size() - 1).getTime(); + return (now.isAfter(firstTime.minusMinutes(20)) && now.isBefore(lastTime.minusMinutes(20))); + } + + /** + * 셔틀버스용 생성된 경로 수정 + */ + private List modifyRoute(DijkstraRes route) { + boolean isShuttleRoute = false; + List path = route.getPath(); + //버스를 2번 이상 탑승 가능하므로 list형태로 저장 + List shuttleIdx = new ArrayList<>(); + List busTimes = new ArrayList<>(); + for (int i = 0; i < path.size() - 1; i++) { + if (path.get(i).getType() != NodeType.SHUTTLE && path.get(i + 1).getType() == NodeType.SHUTTLE) { + isShuttleRoute = true; + shuttleIdx.add(i); + } + } + //셔틀을 이용한 경로가 반환되지 않았을 경우 NOT_FOUND_ROUTE + if (!isShuttleRoute) throw new GlobalException(NOT_FOUND_ROUTE); + Long shuttleWaitTime = 0L; + if (isBusTime()) { + LocalTime currentTime = LocalTime.now(); + //테스트용 currentTime 설정 + //LocalTime currentTime = TEST_TIME; + for (Integer idx : shuttleIdx) { + Long distToShuttle = countDistance(path, idx) + shuttleWaitTime; + LocalTime busTime = calculateBusTime(distToShuttle, path.get(idx), currentTime); + busTimes.add(busTime); + shuttleWaitTime += Duration.between(currentTime, busTime).getSeconds(); + route.setDistance(route.getDistance() + Duration.between(currentTime, busTime).getSeconds()); + } + return busTimes; + } else { + route.setDistance(route.getDistance() + 300); + return null; + } + } + + private Long countDistance(List route, int shuttleIdx) { + long distance = 0L; + for (int i = 0; i < shuttleIdx; i++) { + String[] adjNodes = route.get(i).getAdjacentNode().split(","); + String[] adjDists = route.get(i).getDistance().split(","); + for (int j = 0; j < adjNodes.length; j++) { + if (Long.parseLong(adjNodes[j]) == route.get(i + 1).getId()) { + distance += Long.parseLong(adjDists[j]); + break; + } + } + } + return distance; + } + // findEntity 메서드 private Building findBuilding(Long buildingId) { return buildingRepository.findById(buildingId).orElseThrow(() -> new GlobalException(NOT_FOUND_BUILDING)); @@ -564,11 +754,11 @@ private Node findNode(Long nodeId) { return nodeRepository.findById(nodeId).orElseThrow(() -> new GlobalException(NOT_FOUND_NODE)); } - private List findAllNode(Building building){ + private List findAllNode(Building building) { return nodeRepository.findByBuildingAndRouting(building, true); } - private Checkpoint findCheckpoint(Node node){ + private Checkpoint findCheckpoint(Node node) { return checkpointRepository.findByNode(node); } @@ -590,4 +780,44 @@ public int compareTo(NodeDistancePair other) { } } + /** + * 벡터 계산용 클래스 + */ + public class Vector2D { + private double x; + private double y; + + public Vector2D(double x, double y) { + this.x = x; + this.y = y; + } + + //벡터 덧셈 + public Vector2D add(Vector2D vector2D) { + return new Vector2D(this.x + vector2D.x, this.y + vector2D.y); + } + + //벡터 뺄셈 + public Vector2D subtract(Vector2D vector2D) { + return new Vector2D(this.x - vector2D.x, this.y - vector2D.y); + } + + //내적 + public double dot(Vector2D vector2D) { + return this.x * vector2D.x + this.y * vector2D.y; + } + + //벡터의 제곱 크기 + public double normSquared() { + return this.x * this.x + this.y * this.y; + } + + @Override + public String toString() { + return String.format("Vector2D(%.2f, %.2f)", this.x, this.y); + } + } } + + + diff --git a/src/main/java/devkor/com/teamcback/domain/user/controller/AppleController.java b/src/main/java/devkor/com/teamcback/domain/user/controller/AppleController.java new file mode 100644 index 00000000..e9166b91 --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/user/controller/AppleController.java @@ -0,0 +1,32 @@ +package devkor.com.teamcback.domain.user.controller; + +import com.apple.itunes.storekit.verification.VerificationException; +import devkor.com.teamcback.domain.user.dto.request.AppleNotationReq; +import devkor.com.teamcback.domain.user.dto.response.AppleNotificationRes; +import devkor.com.teamcback.domain.user.service.AppleService; +import devkor.com.teamcback.global.exception.exception.GlobalException; +import devkor.com.teamcback.global.response.CommonResponse; +import devkor.com.teamcback.global.response.ResultCode; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/apple") +public class AppleController { + + private final AppleService appleService; + + /** + * Apple 로그인 계정 상태 알림을 수신하는 엔드포인트 + */ + @PostMapping("/notifications") + public CommonResponse handleAppleNotification(@RequestBody AppleNotationReq request) throws VerificationException { + + return CommonResponse.success(appleService.handleAppleNotification(request)); + + } +} diff --git a/src/main/java/devkor/com/teamcback/domain/user/controller/UserController.java b/src/main/java/devkor/com/teamcback/domain/user/controller/UserController.java index 02315a20..30af7515 100644 --- a/src/main/java/devkor/com/teamcback/domain/user/controller/UserController.java +++ b/src/main/java/devkor/com/teamcback/domain/user/controller/UserController.java @@ -44,21 +44,6 @@ public CommonResponse getUserInfo( return CommonResponse.success(userService.getUserInfo(userDetail.getUser().getUserId())); } - /** - * 로그인 - */ - @Operation(summary = "로그인", description = "FE에서 소셜로그인 진행 후 보내주는 사용자 정보로 토큰 반환") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "정상 처리 되었습니다."), - }) - @PostMapping("/login") - public CommonResponse login( - @Parameter(description = "사용자정보", required = true) - @RequestBody LoginUserReq loginUserReq - ) { - return CommonResponse.success(userService.login(loginUserReq)); - } - /** * 소셜 토큰 확인 로그인 */ diff --git a/src/main/java/devkor/com/teamcback/domain/user/dto/request/AppleNotationReq.java b/src/main/java/devkor/com/teamcback/domain/user/dto/request/AppleNotationReq.java new file mode 100644 index 00000000..e9805d54 --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/user/dto/request/AppleNotationReq.java @@ -0,0 +1,10 @@ +package devkor.com.teamcback.domain.user.dto.request; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class AppleNotationReq { + private String signedPayload; +} diff --git a/src/main/java/devkor/com/teamcback/domain/user/dto/response/AppleNotificationRes.java b/src/main/java/devkor/com/teamcback/domain/user/dto/response/AppleNotificationRes.java new file mode 100644 index 00000000..59e6ee4b --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/user/dto/response/AppleNotificationRes.java @@ -0,0 +1,7 @@ +package devkor.com.teamcback.domain.user.dto.response; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties +public class AppleNotificationRes { +} diff --git a/src/main/java/devkor/com/teamcback/domain/user/service/AppleService.java b/src/main/java/devkor/com/teamcback/domain/user/service/AppleService.java new file mode 100644 index 00000000..9bffb55b --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/user/service/AppleService.java @@ -0,0 +1,97 @@ +package devkor.com.teamcback.domain.user.service; + +import com.apple.itunes.storekit.model.Environment; +import com.apple.itunes.storekit.model.JWSTransactionDecodedPayload; +import com.apple.itunes.storekit.model.ResponseBodyV2DecodedPayload; +import com.apple.itunes.storekit.verification.SignedDataVerifier; +import com.apple.itunes.storekit.verification.VerificationException; +import devkor.com.teamcback.domain.user.dto.request.AppleNotationReq; +import devkor.com.teamcback.domain.user.dto.response.AppleNotificationRes; +import devkor.com.teamcback.domain.user.repository.UserRepository; +import devkor.com.teamcback.global.exception.exception.GlobalException; +import devkor.com.teamcback.global.response.ResultCode; +import jakarta.annotation.PostConstruct; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.util.HashSet; +import java.util.Set; + +@Service +public class AppleService { + + @Autowired + private UserRepository userRepository; + private SignedDataVerifier verifier; + private static final String BUNDLE_ID = "com.devkor.kodaero"; + + @PostConstruct + public void init() throws FileNotFoundException { + + InputStream certInputStream = getClass().getClassLoader().getResourceAsStream("static/apple/AppleRootCA-G3.cer"); + + Set rootCertificates = new HashSet<>(); + rootCertificates.add(certInputStream); + + this.verifier = new SignedDataVerifier( + rootCertificates, + BUNDLE_ID, + null, + Environment.LOCAL_TESTING, + // Environment.PRODUCTION, + false + ); + } + + + @Transactional + public AppleNotificationRes handleAppleNotification(AppleNotationReq request) { + + String signedPayload = request.getSignedPayload(); + + if (signedPayload == null || signedPayload.isEmpty()) { + throw new GlobalException(ResultCode.INVALID_INPUT); + } + + try { + ResponseBodyV2DecodedPayload payload = verifier.verifyAndDecodeNotification(signedPayload); + + String notificationType = payload.getNotificationType().toString(); + String uuid = payload.getNotificationUUID(); + + System.out.println("알림 유형: " + notificationType); + System.out.println("UUID: " + uuid); + + String signedTransactionInfo = payload.getData().getSignedTransactionInfo(); + String signedRenewalInfo = payload.getData().getSignedRenewalInfo(); + + JWSTransactionDecodedPayload transactionInfo = verifier.verifyAndDecodeTransaction(signedTransactionInfo); + String originalTransactionId = transactionInfo.getOriginalTransactionId(); + + // 내부 사용자 ID 조회 + // String userId = userRepository.findUserIdByOriginalTransactionId(originalTransactionId); + + switch (notificationType) { + case "account_delete": + break; + case "email_change": + break; + default: + System.out.println("Unhandled Apple notification: " + notificationType); + break; + } + + return new AppleNotificationRes(); + + } catch (Exception e) { + System.err.println("Apple SiWA Notification processing failed: " + e.getMessage()); + throw new GlobalException(ResultCode.SYSTEM_ERROR); + } + } + +} diff --git a/src/main/java/devkor/com/teamcback/domain/user/service/UserService.java b/src/main/java/devkor/com/teamcback/domain/user/service/UserService.java index a91bdafc..4b5831a5 100644 --- a/src/main/java/devkor/com/teamcback/domain/user/service/UserService.java +++ b/src/main/java/devkor/com/teamcback/domain/user/service/UserService.java @@ -86,24 +86,6 @@ public GetUserInfoRes getUserInfo(Long userId) { return new GetUserInfoRes(user, categoryRepository.countAllByUser(user), level.getLevelNumber(), remainScoreToNextLevel, percent, isUpgraded); } - /** - * 로그인 (안드로이드 배포 수정 후 삭제) - */ - @Transactional - public TempLoginRes login(LoginUserReq loginUserReq) { - User user = userRepository.findByEmailAndProvider(loginUserReq.getEmail(), loginUserReq.getProvider()); // 이메일이 같더라도 소셜이 다르면 다른 사용자 취급 - if(user == null) { // 회원이 없으면 회원가입 - String username = makeRandomName(); - user = userRepository.save(new User(username, loginUserReq.getEmail(), Role.USER, loginUserReq.getProvider())); - - // 기본 카테고리 저장 - Category category = new Category(DEFAULT_CATEGORY, DEFAULT_COLOR, user); - categoryRepository.save(category); - } - - return new TempLoginRes(jwtUtil.createAccessToken(user.getUserId().toString(), user.getRole().getAuthority()), jwtUtil.createRefreshToken(user.getUserId().toString(), user.getRole().getAuthority())); - } - /** * 로그인 */ diff --git a/src/main/java/devkor/com/teamcback/global/response/ResultCode.java b/src/main/java/devkor/com/teamcback/global/response/ResultCode.java index 90c588d7..319fa0e0 100644 --- a/src/main/java/devkor/com/teamcback/global/response/ResultCode.java +++ b/src/main/java/devkor/com/teamcback/global/response/ResultCode.java @@ -87,7 +87,8 @@ public enum ResultCode { NOT_FOUND_DEVICE(HttpStatus.NOT_FOUND, 13000, "해당 placeId에 해당하는 device가 없습니다."), NOT_FOUND_DEVICE_NAME(HttpStatus.NOT_FOUND, 13001, "해당 device 이름이 없습니다."), EXISTING_DEVICE_NAME(HttpStatus.CONFLICT, 13002, "중복되는 device 이름입니다."), - NO_DATA_FOR_DEVICE(HttpStatus.NOT_FOUND, 13003, "device에 해당하는 정보가 없습니다.") + NO_DATA_FOR_DEVICE(HttpStatus.NOT_FOUND, 13003, "device에 해당하는 정보가 없습니다."), + EXISTING_PLACE_FOR_DEVICE(HttpStatus.CONFLICT, 13004, "중복되는 device placeId입니다.") ; private final HttpStatus status; diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml index 80cb41cc..4ecf98dc 100644 --- a/src/main/resources/logback-spring.xml +++ b/src/main/resources/logback-spring.xml @@ -35,8 +35,64 @@ + + + ${LOG_PATH}/error.log + + ERROR + ACCEPT + DENY + + + ${LOG_PATH}/error.%d{yyyy-MM-dd}.%i.log + 100MB + 5 + 500MB + + + + + + + + + + + + + + + + + ${LOG_PATH}/warn.log + + WARN + ACCEPT + DENY + + + ${LOG_PATH}/warn.%d{yyyy-MM-dd}.%i.log + 100MB + 5 + 500MB + + + + + + + + + + + + + + + + diff --git a/src/main/resources/static/apple/AppleRootCA-G3.cer b/src/main/resources/static/apple/AppleRootCA-G3.cer new file mode 100644 index 00000000..228bfa39 Binary files /dev/null and b/src/main/resources/static/apple/AppleRootCA-G3.cer differ