diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml new file mode 100644 index 00000000..06be72e3 --- /dev/null +++ b/.github/workflows/gradle.yml @@ -0,0 +1,74 @@ +name: Java CI and Deploy to AWS EC2 + +on: + push: + branches: [ "Week10" ] + pull_request: + branches: [ "Week10" ] + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - uses: actions/checkout@v4 + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@af1da67850ed9a4cedd57bfd976089dd991e2582 + + - name: Build with Gradle Wrapper + run: ./gradlew clean build + + deploy: + runs-on: ubuntu-latest + needs: build + environment: production + permissions: + contents: read + + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: Decode and save .pem file + env: + EC2_SSH_KEY_BASE64: ${{ secrets.EC2_SSH_KEY_BASE64 }} + run: | + echo "$EC2_SSH_KEY_BASE64" | base64 -d > ec2-key.pem + chmod 600 ec2-key.pem + - name: Upload JAR to EC2 + env: + EC2_HOST: ${{ secrets.EC2_HOST }} + EC2_USER: ${{ secrets.EC2_USER }} + run: | + scp -i ec2-key.pem -o StrictHostKeyChecking=no ./build/libs/your-app.jar $EC2_USER@$EC2_HOST:~/app/your-app.jar + - name: Restart Application on EC2 + env: + EC2_HOST: ${{ secrets.EC2_HOST }} + EC2_USER: ${{ secrets.EC2_USER }} + run: | + ssh -i ec2-key.pem -o StrictHostKeyChecking=no $EC2_USER@$EC2_HOST << 'EOF' + # 현재 실행 중인 Java 프로세스 종료 + echo "현재 실행 중인 Java 프로세스 종료 중..." + pkill -f 'java' || echo "종료할 Java 프로세스 없음" + # 새로운 애플리케이션 실행 + JAR_FILE="dbdr-0.0.1-SNAPSHOT.jar" + echo "새로운 애플리케이션을 실행합니다: $JAR_FILE" + nohup java -jar ~/app/$JAR_FILE --server.port=8080 > ~/app/app.log 2>&1 & + # 실행 확인 + sleep 10 + echo "새로 실행된 Java 프로세스:" + pgrep -f 'java' + EOF diff --git a/README.md b/README.md index 96a9563a..62245852 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,36 @@ -## 📝 이슈 설명 - -다음과 같이 꾸며보려고 합니다 ㅎㅎ -- 서비스 로직의 이유가 정말 중요하다. ex) 왜 sqs를 사용했는지, 어떻게 확장될지.. 등등 사이드 이펙트도 정리해서 말해주기 -- 깃헙 위키 - ---- - -# Team13_BE +# 🤝 [Team 13] 돌봄다리 - 요양원 관리 서비스 + +

+care_bridge_logo +

+
+
+ +> 목차 +> - [🧑‍💻 Collaborators](#-collaborators) +> - [⚙️ 개발 스택](#-개발-스택) +> - [🔗 프로젝트 관련 주소](#-프로젝트-관련-주소) +> - [🤩 샘플 아이디 & 비밀번호](#-샘플-아이디--비밀번호) +> - [🌟 돌봄다리란?](#-돌봄다리란) +> - [🧐 서비스의 필요성](#-서비스의-필요성) +> - [🧩 서비스 핵심 기능](#-서비스-핵심-기능) +> - [🔧 공통 핵심 개발 영역](#-공통-핵심-개발-영역) +> - [🔧 FE 핵심 개발 영역](#-fe-핵심-개발-영역) +> - [🔧 BE 핵심 개발 영역](#-be-핵심-개발-영역) +> - [🧩 ERD](#-erd) +> - [🌌 백엔드 전체 구상도](#-백엔드-전체-구상도) +> - [📄 팀 그라운드 규칙 설명](#-팀-그라운드-규칙-설명) + +# 🧑‍💻 Collaborators
- - -최고의 요양원 관리 서비스, '돌봄다리'의 백엔드 서버입니다. - -해당 레포지토리는 [카카오테크캠퍼스](https://www.kakaotechcampus.com/) 2기 부산대 13조 프로젝트에 기반을 두고 있습니다. +### 🗓️ 개발 기간 +2024.09 ~ 2024.11 (카카오 테크 캠퍼스 2기 - Step3)
-## Collaborators +
+

Backend

@@ -32,7 +45,6 @@

Frontend

-
| **조장** | **타임 키퍼** | @@ -43,70 +55,439 @@
-## Introduction -'돌봄다리'는 요양원, 요양보호사, 보호자 간의 소통을 원활하게 하기 위한 디지털 차트 작성 서비스입니다. 음성 인식과 손글씨 인식 기능을 통합하여 50-70대의 요양보호사들이 간편하고 효율적으로 일지를 작성할 수 있도록 설계되었습니다. 또한, 보호자들은 가족의 상태를 투명하게 확인할 수 있어 신뢰성 있는 서비스입니다. 추가적으로 자체 알림 서비스와 AI 서비스를 도입하여 일지 요약, 보호자와 요양보호사들과의 연락 등을 자동화하였습니다. +
+
+ +--- + +## ⚙️ 개발 스택 + +
+ +![java 17](https://img.shields.io/badge/-Java%2017-ED8B00?style=flat-square&logo=java&logoColor=white) +![spring boot 3.3](https://img.shields.io/badge/Spring%20boot%203.3-6DB33F?style=flat-square&logo=springboot&logoColor=white) +![spring security](https://img.shields.io/badge/spring%20security-6DB33F?style=flat-square&logo=spring&logoColor=white) +![mysql 8.0](https://img.shields.io/badge/MySQL%208.0-005C84?style=flat-square&logo=mysql&logoColor=white) + +![Redis](https://img.shields.io/badge/Redis-DC382D?style=flat-square&logo=Redis&logoColor=white) +![AWS S3](https://img.shields.io/badge/AWS%20S3-569A31?style=flat-square&logo=amazons3&logoColor=white) +![AWS EC2](https://img.shields.io/badge/AWS%20EC2-FF9900?style=flat-square&logo=amazonec2&logoColor=white) +![Amazon sqs](https://img.shields.io/badge/Amazon%20sqs-FF9900?style=flat-square&logo=amazon&logoColor=white) + +![Naver cloud](https://img.shields.io/badge/naver%20cloud-03C75A?style=flat-square&logo=naver&logoColor=white) +![openAI](https://img.shields.io/badge/openAI-FF6C37?style=flat-square&logo=openai&logoColor=white) +![poi](https://img.shields.io/badge/poi-3F6EB5?style=flat-square&logo=apache&logoColor=white) +![line api](https://img.shields.io/badge/line%20api-00C300?style=flat-square&logo=line&logoColor=white) +![coolSms](https://img.shields.io/badge/coolSms-FF6C37?style=flat-square&logo=coolSms&logoColor=white) + +![React](https://img.shields.io/badge/-React%2018-4894FE?style=flat-square&logo=react&logoColor=white) +![Vite](https://img.shields.io/badge/-Vite%205-646CFF?style=flat-square&logo=vite&logoColor=white) +![TypeScript](https://img.shields.io/badge/-TypeScript-3178C6?style=flat-square&logo=typescript&logoColor=white) + +![Chakra UI](https://img.shields.io/badge/-Chakra%20UI-319795?style=flat-square&logo=chakraui&logoColor=white) +![Emotion](https://img.shields.io/badge/-Emotion-FF69B4?style=flat-square&logo=emotion&logoColor=white) +![Styled Components](https://img.shields.io/badge/-Styled%20Components-DB7093?style=flat-square&logo=styledcomponents&logoColor=white) + +![React Query](https://img.shields.io/badge/-React%20Query-FF4154?style=flat-square&logo=reactquery&logoColor=white) +![Axios](https://img.shields.io/badge/-Axios-5A29E4?style=flat-square&logo=axios&logoColor=white) + +![Tesseract.js](https://img.shields.io/badge/-Tesseract.js-3D348B?style=flat-square&logo=tesseract&logoColor=white) + + +
+ +
+
+ +--- + +# 🔗 프로젝트 관련 주소 + +
+ +| 문서 | +|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:| +| [백엔드 배포 주소](https://dbdr-servcie.com) | +| [프론트엔드 배포 주소](https://dbdari.vercel.app/) | +| [API 문서](https://dolbomdari.netlify.app/) | +| [디자인 피그마](https://www.figma.com/design/RvPegHAoDLITbqAxexEok7/%EB%B6%80%EC%82%B0%EB%8C%80-13%EC%A1%B0-%EB%81%9D%EB%82%B4%EC%A3%BC%EC%A1%B0?node-id=19-3&node-type=canvas&t=IzVl1agbkGalr8SU-0) | +| [프로젝트 노션](https://yoookm.notion.site/13-fc918782c8684baab30d46f8c05939f2?pvs=4) | +| [돌봄다리 라인 채널](https://lin.ee/F4hbz9m) | + +
+ +
+
+ +--- +# 🤩 샘플 아이디 & 비밀번호 +### 관리자 +- 로그인 아이디 : string +- 로그인 비밀번호 : string +### 요양원 +- 로그인 아이디 : love +- 로그인 비밀번호 : 1234 +### 요양보호사 +- 로그인 아이디 : 01012341234 +- 로그인 비밀번호 : 1 +### 보호자 +- 로그인 아이디 : 01022223333 +- 로그인 비밀번호 : 1234 + +--- +# 🌟 돌봄다리란? + +> **요양보호사**는 간편하게 차트를 작성하고, +> **보호자**는 이를 실시간으로 확인할 수 있는 **디지털 차트 서비스** + +- 보호자는 **언제 어디서나 가족의 상태를 확인** +- 요양보호사는 **복잡함 없이 기록을 관리** + +**➡️ 신뢰와 편리성을 제공하는 소통 플랫폼** + +
+
+ +--- +# 🧐 서비스의 필요성 + +## 📝 문제 상황 1. 정보 공유의 단절 +- **보호자**는 가족의 상태를 자주 확인하고 싶지만, 요양원에 일일이 연락해야 하는 번거로움과 제한된 정보로 인해 불편을 겪고 있습니다. +- 실시간 상태 확인이 어렵기 때문에, 보호자는 가족의 건강 상태에 대해 지속적인 불안감을 느낄 수 있습니다. + + +``` +보호자의 요구 - 가족의 상태를 실시간으로 확인할 수 있는 간편한 정보 접근 방안이 필요하다. + +➡️ 보호자가 어디서든 가족의 상태를 쉽게 확인할 수 있는 시스템이 필요하다! +``` + +### 🎯 해결 방안 +- **실시간 정보 공유** 기능을 통해 보호자가 언제 어디서나 가족의 최신 상태를 확인할 수 있도록 합니다. +- 보호자와 요양보호사 간의 소통을 원활하게 하여 불안감을 줄이고, 신뢰를 강화합니다. + +
+ +## 📝 문제 상황 2. 요양보호사의 차트 작성 어려움 +- **요양보호사**는 복잡한 디지털 기록 시스템에 익숙하지 않아 핸드폰으로 차트를 작성하는 과정이 번거롭고 어렵습니다. +- 이러한 어려움은 기록의 정확성과 신속성을 저해하고, 요양보호사의 업무 효율성에도 부정적인 영향을 미칩니다. + +

+ caregiver_difficulty +

+ +``` +요양보호사의 요구 - 복잡하지 않고 간단한 차트 작성 방식이 필요하다. +➡️ 요양보호사가 쉽게 차트를 작성할 수 있도록 하는 간편한 기록 시스템이 필요하다! +``` +### 🎯 해결 방안 +- **음성 인식 및 손글씨 인식** 기능을 통해 요양보호사가 복잡한 절차 없이 차트를 쉽게 작성할 수 있도록 지원합니다. +- 기록 작성의 간소화를 통해 요양보호사의 부담을 줄이고, 환자의 상태를 신속하고 정확하게 기록할 수 있도록 합니다. -## System Structure -### 전체 구상도 +
+
+--- +## 🧩 서비스 핵심 기능 -### 백엔드 구상도 +## 보호자 -## ERD +
+| 📝 **돌봄대상자 차트 확인** | 📝 **차트 요약** | +|:-------------------------------------------------------------------------------------------------------------------------------:|:-----------------------------------------------------------------------------------------------------------------------------------:| +| **하루 상태 기록 확인**
사진과 차트 작성 시 **알림 수신** | 긴 차트를 **핵심 내용 요약**
주요 사항을 **간결하게 확인** | +| voice_recognition | chart_summary_feature | +
-## Tech Stack +## 요양보호사
-![java 17](https://img.shields.io/badge/-Java%2017-ED8B00?style=for-the-badge&logo=java&logoColor=white) -![spring boot 3.1.3](https://img.shields.io/badge/Spring%20boot%203.1.3-6DB33F?style=for-the-badge&logo=springboot&logoColor=white) -![spring security](https://img.shields.io/badge/spring%20security-6DB33F?style=for-the-badge&logo=spring&logoColor=white) -![mysql 8.0](https://img.shields.io/badge/MySQL%208.0-005C84?style=for-the-badge&logo=mysql&logoColor=white) +| 🖋️ **요양 일지 작성** | 🎙️ **음성 인식 차트 작성** | +|:--------------------------------------------------------------------------------------------------------------------------:|:-----------------------------------------------------------------------------------------------------------------------------:| +| **음성/사진 인식**, 직접 작성 지원
**다양한 방식으로 간편 작성** | **음성 인식**을 통해 주관식 입력
음성을 텍스트로 **자동 변환** | +| create_chart |voice_recognition| -![Redis 6.2](https://img.shields.io/badge/Redis%206.2-DC382D?style=for-the-badge&logo=Redis&logoColor=white) -![AWS S3](https://img.shields.io/badge/AWS%20S3-569A31?style=for-the-badge&logo=amazons3&logoColor=white) -![AWS EC2](https://img.shields.io/badge/AWS%20EC2-FF9900?style=for-the-badge&logo=amazonec2&logoColor=white) -![Amazon sqs](https://img.shields.io/badge/Amazon%20sqs-FF9900?style=for-the-badge&logo=amazon&logoColor=white) -![Naver cloud](https://img.shields.io/badge/naver%20cloud-03C75A?style=for-the-badge&logo=naver&logoColor=white) -![openAI](https://img.shields.io/badge/openAI-FF6C37?style=for-the-badge&logo=openai&logoColor=white) -![poi](https://img.shields.io/badge/poi-3F6EB5?style=for-the-badge&logo=apache&logoColor=white) -![line api](https://img.shields.io/badge/line%20api-00C300?style=for-the-badge&logo=line&logoColor=white) -![coolSms](https://img.shields.io/badge/coolSms-FF6C37?style=for-the-badge&logo=coolSms&logoColor=white) +| 📷 **OCR 차트 작성** | 📑 **차트 요약 기능** | 🔔 **알림 기능** | +|:--------------------------------------------------------------------------------:|:------------------------------------------------------------------------------------------------------------------------------------:|:-----------------------------------------------------------------------:| +| **차트 양식 프린트 후 사진 인식**
사진 한 장으로 **자동 기록 완성** | **환자 상태 요약 제공**
여러 환자의 **하루 상태 간편 확인** | 사용자가 예약한 시간마다
문자/라인 메시지로 차트 작성 알림 | +| ocr_chart | chart_summary_feature | care_message |
-## 구현 기능 목록 -### 알림 서비스 -1. Line 알림 서비스 - - Line Messaging API - - Amazon sqs - - spring scheduler -2. SMS 알림 서비스 - - coolSms API - - Amazon sqs - - spring scheduler +## 요양원 + +
+ +| 🖥️ **요양사, 보호자, 돌봄대상자 관리** | 📊 **엑셀 업로드** | +|:------------------------------------------------------------------------------------------------------------------------------:|:--------------------------------------------------------------------------------------------------------------------------:| +| **웹사이트로 정보 관리**
요양사, 보호자, 대상자 정보 **수정 가능** | 엑셀 파일로 **대량 데이터 업로드**
제공된 템플릿 파일로 **간편 등록** | +| admin_management |excel_upload | + +
+ +
+
+ +--- + +## 🔧 공통 핵심 개발 영역 + +### 🙆‍ 회원가입 +- 사용자의 연령층을 고려할 때, 직접 회원가입하고 정보를 등록하는 것이 어려울 것이라 생각하여 요양원이나 관리자가 돌봄대상자나 보호자의 아이디, 비밀번호를 생성해줍니다. +- 돌봄대상자와 보호자는 비밀번호만 기억하면 서비스 이용이 가능하도록 아이디는 본인의 전화번호로 등록하도록 구현하였습니다. +- 관리자는 요양원, 보호자, 돌봄대상자, 요양보호사를 등록할 수 있습니다. +- 요양원은 보호자, 돌봄대상자, 요양보호사를 해당 요양원에 등록할 수 있습니다. +- 관리자의 경우 랜딩 페이지에 적힌 이메일로 contact하여 신분과 목적을 인증한 뒤 본 서비스 담당자가 아이디 비밀번호를 부여해줍니다. + +
+
+ +## 🔧 FE 핵심 개발 영역 + +### 📸 OCR 기능 +1. **고려 사항** + +2. **구현 방법** + +3. **문제 해결** + +### 🎙️음성인식 기능 +1. **구현 방법** + - SpeechToText 훅을 구현하여 녹음시작, 녹음중지, 다시 녹음이 가능하게 합니다. + - 녹음 중에 제대로 인식이 되고 있는지 실시간으로 화면에 띄워주어 확인이 가능하게 합니다. + - 조금 부족한 부분은 음성인식 완료 후 작성 페이지에서 직접 수정이 가능하도록 합니다. +2. **문제 해결** + - 녹음 중 텍스트가 화면에 제대로 띄워지지 않고 중지, 시작 버튼이 제대로 작동하지 않았었는데, 상태 변수를 여러개를 사용하다보니 그러한 문제가 발생하였던 것이었습니다. 최대한 적은 상태변수를 사용하여 이러한 문제를 해결하였습니다. + +### 📋 차트작성 기능 +1. **고려 사항** + - 본 사이트의 유저들의 연령대를 고려하여 최대한 단순하고 직관적이게 구현해야합니다. + - 의료와 관련이 있는 서비스이므로 일지 작성에 필수적인 항목들을 빠짐없이 복잡하지 않으면서도 구성해야합니다. +2. **구현 방식** + - 국가에서 규정하고 있는 일지에 들어가야하는 항목들을 대분류에 따라 step으로 나누어 최대한 잘게 나누어서 입력을 받았습니다. + - 다른 형태의 입력을 받더라도 디자인은 최대한 비슷하게 컴포넌트를 구현하여 피로감을 덜었습니다. + +### 📃 차트 수정 및 삭제 +1. **고려 사항** + - 차트를 미리 작성해두고 시간이 지난 뒤에 무분별하게 차트를 수정하면 보호자가 불만을 가질 수 있습니다. 따라서 차트 수정은 당일 작성한 차트만 가능하게 하였습니다. +2. **구현 방식** + - 요양보호사의 권한으로 돌봄대상자 차트 작성 아이콘을 누르면 현재 날짜와 비교하여 존재하는 차트가 있으면 차트 수정을, 없으면 새로운 차트 작성을 하게끔 구현하였습니다. + - 차트 삭제의 경우 요약일지 페이지에 삭제 버튼을 두어 삭제가 가능하게끔 했습니다. + +### 🔓 로그인 및 로그아웃 +1. **로그인** + - 요양보호사와 보호자는 전화번호와 비밀번호로, 요양원과 관리자는 아이디와 비밀번호로 로그인이 가능하게 화면을 구성하였습니다. + - 필요한 항목들을 넣고 로그인을 하면 accessToken과 refreshToken을 받아오고 추후 accessToken이 만료되면 저장된 refreshToken으로 새로운 accessToken과 refreshToken을 발급받습니다. +2. **로그아웃** + - 로그아웃을 자주 이용하는 서비스가 아니므로 로그아웃 기능은 화면 우측 상단에 프로필 사진을 누르면 이동하는 마이페이지에서 로그아웃이 가능하도록 구현하였습니다. + - 로그아웃시 랜딩페이지로 이동하여 다시 로그인이 가능하게끔 합니다. + +### 😙 사용자의 편리를 위해 세세하게 신경 쓴 화면 +1. 단순하고 깔끔한 UI + - 한 화면에 적당한 양의 항목들만을 배치하여 여백을 주어 차트를 조회하거나 작성할 때에 피로하지 않도록 합니다. + - 직관적인 버튼으로 사용이 어렵지 않게 구현합니다. +2. **캘린더**로 존재하는 차트 내역 한눈에 확인 가능 + - 차트를 매일 작성하지는 않는다는 특성상 현재 존재하는 차트의 작성일을 불러와 캘린더에 표시해줍니다. + - 표시된 날짜를 클릭하면 해당 날짜에 작성된 일지에 대한 AI 요약일지, 전체 일지 내역을 확인할 수 있습니다. + - 차트가 존재하지 않는 날짜는 선택할 수 없게 하여 사용하기 용이하게 하였습니다. +3. 일지 작성 또는 열람시 **step**을 표시 + - 단계바 클릭시 해당 step으로 넘어가게 하여 작성시에는 앞서 작성한 내용의 수정이, 열람시에는 원하는 step 확인이 용이하게 합니다. + - 일지 작성시에는 현재 작성 중인 step 이후의 step으로는 넘어가지 못하게 하여 작성 내용 누락을 방지하였습니다. + - 일지 열람시에는 자유롭게 step 이동이 가능합니다. +4. 웹 혹은 태블릿 화면에서의 **화면 너비 고정** + - 단순한 화면을 위하여 단계를 잘게 나눈 만큼 한 화면에서 수행해야할 동작들이 적은 편이고 화면도 단순한 편인 것을 고려하여 화면이 더 넓어지더라도 최대 너비를 고정하여 UI가 깔끔하게 유지할 수 있게끔 구현하였습니다. + - 관리자나 요양 기관에서는 컴퓨터를 사용하여 표를 관리할 것을 고려하여 웹 화면 규격에 맞추어 구현하였습니다. + - 랜딩페이지의 경우 모든 화면에서도 자연스럽게 보이도록 반응형으로 구현하였습니다. + +### 😊 자연스러운 랜딩페이지 +1. **고려 사항** + 웹과 태블릿, 휴대폰 등 다양한 규격으로 사용하는 홈페이지이기에 자칫하면 어색한 배치의 화면이 띄워지는 화면이 띄워질 수 있습니다. 이를 방지하기 위하여 어느 크기의 화면에서도 자연스럽게 보이게 구현하였습니다. +2. **구현 방식** + - breakpoints.ts에 네가지 규격를 저장하여 해당 수치에 못 미치거나 초과할때 글씨 크기나 아이템들의 배치를 수정합니다. + - 배치에 맞게 애니메이션을 추가하여 뚝뚝 끊기지않고 부드러운 느낌이 들게 구현하였습니다. + +
+ +## 🔧 BE 핵심 개발 영역 +### 🔓 로그인 / 회원가입 +  spring security와 JWT를 활용하여 stateless한 인증방식을 선택하여 서버 확장성에 이점을 가지고자 하였습니다. + 또한 권한 검사를 지원하기 위한 커스텀 메소드 어노테이션으로 비즈니스 로직과 권한 검사부분을 분리하였습니다. + 중점적으로 생각한 부분은 서로 다른 table에 속해있는 회원들을 대상으로 인증과 인가가 필요한 상황이였으며, 이를 위해 서비스에 알맞은 AuthenticationProvider와 UserDetails, UserDetailsService를 구현하였습니다. + +### 🪙 리프레시 토큰 +  저희 서비스는 민감한 의료 데이터를 다루기에, 토큰 보안이 중요했습니다. 로그인 시 액세스 토큰과 리프레시 토큰을 발급하고, 리프레시 토큰으로 재발급 시 두 토큰을 모두 새로 발급하는 RTR 방식을 적용해 보안을 강화했습니다. 로그아웃 시에는 Redis에 저장된 리프레시 토큰을 삭제하고, 액세스 토큰은 블랙리스트에 등록해 유효성을 차단했습니다. 이를 통해 로그아웃 시 실시간으로 토큰 만료를 효과적으로 처리할 수 있었습니다. -### 파일 입출력 -- poi 라이브러리 +### 📷 OCR 기능 -### AI -- open AI -- 파인 튜닝 +  요양보호사가 작성한 돌봄 대상자 차트를 효율적으로 디지털화하기 위해 Naver Clova OCR API와 AWS S3의 presigned URL을 사용했습니다. -### OCR -- S3 / presigned url -- naver clovar +  presigned URL을 통해 이미지 파일을 S3에 업로드하고, 백엔드 서버에는 objectKey 값만 전달하여 OCR을 수행하는 방식으로 서버 과부하를 방지하고 성능을 최적화했습니다. 이로써 서버 리소스를 절약하면서도 보안성을 유지한 상태에서 차트를 안전하게 OCR 처리할 수 있도록 구현했습니다. +

+care_bridge_logo +

-### 인증/인가 -- spring security -- redis +
+ +### 🤖 AI 요약 기능 - 파인 튜닝 +1. **고려 사항** + + - 보호자들이 차트 정보를 모두 보면 너무 많은 정보로 인해 돌봄 대상자의 상태를 파악하기 어려울 수 있습니다. 이를 해결하기 위해 차트 정보를 간결하게 요약하여 보여주는 기능을 구현하였습니다. + + +2. **기술 선택 이유(파인튜닝)** + + - 모델 파인튜닝: 기존의 ChatGPT를 사용할 때 원하는 형식으로 결과가 나오지 않거나 불필요한 정보가 포함되는 경우가 있어, 모델을 파인튜닝하는 방법을 선택했습니다. 파인튜닝을 하지 않았다면 매번 JSON 형식으로 특정 방식의 값을 요구해야 했겠지만, 이제는 차트 데이터를 JSON 형식으로 입력하면 원하는 형식의 결과를 바로 받을 수 있습니다. + + +3. **구현 방식** + + - 파인튜닝: 차트 요약과 관련된 데이터셋이 없어 AI-Hub의 한국어 대화 요약 데이터셋을 활용하여 파인튜닝을 진행했습니다. conditionDisease, bodyManagement, nursingManagement, recoveryTraining, cognitiveManagement와 같은 항목별로 요약하도록 만들었습니다. + + - 태그 요약 적용: 프론트엔드에서 사용할 세 가지 태그를 요약하도록 파인튜닝을 추가로 진행했습니다. 프론트엔드와의 연동 과정에서 태그를 추가하는 것이 유용할 것이라는 의견을 반영하여 이를 구현했습니다. 차트 데이터를 준비하는 데 시간이 많이 소요되었기 때문에 태그를 추가하여 다시 파인튜닝하는 것이 어렵다고 판단했고, 대신 태그를 위한 파인튜닝을 별도로 진행하는 것으로 결정했습니다. + +4. **문제 해결** + - 가끔 AI가 null 값을 반환하는 문제가 있었지만, 대부분 한 번 더 시도하면 정상적으로 동작했습니다. 이에 따라 백엔드 서비스에서 첫 번째 시도에 성공하지 않을 경우 최대 세 번까지 재시도하도록 수정하였고, 세 번 시도 후에도 응답이 없을 경우 그때 프론트엔드에 에러 메시지를 보내도록 변경했습니다. + +
+ +### ⏰ 알림 서비스 +1. **구현 방법** +- Spring 스케줄러를 활용하여 매분마다 알림 시간이 도래한 요양보호사와 보호자를 찾아 필요한 알림 메시지를 전송합니다. +- 알림 메시지는 미리 정의된 템플릿을 기반으로 구성하며, 사용자가 선택한 알림 수단(Line 또는 SMS)에 맞춰 발송됩니다. +- 사용자 편의를 위해 ‘마이페이지’에서 Line 알림 서비스와 SMS 알림 서비스를 선택할 수 있는 옵션을 제공했습니다. + +2. **문제 해결** +- 메시지 전송 중복 및 전송 실패 시 오류 처리가 어려웠던 부분은 Amazon SQS를 통해 메시지 큐 관리 기능을 추가하여 문제를 해결했습니다. +- 카카오 비즈니스 채널 가입에 필요한 서류 심사에서 반려되었으나, 장기적으로 카카오 알림톡 도입 가능성을 염두에 두고, 현재는 Line과 SMS API를 대체 수단으로 활용했습니다. + +
+ +### 📊 엑셀 파일 관리 기능 +  엑셀 파일 관리 기능을 통해 요양원에서 다수의 요양보호사, 보호자, 돌봄대상자 정보를 한 번에 효율적으로 등록할 수 있습니다. 요양원은 제공된 엑셀 템플릿 파일을 다운로드해 데이터를 일괄적으로 입력하고 업로드하여 개별 입력보다 시간을 절감할 수 있습니다. + +  업로드된 파일은 서버에서 유효성 검사와 중복 검사를 거쳐 형식이 맞지 않거나 중복된 데이터는 데이터베이스에 저장되지 않습니다. 검사를 통과한 데이터만 데이터베이스에 저장되며, 검사에 통과하지 못한 오류 데이터는 데이터베이스에 저장되지 않아, 정상 데이터만 안전하게 관리됩니다. + +
+ +## 🧩 ERD +

+ caregiver_difficulty +

+ +
+
+ +--- + +## 🌌 백엔드 전체 구상도 +

+ caregiver_difficulty +

+ +
+
+ +--- +## 📄 팀 그라운드 규칙 설명 +### [📑 팀 그라운드 룰](https://www.notion.so/e4ce811fa70d4feb94f988eefef9c380) +### [😀 PR 템플릿 & 이슈 템플릿](https://www.notion.so/PR-e7db382239564304b49614cb6681cf22) +### [⛳️ 커밋 컨벤션](https://www.notion.so/43ef62a4a9b842bdba1d954f1601ef54) + +### 🏛️ 프로젝트 구조 +## 파일 구조 +``` +└───📂src + ├───📂main + │ ├───📂java.dbdr + │ │ ├─── 📁domain + │ │ │ ├───📁admin + │ │ │ ├───📁careworker + │ │ │ ├───📁chart + │ │ │ ├───📁core + │ │ │ │ ├───📁alarm + │ │ │ │ ├───📁base + │ │ │ │ ├───📁messaging + │ │ │ │ ├───📁ocr + │ │ │ │ └───📁s3 + │ │ │ │ + │ │ │ ├───📁excel + │ │ │ ├───📁guardian + │ │ │ ├───📁institution + │ │ │ └───📁recipient + │ │ ├───📁global + │ │ │ ├───📁configuration + │ │ │ ├───📁exception + │ │ │ └───📁util + │ │ ├───📁openai + │ │ └───📁security + │ └───📂resources + │ + └───📂test + ├───📂java.dbdr + │ ├───📁careworker + │ ├───📁chart + │ ├───📁e2etest + │ ├───📁global + │ ├───📁messaging + │ ├───📁openAi + │ ├───📁security + │ └───📁testhelper + └───📂resources +``` + + + +### 🕹️ How to start + +1. 프로젝트를 클론합니다. + + ``` + $ git clone https://github.com/kakao-tech-campus-2nd-step3/Team13_BE.git + ``` + +2. `Temp13_BE/src/resources` 파일에 `application-secret.yml`을 넣어줍니다. + + ``` + $ cd Team13_BE/src/resources # 디렉토리 이동 + $ vi application-seceret.yml # application-secret.yml 파일 수정 및 저장 진행하기 + ``` + 다음과 같은 구조에 키 값들을 꼭 넣어주기!! (단, port의 경우 local과 배포 서버에 설정되는 값이 다릅니다) + ``` + data: + redis: + port: # redis port + host: # redis host + datasoruce: + url: # mysql rds url + username: # mysql username + password: # mysql password + driver-class-name: # mysql driver class name + secret: # jwt secret key + line: + channelAccessToken: # line channel access token + channelSecret: # line channel secret + aws: + accessKey: # aws access key + secretKey: # aws secret key + region: # aws region + openai: + apiKey: # openai api key + naver: + api-url: # naver clova api url + secret-key: # naver clova secret key + ``` + +3. 2.의 방법과 동일하게 테스트 환경에 맞는 `application-test.yml`도 넣어줍니다. + +4. ci/cd 혹은 script를 통해 배포를 진행합니다. diff --git a/docs/source/be_structure.png b/docs/source/be_structure.png new file mode 100644 index 00000000..5648871a Binary files /dev/null and b/docs/source/be_structure.png differ diff --git a/docs/source/care_bridge.png b/docs/source/care_bridge.png new file mode 100644 index 00000000..8e14436b Binary files /dev/null and b/docs/source/care_bridge.png differ diff --git a/docs/source/care_message.jpg b/docs/source/care_message.jpg new file mode 100644 index 00000000..06d6e7fc Binary files /dev/null and b/docs/source/care_message.jpg differ diff --git a/docs/source/caregiver_difficulty.png b/docs/source/caregiver_difficulty.png new file mode 100644 index 00000000..361f5d75 Binary files /dev/null and b/docs/source/caregiver_difficulty.png differ diff --git a/docs/source/chart_summary.png b/docs/source/chart_summary.png new file mode 100644 index 00000000..eb34bec4 Binary files /dev/null and b/docs/source/chart_summary.png differ diff --git a/docs/source/chart_view.png b/docs/source/chart_view.png new file mode 100644 index 00000000..c70bb46a Binary files /dev/null and b/docs/source/chart_view.png differ diff --git a/docs/source/chart_write.png b/docs/source/chart_write.png new file mode 100644 index 00000000..43bf6590 Binary files /dev/null and b/docs/source/chart_write.png differ diff --git a/docs/source/erd.png b/docs/source/erd.png new file mode 100644 index 00000000..4baeb8f8 Binary files /dev/null and b/docs/source/erd.png differ diff --git a/docs/source/ocr_example.png b/docs/source/ocr_example.png new file mode 100644 index 00000000..7725fd54 Binary files /dev/null and b/docs/source/ocr_example.png differ diff --git a/docs/source/voice_recognition.png b/docs/source/voice_recognition.png new file mode 100644 index 00000000..a4ac79db Binary files /dev/null and b/docs/source/voice_recognition.png differ diff --git a/src/main/java/dbdr/domain/core/messaging/util/MessagingScheduler.java b/src/main/java/dbdr/domain/core/messaging/util/MessagingScheduler.java index 39994392..44054275 100644 --- a/src/main/java/dbdr/domain/core/messaging/util/MessagingScheduler.java +++ b/src/main/java/dbdr/domain/core/messaging/util/MessagingScheduler.java @@ -1,6 +1,5 @@ package dbdr.domain.core.messaging.util; -import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.util.List; @@ -61,12 +60,12 @@ public void sendChartUpdate() { String name = careworker.getName(); // (1) Line 채널 알림 보내기 - if (alarm != null && careworker.isLineSubscription()) { + if (alarm != null && careworker.isLineSubscription() && careworker.isWorkingOn(currentDateTime.getDayOfWeek())) { log.info("Line 알림 메세지를 받을 보호자 : {}", name); alarmService.sendAlarmToSqs(alarm, MessageChannel.LINE, name, careworker.getPhone(), careworker.getLineUserId()); } // (2) SMS 알림 보내기 - if (alarm != null && careworker.isSmsSubscription()) { + if (alarm != null && careworker.isSmsSubscription() && careworker.isWorkingOn(currentDateTime.getDayOfWeek())) { log.info("SMS 문자 알림 메세지를 받을 보호자 : {}", name); alarmService.sendAlarmToSqs(alarm, MessageChannel.SMS, name, careworker.getPhone(), careworker.getLineUserId()); } diff --git a/src/main/java/dbdr/exception/ApplicationError.java b/src/main/java/dbdr/exception/ApplicationError.java deleted file mode 100644 index 84a28382..00000000 --- a/src/main/java/dbdr/exception/ApplicationError.java +++ /dev/null @@ -1,39 +0,0 @@ -package dbdr.exception; - -import lombok.AllArgsConstructor; -import lombok.Getter; -import org.springframework.http.HttpStatus; - -@AllArgsConstructor -@Getter -public enum ApplicationError { - - //Auth - ROLE_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 유저가 ROLE을 가지고 있지 않습니다."), - ACCESS_NOT_ALLOWED(HttpStatus.FORBIDDEN, "접근 권한이 없습니다."), - - // Guardian (보호자) - GUARDIAN_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 보호자를 찾을 수가 없습니다."), - - // Careworker (요양보호사) - CAREWORKER_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 요양보호사를 찾을 수가 없습니다."), - - // Recipient (돌봄대상자) - RECIPIENT_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 돌봄대상자를 찾을 수가 없습니다."), - - // Institution (요양원) - INSTITUTION_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 요양원을 찾을 수가 없습니다."), - - // 공통 - DUPLICATE_EMAIL(HttpStatus.CONFLICT, "이미 존재하는 이메일입니다."), - DUPLICATE_PHONE(HttpStatus.CONFLICT, "이미 존재하는 전화번호입니다."), - DUPLICATE_CARE_NUMBER(HttpStatus.CONFLICT, "이미 존재하는 장기요양번호입니다."), - DUPLICATE_INSTITUTION_NUMBER(HttpStatus.CONFLICT, "이미 존재하는 요양기관번호입니다."), - INVALID_INPUT(HttpStatus.BAD_REQUEST, "잘못된 입력값입니다."), - - // 시스템 - DATABASE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "데이터베이스 처리 중 오류가 발생했습니다."); - - private final HttpStatus status; - private final String message; -} diff --git a/src/test/java/dbdr/chart/OcrMockTest.java b/src/test/java/dbdr/chart/OcrMockTest.java new file mode 100644 index 00000000..fcbc00cd --- /dev/null +++ b/src/test/java/dbdr/chart/OcrMockTest.java @@ -0,0 +1,5 @@ +package dbdr.chart; + +public class OcrMockTest { + +} diff --git a/src/test/java/dbdr/messaging/AlarmServiceTest.java b/src/test/java/dbdr/messaging/AlarmServiceTest.java new file mode 100644 index 00000000..dca7586d --- /dev/null +++ b/src/test/java/dbdr/messaging/AlarmServiceTest.java @@ -0,0 +1,103 @@ +package dbdr.messaging; + +import static org.mockito.Mockito.*; +import static org.junit.jupiter.api.Assertions.*; + +import java.time.LocalDateTime; +import java.util.Optional; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import dbdr.domain.core.alarm.entity.Alarm; +import dbdr.domain.core.alarm.repository.AlarmRepository; +import dbdr.domain.core.alarm.service.AlarmService; +import dbdr.domain.core.messaging.MessageChannel; +import dbdr.domain.core.messaging.dto.SqsMessageDto; +import dbdr.domain.core.messaging.service.CallSqsService; +import dbdr.domain.guardian.entity.Guardian; +import dbdr.domain.guardian.repository.GuardianRepository; +import dbdr.domain.recipient.entity.Recipient; +import dbdr.domain.chart.entity.Chart; +import dbdr.domain.recipient.service.RecipientService; +import dbdr.domain.chart.repository.ChartRepository; +import dbdr.openai.dto.response.SummaryResponse; +import dbdr.openai.service.SummaryService; + +class AlarmServiceTest { + + @Mock + private AlarmRepository alarmRepository; + + @Mock + private CallSqsService callSqsService; + + @Mock + private GuardianRepository guardianRepository; + + @Mock + private RecipientService recipientService; + + @Mock + private ChartRepository chartRepository; + + @Mock + private SummaryService summaryService; + + @InjectMocks + private AlarmService alarmService; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + void testSendAlarmToSqs() { + Alarm alarm = mock(Alarm.class); + String name = "테스트 보호자"; + String phoneNumber = "01012345678"; + String lineUserId = "test_line_user"; + + String expectedMessage = String.format("알림 메시지 %s", name); + SqsMessageDto messageDto = new SqsMessageDto(MessageChannel.LINE, lineUserId, expectedMessage, phoneNumber); + + when(alarm.getMessage()).thenReturn("알림 메시지 %s"); + when(alarmRepository.save(alarm)).thenReturn(alarm); + + alarmService.sendAlarmToSqs(alarm, MessageChannel.LINE, name, phoneNumber, lineUserId); + + verify(callSqsService).sendMessage(messageDto); + verify(alarmRepository).save(alarm); + } + + @Test + void testGetGuardianAlarmMessage() { + when(recipientService.isChartWrittenYesterday(anyLong())).thenReturn(true); + + Guardian guardian = mock(Guardian.class); + when(guardian.getPhone()).thenReturn("01012345678"); + when(guardianRepository.findById(anyLong())).thenReturn(Optional.of(guardian)); + + // Recipient 객체 모킹 + Recipient recipient = mock(Recipient.class); + when(recipient.getGuardian()).thenReturn(guardian); + + // Chart 객체 모킹 + Chart chart = mock(Chart.class); + when(chart.getRecipient()).thenReturn(recipient); + when(chartRepository.findById(anyLong())).thenReturn(Optional.of(chart)); + + // SummaryResponse 모킹 + SummaryResponse mockSummaryResponse = new SummaryResponse("condition", "body", "nursing", "cognitive", "recovery"); + when(summaryService.getSummarization(anyLong())).thenReturn(mockSummaryResponse); + + // 테스트 실행 + Alarm alarm = alarmService.getGuardianAlarmMessage(1L, LocalDateTime.now()); + assertNotNull(alarm); + verify(recipientService).isChartWrittenYesterday(anyLong()); + } +} diff --git a/src/test/java/dbdr/messaging/CallSqsServiceTest.java b/src/test/java/dbdr/messaging/CallSqsServiceTest.java new file mode 100644 index 00000000..3e14f396 --- /dev/null +++ b/src/test/java/dbdr/messaging/CallSqsServiceTest.java @@ -0,0 +1,96 @@ +package dbdr.messaging; + +import static org.mockito.Mockito.*; +import static org.junit.jupiter.api.Assertions.*; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.awspring.cloud.sqs.operations.SendResult; +import io.awspring.cloud.sqs.operations.SqsTemplate; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import dbdr.domain.core.messaging.MessageChannel; +import dbdr.domain.core.messaging.dto.SqsMessageDto; +import dbdr.domain.core.messaging.service.CallSqsService; +import dbdr.domain.core.messaging.service.LineMessagingService; +import dbdr.domain.core.messaging.service.SmsMessagingService; + +class CallSqsServiceTest { + + @Mock + private SqsTemplate sqsTemplate; // 제네릭 타입 없이 그대로 사용 + + @Mock + private LineMessagingService lineMessagingService; + + @Mock + private SmsMessagingService smsMessagingService; + + @Mock + private ObjectMapper objectMapper; + + @InjectMocks + private CallSqsService callSqsService; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + void testSendMessage() throws Exception { + // Mock 데이터 생성 + SqsMessageDto messageDto = new SqsMessageDto(MessageChannel.LINE, "test_user", "Test Message", "01012345678"); + String messageJson = "{ \"message\": \"Test Message\" }"; + SendResult sendResult = mock(SendResult.class); + + // Mocking + when(objectMapper.writeValueAsString(any(SqsMessageDto.class))).thenReturn(messageJson); + when(sqsTemplate.send(any())).thenReturn((SendResult) sendResult); // 강제 형변환을 SendResult로 처리 + + // 메서드 실행 + SendResult result = callSqsService.sendMessage(messageDto); + + // 검증 + verify(objectMapper).writeValueAsString(messageDto); + verify(sqsTemplate).send(any()); + assertNotNull(result); + } + + @Test + void testReceiveMessageWithLineChannel() throws Exception { + // Mock 데이터 생성 + SqsMessageDto messageDto = new SqsMessageDto(MessageChannel.LINE, "test_user", "Test Message", "01012345678"); + String messageJson = "{ \"message\": \"Test Message\" }"; + + // Mocking + when(objectMapper.readValue(messageJson, SqsMessageDto.class)).thenReturn(messageDto); + + // 메서드 실행 + callSqsService.receiveMessage(messageJson); + + // 검증 + verify(lineMessagingService).pushAlarmMessage(messageDto.getUserId(), messageDto.getMessage()); + verify(smsMessagingService, never()).sendMessageToUser(anyString(), anyString()); + } + + @Test + void testReceiveMessageWithSmsChannel() throws Exception { + // Mock 데이터 생성 + SqsMessageDto messageDto = new SqsMessageDto(MessageChannel.SMS, null, "Test Message", "01012345678"); + String messageJson = "{ \"message\": \"Test Message\" }"; + + // Mocking + when(objectMapper.readValue(messageJson, SqsMessageDto.class)).thenReturn(messageDto); + + // 메서드 실행 + callSqsService.receiveMessage(messageJson); + + // 검증 + verify(smsMessagingService).sendMessageToUser(messageDto.getPhoneNumber(), messageDto.getMessage()); + verify(lineMessagingService, never()).pushAlarmMessage(anyString(), anyString()); + } +} diff --git a/src/test/java/dbdr/messaging/MessagingSchedulerTest.java b/src/test/java/dbdr/messaging/MessagingSchedulerTest.java new file mode 100644 index 00000000..7d43cae3 --- /dev/null +++ b/src/test/java/dbdr/messaging/MessagingSchedulerTest.java @@ -0,0 +1,71 @@ +package dbdr.messaging; + +import static org.mockito.Mockito.*; +import static org.junit.jupiter.api.Assertions.*; + +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import dbdr.domain.careworker.entity.Careworker; +import dbdr.domain.careworker.service.CareworkerService; +import dbdr.domain.core.alarm.entity.Alarm; +import dbdr.domain.core.alarm.service.AlarmService; +import dbdr.domain.core.messaging.MessageChannel; +import dbdr.domain.guardian.entity.Guardian; +import dbdr.domain.guardian.service.GuardianService; +import dbdr.domain.core.messaging.util.MessagingScheduler; + +class MessagingSchedulerTest { + + @Mock + private GuardianService guardianService; + + @Mock + private CareworkerService careworkerService; + + @Mock + private AlarmService alarmService; + + @InjectMocks + private MessagingScheduler messagingScheduler; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + void testSendChartUpdate() { + // Mock 데이터 생성 + Guardian guardian = mock(Guardian.class); + Careworker careworker = mock(Careworker.class); + Alarm guardianAlarm = mock(Alarm.class); + Alarm careworkerAlarm = mock(Alarm.class); + + when(guardianService.findByAlertTime(any(LocalTime.class))).thenReturn(List.of(guardian)); + when(careworkerService.findByAlertTime(any(LocalTime.class))).thenReturn(List.of(careworker)); + when(alarmService.getGuardianAlarmMessage(anyLong(), any(LocalDateTime.class))).thenReturn(guardianAlarm); + when(alarmService.getCareworkerAlarmMessage(anyLong(), any(LocalDateTime.class))).thenReturn(careworkerAlarm); + when(guardian.isLineSubscription()).thenReturn(true); + when(guardian.isSmsSubscription()).thenReturn(true); + when(careworker.isLineSubscription()).thenReturn(true); + when(careworker.isSmsSubscription()).thenReturn(true); + when(careworker.isWorkingOn(any())).thenReturn(true); + + // 메서드 실행 + messagingScheduler.sendChartUpdate(); + + // 알람 서비스 호출 확인 + verify(alarmService).sendAlarmToSqs(guardianAlarm, MessageChannel.LINE, guardian.getName(), guardian.getPhone(), guardian.getLineUserId()); + verify(alarmService).sendAlarmToSqs(guardianAlarm, MessageChannel.SMS, guardian.getName(), guardian.getPhone(), guardian.getLineUserId()); + verify(alarmService).sendAlarmToSqs(careworkerAlarm, MessageChannel.LINE, careworker.getName(), careworker.getPhone(), careworker.getLineUserId()); + verify(alarmService).sendAlarmToSqs(careworkerAlarm, MessageChannel.SMS, careworker.getName(), careworker.getPhone(), careworker.getLineUserId()); + } +}