diff --git a/build.gradle b/build.gradle index 589369bf..e711479e 100644 --- a/build.gradle +++ b/build.gradle @@ -67,6 +67,19 @@ dependencies { // Redis implementation 'org.springframework.boot:spring-boot-starter-data-redis' + + // SQS + implementation platform("io.awspring.cloud:spring-cloud-aws-dependencies:3.0.1") + implementation 'io.awspring.cloud:spring-cloud-aws-starter-sqs' + + // CoolSMS + implementation 'net.nurigo:sdk:4.3.0' + + // AWS S3 + implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' + + // Naver Clova OCR API + implementation 'org.springframework.boot:spring-boot-starter-webflux' // WebClient 사용을 위한 라이브러리 } tasks.named('test') { diff --git a/src/main/java/dbdr/domain/careworker/WorkDay.java b/src/main/java/dbdr/domain/careworker/WorkDay.java new file mode 100644 index 00000000..a62b9b83 --- /dev/null +++ b/src/main/java/dbdr/domain/careworker/WorkDay.java @@ -0,0 +1,21 @@ +package dbdr.domain.careworker; + +public enum WorkDay { + MONDAY(1), + TUESDAY(2), + WEDNESDAY(4), + THURSDAY(8), + FRIDAY(16), + SATURDAY(32), + SUNDAY(64); + + private final int value; + + WorkDay(int value) { + this.value = value; + } + + public int getValue() { + return value; + } +} diff --git a/src/main/java/dbdr/domain/careworker/controller/CareworkerController.java b/src/main/java/dbdr/domain/careworker/controller/CareworkerController.java index 8bc9a5d9..6e88a224 100644 --- a/src/main/java/dbdr/domain/careworker/controller/CareworkerController.java +++ b/src/main/java/dbdr/domain/careworker/controller/CareworkerController.java @@ -23,7 +23,7 @@ import org.springframework.web.bind.annotation.RestController; -@Tag(name = "요양보호사 마이페이지", description = "요양보호사 본인의 정보 조회 및 수정") +@Tag(name = "[요양보호사] 마이페이지", description = "요양보호사 본인의 정보 조회 및 수정") @RestController @RequestMapping("/${spring.app.version}/careworker") @RequiredArgsConstructor @@ -51,4 +51,4 @@ public ResponseEntity> updateCarewo careworkerRequest); return ResponseEntity.ok(ApiUtils.success(updatedResponse)); } -} \ No newline at end of file +} diff --git a/src/main/java/dbdr/domain/careworker/controller/CareworkerInstitutionController.java b/src/main/java/dbdr/domain/careworker/controller/CareworkerInstitutionController.java index ac298c2c..106b16bd 100644 --- a/src/main/java/dbdr/domain/careworker/controller/CareworkerInstitutionController.java +++ b/src/main/java/dbdr/domain/careworker/controller/CareworkerInstitutionController.java @@ -7,6 +7,7 @@ import dbdr.global.util.api.ApiUtils; import dbdr.security.LoginInstitution; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; @@ -28,7 +29,7 @@ public class CareworkerInstitutionController { @Operation(summary = "특정 요양원아이디로 전체 요양보호사 정보 조회", security = @SecurityRequirement(name = "JWT")) @GetMapping("/institution") public ResponseEntity>> getAllCareworkers( - @LoginInstitution Institution institution) { + @Parameter(hidden = true) @LoginInstitution Institution institution) { List institutions = careworkerService.getCareworkersByInstitution(institution.getId()); return ResponseEntity.ok(ApiUtils.success(institutions)) ; } @@ -37,7 +38,7 @@ public ResponseEntity>> getAllCarewo @GetMapping("/{careworkerId}") public ResponseEntity> getCareworkerById( @PathVariable("careworkerId") Long careworkerId, - @LoginInstitution Institution institution) { + @Parameter(hidden = true) @LoginInstitution Institution institution) { CareworkerResponse careworker = careworkerService.getCareworkerByInstitution(careworkerId, institution.getId()); return ResponseEntity.ok(ApiUtils.success(careworker)) ; } @@ -46,7 +47,7 @@ public ResponseEntity> getCareworkerById( @Operation(summary = "요양보호사 추가", security = @SecurityRequirement(name = "JWT")) @PostMapping public ResponseEntity> createCareworker( - @LoginInstitution Institution institution, + @Parameter(hidden = true) @LoginInstitution Institution institution, @Valid @RequestBody CareworkerRequest careworkerDTO) { CareworkerResponse newCareworker = careworkerService.createCareworker(careworkerDTO); return ResponseEntity.status(HttpStatus.CREATED).body(ApiUtils.success(newCareworker)); @@ -58,7 +59,7 @@ public ResponseEntity> createCareworker( @PutMapping("/{careworkerId}") public ResponseEntity> updateCareworker( @PathVariable Long careworkerId, - @LoginInstitution Institution institution, + @Parameter(hidden = true) @LoginInstitution Institution institution, @RequestBody CareworkerRequest careworkerDTO) { CareworkerResponse updatedCareworker = careworkerService.updateCareworker(careworkerId, careworkerDTO); return ResponseEntity.ok(ApiUtils.success(updatedCareworker)); @@ -68,8 +69,8 @@ public ResponseEntity> updateCareworker( @DeleteMapping("/{careworkerId}") public ResponseEntity> deleteCareworker( @PathVariable Long careworkerId, - @LoginInstitution Institution institution) { + @Parameter(hidden = true) @LoginInstitution Institution institution) { careworkerService.deleteCareworker(careworkerId, institution.getId()); return ResponseEntity.status(HttpStatus.NO_CONTENT).build(); } -} \ No newline at end of file +} diff --git a/src/main/java/dbdr/domain/careworker/dto/request/CareworkerUpdateRequest.java b/src/main/java/dbdr/domain/careworker/dto/request/CareworkerUpdateRequest.java index 50055745..546376d2 100644 --- a/src/main/java/dbdr/domain/careworker/dto/request/CareworkerUpdateRequest.java +++ b/src/main/java/dbdr/domain/careworker/dto/request/CareworkerUpdateRequest.java @@ -1,6 +1,7 @@ package dbdr.domain.careworker.dto.request; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import lombok.AllArgsConstructor; import lombok.Getter; @@ -9,13 +10,18 @@ import java.time.LocalTime; import java.util.Set; +import com.fasterxml.jackson.annotation.JsonFormat; + @Getter @AllArgsConstructor public class CareworkerUpdateRequest { @NotNull(message = "근무일은 필수 항목입니다.") + @Schema(description = "근무 요일 목록", example = "[\"MONDAY\", \"WEDNESDAY\", \"FRIDAY\"]") private Set workingDays; + @JsonFormat(pattern = "HH:mm") @NotNull(message = "알림 시간은 필수 항목입니다.") + @Schema(description = "알림 시간 (HH:mm 형식)", example = "17:00") private LocalTime alertTime; } diff --git a/src/main/java/dbdr/domain/careworker/dto/response/CareworkerMyPageResponse.java b/src/main/java/dbdr/domain/careworker/dto/response/CareworkerMyPageResponse.java index 85a92b66..cf5565a5 100644 --- a/src/main/java/dbdr/domain/careworker/dto/response/CareworkerMyPageResponse.java +++ b/src/main/java/dbdr/domain/careworker/dto/response/CareworkerMyPageResponse.java @@ -13,6 +13,6 @@ public class CareworkerMyPageResponse { private String name; private String phone; private String institutionName; - private Set workingDays; private LocalTime alertTime; -} \ No newline at end of file + private Set workingDays; +} diff --git a/src/main/java/dbdr/domain/careworker/entity/Careworker.java b/src/main/java/dbdr/domain/careworker/entity/Careworker.java index eb7c6a77..c64beaef 100644 --- a/src/main/java/dbdr/domain/careworker/entity/Careworker.java +++ b/src/main/java/dbdr/domain/careworker/entity/Careworker.java @@ -1,5 +1,8 @@ package dbdr.domain.careworker.entity; +import java.time.DayOfWeek; +import java.time.LocalTime; + import dbdr.domain.core.base.entity.BaseEntity; import dbdr.domain.institution.entity.Institution; import jakarta.persistence.Column; @@ -14,6 +17,7 @@ import jakarta.validation.constraints.Pattern; import java.time.DayOfWeek; import java.time.LocalTime; +import java.util.EnumSet; import java.util.Set; import lombok.AccessLevel; import lombok.Builder; @@ -43,29 +47,18 @@ public class Careworker extends BaseEntity { @JoinColumn(name = "institution_id") private Institution institution; - // 근무일을 요일로 설정 - @ElementCollection(fetch = FetchType.LAZY) - @Enumerated(EnumType.STRING) - private Set workingDays; + @Column(nullable = false) + private int workDays; // 비트 플래그로 요일 저장 @Column(nullable = true) private String lineUserId; @Column(nullable = true) - private LocalTime alertTime; + private LocalTime alertTime = LocalTime.of(17, 0); // 오후 5시로 초기화 @Column(unique = true) private String email; -// @Builder -// public Careworker(Institution institution, String name, String email, String phone) { -// this.institution = institution; -// this.name = name; -// this.email = email; -// this.phone = phone; -// this.alertTime = LocalTime.of(17, 0); // 오후 5시로 초기화 -// } - @Builder public Careworker(String loginPassword, String phone, String name, Institution institution, String email) { @@ -74,6 +67,7 @@ public Careworker(String loginPassword, String phone, String name, Institution i this.name = name; this.institution = institution; this.email = email; + this.alertTime = LocalTime.of(17, 0); // 오후 5시로 초기화 } public void updateCareworker(Careworker careworker) { @@ -87,15 +81,47 @@ public void updateLineUserId(String lineUserId) { } public void updateWorkingDays(Set workingDays) { - this.workingDays = workingDays; + this.workDays = 0; // 초기화하여 기존 값을 제거합니다. + for (DayOfWeek day : workingDays) { + this.workDays |= (1 << (day.getValue() - 1)); // 각 요일을 비트 플래그로 추가합니다. + } } + public Set getWorkingDays() { + Set workingDays = EnumSet.noneOf(DayOfWeek.class); + for (DayOfWeek day : DayOfWeek.values()) { + if ((this.workDays & (1 << (day.getValue() - 1))) != 0) { + workingDays.add(day); + } + } + return workingDays; + } public void updateAlertTime(LocalTime alertTime) { this.alertTime = alertTime; } - public void updateInstitution(Institution institution) { - this.institution = institution; + public void updateInstitution(Institution institution) { + this.institution = institution; + } + + // 요일 설정 및 조회 메서드 + public void addWorkDay(DayOfWeek day) { + this.workDays |= day.getValue(); } -} \ No newline at end of file + // 다음 근무일 찾기 + public DayOfWeek getNextWorkingDay(DayOfWeek currentDay) { + for (int i = 1; i <= 7; i++) { // 최대 7일을 순환하여 다음 근무일 찾기 + DayOfWeek nextDay = currentDay.plus(i); + if (isWorkingOn(nextDay)) { + return nextDay; + } + } + return null; + } + + // 근무일인지 확인하기 + public boolean isWorkingOn(DayOfWeek day) { + return (this.workDays & (1 << (day.getValue() - 1))) != 0; + } +} diff --git a/src/main/java/dbdr/domain/careworker/service/CareworkerMessagingService.java b/src/main/java/dbdr/domain/careworker/service/CareworkerMessagingService.java deleted file mode 100644 index 50f5223b..00000000 --- a/src/main/java/dbdr/domain/careworker/service/CareworkerMessagingService.java +++ /dev/null @@ -1,54 +0,0 @@ -package dbdr.domain.careworker.service; - -import java.time.LocalTime; - -import org.springframework.stereotype.Service; - -import dbdr.domain.careworker.entity.Careworker; -import dbdr.domain.careworker.repository.CareworkerRepository; - -import org.springframework.transaction.annotation.Transactional; - -import dbdr.global.util.line.LineMessagingUtil; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -@Service -@RequiredArgsConstructor -@Slf4j -public class CareworkerMessagingService { - - private final CareworkerService careworkerService; - private final CareworkerRepository careworkerRepository; - private final LineMessagingUtil lineMessagingUtil; - - @Transactional - public void handleCareworkerPhoneMessage(String userId, String phoneNumber) { - Careworker careworker = careworkerService.findByPhone(phoneNumber); - String userName = careworker.getName(); - careworker.updateLineUserId(userId); - careworkerRepository.save(careworker); - - String welcomeMessage = - " " + userName + " 요양보호사님, 안녕하세요! 🌸 \n" + - " 최고의 요양원 서비스 돌봄다리입니다. 🤗\n" + - " 저희와 함께 해주셔서 정말 감사합니다! 🙏\n" + - " 기본적인 알림 시간은 매일 오후 5시로 설정되어있습니다. 😄\n" + - " 알림을 받고 싶은 시간을 수정하고 싶으시다면 알려주세요! 💬\n" + - " 예 : `오후 7시' 혹은 '오후 7시 30분'"; - - log.info("Careworker {} has been registered with Line ID {}", userName, userId); - log.info("Sending welcome message to CareworkerPhone {}", careworker.getPhone()); - - lineMessagingUtil.sendMessageToUser(userId, welcomeMessage); - } - - @Transactional - public void updateCareworkerAlertTime(String userId, String ampm, String hour, String minute) { - Careworker careworker = careworkerService.findByLineUserId(userId); - int minuteValue = (minute != null) ? Integer.parseInt(minute) : 0; // minute이 null이면 0으로 처리 - LocalTime alertTime = lineMessagingUtil.convertToLocalTime(ampm, Integer.parseInt(hour), minuteValue); - careworker.updateAlertTime(alertTime); - careworkerRepository.save(careworker); - } -} diff --git a/src/main/java/dbdr/domain/careworker/service/CareworkerService.java b/src/main/java/dbdr/domain/careworker/service/CareworkerService.java index b3282e4f..c5b908c6 100644 --- a/src/main/java/dbdr/domain/careworker/service/CareworkerService.java +++ b/src/main/java/dbdr/domain/careworker/service/CareworkerService.java @@ -7,6 +7,7 @@ import dbdr.domain.careworker.dto.response.CareworkerResponse; import dbdr.domain.careworker.entity.Careworker; import dbdr.domain.careworker.repository.CareworkerRepository; +import dbdr.domain.core.alarm.service.AlarmService; import dbdr.domain.institution.entity.Institution; import dbdr.domain.institution.service.InstitutionService; import dbdr.global.exception.ApplicationError; @@ -16,6 +17,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.time.LocalTime; import java.util.List; @Service @@ -24,6 +26,7 @@ public class CareworkerService { private final CareworkerRepository careworkerRepository; private final InstitutionService institutionService; + private final AlarmService alarmService; private final CareworkerMapper careworkerMapper; @Transactional(readOnly = true) @@ -73,6 +76,8 @@ public CareworkerResponse createCareworker(CareworkerRequest careworkerRequestDT Careworker careworker = careworkerMapper.toEntity(careworkerRequestDTO); careworkerRepository.save(careworker); + alarmService.createCareworkerAlarm(careworker); + return careworkerMapper.toResponse(careworker); } @@ -176,8 +181,8 @@ private void ensureUniqueEmailButNotId(String phone, Long id) { } } - public Careworker findByLineUserId(String userId) { - return careworkerRepository.findByLineUserId(userId).orElse(null); + public List findByAlertTime(LocalTime currentTime) { + return careworkerRepository.findByAlertTime(currentTime); } public Careworker findByPhone(String phoneNumber) { @@ -189,8 +194,17 @@ private CareworkerMyPageResponse toMyPageResponseDTO(Careworker careworker) { careworker.getName(), careworker.getPhone(), careworker.getInstitution().getInstitutionName(), - careworker.getWorkingDays(), - careworker.getAlertTime() + careworker.getAlertTime(), + careworker.getWorkingDays() ); } + + @Transactional + public void updateLineUserId(String userId, String phoneNumber) { + Careworker careworker = findByPhone(phoneNumber); + careworker.updateLineUserId(userId); + careworkerRepository.save(careworker); + } + + } diff --git a/src/main/java/dbdr/domain/chart/controller/CareWorkerChartController.java b/src/main/java/dbdr/domain/chart/controller/CareWorkerChartController.java index 12181b32..ed511193 100644 --- a/src/main/java/dbdr/domain/chart/controller/CareWorkerChartController.java +++ b/src/main/java/dbdr/domain/chart/controller/CareWorkerChartController.java @@ -26,7 +26,7 @@ import org.springframework.web.bind.annotation.RestController; // 요양사 권한 필요 -@Tag(name = "[요양관리사] 차트 관리", description = "요양관리사의 차트 조회, 추가, 수정, 삭제") +@Tag(name = "[요양보호사] 차트 관리", description = "요양보호사의 차트 조회, 추가, 수정, 삭제") @RestController @RequestMapping("/${spring.app.version}/careworker/chart") @RequiredArgsConstructor diff --git a/src/main/java/dbdr/domain/core/alarm/entity/Alarm.java b/src/main/java/dbdr/domain/core/alarm/entity/Alarm.java new file mode 100644 index 00000000..fae25645 --- /dev/null +++ b/src/main/java/dbdr/domain/core/alarm/entity/Alarm.java @@ -0,0 +1,74 @@ +package dbdr.domain.core.alarm.entity; + +import java.time.LocalDateTime; + +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SQLRestriction; + +import dbdr.domain.core.base.entity.BaseEntity; +import dbdr.domain.core.messaging.MessageChannel; +import dbdr.domain.core.messaging.Role; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Table(name = "alarms") +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@SQLDelete(sql = "UPDATE alarms SET is_active = false WHERE id = ?") +@SQLRestriction("is_active = true") +public class Alarm extends BaseEntity { + + @Column(nullable = false) + private LocalDateTime alertTime; + + @Column(nullable = false) + private String message; + + @Enumerated(EnumType.STRING) + @Column(nullable = true) + private MessageChannel channel; + + @Column(nullable = true) + private String channelId; + + @Column(nullable = false) + private String phone; + + @Column(nullable = true) + private boolean isSend = false; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private Role role; + + @Column(nullable = false) + private Long roleId; + + public Alarm(LocalDateTime alertTime, String message, String phone, Role role, Long roleId) { + this.alertTime = alertTime; + this.message = message; + this.phone = phone; + this.role = role; + this.roleId = roleId; + } + + public Alarm(LocalDateTime alertTime, MessageChannel channel, String channelId, String message, String phone, Role role, Long roleId) { + this.alertTime = alertTime; + this.channel = channel; + this.channelId = channelId; + this.message = message; + this.phone = phone; + this.role = role; + this.roleId = roleId; + } +} diff --git a/src/main/java/dbdr/domain/core/alarm/repository/AlarmRepository.java b/src/main/java/dbdr/domain/core/alarm/repository/AlarmRepository.java new file mode 100644 index 00000000..e0b0c558 --- /dev/null +++ b/src/main/java/dbdr/domain/core/alarm/repository/AlarmRepository.java @@ -0,0 +1,12 @@ +package dbdr.domain.core.alarm.repository; + +import java.time.LocalDateTime; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import dbdr.domain.core.alarm.entity.Alarm; + +public interface AlarmRepository extends JpaRepository { + Optional findByPhone(String phone); + + Optional findByPhoneAndAlertTime(String phone, LocalDateTime alertTime); +} diff --git a/src/main/java/dbdr/domain/core/alarm/service/AlarmService.java b/src/main/java/dbdr/domain/core/alarm/service/AlarmService.java new file mode 100644 index 00000000..9062a07a --- /dev/null +++ b/src/main/java/dbdr/domain/core/alarm/service/AlarmService.java @@ -0,0 +1,132 @@ +package dbdr.domain.core.alarm.service; + +import java.time.DayOfWeek; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.temporal.TemporalAdjusters; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import dbdr.domain.careworker.entity.Careworker; +import dbdr.domain.core.alarm.entity.Alarm; +import dbdr.domain.core.messaging.MessageChannel; +import dbdr.domain.core.messaging.MessageTemplate; +import dbdr.domain.core.messaging.Role; +import dbdr.domain.core.messaging.dto.SqsMessageDto; +import dbdr.domain.core.alarm.repository.AlarmRepository; +import dbdr.domain.core.messaging.service.CallSqsService; +import dbdr.domain.guardian.entity.Guardian; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Service +@RequiredArgsConstructor +public class AlarmService { + private final AlarmRepository alarmRepository; + private final CallSqsService callSqsService; + + @Transactional + public void createCareworkerAlarm(Careworker careworker) { + Alarm alarm = new Alarm( + LocalDateTime.now().with(LocalTime.of(17, 0)), // 오늘 17:00으로 설정 + MessageTemplate.CAREWORKER_ALARM_MESSAGE.getTemplate(), + careworker.getPhone(), + Role.CAREWORKER, + careworker.getId() + ); + + alarmRepository.save(alarm); + } + + @Transactional + public void createCareworkerNextWorkingdayAlarm(Careworker careworker) { + LocalDateTime currentDateTime = LocalDateTime.now(); + DayOfWeek currentDay = currentDateTime.getDayOfWeek(); + Alarm currentAlarm = getAlarmByPhone(careworker.getPhone()); + DayOfWeek nextWorkDay = careworker.getNextWorkingDay(currentDay); // 다음 근무일 계산 + if (nextWorkDay != null) { + LocalDateTime nextAlertTime = LocalDateTime.of( + currentDateTime.with(TemporalAdjusters.next(nextWorkDay)).toLocalDate(), + careworker.getAlertTime() + ); + Alarm alarm = new Alarm( + nextAlertTime, + currentAlarm.getChannel(), + currentAlarm.getChannelId(), + MessageTemplate.CAREWORKER_ALARM_MESSAGE.getTemplate(), + careworker.getPhone(), + Role.CAREWORKER, + careworker.getId() + ); + alarmRepository.save(alarm); + } else { + log.warn("{} 요양보호사의 다음 근무일이 지정되지 않았습니다.", careworker.getName()); + } + } + + @Transactional + public void createGuardianAlarm(Guardian guardian) { + Alarm alarm = new Alarm( + LocalDateTime.now().with(LocalTime.of(9, 0)), // 오늘 09:00으로 설정 + MessageTemplate.NO_CHART_MESSAGE.getTemplate(), + guardian.getPhone(), + Role.GUARDIAN, + guardian.getId() + ); + + alarmRepository.save(alarm); + } + + // 내일 알림 생성 + @Transactional + public void createGuardianNextDayAlarm(Guardian guardian) { + LocalDateTime currentDateTime = LocalDateTime.now(); + DayOfWeek currentDay = currentDateTime.getDayOfWeek(); + Alarm currentAlarm = getAlarmByPhone(guardian.getPhone()); + DayOfWeek nextDay = currentDay.plus(1); // 다음 날 계산 + LocalDateTime nextAlertTime = LocalDateTime.of( + currentDateTime.with(TemporalAdjusters.next(nextDay)).toLocalDate(), + guardian.getAlertTime() + ); + Alarm alarm = new Alarm( + nextAlertTime, + currentAlarm.getChannel(), + currentAlarm.getChannelId(), + MessageTemplate.NO_CHART_MESSAGE.getTemplate(), + guardian.getPhone(), + Role.GUARDIAN, + guardian.getId() + ); + alarmRepository.save(alarm); + } + + @Transactional(readOnly = true) + public Alarm getAlarmByPhoneAndAlertTime(String phone, LocalDateTime alertTime) { + return alarmRepository.findByPhoneAndAlertTime(phone, alertTime).orElse(null); + } + + @Transactional(readOnly = true) + public Alarm getAlarmByPhone(String phone) { + return alarmRepository.findByPhone(phone).orElse(null); + } + + @Transactional + public void sendAlarmToSqs(Alarm alarm, String lineUserId, String name) { + String alarmMessage = String.format(alarm.getMessage(), name); + callSqsService.sendMessage(new SqsMessageDto(lineUserId, alarmMessage)); + alarm.setSend(true); // 메시지 전송 상태 업데이트 + alarmRepository.save(alarm); + } + + @Transactional + public void updateNewLineUser(String phone, String lineUserId) { + Alarm alarm = alarmRepository.findByPhone(phone).orElse(null); + if (alarm != null) { + alarm.setChannel(MessageChannel.LINE); + alarm.setChannelId(lineUserId); + alarmRepository.save(alarm); + } + } +} diff --git a/src/main/java/dbdr/domain/core/messaging/MessageChannel.java b/src/main/java/dbdr/domain/core/messaging/MessageChannel.java new file mode 100644 index 00000000..3f50f548 --- /dev/null +++ b/src/main/java/dbdr/domain/core/messaging/MessageChannel.java @@ -0,0 +1,7 @@ +package dbdr.domain.core.messaging; + +public enum MessageChannel { + SMS, + LINE, + KAKAO +} diff --git a/src/main/java/dbdr/domain/core/messaging/MessageTemplate.java b/src/main/java/dbdr/domain/core/messaging/MessageTemplate.java new file mode 100644 index 00000000..d038c4a7 --- /dev/null +++ b/src/main/java/dbdr/domain/core/messaging/MessageTemplate.java @@ -0,0 +1,27 @@ +package dbdr.domain.core.messaging; + +public enum MessageTemplate { + + FOLLOW_MESSAGE("안녕하세요! 🌸\n 최고의 요양원 서비스 돌봄다리입니다. 🤗\n 서비스를 시작하려면 전화번호를 다음과 같은 형식으로 입력해주시기 바랍니다. 😄\n 예시 : 01012345678"), + STRANGER_FOLLOW_MESSAGE("%s님, 안녕하세요! 🌸\n 저희 서비스는 보호자와 요양보호사를 위한 서비스입니다. \n 회원가입을 통해 이용해주시기 바랍니다. 😅"), + INVALID_PHONE_INPUT_MESSAGE("전화번호 입력값이 잘못되었습니다! 😅\n 다시 입력해주세요. 예시 : 01012345678💬"), + RESERVATION_CONFIRMATION_MESSAGE("감사합니다! 😊\n 입력하신 시간 %s %s시%s에 알림을 보내드릴게요. 💬\n 언제든지 알림 시간을 변경하고 싶으시면 다시 알려주세요!"), + CAREWORKER_WELCOME_MESSAGE("%s 요양보호사님, 안녕하세요! 🌸\n 최고의 요양원 서비스 돌봄다리입니다. 🤗\n 저희와 함께 해주셔서 정말 감사합니다! 🙏\n 기본적인 알림 시간은 매일 오후 5시로 설정되어있습니다. 😄\n 돌봄다리 서비스의 마이페이지에서 알림 시간을 수정할 수 있습니다."), + GUARDIAN_WELCOME_MESSAGE("%s 보호자님, 안녕하세요! 🌸\n 최고의 요양원 서비스 돌봄다리입니다. 🤗\n 저희와 함께 해주셔서 정말 감사합니다! 🙏\n 새롭게 작성된 일지 내용을 원하시는 시간에 맞춰 알려드릴 수 있어요. ⏰\n 기본적인 알림 시간은 매일 오전 9시로 설정되어있습니다. 😄\n 돌봄다리 서비스의 마이페이지에서 알림 시간을 수정할 수 있습니다.\""), + NO_CHART_MESSAGE("새롭게 작성된 차트 내용이 없습니다! 😅"), + CAREWORKER_ALARM_MESSAGE("%s 요양보호사님, 오늘 하루는 어떠셨나요? 😊\n이제 차트를 작성하실 시간입니다. 잊지 마시고 차트 작성 부탁드립니다! 📝"); + + private final String template; + + MessageTemplate(String template) { + this.template = template; + } + + public String getTemplate() { + return template; + } + + public String format(Object... args) { + return String.format(template, args); + } +} diff --git a/src/main/java/dbdr/domain/core/messaging/Role.java b/src/main/java/dbdr/domain/core/messaging/Role.java new file mode 100644 index 00000000..e4bda447 --- /dev/null +++ b/src/main/java/dbdr/domain/core/messaging/Role.java @@ -0,0 +1,6 @@ +package dbdr.domain.core.messaging; + +public enum Role { + CAREWORKER, + GUARDIAN +} diff --git a/src/main/java/dbdr/domain/core/messaging/controller/LineMessagingController.java b/src/main/java/dbdr/domain/core/messaging/controller/LineController.java similarity index 67% rename from src/main/java/dbdr/domain/core/messaging/controller/LineMessagingController.java rename to src/main/java/dbdr/domain/core/messaging/controller/LineController.java index ab6cce27..72e99108 100644 --- a/src/main/java/dbdr/domain/core/messaging/controller/LineMessagingController.java +++ b/src/main/java/dbdr/domain/core/messaging/controller/LineController.java @@ -5,17 +5,17 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import dbdr.domain.core.messaging.service.LineMessagingService; +import dbdr.domain.core.messaging.service.LineService; import lombok.RequiredArgsConstructor; @RestController @RequestMapping("/line") @RequiredArgsConstructor -public class LineMessagingController { - private final LineMessagingService lineMessagingService; +public class LineController { + private final LineService lineMessagingService; @PostMapping - public void handleLineEvent(@RequestBody String requestBody) { + public void lineEvent(@RequestBody String requestBody) { lineMessagingService.handleLineEvent(requestBody); } } diff --git a/src/main/java/dbdr/domain/core/messaging/controller/SmsController.java b/src/main/java/dbdr/domain/core/messaging/controller/SmsController.java new file mode 100644 index 00000000..616bab7e --- /dev/null +++ b/src/main/java/dbdr/domain/core/messaging/controller/SmsController.java @@ -0,0 +1,24 @@ +package dbdr.domain.core.messaging.controller; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import dbdr.domain.core.messaging.service.SmsService; +import lombok.RequiredArgsConstructor; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/sms") +public class SmsController { + private final SmsService smsService; + + @GetMapping("/send-sms/{to}/{message}") + public void sendSms( + @PathVariable("to") String to, + @PathVariable("message") String message + ) { + smsService.sendSms(to, message); + } +} diff --git a/src/main/java/dbdr/domain/core/messaging/dto/SqsMessageDto.java b/src/main/java/dbdr/domain/core/messaging/dto/SqsMessageDto.java new file mode 100644 index 00000000..534699f5 --- /dev/null +++ b/src/main/java/dbdr/domain/core/messaging/dto/SqsMessageDto.java @@ -0,0 +1,13 @@ +package dbdr.domain.core.messaging.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class SqsMessageDto { + private String userId; + private String message; +} diff --git a/src/main/java/dbdr/domain/core/messaging/service/CallSqsService.java b/src/main/java/dbdr/domain/core/messaging/service/CallSqsService.java new file mode 100644 index 00000000..6ab9ecb5 --- /dev/null +++ b/src/main/java/dbdr/domain/core/messaging/service/CallSqsService.java @@ -0,0 +1,55 @@ +package dbdr.domain.core.messaging.service; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import dbdr.domain.core.messaging.dto.SqsMessageDto; +import io.awspring.cloud.sqs.annotation.SqsListener; +import io.awspring.cloud.sqs.operations.SendResult; +import io.awspring.cloud.sqs.operations.SqsTemplate; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Service +@RequiredArgsConstructor +public class CallSqsService { + + private final SqsTemplate queueMessagingTemplate; + private final LineMessagingService lineMessagingService; + private final ObjectMapper objectMapper; // JSON 변환용 ObjectMapper + + @Value("${cloud.aws.sqs.queue-name}") + private String QUEUE_NAME; + + // SqsMessageDto를 받아 JSON 형식으로 변환해 SQS에 전송 + public SendResult sendMessage(SqsMessageDto messageDto) { + try { + String messageJson = objectMapper.writeValueAsString(messageDto); + log.info("Sending message to SQS: {}", messageJson); + return queueMessagingTemplate.send(to -> to + .queue(QUEUE_NAME) + .payload(messageJson)); + } catch (Exception e) { + log.error("Failed to send message to SQS", e); + throw new RuntimeException("Failed to send message", e); + } + } + + // SQS에서 메시지를 수신하고 사용자에게 전달 + @SqsListener("${cloud.aws.sqs.queue-name}") + public void receiveMessage(String messageJson) { + try { + // JSON을 SqsMessageDto 객체로 변환 + SqsMessageDto messageDto = objectMapper.readValue(messageJson, SqsMessageDto.class); + log.info("Received message from SQS: {}", messageDto); + + // LineMessagingService를 통해 사용자에게 메시지 전송 + lineMessagingService.pushAlarmMessage(messageDto.getUserId(), messageDto.getMessage()); + } catch (Exception e) { + log.error("Failed to process message from SQS", e); + } + } +} diff --git a/src/main/java/dbdr/domain/core/messaging/service/LineMessagingService.java b/src/main/java/dbdr/domain/core/messaging/service/LineMessagingService.java index d046f27b..756b0a24 100644 --- a/src/main/java/dbdr/domain/core/messaging/service/LineMessagingService.java +++ b/src/main/java/dbdr/domain/core/messaging/service/LineMessagingService.java @@ -1,147 +1,49 @@ package dbdr.domain.core.messaging.service; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.linecorp.bot.model.event.FollowEvent; -import com.linecorp.bot.model.event.MessageEvent; -import com.linecorp.bot.model.event.message.TextMessageContent; +import com.linecorp.bot.client.LineMessagingClient; +import com.linecorp.bot.model.PushMessage; +import com.linecorp.bot.model.message.TextMessage; -import dbdr.domain.careworker.service.CareworkerMessagingService; -import dbdr.domain.careworker.service.CareworkerService; -import dbdr.domain.guardian.service.GuardianMessagingService; -import dbdr.domain.guardian.service.GuardianService; import dbdr.global.exception.ApplicationError; import dbdr.global.exception.ApplicationException; -import dbdr.global.util.line.LineMessagingUtil; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @Service @RequiredArgsConstructor @Slf4j -public class LineMessagingService { +public class LineMessagingService implements MessagingService{ + private final LineMessagingClient lineMessagingClient; - private final ObjectMapper objectMapper; - private final GuardianMessagingService guardianMessagingService; - private final CareworkerMessagingService careworkerMessagingService; - private final GuardianService guardianService; - private final CareworkerService careworkerService; - private final LineMessagingUtil lineMessagingUtil; + // 사용자에게 메시지를 보내는 메서드 + @Override + public void sendMessageToUser(String userId, String message) { + TextMessage textMessage = new TextMessage(message); + PushMessage pushMessage = new PushMessage(userId, textMessage); - @Transactional - public void handleLineEvent(String requestBody) { try { - JsonNode rootNode = objectMapper.readTree(requestBody); - JsonNode eventsNode = rootNode.get("events"); - - if (eventsNode != null && eventsNode.isArray()) { - for (JsonNode eventNode : eventsNode) { - String eventType = eventNode.get("type").asText(); - - switch (eventType) { - case "follow": - FollowEvent followEvent = objectMapper.treeToValue(eventNode, FollowEvent.class); - handleFollowEvent(followEvent); - break; - case "message": - MessageEvent messageEvent = objectMapper.treeToValue(eventNode, MessageEvent.class); - handleMessageEvent(messageEvent); - break; - default: - throw new ApplicationException(ApplicationError.CANNOT_FIND_EVENT); - } - } - } else { - throw new ApplicationException(ApplicationError.EVENT_ARRAY_NOT_FOUND); - } + lineMessagingClient.pushMessage(pushMessage).get(); + log.info("Message sent successfully to user: {}", userId); } catch (Exception e) { - log.error("Error processing Line event : {}", e.getMessage()); - throw new ApplicationException(ApplicationError.EVENT_ERROR); - } - } - - // 1. Follow Event 처리 - // 사용자가 라인 채널을 추가하였을 때 발생하는 이벤트 처리 - @Transactional - public void handleFollowEvent(FollowEvent event) { - String userId = event.getSource().getUserId(); - String followMessage = "안녕하세요! 🌸\n" + - " 최고의 요양원 서비스 돌봄다리입니다. 🤗\n" + - " 서비스를 시작하려면 전화번호를 다음과 같은 형식으로 입력해주시기 바랍니다. 😄\n" + - " 예 : 01012345678"; - lineMessagingUtil.sendMessageToUser(userId, followMessage); - } - - private void sendStrangerFollowMessage(String userId, String userName) { - String welcomeMessage = - " " + userName + "님, 안녕하세요! 🌸\n" + - " 저희 서비스는 보호자와 요양보호사를 위한 서비스입니다. \n" + - " 회원가입을 통해 이용해주시기 바랍니다. 😅"; - lineMessagingUtil.sendMessageToUser(userId, welcomeMessage); - } - - // 2. Message Event 처리 - @Transactional - public void handleMessageEvent(MessageEvent event) { - String userId = event.getSource().getUserId(); - String messageText = event.getMessage().getText(); - - // 전화번호 형식인지 확인 - Pattern phoneNumber = Pattern.compile("01[0-9]{8,9}"); - Matcher matcherPhone = phoneNumber.matcher(messageText); - - // 알림 예약 형식인지 확인 - Pattern reservation = Pattern.compile("(오전|오후)\\s*(\\d{1,2})시(?:\\s*(\\d{1,2})분)?"); - Matcher matcherReservation = reservation.matcher(messageText); - - if (matcherPhone.find()) { - handlePhoneNumberMessage(userId, matcherPhone.group()); - } else if (matcherReservation.find()) { - handleReservationMessage(userId, matcherReservation.group(1), matcherReservation.group(2), matcherReservation.group(3)); - } else { - String errorMessage = - " 입력값이 잘못되었습니다! 😅\n" + - " 다시 입력해주세요. 💬\n"; - lineMessagingUtil.sendMessageToUser(userId, errorMessage); - } - } - - - // 사용자가 전화 번호를 입력했을 때 발생하는 이벤트 처리 - private void handlePhoneNumberMessage(String userId, String phoneNumber) { - String userName = lineMessagingUtil.getUserProfile(userId).getDisplayName(); - - if (guardianService.findByPhone(phoneNumber) != null) { - guardianMessagingService.handleGuardianPhoneMessage(userId, phoneNumber); - } else if (careworkerService.findByPhone(phoneNumber) != null) { - careworkerMessagingService.handleCareworkerPhoneMessage(userId, phoneNumber); - } else { - sendStrangerFollowMessage(userId, userName); + log.error("Failed to send message to user: {}", userId, e); + throw new ApplicationException(ApplicationError.MESSAGE_SEND_FAILED); } } - // 사용자가 알림 예약 메시지를 보냈을 때 발생하는 이벤트 처리 - @Transactional - public void handleReservationMessage(String userId, String ampm, String hour, String minute) { - String confirmationMessage = - " 감사합니다! 😊\n" + - " 입력하신 시간 " + ampm + " " + hour + "시" + (minute != null ? " " + minute + "분" : "") + "에 알림을 보내드릴게요. 💬\n" + - " 언제든지 알림 시간을 변경하고 싶으시면 다시 알려주세요!"; + // 사용자에게 알람 메시지를 보내는 메서드 + public void pushAlarmMessage(String userId, String message) { + TextMessage textMessage = new TextMessage(message); + PushMessage pushMessage = new PushMessage(userId, textMessage); - if (guardianService.findByLineUserId(userId) != null) { - lineMessagingUtil.sendMessageToUser(userId, confirmationMessage); - guardianMessagingService.updateGuardianAlertTime(userId, ampm, hour, minute); - } else if (careworkerService.findByLineUserId(userId) != null) { - lineMessagingUtil.sendMessageToUser(userId, confirmationMessage); - careworkerMessagingService.updateCareworkerAlertTime(userId, ampm, hour, minute); - } else { - lineMessagingUtil.userFoundFailedMessage(userId); + try { + lineMessagingClient.pushMessage(pushMessage).get(); + log.info("Message sent successfully to user: {}", lineMessagingClient.getProfile(userId).get().getDisplayName()); + } catch (Exception e) { + log.error("Failed to send message to user: {}", userId, e); + throw new ApplicationException(ApplicationError.MESSAGE_SEND_FAILED); } } } diff --git a/src/main/java/dbdr/domain/core/messaging/service/LineService.java b/src/main/java/dbdr/domain/core/messaging/service/LineService.java new file mode 100644 index 00000000..0260e01f --- /dev/null +++ b/src/main/java/dbdr/domain/core/messaging/service/LineService.java @@ -0,0 +1,119 @@ +package dbdr.domain.core.messaging.service; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.linecorp.bot.client.LineMessagingClient; +import com.linecorp.bot.model.event.FollowEvent; +import com.linecorp.bot.model.event.MessageEvent; +import com.linecorp.bot.model.event.message.TextMessageContent; +import com.linecorp.bot.model.profile.UserProfileResponse; + +import dbdr.domain.core.alarm.service.AlarmService; +import dbdr.domain.careworker.service.CareworkerService; +import dbdr.domain.core.messaging.MessageTemplate; +import dbdr.domain.guardian.service.GuardianService; +import dbdr.global.exception.ApplicationError; +import dbdr.global.exception.ApplicationException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Service +@RequiredArgsConstructor +@Slf4j +public class LineService { + private final LineMessagingClient lineMessagingClient; + private final ObjectMapper objectMapper; + private final GuardianService guardianService; + private final CareworkerService careworkerService; + private final LineMessagingService lineMessagingService; + private final AlarmService alarmService; + + // 0. Line Event 처리 + @Transactional + public void handleLineEvent(String requestBody) { + try { + JsonNode rootNode = objectMapper.readTree(requestBody); + JsonNode eventsNode = rootNode.get("events"); + if (eventsNode != null && eventsNode.isArray()) { + for (JsonNode eventNode : eventsNode) { + String eventType = eventNode.get("type").asText(); + switch (eventType) { + case "follow": // 사용자가 라인 채널을 추가하였을 때 발생하는 이벤트 + FollowEvent followEvent = objectMapper.treeToValue(eventNode, FollowEvent.class); + handleFollowEvent(followEvent); + break; + case "message": // 사용자가 라인 채널에 메시지를 보냈을 때 발생하는 이벤트 + MessageEvent messageEvent = objectMapper.treeToValue(eventNode, MessageEvent.class); + handleMessageEvent(messageEvent); + break; + default: + throw new ApplicationException(ApplicationError.CANNOT_FIND_EVENT); + } + } + } else { + throw new ApplicationException(ApplicationError.EVENT_ARRAY_NOT_FOUND); + } + } catch (Exception e) { + log.error("Error processing Line event : {}", e.getMessage()); + throw new ApplicationException(ApplicationError.EVENT_ERROR); + } + } + + // 1. Follow Event 처리 + // 사용자가 라인 채널을 추가하였을 때 웰컴 메시지 전송 + @Transactional + public void handleFollowEvent(FollowEvent event) { + String userId = event.getSource().getUserId(); + lineMessagingService.sendMessageToUser(userId, MessageTemplate.FOLLOW_MESSAGE.getTemplate()); + } + + // 2. Message Event 처리 + // 사용자가 라인 채널에 메시지를 보냈을 때 발생하는 이벤트 처리 + @Transactional + public void handleMessageEvent(MessageEvent event) { + String userId = event.getSource().getUserId(); + String messageText = event.getMessage().getText(); + Pattern phoneNumber = Pattern.compile("01[0-9]{8,9}"); // 전화번호 정규식 + Matcher matcherPhone = phoneNumber.matcher(messageText); + + if (matcherPhone.find()) { + receivePhoneNumber(userId, matcherPhone.group()); + } else { + lineMessagingService.sendMessageToUser(userId, MessageTemplate.INVALID_PHONE_INPUT_MESSAGE.getTemplate()); + } + } + + // 사용자가 전화 번호를 입력했을 때 발생하는 이벤트 처리 + private void receivePhoneNumber(String userId, String phoneNumber) { + String userName = getProfile(userId).getDisplayName(); + + if (guardianService.findByPhone(phoneNumber) != null) { + guardianService.updateLineUserId(userId, phoneNumber); + alarmService.updateNewLineUser(phoneNumber, userId); + lineMessagingService.sendMessageToUser(userId, MessageTemplate.GUARDIAN_WELCOME_MESSAGE.format(userName)); + } else if (careworkerService.findByPhone(phoneNumber) != null) { + careworkerService.updateLineUserId(userId, phoneNumber); + alarmService.updateNewLineUser(phoneNumber, userId); + lineMessagingService.sendMessageToUser(userId, MessageTemplate.CAREWORKER_WELCOME_MESSAGE.format(userName)); + } else { + // 보호자나 요양보호사가 아닌 경우 + lineMessagingService.sendMessageToUser(userId, MessageTemplate.STRANGER_FOLLOW_MESSAGE.format(userName)); + } + } + + // UserId를 통해 라인 사용자 프로필 정보 가져오는 메서드 + public UserProfileResponse getProfile(String userId) { + try { + return lineMessagingClient.getProfile(userId).get(); + } catch (Exception e) { + log.error("Failed to get user profile: {}", userId, e); + throw new ApplicationException(ApplicationError.FAILED_TO_GET_USER_PROFILE); + } + } +} diff --git a/src/main/java/dbdr/domain/core/messaging/service/MessagingService.java b/src/main/java/dbdr/domain/core/messaging/service/MessagingService.java new file mode 100644 index 00000000..8ea138e6 --- /dev/null +++ b/src/main/java/dbdr/domain/core/messaging/service/MessagingService.java @@ -0,0 +1,5 @@ +package dbdr.domain.core.messaging.service; + +public interface MessagingService { + void sendMessageToUser(String userId, String message); +} diff --git a/src/main/java/dbdr/domain/core/messaging/service/SmsMessagingService.java b/src/main/java/dbdr/domain/core/messaging/service/SmsMessagingService.java new file mode 100644 index 00000000..d0658d27 --- /dev/null +++ b/src/main/java/dbdr/domain/core/messaging/service/SmsMessagingService.java @@ -0,0 +1,39 @@ +package dbdr.domain.core.messaging.service; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import net.nurigo.sdk.NurigoApp; +import net.nurigo.sdk.message.model.Message; +import net.nurigo.sdk.message.request.SingleMessageSendingRequest; +import net.nurigo.sdk.message.response.SingleMessageSentResponse; +import net.nurigo.sdk.message.service.DefaultMessageService; + + +@Service +public class SmsMessagingService implements MessagingService { + private final DefaultMessageService defaultMessageService; + + @Value("${sms.from-number}") + private String fromNumber; + + public SmsMessagingService(@Value("${sms.api-key}") String apiKey, + @Value("${sms.api-secret}") String apiSecret, + @Value("${sms.domain}") String domain) { + this.defaultMessageService = NurigoApp.INSTANCE.initialize(apiKey, apiSecret, domain); + } + + @Override + public void sendMessageToUser(String userNumber, String message) { + Message smsMessage = new Message(); + smsMessage.setFrom(fromNumber); + smsMessage.setTo(userNumber); + smsMessage.setText(message); + + SingleMessageSentResponse response = defaultMessageService.sendOne(new SingleMessageSendingRequest(smsMessage)); + String statusCode = response.getStatusCode(); + if (!statusCode.equals("2000")) { + throw new RuntimeException("Failed to send message"); + } + } +} diff --git a/src/main/java/dbdr/domain/core/messaging/service/SmsService.java b/src/main/java/dbdr/domain/core/messaging/service/SmsService.java new file mode 100644 index 00000000..80f4fd5e --- /dev/null +++ b/src/main/java/dbdr/domain/core/messaging/service/SmsService.java @@ -0,0 +1,18 @@ +package dbdr.domain.core.messaging.service; + +import org.springframework.stereotype.Service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Service +@RequiredArgsConstructor +@Slf4j +public class SmsService { + private final SmsMessagingService smsMessagingService; + + public void sendSms(String to, String message) { + log.info("Sending SMS to {} with message: {}", to, message); + smsMessagingService.sendMessageToUser(to, message); + } +} diff --git a/src/main/java/dbdr/domain/core/messaging/util/MessagingScheduler.java b/src/main/java/dbdr/domain/core/messaging/util/MessagingScheduler.java new file mode 100644 index 00000000..4af1d4fd --- /dev/null +++ b/src/main/java/dbdr/domain/core/messaging/util/MessagingScheduler.java @@ -0,0 +1,64 @@ +package dbdr.domain.core.messaging.util; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.List; + +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import dbdr.domain.careworker.entity.Careworker; +import dbdr.domain.careworker.service.CareworkerService; +import dbdr.domain.core.messaging.MessageChannel; +import dbdr.domain.core.alarm.entity.Alarm; +import dbdr.domain.core.alarm.service.AlarmService; +import dbdr.domain.guardian.entity.Guardian; +import dbdr.domain.guardian.service.GuardianService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Component +@RequiredArgsConstructor +@Slf4j +public class MessagingScheduler { + private final GuardianService guardianService; + private final CareworkerService careworkerService; + private final AlarmService alarmService; + + @Scheduled(cron = "0 0/1 * * * ?") + public void sendChartUpdate() { + // 초와 나노초를 제거하고 분 단위로 비교하기 위해 현재 시간을 가져옴 + LocalTime currentTime = LocalTime.now().withSecond(0).withNano(0); + LocalDateTime currentDateTime = LocalDateTime.now().withSecond(0).withNano(0); + + // DB에서 알림 시간을 설정한 사용자들을 조회합니다. + List guardians = guardianService.findByAlertTime(currentTime); + List careworkers = careworkerService.findByAlertTime(currentTime); + + // 보호자에게 알람 메시지를 SQS로 전송합니다. + for (Guardian guardian : guardians) { + String phone = guardian.getPhone(); + LocalDateTime alertTime = LocalDateTime.of(LocalDate.now(), currentTime); + Alarm alarm = alarmService.getAlarmByPhoneAndAlertTime(phone, alertTime); + String name = guardian.getName(); + if (alarm != null && alarm.getChannel().equals(MessageChannel.LINE)) { + log.info("알림 보낼 보호자 : {}", name); + alarmService.sendAlarmToSqs(alarm, alarm.getChannelId(), name); + alarmService.createGuardianNextDayAlarm(guardian); + } + } + + // 요양보호사에게 알람 메시지를 SQS로 전송합니다. + for (Careworker careworker : careworkers) { + String phone = careworker.getPhone(); + Alarm alarm = alarmService.getAlarmByPhoneAndAlertTime(phone, currentDateTime); + String name = careworker.getName(); + if (alarm != null && alarm.getChannel().equals(MessageChannel.LINE) && !alarm.isSend()) { + log.info("알림 보낼 요양보호사 : {}", name); + alarmService.sendAlarmToSqs(alarm, alarm.getChannelId(), name); + alarmService.createCareworkerNextWorkingdayAlarm(careworker); + } + } + } +} diff --git a/src/main/java/dbdr/domain/core/ocr/controller/OcrController.java b/src/main/java/dbdr/domain/core/ocr/controller/OcrController.java new file mode 100644 index 00000000..f821b990 --- /dev/null +++ b/src/main/java/dbdr/domain/core/ocr/controller/OcrController.java @@ -0,0 +1,44 @@ +package dbdr.domain.core.ocr.controller; + +import java.net.URL; + +import dbdr.domain.core.ocr.service.OcrService; +import dbdr.domain.core.s3.service.S3Service; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +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 reactor.core.publisher.Mono; + +@Tag(name = "[요양보호사] OCR로 차트 작성", description = "Naver Clova OCR을 통해 표 이미지에서 텍스트 추출하기") +@RestController +@Slf4j +@RequiredArgsConstructor +@RequestMapping("/v1/ocr/chart") +public class OcrController { + + private final OcrService ocrService; + private final S3Service s3Service; + + @Operation(summary = "OCR 수행 후 추출된 텍스트 반환", description = "S3에 업로드된 표 이미지를 가지고 OCR을 수행합니다.") + @GetMapping("/perform") + public ResponseEntity performOcr(@RequestParam String objectKey) { + try { + // S3에서 이미지 URL 가져오기 + URL imageUrl = s3Service.getS3FileUrl(objectKey); + + // 클로바 OCR API 호출하여 텍스트 추출 + return ocrService.performOcr(imageUrl, objectKey) + .map(result -> ResponseEntity.ok(result)) // 성공 시 OCR 결과 반환 + .block(); // Mono를 동기식으로 반환하여 ResponseEntity 타입을 유지 + } catch (Exception e) { + log.error("이미지 URL 가져오기 실패: {}", e.getMessage()); + return ResponseEntity.internalServerError().body("이미지 URL 가져오기 실패: " + e.getMessage()); + } + } +} diff --git a/src/main/java/dbdr/domain/core/ocr/entity/OcrData.java b/src/main/java/dbdr/domain/core/ocr/entity/OcrData.java new file mode 100644 index 00000000..59c25f73 --- /dev/null +++ b/src/main/java/dbdr/domain/core/ocr/entity/OcrData.java @@ -0,0 +1,31 @@ +package dbdr.domain.core.ocr.entity; + +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SQLRestriction; + +import dbdr.domain.core.base.entity.BaseEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Lob; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Table(name = "ocr_data") +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@SQLDelete(sql = "UPDATE ocr_data SET is_active = false WHERE id = ?") +@SQLRestriction("is_active = true") +public class OcrData extends BaseEntity { + @Column(nullable = false, unique = true) + private String objectKey; + + @Lob + @Column(nullable = true, columnDefinition = "TEXT") // 명시적으로 TEXT로 설정 + private String ocrResult; +} diff --git a/src/main/java/dbdr/domain/core/ocr/repository/OcrRepository.java b/src/main/java/dbdr/domain/core/ocr/repository/OcrRepository.java new file mode 100644 index 00000000..e848914e --- /dev/null +++ b/src/main/java/dbdr/domain/core/ocr/repository/OcrRepository.java @@ -0,0 +1,8 @@ +package dbdr.domain.core.ocr.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import dbdr.domain.core.ocr.entity.OcrData; + +public interface OcrRepository extends JpaRepository { + OcrData findByObjectKey(String objectKey); +} diff --git a/src/main/java/dbdr/domain/core/ocr/service/OcrService.java b/src/main/java/dbdr/domain/core/ocr/service/OcrService.java new file mode 100644 index 00000000..4fdbdf26 --- /dev/null +++ b/src/main/java/dbdr/domain/core/ocr/service/OcrService.java @@ -0,0 +1,107 @@ +package dbdr.domain.core.ocr.service; + +import dbdr.domain.core.ocr.entity.OcrData; +import dbdr.domain.core.ocr.repository.OcrRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.function.client.WebClientResponseException; +import reactor.core.publisher.Mono; + +import java.net.URL; +import java.util.Map; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +@Service +@Slf4j +@RequiredArgsConstructor +public class OcrService { + private final OcrRepository ocrRepository; + private final WebClient webClient = WebClient.builder().build(); + + @Value("${clova-ocr.api-url}") + private String apiUrl; + + @Value("${clova-ocr.secret-key}") + private String secretKey; + + // OCR 요청 메서드 + @Transactional + public Mono performOcr(URL imageUrl, String objectKey) { + return sendOcrRequest(imageUrl) + .flatMap(response -> { + String extractedText = extractTableText(response); + updateOcrData(objectKey, extractedText); + return Mono.just(extractedText); + }) + .doOnError(error -> log.error("OCR 요청 실패: {}", error.getMessage())) + .onErrorResume(WebClientResponseException.class, ex -> Mono.error(new RuntimeException("클로바 OCR 요청 실패: " + ex.getMessage()))); + } + + // 클로바 OCR API에 요청을 보내는 메서드 (TABLE 타입으로 고정) + private Mono sendOcrRequest(URL imageUrl) { + return webClient.post() + .uri(apiUrl) + .header("X-OCR-SECRET", secretKey) + .bodyValue(Map.of( + "version", "V2", + "requestId", "unique-request-id", + "timestamp", System.currentTimeMillis(), + "images", new Object[]{ + Map.of( + "format", "jpg", + "name", "sample", + "url", imageUrl.toString(), + "type", "TABLE" // 항상 TABLE 타입으로 설정 + ) + } + )) + .retrieve() + .bodyToMono(String.class); + } + + // JSON 응답에서 표 데이터를 추출하는 메서드 + private String extractTableText(String response) { + StringBuilder tableText = new StringBuilder(); + try { + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode root = objectMapper.readTree(response); + JsonNode fields = root.path("images").get(0).path("fields"); + + if (fields.isMissingNode() || fields.isEmpty()) { + log.warn("OCR 응답에 'fields' 데이터가 없습니다."); + return "데이터가 없습니다."; + } + + for (JsonNode field : fields) { + String inferText = field.path("inferText").asText(); + tableText.append(inferText).append(" "); // 텍스트 조각을 공백으로 구분하여 추가 + } + } catch (Exception e) { + log.error("데이터 추출 중 오류 발생: {}", e.getMessage()); + } + return tableText.toString().trim(); + } + + // OCR 데이터 저장 + @Transactional + public void createOcrDate(String objectKey) { + OcrData ocrData = new OcrData(); + ocrData.setObjectKey(objectKey); + ocrRepository.save(ocrData); + log.info("새로운 OCR 데이터 저장: {}", ocrData); + } + + // OCR 데이터 업데이트 + @Transactional + public void updateOcrData(String objectKey, String ocrResult) { + OcrData ocrData = ocrRepository.findByObjectKey(objectKey); + ocrData.setOcrResult(ocrResult); + ocrRepository.save(ocrData); + log.info("기존 OCR 데이터 업데이트: {}", ocrData); + } +} diff --git a/src/main/java/dbdr/domain/core/s3/controller/S3Controller.java b/src/main/java/dbdr/domain/core/s3/controller/S3Controller.java new file mode 100644 index 00000000..7a698ba7 --- /dev/null +++ b/src/main/java/dbdr/domain/core/s3/controller/S3Controller.java @@ -0,0 +1,76 @@ +package dbdr.domain.core.s3.controller; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import dbdr.domain.core.s3.service.S3Service; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.net.URL; + +@Tag(name = "[프론트엔드] Presgined URL", description = "이미지 업로드를 위한 Presigned URL 생성 및 이미지 URL 저장하는 API") +@RestController +@RequiredArgsConstructor +@RequestMapping("/v1/s3/chart") +public class S3Controller { + private final S3Service s3Service; + + // Presigned URL 생성 API + @Operation(summary = "Presigned URL 생성", description = "S3에 이미지 업로드를 위한 Presigned URL 생성") + @GetMapping("/generate-url") + public ResponseEntity generatePresignedUrl(@RequestParam String objectKey) { + URL presignedUrl = s3Service.generatePresignedUrl(objectKey); + return ResponseEntity.status(HttpStatus.OK).body(presignedUrl.toString()); + } + + // 프론트엔드 쪽에서 이미지 업로드 완료 후 키 값을 주면 DB에 저장하는 API + @Operation(summary = "이미지 URL DB에 저장", description = "S3에 업로드된 이미지 URL을 DB에 저장") + @PostMapping("/save-image-url") + public ResponseEntity saveImageUrl(@RequestParam String objectKey) { + try { + // S3에서 이미지 URL 가져오기 + URL imageUrl = s3Service.getS3FileUrl(objectKey); + // DB에 URL과 objectKey 저장 + s3Service.saveImageUrlToDatabase(objectKey); + return ResponseEntity.ok("이미지 URL 저장 완료하였습니다."); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("이미지 URL 저장 중 오류가 발생했습니다."); + } + } + + // test : Presigned URL을 이용한 파일 업로드 테스트 API + @PostMapping("/test-upload") + public String testUpload(@RequestParam String objectKey, @RequestParam("file") MultipartFile multipartFile) { + // MultipartFile을 File 객체로 변환 + File file = convertMultipartFileToFile(multipartFile); + if (file == null) { + return "파일 변환에 실패했습니다."; + } + + // S3에 업로드 + s3Service.uploadFileToS3(objectKey, file); + + // 임시 파일 삭제 + file.delete(); + + return "테스트 업로드 완료!"; + } + + private File convertMultipartFileToFile(MultipartFile file) { + File convFile = new File(System.getProperty("java.io.tmpdir") + "/" + file.getOriginalFilename()); + try (FileOutputStream fos = new FileOutputStream(convFile)) { + fos.write(file.getBytes()); + } catch (IOException e) { + e.printStackTrace(); + return null; + } + return convFile; + } +} diff --git a/src/main/java/dbdr/domain/core/s3/service/S3Service.java b/src/main/java/dbdr/domain/core/s3/service/S3Service.java new file mode 100644 index 00000000..7d2703de --- /dev/null +++ b/src/main/java/dbdr/domain/core/s3/service/S3Service.java @@ -0,0 +1,86 @@ +package dbdr.domain.core.s3.service; + +import com.amazonaws.HttpMethod; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.io.File; +import java.io.FileInputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.Date; + +import dbdr.domain.core.ocr.service.OcrService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Service +@Slf4j +@RequiredArgsConstructor +public class S3Service { + private final AmazonS3 amazonS3; + @Value("${cloud.aws.s3.bucket-name}") + private String bucketName; + private final OcrService ocrService; + + // Presigned URL 생성 메서드 + @Transactional + public URL generatePresignedUrl(String objectKey) { + Date expirationDate = new Date(System.currentTimeMillis() + 5 * 60 * 1000); // 만료 시간 5분 설정 (2분동안만 URL을 사용하여 파일 업로드 가능) + GeneratePresignedUrlRequest generatePresignedUrlRequest = + new GeneratePresignedUrlRequest(bucketName, objectKey) + .withMethod(HttpMethod.PUT) + .withExpiration(expirationDate); + return amazonS3.generatePresignedUrl(generatePresignedUrlRequest); + } + + public URL getS3FileUrl(String objectKey) { + return amazonS3.getUrl(bucketName, objectKey); // objectKey를 통해 S3에서 이미지 URL 가져오기 + } + + @Transactional + public void saveImageUrlToDatabase(String objectKey) { + ocrService.createOcrDate(objectKey); + } + + // test : Presigned URL을 이용해 S3에 파일 업로드 테스트 메서드 + public void uploadFileToS3(String objectKey, File file) { + try { + // Presigned URL 생성 + URL presignedUrl = generatePresignedUrl(objectKey); + + // HttpURLConnection을 사용해 파일 업로드 + HttpURLConnection connection = (HttpURLConnection) presignedUrl.openConnection(); + connection.setDoOutput(true); + connection.setRequestMethod("PUT"); + connection.setRequestProperty("Content-Type", "image/jpeg"); // 파일 타입 설정 + + // 파일을 Presigned URL로 전송 + try (OutputStream outputStream = connection.getOutputStream(); + FileInputStream inputStream = new FileInputStream(file)) { + byte[] buffer = new byte[4096]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, bytesRead); + } + outputStream.flush(); + } + + // 응답 확인 + int responseCode = connection.getResponseCode(); + if (responseCode == HttpURLConnection.HTTP_OK) { + System.out.println("파일이 성공적으로 S3에 업로드되었습니다."); + } else { + System.out.println("파일 업로드 실패. 응답 코드: " + responseCode); + } + + } catch (Exception e) { + e.printStackTrace(); + System.out.println("S3 업로드 중 오류가 발생했습니다."); + } + } +} diff --git a/src/main/java/dbdr/domain/excel/controller/ExcelController.java b/src/main/java/dbdr/domain/excel/controller/ExcelController.java index 23ef4da3..274f4a89 100644 --- a/src/main/java/dbdr/domain/excel/controller/ExcelController.java +++ b/src/main/java/dbdr/domain/excel/controller/ExcelController.java @@ -16,8 +16,7 @@ import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; -@Tag(name = "엑셀-요양보호사,보호자,돌봄대상자", description = "엑셀 다운로드와 업로드") -@RestController +@Tag(name = "엑셀 양식 다운로드 및 업로드", description = "요양 관리사, 보호자, 돌봄 대상자 정보를 엑셀 파일을 통해 대량으로 업로ㅇ할 수 있습니다.")@RestController @RequestMapping("/${spring.app.version}/excel") @RequiredArgsConstructor public class ExcelController { @@ -99,4 +98,4 @@ public ResponseEntity uploadRecipientData( RecipientFileUploadResponse result = excelUploadService.uploadRecipientExcel(file, institution.getId()); return ResponseEntity.ok(result); } -} \ No newline at end of file +} diff --git a/src/main/java/dbdr/domain/guardian/controller/GuardianController.java b/src/main/java/dbdr/domain/guardian/controller/GuardianController.java index 8cf17815..8c243ebe 100644 --- a/src/main/java/dbdr/domain/guardian/controller/GuardianController.java +++ b/src/main/java/dbdr/domain/guardian/controller/GuardianController.java @@ -20,7 +20,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -@Tag(name = "보호자 (Guardian)", description = "보호자 정보 조회, 수정") +@Tag(name = "[보호자]", description = "보호자 정보 조회, 수정") @RestController @RequestMapping("/${spring.app.version}/guardian") @RequiredArgsConstructor diff --git a/src/main/java/dbdr/domain/guardian/controller/GuardianInstitutionController.java b/src/main/java/dbdr/domain/guardian/controller/GuardianInstitutionController.java index 835a8b00..f78a1225 100644 --- a/src/main/java/dbdr/domain/guardian/controller/GuardianInstitutionController.java +++ b/src/main/java/dbdr/domain/guardian/controller/GuardianInstitutionController.java @@ -20,7 +20,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -@Tag(name = "[요양원관리자] 보호자 (Guardian)", description = "보호자 정보 조회, 추가, 수정, 삭제") +@Tag(name = "[요양원] 보호자 관리", description = "보호자 정보 조회, 추가, 수정, 삭제") @RestController @RequestMapping("/${spring.app.version}/institution/guardian") @RequiredArgsConstructor diff --git a/src/main/java/dbdr/domain/guardian/entity/Guardian.java b/src/main/java/dbdr/domain/guardian/entity/Guardian.java index 3f9ac986..7be1824f 100644 --- a/src/main/java/dbdr/domain/guardian/entity/Guardian.java +++ b/src/main/java/dbdr/domain/guardian/entity/Guardian.java @@ -43,7 +43,7 @@ public class Guardian extends BaseEntity { private String lineUserId; @Column(nullable = true) - private LocalTime alertTime; + private LocalTime alertTime = LocalTime.of(18, 0); // 오후 6시로 초기화 @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "institution_id") diff --git a/src/main/java/dbdr/domain/guardian/service/GuardianMessagingService.java b/src/main/java/dbdr/domain/guardian/service/GuardianMessagingService.java deleted file mode 100644 index bc9a3853..00000000 --- a/src/main/java/dbdr/domain/guardian/service/GuardianMessagingService.java +++ /dev/null @@ -1,50 +0,0 @@ -package dbdr.domain.guardian.service; - -import java.time.LocalTime; - -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import dbdr.domain.guardian.entity.Guardian; -import dbdr.domain.guardian.repository.GuardianRepository; -import dbdr.global.util.line.LineMessagingUtil; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -@Service -@RequiredArgsConstructor -@Slf4j -public class GuardianMessagingService { - - private final GuardianService guardianService; - private final GuardianRepository guardianRepository; - private final LineMessagingUtil lineMessagingUtil; - - @Transactional - public void handleGuardianPhoneMessage(String userId, String phoneNumber) { - Guardian guardian = guardianService.findByPhone(phoneNumber); - String userName = guardian.getName(); - guardian.updateLineUserId(userId); - guardianRepository.save(guardian); - - String welcomeMessage = - " " + userName + " 보호자님, 안녕하세요! 🌸\n" + - " 최고의 요양원 서비스 돌봄다리입니다. 🤗\n" + - " 저희와 함께 해주셔서 정말 감사합니다! 🙏\n" + - " 새롭게 작성된 일지 내용을 원하시는 시간에 맞춰 알려드릴 수 있어요. ⏰\n" + - " 기본적인 알림 시간은 매일 오전 9시로 설정되어있습니다. 😄\n" + - " 알림을 받고 싶은 시간을 수정하고 싶으시다면 알려주세요! 💬\n" + - " 예 : `오전 10시' 혹은 '오전 10시 30분'"; - - lineMessagingUtil.sendMessageToUser(userId, welcomeMessage); - } - - @Transactional - public void updateGuardianAlertTime(String userId, String ampm, String hour, String minute) { - Guardian guardian = guardianService.findByLineUserId(userId); - int minuteValue = (minute != null) ? Integer.parseInt(minute) : 0; // minute이 null이면 0으로 처리 - LocalTime alertTime = lineMessagingUtil.convertToLocalTime(ampm, Integer.parseInt(hour), minuteValue); - guardian.updateAlertTime(alertTime); - guardianRepository.save(guardian); - } -} diff --git a/src/main/java/dbdr/domain/guardian/service/GuardianService.java b/src/main/java/dbdr/domain/guardian/service/GuardianService.java index 07736f64..f3d57b79 100644 --- a/src/main/java/dbdr/domain/guardian/service/GuardianService.java +++ b/src/main/java/dbdr/domain/guardian/service/GuardianService.java @@ -1,5 +1,6 @@ package dbdr.domain.guardian.service; +import dbdr.domain.core.alarm.service.AlarmService; import dbdr.domain.guardian.dto.request.GuardianAlertTimeRequest; import dbdr.domain.guardian.dto.response.GuardianMyPageResponse; import dbdr.domain.guardian.entity.Guardian; @@ -10,6 +11,7 @@ import dbdr.global.exception.ApplicationException; import lombok.RequiredArgsConstructor; +import java.time.LocalTime; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.crypto.password.PasswordEncoder; @@ -21,6 +23,7 @@ public class GuardianService { private final GuardianRepository guardianRepository; + private final AlarmService alarmService; @Autowired PasswordEncoder passwordEncoder; @@ -80,6 +83,7 @@ public GuardianResponse addGuardian(GuardianRequest guardianRequest) { .loginPassword(password) .build(); guardian = guardianRepository.save(guardian); + alarmService.createGuardianAlarm(guardian); return new GuardianResponse(guardian.getPhone(), guardian.getName(), guardian.isActive()); } @@ -116,4 +120,15 @@ public Guardian findByPhone(String phone) { return guardianRepository.findByPhone(phone) .orElse(null); } + + @Transactional + public void updateLineUserId(String userId, String phoneNumber) { + Guardian guardian = findByPhone(phoneNumber); + guardian.updateLineUserId(userId); + guardianRepository.save(guardian); + } + + public List findByAlertTime(LocalTime currentTime) { + return guardianRepository.findByAlertTime(currentTime); + } } diff --git a/src/main/java/dbdr/domain/recipient/controller/RecipientCareworkerController.java b/src/main/java/dbdr/domain/recipient/controller/RecipientCareworkerController.java index 397c554b..dc15189d 100644 --- a/src/main/java/dbdr/domain/recipient/controller/RecipientCareworkerController.java +++ b/src/main/java/dbdr/domain/recipient/controller/RecipientCareworkerController.java @@ -7,6 +7,7 @@ import dbdr.global.util.api.ApiUtils; import dbdr.security.LoginCareworker; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; @@ -28,7 +29,7 @@ public class RecipientCareworkerController { @Operation(summary = "담당 돌봄대상자 전체 조회 ", security = @SecurityRequirement(name = "JWT")) @GetMapping public ResponseEntity>> getAllRecipients( - @LoginCareworker Careworker careworker) { + @Parameter(hidden = true) @LoginCareworker Careworker careworker) { List recipients = recipientService.getRecipientsByCareworker(careworker.getId()); return ResponseEntity.ok(ApiUtils.success(recipients)); } @@ -37,7 +38,7 @@ public ResponseEntity>> getAllRecipie @GetMapping("/{recipientId}") public ResponseEntity> getRecipientById( @PathVariable("recipientId") Long recipientId, - @LoginCareworker Careworker careworker) { + @Parameter(hidden = true) @LoginCareworker Careworker careworker) { RecipientResponse recipient = recipientService.getRecipientByCareworker(recipientId, careworker.getId()); return ResponseEntity.ok(ApiUtils.success(recipient)); } @@ -46,7 +47,7 @@ public ResponseEntity> getRecipientById( @PostMapping public ResponseEntity> createRecipient( @Valid @RequestBody RecipientRequest recipientDTO, - @LoginCareworker Careworker careworker) { + @Parameter(hidden = true) @LoginCareworker Careworker careworker) { RecipientResponse newRecipient = recipientService.createRecipientForCareworker(recipientDTO, careworker.getId()); return ResponseEntity.status(HttpStatus.CREATED).body(ApiUtils.success(newRecipient)); } @@ -55,7 +56,7 @@ public ResponseEntity> createRecipient( @PutMapping("/{recipientId}") public ResponseEntity> updateRecipient( @PathVariable("recipientId") Long recipientId, - @LoginCareworker Careworker careworker, + @Parameter(hidden = true) @LoginCareworker Careworker careworker, @Valid @RequestBody RecipientRequest recipientDTO) { RecipientResponse updatedRecipient = recipientService.updateRecipientForCareworker(recipientId, recipientDTO, careworker.getId()); return ResponseEntity.ok(ApiUtils.success(updatedRecipient)); @@ -65,8 +66,8 @@ public ResponseEntity> updateRecipient( @DeleteMapping("/{recipientId}") public ResponseEntity> deleteRecipient( @PathVariable("recipientId") Long recipientId, - @LoginCareworker Careworker careworker) { + @Parameter(hidden = true) @LoginCareworker Careworker careworker) { recipientService.deleteRecipientForCareworker(recipientId, careworker.getId()); return ResponseEntity.status(HttpStatus.NO_CONTENT).build(); } -} \ No newline at end of file +} diff --git a/src/main/java/dbdr/domain/recipient/controller/RecipientInstitutionController.java b/src/main/java/dbdr/domain/recipient/controller/RecipientInstitutionController.java index 02a9c622..112852cb 100644 --- a/src/main/java/dbdr/domain/recipient/controller/RecipientInstitutionController.java +++ b/src/main/java/dbdr/domain/recipient/controller/RecipientInstitutionController.java @@ -7,6 +7,7 @@ import dbdr.global.util.api.ApiUtils; import dbdr.security.LoginInstitution; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; @@ -27,7 +28,7 @@ public class RecipientInstitutionController { @Operation(summary = "전체 돌봄대상자 조회 ", security = @SecurityRequirement(name = "JWT")) @GetMapping public ResponseEntity>> getAllRecipients( - @LoginInstitution Institution institution) { + @Parameter(hidden = true) @LoginInstitution Institution institution) { List recipients = recipientService.getRecipientsByInstitution(institution.getId()); return ResponseEntity.ok(ApiUtils.success(recipients)); } @@ -36,7 +37,7 @@ public ResponseEntity>> getAllRecipie @GetMapping("/{recipientId}") public ResponseEntity> getRecipientById( @PathVariable("recipientId") Long recipientId, - @LoginInstitution Institution institution) { + @Parameter(hidden = true) @LoginInstitution Institution institution) { RecipientResponse recipient = recipientService.getRecipientByInstitution(recipientId, institution.getId()); return ResponseEntity.ok(ApiUtils.success(recipient)); } @@ -44,7 +45,7 @@ public ResponseEntity> getRecipientById( @Operation(summary = "돌봄대상자 추가", security = @SecurityRequirement(name = "JWT")) @PostMapping public ResponseEntity> createRecipient( - @LoginInstitution Institution institution, + @Parameter(hidden = true) @LoginInstitution Institution institution, @Valid @RequestBody RecipientRequest recipientDTO) { RecipientResponse newRecipient = recipientService.createRecipientForInstitution(recipientDTO, institution.getId()); return ResponseEntity.status(HttpStatus.CREATED).body(ApiUtils.success(newRecipient)); @@ -54,7 +55,7 @@ public ResponseEntity> createRecipient( @PutMapping("/{recipientId}") public ResponseEntity> updateRecipient( @PathVariable("recipientId") Long recipientId, - @LoginInstitution Institution institution, + @Parameter(hidden = true) @LoginInstitution Institution institution, @Valid @RequestBody RecipientRequest recipientDTO) { RecipientResponse updatedRecipient = recipientService.updateRecipientForInstitution(recipientId, recipientDTO, institution.getId()); return ResponseEntity.ok(ApiUtils.success(updatedRecipient)); @@ -64,7 +65,7 @@ public ResponseEntity> updateRecipient( @DeleteMapping("/{recipientId}") public ResponseEntity> deleteRecipient( @PathVariable("recipientId") Long recipientId, - @LoginInstitution Institution institution) { + @Parameter(hidden = true) @LoginInstitution Institution institution) { recipientService.deleteRecipientForInstitution(recipientId, institution.getId()); return ResponseEntity.status(HttpStatus.NO_CONTENT).build(); } diff --git a/src/main/java/dbdr/global/configuration/OpenApiConfiguration.java b/src/main/java/dbdr/global/configuration/OpenApiConfiguration.java index b3722651..940c5969 100644 --- a/src/main/java/dbdr/global/configuration/OpenApiConfiguration.java +++ b/src/main/java/dbdr/global/configuration/OpenApiConfiguration.java @@ -9,6 +9,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +// 스웨거 설정 @Configuration public class OpenApiConfiguration { @@ -27,7 +28,7 @@ public OpenAPI openAPI() { Info info = new Info() .title("돌봄다리 API Document") .version("v1.0.0") - .description("돌봄다리 서버 API 명세서입니다."); + .description("돌봄다리 백엔드 서버 API 명세서입니다."); return new OpenAPI() .components(components) @@ -62,15 +63,6 @@ public GroupedOpenApi adminApi() { .build(); } - @Bean - public GroupedOpenApi recipientApi() { - return GroupedOpenApi.builder() - .group("recipient") - .displayName("Recipient API") - .pathsToMatch("/v*/recipient/**") - .build(); - } - @Bean public GroupedOpenApi guardianApi() { return GroupedOpenApi.builder() @@ -115,13 +107,4 @@ public GroupedOpenApi authentication() { .pathsToMatch("/v*/auth/**") .build(); } - - @Bean - public GroupedOpenApi summarization() { - return GroupedOpenApi.builder() - .group("summarization") - .displayName("Summary API") - .pathsToMatch("/v*/summary/**") - .build(); - } } diff --git a/src/main/java/dbdr/global/configuration/S3Config.java b/src/main/java/dbdr/global/configuration/S3Config.java new file mode 100644 index 00000000..80e6ab82 --- /dev/null +++ b/src/main/java/dbdr/global/configuration/S3Config.java @@ -0,0 +1,37 @@ +package dbdr.global.configuration; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; + +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; + +@Configuration +public class S3Config { + @Value("${cloud.aws.credentials.access-key}") + private String accessKey; + @Value("${cloud.aws.credentials.secret-key}") + private String secretKey; + @Value("${cloud.aws.region.static}") + private String region; + + @Bean + @Primary + public BasicAWSCredentials awsCredentialsProvider(){ + BasicAWSCredentials basicAWSCredentials = new BasicAWSCredentials(accessKey, secretKey); + return basicAWSCredentials; + } + + @Bean + public AmazonS3 amazonS3() { + AmazonS3 s3Builder = AmazonS3ClientBuilder.standard() + .withRegion(region) + .withCredentials(new AWSStaticCredentialsProvider(awsCredentialsProvider())) + .build(); + return s3Builder; + } +} diff --git a/src/main/java/dbdr/global/configuration/SqsConfig.java b/src/main/java/dbdr/global/configuration/SqsConfig.java new file mode 100644 index 00000000..e15b57ae --- /dev/null +++ b/src/main/java/dbdr/global/configuration/SqsConfig.java @@ -0,0 +1,68 @@ +package dbdr.global.configuration; + +import java.time.Duration; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import io.awspring.cloud.sqs.config.SqsMessageListenerContainerFactory; +import io.awspring.cloud.sqs.listener.acknowledgement.AcknowledgementOrdering; +import io.awspring.cloud.sqs.listener.acknowledgement.handler.AcknowledgementMode; +import io.awspring.cloud.sqs.operations.SqsTemplate; +import software.amazon.awssdk.auth.credentials.AwsCredentials; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.sqs.SqsAsyncClient; + +@Configuration +public class SqsConfig { + + @Value("${cloud.aws.credentials.access-key}") + private String AWS_ACCESS_KEY; + + @Value("${cloud.aws.credentials.secret-key}") + private String AWS_SECRET_KEY; + + @Value("${cloud.aws.region.static}") + private String AWS_REGION; + + // 클라이언트 설정: region과 자격증명 + @Bean + public SqsAsyncClient sqsAsyncClient() { + return SqsAsyncClient.builder() + .credentialsProvider(() -> new AwsCredentials() { + @Override + public String accessKeyId() { + return AWS_ACCESS_KEY; + } + + @Override + public String secretAccessKey() { + return AWS_SECRET_KEY; + } + }) + .region(Region.of(AWS_REGION)) + .build(); + } + + // Listener Factory 설정 (Listener 쪽) + @Bean + SqsMessageListenerContainerFactory defaultSqsListenerContainerFactory(SqsAsyncClient sqsAsyncClient) { + return SqsMessageListenerContainerFactory + .builder() + .configure(options -> options + .acknowledgementMode(AcknowledgementMode.ALWAYS) + .acknowledgementInterval(Duration.ofSeconds(3)) + .acknowledgementThreshold(5) + .acknowledgementOrdering(AcknowledgementOrdering.ORDERED) + ) + .sqsAsyncClient(sqsAsyncClient) + .build(); + } + + // 메시지 발송을 위한 SQS 템플릿 설정 (Sender 쪽) + @Bean + public SqsTemplate sqsTemplate() { + return SqsTemplate.newTemplate(sqsAsyncClient()); + } +} diff --git a/src/main/java/dbdr/global/util/line/LineMessagingScheduler.java b/src/main/java/dbdr/global/util/line/LineMessagingScheduler.java deleted file mode 100644 index 966c1b8e..00000000 --- a/src/main/java/dbdr/global/util/line/LineMessagingScheduler.java +++ /dev/null @@ -1,65 +0,0 @@ -package dbdr.global.util.line; - -import java.time.LocalTime; -import java.util.List; - -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Component; - -import com.linecorp.bot.client.LineMessagingClient; -import com.linecorp.bot.model.PushMessage; -import com.linecorp.bot.model.message.TextMessage; - -import dbdr.domain.careworker.entity.Careworker; -import dbdr.domain.careworker.repository.CareworkerRepository; -import dbdr.domain.guardian.entity.Guardian; -import dbdr.domain.guardian.repository.GuardianRepository; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -@Component -@RequiredArgsConstructor -@Slf4j -public class LineMessagingScheduler { - private final LineMessagingClient lineMessagingClient; - private final GuardianRepository guardianRepository; - private final CareworkerRepository careworkerRepository; - - @Scheduled(cron = "0 0/30 * * * ?") - public void sendChartUpdate() { - LocalTime currentTime = LocalTime.now().withSecond(0).withNano(0); // 초와 나노초를 제거하고 분 단위로 비교 - - // DB에서 알림 시간을 설정한 사용자들을 조회합니다. - List guardians = guardianRepository.findByAlertTime(currentTime); - List careworkers = careworkerRepository.findByAlertTime(currentTime); - - // 각 사용자에게 차트 내용을 보냅니다. - for (Guardian guardian : guardians) { - String userId = guardian.getLineUserId(); - String chartMessage = "오늘의 차트 내용: ..."; // 차트 정보를 DB에서 가져와서 메시지 생성 - - PushMessage pushMessage = new PushMessage(userId, new TextMessage(chartMessage)); - - try { - lineMessagingClient.pushMessage(pushMessage).get(); - log.info("Message sent to user: {}", userId); - } catch (Exception e) { - log.error("Failed to send message to user: {}", userId, e); - } - } - - for (Careworker careworker : careworkers) { - String userId = careworker.getLineUserId(); - String chartMessage = "오늘의 차트 내용: ..."; // 차트 정보를 DB에서 가져와서 메시지 생성 - - PushMessage pushMessage = new PushMessage(userId, new TextMessage(chartMessage)); - - try { - lineMessagingClient.pushMessage(pushMessage).get(); - log.info("Message sent to user: {}", userId); - } catch (Exception e) { - log.error("Failed to send message to user: {}", userId, e); - } - } - } -} diff --git a/src/main/java/dbdr/global/util/line/LineMessagingUtil.java b/src/main/java/dbdr/global/util/line/LineMessagingUtil.java deleted file mode 100644 index 41de7205..00000000 --- a/src/main/java/dbdr/global/util/line/LineMessagingUtil.java +++ /dev/null @@ -1,64 +0,0 @@ -package dbdr.global.util.line; - -import java.time.LocalTime; - -import org.springframework.stereotype.Component; - -import com.linecorp.bot.client.LineMessagingClient; -import com.linecorp.bot.model.PushMessage; -import com.linecorp.bot.model.message.TextMessage; -import com.linecorp.bot.model.profile.UserProfileResponse; - -import dbdr.global.exception.ApplicationError; -import dbdr.global.exception.ApplicationException; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -@Component -@RequiredArgsConstructor -@Slf4j -public class LineMessagingUtil { - private final LineMessagingClient lineMessagingClient; - - public void userFoundFailedMessage(String userId) { - String errorMessage = - "등록된 정보가 확인되지 않았습니다. 😊\n" + - "서비스 이용을 위해 요양원에 문의하시거나, 전화번호를 다시 인증해주시기 바랍니다. ☎️\n" + - "예: 01012345678"; - sendMessageToUser(userId, errorMessage); - } - - // 사용자에게 메시지를 보내는 메서드 - public void sendMessageToUser(String userId, String message) { - TextMessage textMessage = new TextMessage(message); - PushMessage pushMessage = new PushMessage(userId, textMessage); - - try { - lineMessagingClient.pushMessage(pushMessage).get(); - log.info("Message sent successfully to user: {}", userId); - } catch (Exception e) { - log.error("Failed to send message to user: {}", userId, e); - throw new ApplicationException(ApplicationError.MESSAGE_SEND_FAILED); - } - } - - // UserId를 통해 라인 사용자 프로필 정보 가져오는 메서드 - public UserProfileResponse getUserProfile(String userId) { - try { - return lineMessagingClient.getProfile(userId).get(); - } catch (Exception e) { - log.error("Failed to get user profile: {}", userId, e); - throw new ApplicationException(ApplicationError.FAILED_TO_GET_USER_PROFILE); - } - } - - // AM/PM 및 시간을 LocalTime으로 변환하는 메서드 - public LocalTime convertToLocalTime(String ampm, int hour, int minute) { - if (ampm.equalsIgnoreCase("오후") && hour != 12) { - hour += 12; - } else if (ampm.equalsIgnoreCase("오전") && hour == 12) { - hour = 0; // 오전 12시는 0시로 변환 - } - return LocalTime.of(hour, minute); // 시간과 분을 함께 설정 - } -} diff --git a/src/main/java/dbdr/openai/controller/SummaryController.java b/src/main/java/dbdr/openai/controller/SummaryController.java index b701c4bc..874b349a 100644 --- a/src/main/java/dbdr/openai/controller/SummaryController.java +++ b/src/main/java/dbdr/openai/controller/SummaryController.java @@ -12,15 +12,15 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -@Tag(name = "요약 API", description = "차트 하루 요약") +@Tag(name = "Chart 요약 API", description = "차트 하루 요약") @RestController @RequiredArgsConstructor -@RequestMapping("/${spring.app.version}/summary") +@RequestMapping("/${spring.app.version}/chart/summary") public class SummaryController { private final SummaryService summaryService; - @Operation(summary = "요약한 값과 태그를 DB에서 불러온다.", description = "차트 아이디로 요약 데이터와 요약태그를 불러온다.") + @Operation(summary = "차트 요약 후 요약한 값과 태그를 DB에서 불러온다.", description = "차트 아이디로 요약 데이터와 요약태그를 불러온다.") @GetMapping public ResponseEntity> getSummary(@RequestParam("chartId") Long chartId) { return ResponseEntity.ok(ApiUtils.success(summaryService.getFinalSummary(chartId))); diff --git a/src/main/java/dbdr/security/config/SecurityConfig.java b/src/main/java/dbdr/security/config/SecurityConfig.java index 74c198e3..2f9b1e5a 100644 --- a/src/main/java/dbdr/security/config/SecurityConfig.java +++ b/src/main/java/dbdr/security/config/SecurityConfig.java @@ -57,8 +57,12 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .authenticationProvider(baseAuthenticationProvider()) .authorizeHttpRequests((authorize) -> { authorize + .requestMatchers("/v1/ocr/**") + .permitAll() .requestMatchers("/v1/admin/**") .permitAll() + .requestMatchers("/v1/s3/chart/**") + .permitAll() .requestMatchers( "/swagger-ui/**", "/v3/api-docs/**",