diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml
new file mode 100644
index 00000000..da4260e2
--- /dev/null
+++ b/.github/workflows/gradle.yml
@@ -0,0 +1,69 @@
+name: Java CI and Deploy to AWS EC2 (Mock Docker Deployment)
+
+on:
+ push:
+ branches: [ "readme" ]
+
+jobs:
+ deploy:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Save SSH key
+ env:
+ EC2_SSH_KEY: ${{ secrets.EC2_SSH_KEY }}
+ run: |
+ echo "$EC2_SSH_KEY" > ec2-key.pem
+ chmod 600 ec2-key.pem
+
+ - name: Upload source code to EC2
+ env:
+ EC2_HOST: ${{ secrets.EC2_HOST }}
+ EC2_USER: ${{ secrets.EC2_USER }}
+ run: |
+ scp -i ec2-key.pem -o StrictHostKeyChecking=no -r ./* $EC2_USER@$EC2_HOST:/home/$EC2_USER/app
+
+ - name: Mock Build and Deploy 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'
+
+ MOCK=true
+
+ # 빌드 단계
+ if [ "$MOCK" = true ]; then
+ echo "EC2에서 애플리케이션 빌드 중..."
+ echo "./gradlew clean build -x test"
+ else
+ ./gradlew clean build -x test
+ fi
+
+ # Docker 이미지 빌드 단계
+ if [ "$MOCK" = true ]; then
+ echo "Docker 이미지를 빌드하는 중..."
+ echo "docker build -t care-app:latest ."
+ else
+ docker build -t care-app:latest .
+ fi
+
+ # 기존 컨테이너 중지 및 삭제
+ if [ "$MOCK" = true ]; then
+ echo "기존 Docker 컨테이너 중지 및 삭제 중..."
+ echo "docker stop care-app-container || true"
+ echo "docker rm care-app-container || true"
+ else
+ docker stop care-app-container || true
+ docker rm care-app-container || true
+ fi
+
+ # 새 Docker 컨테이너 실행
+ if [ "$MOCK" = true ]; then
+ echo "새로운 Docker 컨테이너 실행 중..."
+ echo "docker run -d --name care-app-container -v /home/$EC2_USER/app/config/application-secret.yml:/app/config/application-secret.yml -p 8080:8080 care-app:latest"
+ else
+ docker run -d --name care-app-container -v /home/$EC2_USER/app/config/application-secret.yml:/app/config/application-secret.yml -p 8080:8080 care-app:latest
+ fi
+
+ echo "배포 완료"
+ EOF
diff --git a/README.md b/README.md
index 0521e853..62245852 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,493 @@
-# Team13_BE
-13조 백엔드
+# 🤝 [Team 13] 돌봄다리 - 요양원 관리 서비스
+
+
+
+
+
+
+
+> 목차
+> - [🧑💻 Collaborators](#-collaborators)
+> - [⚙️ 개발 스택](#-개발-스택)
+> - [🔗 프로젝트 관련 주소](#-프로젝트-관련-주소)
+> - [🤩 샘플 아이디 & 비밀번호](#-샘플-아이디--비밀번호)
+> - [🌟 돌봄다리란?](#-돌봄다리란)
+> - [🧐 서비스의 필요성](#-서비스의-필요성)
+> - [🧩 서비스 핵심 기능](#-서비스-핵심-기능)
+> - [🔧 공통 핵심 개발 영역](#-공통-핵심-개발-영역)
+> - [🔧 FE 핵심 개발 영역](#-fe-핵심-개발-영역)
+> - [🔧 BE 핵심 개발 영역](#-be-핵심-개발-영역)
+> - [🧩 ERD](#-erd)
+> - [🌌 백엔드 전체 구상도](#-백엔드-전체-구상도)
+> - [📄 팀 그라운드 규칙 설명](#-팀-그라운드-규칙-설명)
+
+# 🧑💻 Collaborators
+
+
+### 🗓️ 개발 기간
+2024.09 ~ 2024.11 (카카오 테크 캠퍼스 2기 - Step3)
+
+
+
+
+
+
+Backend
+
+
+
+| **테크 리더** | **기획 리더** | **리액셔너** | **리마인더** | **리마인더** |
+| ------------- | ------------- | ------------ | ------------ | ------------ |
+|
[이영준](https://github.com/20jcode)
|
[김태윤](https://github.com/pykido)
|
[유경미](https://github.com/yooookm)
|
[박혜연](https://github.com/hyyyh0x)
|
[이진솔](https://github.com/mogld)
|
+|
|
|
|
|
|
+
+
+
+Frontend
+
+
+
+| **조장** | **타임 키퍼** |
+| ------------- | ------------- |
+|
[문정윤](https://github.com/nnoonjy)
|
[이지수](https://github.com/dlwltn0430)
|
+|
|
|
+
+
+
+
+
+
+
+---
+
+## ⚙️ 개발 스택
+
+
+
+![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. 요양보호사의 차트 작성 어려움
+- **요양보호사**는 복잡한 디지털 기록 시스템에 익숙하지 않아 핸드폰으로 차트를 작성하는 과정이 번거롭고 어렵습니다.
+- 이러한 어려움은 기록의 정확성과 신속성을 저해하고, 요양보호사의 업무 효율성에도 부정적인 영향을 미칩니다.
+
+
+
+
+
+```
+요양보호사의 요구 - 복잡하지 않고 간단한 차트 작성 방식이 필요하다.
+
+➡️ 요양보호사가 쉽게 차트를 작성할 수 있도록 하는 간편한 기록 시스템이 필요하다!
+```
+
+### 🎯 해결 방안
+- **음성 인식 및 손글씨 인식** 기능을 통해 요양보호사가 복잡한 절차 없이 차트를 쉽게 작성할 수 있도록 지원합니다.
+- 기록 작성의 간소화를 통해 요양보호사의 부담을 줄이고, 환자의 상태를 신속하고 정확하게 기록할 수 있도록 합니다.
+
+
+
+
+---
+
+## 🧩 서비스 핵심 기능
+
+
+
+## 보호자
+
+
+
+| 📝 **돌봄대상자 차트 확인** | 📝 **차트 요약** |
+|:-------------------------------------------------------------------------------------------------------------------------------:|:-----------------------------------------------------------------------------------------------------------------------------------:|
+| **하루 상태 기록 확인**
사진과 차트 작성 시 **알림 수신** | 긴 차트를 **핵심 내용 요약**
주요 사항을 **간결하게 확인** |
+|
|
|
+
+
+## 요양보호사
+
+
+
+| 🖋️ **요양 일지 작성** | 🎙️ **음성 인식 차트 작성** |
+|:--------------------------------------------------------------------------------------------------------------------------:|:-----------------------------------------------------------------------------------------------------------------------------:|
+| **음성/사진 인식**, 직접 작성 지원
**다양한 방식으로 간편 작성** | **음성 인식**을 통해 주관식 입력
음성을 텍스트로 **자동 변환** |
+|
|
|
+
+
+| 📷 **OCR 차트 작성** | 📑 **차트 요약 기능** | 🔔 **알림 기능** |
+|:--------------------------------------------------------------------------------:|:------------------------------------------------------------------------------------------------------------------------------------:|:-----------------------------------------------------------------------:|
+| **차트 양식 프린트 후 사진 인식**
사진 한 장으로 **자동 기록 완성** | **환자 상태 요약 제공**
여러 환자의 **하루 상태 간편 확인** | 사용자가 예약한 시간마다
문자/라인 메시지로 차트 작성 알림 |
+|
|
|
|
+
+
+
+## 요양원
+
+
+
+| 🖥️ **요양사, 보호자, 돌봄대상자 관리** | 📊 **엑셀 업로드** |
+|:------------------------------------------------------------------------------------------------------------------------------:|:--------------------------------------------------------------------------------------------------------------------------:|
+| **웹사이트로 정보 관리**
요양사, 보호자, 대상자 정보 **수정 가능** | 엑셀 파일로 **대량 데이터 업로드**
제공된 템플릿 파일로 **간편 등록** |
+|
|
|
+
+
+
+
+
+
+---
+
+## 🔧 공통 핵심 개발 영역
+
+### 🙆 회원가입
+- 사용자의 연령층을 고려할 때, 직접 회원가입하고 정보를 등록하는 것이 어려울 것이라 생각하여 요양원이나 관리자가 돌봄대상자나 보호자의 아이디, 비밀번호를 생성해줍니다.
+- 돌봄대상자와 보호자는 비밀번호만 기억하면 서비스 이용이 가능하도록 아이디는 본인의 전화번호로 등록하도록 구현하였습니다.
+- 관리자는 요양원, 보호자, 돌봄대상자, 요양보호사를 등록할 수 있습니다.
+- 요양원은 보호자, 돌봄대상자, 요양보호사를 해당 요양원에 등록할 수 있습니다.
+- 관리자의 경우 랜딩 페이지에 적힌 이메일로 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에 저장된 리프레시 토큰을 삭제하고, 액세스 토큰은 블랙리스트에 등록해 유효성을 차단했습니다. 이를 통해 로그아웃 시 실시간으로 토큰 만료를 효과적으로 처리할 수 있었습니다.
+
+### 📷 OCR 기능
+
+ 요양보호사가 작성한 돌봄 대상자 차트를 효율적으로 디지털화하기 위해 Naver Clova OCR API와 AWS S3의 presigned URL을 사용했습니다.
+
+ presigned URL을 통해 이미지 파일을 S3에 업로드하고, 백엔드 서버에는 objectKey 값만 전달하여 OCR을 수행하는 방식으로 서버 과부하를 방지하고 성능을 최적화했습니다. 이로써 서버 리소스를 절약하면서도 보안성을 유지한 상태에서 차트를 안전하게 OCR 처리할 수 있도록 구현했습니다.
+
+
+
+
+
+
+### 🤖 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
+
+
+
+
+
+
+
+---
+
+## 🌌 백엔드 전체 구상도
+
+
+
+
+
+
+
+---
+
+## 📄 팀 그라운드 규칙 설명
+### [📑 팀 그라운드 룰](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/DbdrApplication.java b/src/main/java/dbdr/DbdrApplication.java
index 95209512..cbf112dd 100644
--- a/src/main/java/dbdr/DbdrApplication.java
+++ b/src/main/java/dbdr/DbdrApplication.java
@@ -5,13 +5,19 @@
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.scheduling.annotation.EnableScheduling;
+import java.util.TimeZone;
+
@SpringBootApplication
@EnableJpaAuditing
@EnableScheduling
public class DbdrApplication {
+ static {
+ // 애플리케이션이 시작되기 전에 서울 시간으로 타임존을 설정
+ TimeZone.setDefault(TimeZone.getTimeZone("Asia/Seoul"));
+ }
+
public static void main(String[] args) {
SpringApplication.run(DbdrApplication.class, args);
}
-
}
diff --git a/src/main/java/dbdr/controller/AdminGuardiansController.java b/src/main/java/dbdr/controller/AdminGuardiansController.java
deleted file mode 100644
index 9a2db008..00000000
--- a/src/main/java/dbdr/controller/AdminGuardiansController.java
+++ /dev/null
@@ -1,61 +0,0 @@
-package dbdr.controller;
-
-import dbdr.dto.request.GuardiansRequest;
-import dbdr.dto.response.GuardiansResponse;
-import dbdr.service.GuardiansService;
-import jakarta.validation.Valid;
-import java.util.List;
-import lombok.RequiredArgsConstructor;
-import org.springframework.http.HttpStatus;
-import org.springframework.http.ResponseEntity;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.PutMapping;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
-
-@RestController
-@RequestMapping("/v1/admin/guardians")
-@RequiredArgsConstructor
-public class AdminGuardiansController {
-
- private final GuardiansService guardiansService;
-
- @GetMapping
- public ResponseEntity> showAllGuardians() {
- List guardiansResponseList = guardiansService.getAllGuardians();
- return ResponseEntity.ok(guardiansResponseList);
- }
-
- @GetMapping("/{guardianId}")
- public ResponseEntity showOneGuardian(
- @PathVariable("guardianId") Long guardianId) {
- GuardiansResponse guardiansResponse = guardiansService.getGuardianById(guardianId);
- return ResponseEntity.ok(guardiansResponse);
- }
-
- @PostMapping
- public ResponseEntity addGuardian(
- @Valid @RequestBody GuardiansRequest guardiansRequest) {
- GuardiansResponse guardiansResponse = guardiansService.addGuardian(guardiansRequest);
- return ResponseEntity.status(HttpStatus.CREATED).body(guardiansResponse);
- }
-
- @PutMapping("/{guardianId}/update")
- public ResponseEntity updateGuardianAuth(
- @PathVariable("guardianId") Long guardianId,
- @Valid @RequestBody GuardiansRequest guardiansRequest) {
- GuardiansResponse guardiansResponse = guardiansService.updateGuardianById(guardianId,
- guardiansRequest);
- return ResponseEntity.ok(guardiansResponse);
- }
-
- @PutMapping("/{guardianId}/delete")
- public ResponseEntity deleteGuardianAuth(
- @PathVariable("guardianId") Long guardianId) {
- guardiansService.deleteGuardianById(guardianId);
- return ResponseEntity.noContent().build();
- }
-}
diff --git a/src/main/java/dbdr/controller/CareworkerController.java b/src/main/java/dbdr/controller/CareworkerController.java
deleted file mode 100644
index 2ab1bd54..00000000
--- a/src/main/java/dbdr/controller/CareworkerController.java
+++ /dev/null
@@ -1,59 +0,0 @@
-package dbdr.controller;
-
-import dbdr.dto.request.CareworkerRequestDTO;
-import dbdr.dto.response.CareworkerResponseDTO;
-import dbdr.service.CareworkerService;
-import jakarta.validation.Valid;
-import org.springframework.http.ResponseEntity;
-import org.springframework.web.bind.annotation.*;
-
-import java.net.URI;
-import java.util.List;
-
-@RestController
-@RequestMapping("/v1/careworkers")
-public class CareworkerController {
-
- private final CareworkerService careworkerService;
-
- public CareworkerController(CareworkerService careworkerService) {
- this.careworkerService = careworkerService;
- }
-
- @GetMapping
- public ResponseEntity> getAllCareworkers(
- @RequestParam(value = "institutionId", required = false) Long institutionId) {
- List careworkers;
- if (institutionId != null) {
- careworkers = careworkerService.getCareworkersByInstitution(institutionId);
- } else {
- careworkers = careworkerService.getAllCareworkers();
- }
- return ResponseEntity.ok(careworkers);
- }
-
- @GetMapping("/{id}")
- public ResponseEntity getCareworkerById(@PathVariable Long id) {
- CareworkerResponseDTO careworker = careworkerService.getCareworkerById(id);
- return ResponseEntity.ok(careworker);
- }
-
- @PostMapping
- public ResponseEntity createCareworker(@Valid @RequestBody CareworkerRequestDTO careworkerDTO) {
- CareworkerResponseDTO newCareworker = careworkerService.createCareworker(careworkerDTO);
- return ResponseEntity.created(URI.create("/v1/careworkers/" + newCareworker.getId()))
- .body(newCareworker);
- }
-
- @PutMapping("/{id}")
- public ResponseEntity updateCareworker(@PathVariable Long id, @Valid @RequestBody CareworkerRequestDTO careworkerDTO) {
- CareworkerResponseDTO updatedCareworker = careworkerService.updateCareworker(id, careworkerDTO);
- return ResponseEntity.ok(updatedCareworker);
- }
-
- @DeleteMapping("/{id}")
- public ResponseEntity deleteCareworker(@PathVariable Long id) {
- careworkerService.deleteCareworker(id);
- return ResponseEntity.noContent().build();
- }
-}
diff --git a/src/main/java/dbdr/controller/GuardiansController.java b/src/main/java/dbdr/controller/GuardiansController.java
deleted file mode 100644
index a8e569b4..00000000
--- a/src/main/java/dbdr/controller/GuardiansController.java
+++ /dev/null
@@ -1,38 +0,0 @@
-package dbdr.controller;
-
-import dbdr.dto.request.GuardiansRequest;
-import dbdr.dto.response.GuardiansResponse;
-import dbdr.service.GuardiansService;
-import jakarta.validation.Valid;
-import lombok.RequiredArgsConstructor;
-import org.springframework.http.ResponseEntity;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.PutMapping;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
-
-@RestController
-@RequestMapping("/v1/guardians")
-@RequiredArgsConstructor
-public class GuardiansController {
-
- private final GuardiansService guardiansService;
-
- @GetMapping("/{guardianId}")
- public ResponseEntity showGuardianInfo(
- @PathVariable("guardianId") Long guardianId) {
- GuardiansResponse guardiansResponse = guardiansService.getGuardianById(guardianId);
- return ResponseEntity.ok(guardiansResponse);
- }
-
- @PutMapping("/{guardianId}")
- public ResponseEntity updateGuardianInfo(
- @PathVariable("guardianId") Long guardianId,
- @Valid @RequestBody GuardiansRequest guardiansRequest) {
- GuardiansResponse guardiansResponse = guardiansService.updateGuardianById(guardianId,
- guardiansRequest);
- return ResponseEntity.ok(guardiansResponse);
- }
-}
diff --git a/src/main/java/dbdr/controller/RecipientController.java b/src/main/java/dbdr/controller/RecipientController.java
deleted file mode 100644
index fa666658..00000000
--- a/src/main/java/dbdr/controller/RecipientController.java
+++ /dev/null
@@ -1,53 +0,0 @@
-package dbdr.controller;
-
-import dbdr.dto.request.RecipientRequestDTO;
-import dbdr.dto.response.RecipientResponseDTO;
-import dbdr.service.RecipientService;
-import org.springframework.http.ResponseEntity;
-import org.springframework.web.bind.annotation.*;
-import jakarta.validation.Valid;
-
-import java.net.URI;
-import java.util.List;
-
-@RestController
-@RequestMapping("/v1/recipients")
-public class RecipientController {
-
- private final RecipientService recipientService;
-
- public RecipientController(RecipientService recipientService) {
- this.recipientService = recipientService;
- }
-
- @GetMapping
- public ResponseEntity> getAllRecipients() {
- List recipients = recipientService.getAllRecipients();
- return ResponseEntity.ok(recipients);
- }
-
- @GetMapping("/{id}")
- public ResponseEntity getRecipientById(@PathVariable Long id) {
- RecipientResponseDTO recipient = recipientService.getRecipientById(id);
- return ResponseEntity.ok(recipient);
- }
-
- @PostMapping
- public ResponseEntity createRecipient(@Valid @RequestBody RecipientRequestDTO recipientDTO) {
- RecipientResponseDTO newRecipient = recipientService.createRecipient(recipientDTO);
- return ResponseEntity.created(URI.create("/v1/recipients/" + newRecipient.getId()))
- .body(newRecipient);
- }
-
- @PutMapping("/{id}")
- public ResponseEntity updateRecipient(@PathVariable Long id, @RequestBody RecipientRequestDTO recipientDTO) {
- RecipientResponseDTO updatedRecipient = recipientService.updateRecipient(id, recipientDTO);
- return ResponseEntity.ok(updatedRecipient);
- }
-
- @DeleteMapping("/{id}")
- public ResponseEntity deleteRecipient(@PathVariable Long id) {
- recipientService.deleteRecipient(id);
- return ResponseEntity.noContent().build();
- }
-}
diff --git a/src/main/java/dbdr/domain/BaseEntity.java b/src/main/java/dbdr/domain/BaseEntity.java
deleted file mode 100644
index 2ef6de67..00000000
--- a/src/main/java/dbdr/domain/BaseEntity.java
+++ /dev/null
@@ -1,40 +0,0 @@
-package dbdr.domain;
-
-import jakarta.persistence.Column;
-import jakarta.persistence.EntityListeners;
-import jakarta.persistence.GeneratedValue;
-import jakarta.persistence.GenerationType;
-import jakarta.persistence.Id;
-import jakarta.persistence.MappedSuperclass;
-import java.time.LocalDateTime;
-import lombok.Getter;
-import org.hibernate.annotations.ColumnDefault;
-import org.springframework.data.annotation.CreatedDate;
-import org.springframework.data.annotation.LastModifiedDate;
-import org.springframework.data.jpa.domain.support.AuditingEntityListener;
-
-@Getter
-@MappedSuperclass
-@EntityListeners(AuditingEntityListener.class)
-public abstract class BaseEntity {
-
- @Id
- @GeneratedValue(strategy = GenerationType.IDENTITY)
- private Long id;
-
- @Column(updatable = false, nullable = false)
- @CreatedDate
- private LocalDateTime createdAt;
-
- @Column(nullable = true)
- @LastModifiedDate
- private LocalDateTime updateAt;
-
- @Column(nullable = false)
- @ColumnDefault("true")
- private boolean isActive = true;
-
- public void deactivate() {
- this.isActive = false;
- }
-}
\ No newline at end of file
diff --git a/src/main/java/dbdr/domain/Careworker.java b/src/main/java/dbdr/domain/Careworker.java
deleted file mode 100644
index 2fa06728..00000000
--- a/src/main/java/dbdr/domain/Careworker.java
+++ /dev/null
@@ -1,44 +0,0 @@
-package dbdr.domain;
-
-import dbdr.dto.request.CareworkerRequestDTO;
-import jakarta.persistence.*;
-import lombok.Getter;
-import lombok.NoArgsConstructor;
-import org.hibernate.annotations.SQLDelete;
-import org.hibernate.annotations.SQLRestriction;
-
-@Entity
-@Getter
-@NoArgsConstructor
-@Table(name = "careworker")
-@SQLDelete(sql = "UPDATE careworker SET is_active = false WHERE id = ?")
-@SQLRestriction("is_active= true")
-public class Careworker extends BaseEntity {
-
- @Column(nullable = false)
- private Long institutionId;
-
- @Column(nullable = false)
- private String name;
-
- @Column(nullable = false, unique = true)
- private String email;
-
- @Column(nullable = false)
- private String phone;
-
-
- public Careworker(Long institutionId, String name, String email, String phone) {
- this.institutionId = institutionId;
- this.name = name;
- this.email = email;
- this.phone = phone;
- }
-
- public void updateCareworker(CareworkerRequestDTO careworkerDTO) {
- //this.institutionId = careworkerDTO.getInstitutionId();
- this.name = careworkerDTO.getName();
- this.email = careworkerDTO.getEmail();
- this.phone = careworkerDTO.getPhone();
- }
-}
\ No newline at end of file
diff --git a/src/main/java/dbdr/domain/Guardians.java b/src/main/java/dbdr/domain/Guardians.java
deleted file mode 100644
index 41f79b3a..00000000
--- a/src/main/java/dbdr/domain/Guardians.java
+++ /dev/null
@@ -1,33 +0,0 @@
-package dbdr.domain;
-
-import jakarta.persistence.Column;
-import jakarta.persistence.Entity;
-import jakarta.persistence.Table;
-import lombok.AccessLevel;
-import lombok.Builder;
-import lombok.Getter;
-import lombok.NoArgsConstructor;
-
-@Entity
-@Getter
-@Table(name = "guardians")
-@NoArgsConstructor(access = AccessLevel.PROTECTED)
-public class Guardians extends BaseEntity {
-
- @Column(nullable = false, unique = true)
- private String phone;
- @Column(nullable = false, length = 50)
- private String name;
-
- @Builder
- public Guardians(String phone, String name) {
- this.phone = phone;
- this.name = name;
- }
-
- @Builder
- public void updateGuardian(String phone, String name) {
- this.phone = phone;
- this.name = name;
- }
-}
diff --git a/src/main/java/dbdr/domain/Recipient.java b/src/main/java/dbdr/domain/Recipient.java
deleted file mode 100644
index a3875cd2..00000000
--- a/src/main/java/dbdr/domain/Recipient.java
+++ /dev/null
@@ -1,62 +0,0 @@
-package dbdr.domain;
-
-import dbdr.dto.request.RecipientRequestDTO;
-import jakarta.persistence.*;
-import lombok.AllArgsConstructor;
-import lombok.Getter;
-import lombok.NoArgsConstructor;
-import org.hibernate.annotations.SQLDelete;
-import org.hibernate.annotations.SQLRestriction;
-
-import java.time.LocalDate;
-
-@Entity
-@Getter
-@NoArgsConstructor
-@AllArgsConstructor
-@Table(name = "recipient")
-@SQLDelete(sql = "UPDATE recipient SET is_active = false WHERE id = ?")
-@SQLRestriction("is_active = true")
-public class Recipient extends BaseEntity {
-
- @Column(nullable = false)
- private String name;
-
- @Column(nullable = false)
- private LocalDate birth;
-
- @Column(nullable = false)
- private String gender;
-
- @Column(nullable = false)
- private String careLevel;
-
- @Column(nullable = false, unique = true)
- private String careNumber;
-
- @Column(nullable = false)
- private LocalDate startDate;
-
- @Column(nullable = false)
- private String institution;
-
- @Column(nullable = false)
- private Long institutionNumber;
-
- @ManyToOne(fetch = FetchType.LAZY)
- @JoinColumn(name = "careworker_id")
- private Careworker careworker;
-
- public void updateRecipient(RecipientRequestDTO recipientDTO) {
- this.name = recipientDTO.getName();
- this.birth = recipientDTO.getBirth();
- this.gender = recipientDTO.getGender();
- this.careLevel = recipientDTO.getCareLevel();
- this.careNumber = recipientDTO.getCareNumber();
- this.startDate = recipientDTO.getStartDate();
- this.institution = recipientDTO.getInstitution();
- this.institutionNumber = recipientDTO.getInstitutionNumber();
- //this.careworker = careworker;
- }
-
-}
\ No newline at end of file
diff --git a/src/main/java/dbdr/domain/admin/controller/AdminController.java b/src/main/java/dbdr/domain/admin/controller/AdminController.java
index fa097516..40dec8c6 100644
--- a/src/main/java/dbdr/domain/admin/controller/AdminController.java
+++ b/src/main/java/dbdr/domain/admin/controller/AdminController.java
@@ -44,6 +44,7 @@ public ResponseEntity getAdmin(@PathVariable(name = "id") Long id){
@Operation(summary = "서버관리자 추가")
@PostMapping("/add")
+ @DbdrAuth(targetRole = Role.ADMIN)
public ResponseEntity addAdmin(@RequestBody AdminCreateRequest adminCreateRequest){
log.info("어드민 생성 시작, {} {}", adminCreateRequest.loginId(), adminCreateRequest.loginPassword());
diff --git a/src/main/java/dbdr/domain/careworker/controller/CareworkerAdminController.java b/src/main/java/dbdr/domain/careworker/controller/CareworkerAdminController.java
index e0fc02e1..681ed8a9 100644
--- a/src/main/java/dbdr/domain/careworker/controller/CareworkerAdminController.java
+++ b/src/main/java/dbdr/domain/careworker/controller/CareworkerAdminController.java
@@ -26,7 +26,6 @@
public class CareworkerAdminController {
private final CareworkerService careworkerService;
- private final InstitutionService institutionService;
@DbdrAuth(targetRole = Role.ADMIN)
@Operation(summary = "전체 요양보호사 정보 조회", security = @SecurityRequirement(name = "JWT"))
diff --git a/src/main/java/dbdr/domain/careworker/controller/CareworkerController.java b/src/main/java/dbdr/domain/careworker/controller/CareworkerController.java
index 1a9ea81f..1073be00 100644
--- a/src/main/java/dbdr/domain/careworker/controller/CareworkerController.java
+++ b/src/main/java/dbdr/domain/careworker/controller/CareworkerController.java
@@ -42,13 +42,13 @@ public ResponseEntity> showCarework
return ResponseEntity.ok(ApiUtils.success(response));
}
- @Operation(summary = "요양보호사 본인의 근무일과 알림 시간 수정", security = @SecurityRequirement(name = "JWT"))
+ @Operation(summary = "요양보호사 본인의 정보 수정", security = @SecurityRequirement(name = "JWT"))
@PutMapping
@DbdrAuth(targetRole = Role.CAREWORKER,authParam = AuthParam.LOGIN_CAREWORKER)
public ResponseEntity> updateCareworkerInfo(
@Parameter(hidden = true) @LoginCareworker Careworker careworker,
@Valid @RequestBody CareworkerUpdateRequest careworkerRequest) {
- CareworkerMyPageResponse updatedResponse = careworkerService.updateWorkingDaysAndAlertTime(careworker.getId(),
+ CareworkerMyPageResponse updatedResponse = careworkerService.getMyPageCareworkerInfo(careworker.getId(),
careworkerRequest);
return ResponseEntity.ok(ApiUtils.success(updatedResponse));
}
diff --git a/src/main/java/dbdr/domain/careworker/controller/CareworkerInstitutionController.java b/src/main/java/dbdr/domain/careworker/controller/CareworkerInstitutionController.java
index 9d4bfae0..c05a1f72 100644
--- a/src/main/java/dbdr/domain/careworker/controller/CareworkerInstitutionController.java
+++ b/src/main/java/dbdr/domain/careworker/controller/CareworkerInstitutionController.java
@@ -31,7 +31,7 @@ public class CareworkerInstitutionController {
@DbdrAuth(targetRole = Role.INSTITUTION)
@Operation(summary = "특정 요양원아이디로 전체 요양보호사 정보 조회", security = @SecurityRequirement(name = "JWT"))
- @GetMapping("/institution")
+ @GetMapping
public ResponseEntity>> getAllCareworkers(
@Parameter(hidden = true) @LoginInstitution Institution institution) {
List institutions = careworkerService.getCareworkersByInstitution(institution.getId());
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 03719794..afb39642 100644
--- a/src/main/java/dbdr/domain/careworker/dto/request/CareworkerUpdateRequest.java
+++ b/src/main/java/dbdr/domain/careworker/dto/request/CareworkerUpdateRequest.java
@@ -1,26 +1,21 @@
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;
-
-import java.time.DayOfWeek;
import java.time.LocalTime;
import java.util.Set;
+import java.time.DayOfWeek;
-import com.fasterxml.jackson.annotation.JsonFormat;
+public record CareworkerUpdateRequest(
+ @Schema(description = "요양보호사의 알림 시간", example = "17:00:00")
+ LocalTime alertTime,
-@Getter
-@AllArgsConstructor
-public class CareworkerUpdateRequest {
+ @Schema(description = "근무 요일")
+ Set workingDays,
- @NotNull(message = "근무일은 필수 항목입니다.")
- @Schema(description = "근무 요일 목록", example = "[\"MONDAY\", \"WEDNESDAY\", \"FRIDAY\"]")
- private Set workingDays;
+ @Schema(description = "SMS 수신 동의 여부", example = "true")
+ boolean smsSubscription,
- @JsonFormat(pattern = "HH:mm")
- @NotNull(message = "알림 시간은 필수 항목입니다.")
- @Schema(description = "알림 시간 (HH:mm 형식)", example = "17:00")
- private LocalTime alertTime;
+ @Schema(description = "LINE 수신 동의 여부", example = "true")
+ boolean lineSubscription
+) {
}
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 cf5565a5..cfb123f2 100644
--- a/src/main/java/dbdr/domain/careworker/dto/response/CareworkerMyPageResponse.java
+++ b/src/main/java/dbdr/domain/careworker/dto/response/CareworkerMyPageResponse.java
@@ -15,4 +15,6 @@ public class CareworkerMyPageResponse {
private String institutionName;
private LocalTime alertTime;
private Set workingDays;
+ private boolean smsSubscription;
+ private boolean lineSubscription;
}
diff --git a/src/main/java/dbdr/domain/careworker/dto/response/CareworkerResponse.java b/src/main/java/dbdr/domain/careworker/dto/response/CareworkerResponse.java
index 50ac672e..69ef6f43 100644
--- a/src/main/java/dbdr/domain/careworker/dto/response/CareworkerResponse.java
+++ b/src/main/java/dbdr/domain/careworker/dto/response/CareworkerResponse.java
@@ -10,7 +10,6 @@
@NoArgsConstructor
@AllArgsConstructor
public class CareworkerResponse {
-
private Long id;
private Long institutionId;
private String name;
diff --git a/src/main/java/dbdr/domain/careworker/entity/Careworker.java b/src/main/java/dbdr/domain/careworker/entity/Careworker.java
index 2b393f8a..65280dc8 100644
--- a/src/main/java/dbdr/domain/careworker/entity/Careworker.java
+++ b/src/main/java/dbdr/domain/careworker/entity/Careworker.java
@@ -6,17 +6,12 @@
import dbdr.domain.core.base.entity.BaseEntity;
import dbdr.domain.institution.entity.Institution;
import jakarta.persistence.Column;
-import jakarta.persistence.ElementCollection;
import jakarta.persistence.Entity;
-import jakarta.persistence.EnumType;
-import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import jakarta.validation.constraints.Pattern;
-import java.time.DayOfWeek;
-import java.time.LocalTime;
import java.util.EnumSet;
import java.util.Set;
import lombok.AccessLevel;
@@ -53,9 +48,15 @@ public class Careworker extends BaseEntity {
@Column(nullable = true)
private String lineUserId;
- @Column(nullable = true)
+ @Column(nullable = false)
private LocalTime alertTime = LocalTime.of(17, 0); // 오후 5시로 초기화
+ @Column(nullable = false)
+ private boolean smsSubscription = false;
+
+ @Column(nullable = false)
+ private boolean lineSubscription = false;
+
@Column(unique = true)
private String email;
@@ -124,4 +125,9 @@ public boolean isWorkingOn(DayOfWeek day) {
public void updateInstitution(Institution institution) {
this.institution = institution;
}
+
+ public void updateSubscriptions(boolean smsSubscription, boolean lineSubscription) {
+ this.smsSubscription = smsSubscription;
+ this.lineSubscription = lineSubscription;
+ }
}
diff --git a/src/main/java/dbdr/domain/careworker/service/CareworkerService.java b/src/main/java/dbdr/domain/careworker/service/CareworkerService.java
index d597ac1d..c648470f 100644
--- a/src/main/java/dbdr/domain/careworker/service/CareworkerService.java
+++ b/src/main/java/dbdr/domain/careworker/service/CareworkerService.java
@@ -27,7 +27,6 @@ public class CareworkerService {
private final CareworkerRepository careworkerRepository;
private final InstitutionService institutionService;
- private final AlarmService alarmService;
private final CareworkerMapper careworkerMapper;
@Transactional(readOnly = true)
@@ -73,12 +72,8 @@ public List getAllCareworkers() {
public CareworkerResponse createCareworker(CareworkerRequest careworkerRequestDTO) {
ensureUniqueEmail(careworkerRequestDTO.getEmail());
ensureUniquePhone(careworkerRequestDTO.getPhone());
-
Careworker careworker = careworkerMapper.toEntity(careworkerRequestDTO);
-
careworkerRepository.save(careworker);
- alarmService.createCareworkerAlarm(careworker);
-
return careworkerMapper.toResponse(careworker);
}
@@ -94,7 +89,6 @@ public CareworkerResponse createCareworkerInstitution(CareworkerRequest carework
Careworker careworker = careworkerMapper.toEntity(careworkerRequestDTO);
careworkerRepository.save(careworker);
- alarmService.createCareworkerAlarm(careworker);
return careworkerMapper.toResponse(careworker);
}
@@ -106,10 +100,6 @@ public CareworkerResponse updateCareworker(Long careworkerId, CareworkerRequest
ensureUniqueEmailButNotId(request.getEmail(), careworkerId);
Careworker careworker = findCareworkerById(careworkerId);
- /*if (!careworker.getInstitution().equals(institution)) {
- throw new ApplicationException(ApplicationError.ACCESS_NOT_ALLOWED);
- }*/
-
careworker.updateCareworker(careworkerMapper.toEntity(request));
return careworkerMapper.toResponse(careworker);
}
@@ -136,7 +126,6 @@ public CareworkerResponse updateCareworkerByInstitution(Long careworkerId, Carew
ensureUniqueEmailButNotId(request.getEmail(), careworkerId);
Careworker careworker = findCareworkerById(careworkerId);
-
careworker.updateCareworker(toEntity(request, careworker));
return careworkerMapper.toResponse(careworker);
}
@@ -165,13 +154,17 @@ public CareworkerMyPageResponse getMyPageInfo(Long careworkerId) {
}
@Transactional
- public CareworkerMyPageResponse updateWorkingDaysAndAlertTime(Long careworkerId, CareworkerUpdateRequest request) {
+ public CareworkerMyPageResponse getMyPageCareworkerInfo(Long careworkerId, CareworkerUpdateRequest request) {
Careworker careworker = careworkerRepository.findById(careworkerId)
- .orElseThrow(() -> new ApplicationException(ApplicationError.CAREWORKER_NOT_FOUND));
+ .orElseThrow(() -> new ApplicationException(ApplicationError.CAREWORKER_NOT_FOUND));
- careworker.updateWorkingDays(request.getWorkingDays());
- careworker.updateAlertTime(request.getAlertTime());
- alarmService.updateAlarmByLocalTime(request.getAlertTime(), careworker.getPhone());
+ if (request.workingDays() != null) {
+ careworker.updateWorkingDays(request.workingDays());
+ }
+ if (request.alertTime() != null) {
+ careworker.updateAlertTime(request.alertTime());
+ }
+ careworker.updateSubscriptions(request.smsSubscription(), request.lineSubscription());
return toMyPageResponseDTO(careworker);
}
@@ -194,11 +187,6 @@ private void ensureUniquePhone(String phone) {
}
}
- /*private CareworkerResponseDTO toResponseDTO(Careworker careworker) {
- return new CareworkerResponseDTO(careworker.getId(), careworker.getInstitution().getId(),
- careworker.getName(), careworker.getEmail(), careworker.getPhone());
- }*/
-
private void ensureUniquePhoneButNotId(String phone, Long id) {
if(careworkerRepository.existsByPhoneAndIdNot(phone, id)) {
throw new ApplicationException(ApplicationError.DUPLICATE_PHONE);
@@ -221,11 +209,13 @@ public Careworker findByPhone(String phoneNumber) {
private CareworkerMyPageResponse toMyPageResponseDTO(Careworker careworker) {
return new CareworkerMyPageResponse(
- careworker.getName(),
- careworker.getPhone(),
- careworker.getInstitution().getInstitutionName(),
- careworker.getAlertTime(),
- careworker.getWorkingDays()
+ careworker.getName(),
+ careworker.getPhone(),
+ careworker.getInstitution().getInstitutionName(),
+ careworker.getAlertTime(),
+ careworker.getWorkingDays(),
+ careworker.isSmsSubscription(),
+ careworker.isLineSubscription()
);
}
@@ -257,5 +247,4 @@ public void updateLineUserId(String userId, String phoneNumber) {
careworker.updateLineUserId(userId);
careworkerRepository.save(careworker);
}
-
}
diff --git a/src/main/java/dbdr/domain/chart/BodyManagement.java b/src/main/java/dbdr/domain/chart/BodyManagement.java
deleted file mode 100644
index e3f7efa5..00000000
--- a/src/main/java/dbdr/domain/chart/BodyManagement.java
+++ /dev/null
@@ -1,28 +0,0 @@
-package dbdr.domain.chart;
-
-import dbdr.domain.BaseEntity;
-import jakarta.persistence.Column;
-import jakarta.persistence.Embedded;
-import jakarta.persistence.Entity;
-import jakarta.persistence.Table;
-import org.hibernate.annotations.SQLDelete;
-
-@Entity
-@Table(name = "body_management")
-@SQLDelete(sql = "UPDATE body_management SET is_active = false WHERE id = ?")
-public class BodyManagement extends BaseEntity {
- @Embedded
- private PhysicalClear physicalClear; // 세면 및 목욕 체크박스
-
- @Embedded
- private PhysicalMeal physicalMeal; // 식사 종류와 섭취량
-
- @Column(length = 50)
- private int physicalRestroom; // 화장실 횟수
-
- @Column(length = 255)
- private String physicalWalk; // 산책 or 외출 동행
-
- @Column(length = 1000)
- private String physicalNote; // 특이사항 입력
-}
diff --git a/src/main/java/dbdr/domain/chart/Chart.java b/src/main/java/dbdr/domain/chart/Chart.java
deleted file mode 100644
index 0c955745..00000000
--- a/src/main/java/dbdr/domain/chart/Chart.java
+++ /dev/null
@@ -1,35 +0,0 @@
-package dbdr.domain.chart;
-
-
-import dbdr.domain.BaseEntity;
-import dbdr.domain.Recipient;
-import jakarta.persistence.Entity;
-import jakarta.persistence.FetchType;
-import jakarta.persistence.JoinColumn;
-import jakarta.persistence.ManyToOne;
-import jakarta.persistence.OneToOne;
-import jakarta.persistence.Table;
-import org.hibernate.annotations.SQLDelete;
-import org.hibernate.annotations.SQLRestriction;
-
-@Entity
-@Table(name = "chart")
-@SQLDelete(sql = "UPDATE chart SET is_active = false WHERE id = ?")
-@SQLRestriction("is_active = true")
-public class Chart extends BaseEntity {
- @ManyToOne(fetch = FetchType.LAZY)
- @JoinColumn(name = "recipient_id", nullable = false)
- private Recipient recipient;
-
- @OneToOne
- @JoinColumn(name = "body_management_id")
- private BodyManagement bodyManagement;
-
- @OneToOne
- @JoinColumn(name = "nursing_management_id")
- private NursingManagement nursingManagement;
-
- @OneToOne
- @JoinColumn(name = "recovery_training_id")
- private RecoveryTraining recoveryTraining;
-}
diff --git a/src/main/java/dbdr/domain/chart/HealthBloodPressure.java b/src/main/java/dbdr/domain/chart/HealthBloodPressure.java
deleted file mode 100644
index 2fbc8085..00000000
--- a/src/main/java/dbdr/domain/chart/HealthBloodPressure.java
+++ /dev/null
@@ -1,16 +0,0 @@
-package dbdr.domain.chart;
-
-import jakarta.persistence.Embeddable;
-import lombok.AllArgsConstructor;
-import lombok.Getter;
-import lombok.NoArgsConstructor;
-
-@Embeddable
-@Getter
-@AllArgsConstructor
-@NoArgsConstructor
-public class HealthBloodPressure {
-
- private int systolic; // 혈압 최고
- private int diastolic; // 혈압 최저
-}
\ No newline at end of file
diff --git a/src/main/java/dbdr/domain/chart/NursingManagement.java b/src/main/java/dbdr/domain/chart/NursingManagement.java
deleted file mode 100644
index d6fd1624..00000000
--- a/src/main/java/dbdr/domain/chart/NursingManagement.java
+++ /dev/null
@@ -1,22 +0,0 @@
-package dbdr.domain.chart;
-
-import dbdr.domain.BaseEntity;
-import jakarta.persistence.Column;
-import jakarta.persistence.Embedded;
-import jakarta.persistence.Entity;
-import jakarta.persistence.Table;
-import org.hibernate.annotations.SQLDelete;
-
-@Entity
-@Table(name = "nursing_management")
-@SQLDelete(sql = "UPDATE nursing_management SET is_active = false WHERE id = ?")
-public class NursingManagement extends BaseEntity {
- @Embedded
- private HealthBloodPressure healthBloodPressure; // 혈압 임베디드 타입
-
- private String healthTemperature; // 체온
-
- @Column(length = 1000)
- private String healthNote; // 건강 및 간호관리 특이사항
-
-}
diff --git a/src/main/java/dbdr/domain/chart/PhysicalClear.java b/src/main/java/dbdr/domain/chart/PhysicalClear.java
deleted file mode 100644
index 04d56508..00000000
--- a/src/main/java/dbdr/domain/chart/PhysicalClear.java
+++ /dev/null
@@ -1,18 +0,0 @@
-package dbdr.domain.chart;
-
-import jakarta.persistence.Embeddable;
-import lombok.AllArgsConstructor;
-import lombok.Getter;
-import lombok.NoArgsConstructor;
-
-@Embeddable
-@Getter
-@AllArgsConstructor
-@NoArgsConstructor
-public class PhysicalClear {
- // 세면 유무
- private boolean wash;
-
- // 목욕 유무
- private boolean bath;
-}
\ No newline at end of file
diff --git a/src/main/java/dbdr/domain/chart/PhysicalMeal.java b/src/main/java/dbdr/domain/chart/PhysicalMeal.java
deleted file mode 100644
index de49e309..00000000
--- a/src/main/java/dbdr/domain/chart/PhysicalMeal.java
+++ /dev/null
@@ -1,18 +0,0 @@
-package dbdr.domain.chart;
-
-import jakarta.persistence.Embeddable;
-import lombok.AllArgsConstructor;
-import lombok.Getter;
-import lombok.NoArgsConstructor;
-
-@Embeddable
-@Getter
-@AllArgsConstructor
-@NoArgsConstructor
-public class PhysicalMeal {
- // 식사 종류
- private String mealType;
-
- // 섭취량
- private String intakeAmount;
-}
diff --git a/src/main/java/dbdr/domain/chart/RecoveryTraining.java b/src/main/java/dbdr/domain/chart/RecoveryTraining.java
deleted file mode 100644
index 8539128a..00000000
--- a/src/main/java/dbdr/domain/chart/RecoveryTraining.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package dbdr.domain.chart;
-
-import dbdr.domain.BaseEntity;
-import jakarta.persistence.Column;
-import jakarta.persistence.Entity;
-import jakarta.persistence.Table;
-import org.hibernate.annotations.SQLDelete;
-import org.hibernate.annotations.SQLRestriction;
-
-@Entity
-@Table(name = "recovery_training")
-@SQLDelete(sql = "UPDATE recovery_training SET is_active = false WHERE id = ?")
-@SQLRestriction("is_active = true")
-public class RecoveryTraining extends BaseEntity {
- @Column(length = 255)
- private String recoveryProgram; // 회복 프로그램 이름
-
- private boolean recoveryTraining; // 회복훈련 완료 여부
-
- @Column(length = 1000)
- private String recoveryNote; // 회복훈련 특이사항
-
-}
diff --git a/src/main/java/dbdr/domain/chart/repository/ChartRepository.java b/src/main/java/dbdr/domain/chart/repository/ChartRepository.java
index 7632bcc0..77c21c8e 100644
--- a/src/main/java/dbdr/domain/chart/repository/ChartRepository.java
+++ b/src/main/java/dbdr/domain/chart/repository/ChartRepository.java
@@ -1,9 +1,14 @@
package dbdr.domain.chart.repository;
import dbdr.domain.chart.entity.Chart;
+
+import java.time.LocalDateTime;
import java.util.List;
+import java.util.Optional;
+
import org.springframework.data.jpa.repository.JpaRepository;
public interface ChartRepository extends JpaRepository {
- List findAllByRecipientId(Long recipientId);
-}
+ List findAllByRecipientId(Long recipientId);
+
+ Optional findByRecipientIdAndCreatedAtBetween(Long recipientId, LocalDateTime startDateTime, LocalDateTime endDateTime);}
diff --git a/src/main/java/dbdr/domain/chart/service/ChartService.java b/src/main/java/dbdr/domain/chart/service/ChartService.java
index f5172ab5..75775849 100644
--- a/src/main/java/dbdr/domain/chart/service/ChartService.java
+++ b/src/main/java/dbdr/domain/chart/service/ChartService.java
@@ -8,14 +8,11 @@
import dbdr.domain.chart.dto.response.ChartOverviewResponse;
import dbdr.domain.chart.entity.Chart;
import dbdr.domain.chart.repository.ChartRepository;
-import dbdr.domain.core.alarm.service.AlarmService;
import dbdr.global.exception.ApplicationError;
import dbdr.global.exception.ApplicationException;
import java.util.List;
import java.util.stream.Collectors;
import dbdr.global.configuration.OpenAiSummarizationConfig;
-import dbdr.global.exception.ApplicationError;
-import dbdr.global.exception.ApplicationException;
import dbdr.openai.dto.etc.Message;
import dbdr.openai.dto.request.ChartDataRequest;
import dbdr.openai.dto.request.OpenAiSummaryRequest;
@@ -27,15 +24,11 @@
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
-import java.util.List;
import java.util.Map;
import java.util.function.Function;
-import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
-import org.springframework.data.domain.Page;
-import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
@@ -52,7 +45,6 @@ public class ChartService {
private final ChartMapper chartMapper;
private final SummaryRepository summaryRepository;
private final OpenAiSummarizationConfig summarizationConfig;
- private final AlarmService alarmService;
@Value("${openai.chat-completions}")
private String chatUrl;
@@ -92,7 +84,6 @@ public ChartDetailResponse saveChart(ChartDetailRequest request) {
summaryResponse.conditionDisease(), summaryResponse.nursingManagement(),
tagResponse.tag1(), tagResponse.tag2(), tagResponse.tag3()));
ChartDetailResponse chartDetailResponse = chartMapper.toResponse(savedChart);
- alarmService.updateGuardianAlarmMessage(chartDetailResponse);
return chartDetailResponse;
}
@@ -132,14 +123,20 @@ private TagResponse parseTagString(String tagString) {
}
public OpenAiSummaryResponse openAiResponse(String str, String tempModel) {
- HttpHeaders headers = summarizationConfig.httpHeaders();
- Message userMessage = new Message("user", str);
- List messageList = List.of(userMessage);
- OpenAiSummaryRequest request = new OpenAiSummaryRequest(tempModel, messageList);
- ResponseEntity response = summarizationConfig.restTemplate()
- .exchange(chatUrl, HttpMethod.POST, new HttpEntity<>(request, headers),
- OpenAiSummaryResponse.class);
- return response.getBody();
+ int cnt = 0;
+ while(cnt<3) {
+ HttpHeaders headers = summarizationConfig.httpHeaders();
+ Message userMessage = new Message("user", str);
+ List messageList = List.of(userMessage);
+ OpenAiSummaryRequest request = new OpenAiSummaryRequest(tempModel, messageList);
+ ResponseEntity response = summarizationConfig.restTemplate()
+ .exchange(chatUrl, HttpMethod.POST, new HttpEntity<>(request, headers),
+ OpenAiSummaryResponse.class);
+ if (response!=null && response.getBody() != null && response.getStatusCode().is2xxSuccessful())
+ return response.getBody();
+ else cnt++;
+ }
+ throw new ApplicationException(ApplicationError.OPEN_AI_ERROR);
}
private SummaryResponse getTextAndGetSummary(Chart chart) {
diff --git a/src/main/java/dbdr/domain/core/alarm/entity/Alarm.java b/src/main/java/dbdr/domain/core/alarm/entity/Alarm.java
index fae25645..bcc7fbe5 100644
--- a/src/main/java/dbdr/domain/core/alarm/entity/Alarm.java
+++ b/src/main/java/dbdr/domain/core/alarm/entity/Alarm.java
@@ -38,9 +38,6 @@ public class Alarm extends BaseEntity {
@Column(nullable = true)
private MessageChannel channel;
- @Column(nullable = true)
- private String channelId;
-
@Column(nullable = false)
private String phone;
@@ -61,14 +58,4 @@ public Alarm(LocalDateTime alertTime, String message, String phone, Role role, L
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
index e0b0c558..4eec6c6b 100644
--- a/src/main/java/dbdr/domain/core/alarm/repository/AlarmRepository.java
+++ b/src/main/java/dbdr/domain/core/alarm/repository/AlarmRepository.java
@@ -1,12 +1,7 @@
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
index 55eca434..a1d33846 100644
--- a/src/main/java/dbdr/domain/core/alarm/service/AlarmService.java
+++ b/src/main/java/dbdr/domain/core/alarm/service/AlarmService.java
@@ -1,15 +1,13 @@
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.chart.dto.response.ChartDetailResponse;
+import dbdr.domain.careworker.repository.CareworkerRepository;
+import dbdr.domain.chart.entity.Chart;
+import dbdr.domain.chart.repository.ChartRepository;
import dbdr.domain.core.alarm.entity.Alarm;
import dbdr.domain.core.messaging.MessageChannel;
import dbdr.domain.core.messaging.MessageTemplate;
@@ -17,11 +15,10 @@
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 dbdr.domain.recipient.dto.response.RecipientResponse;
-import dbdr.domain.recipient.entity.Recipient;
-import dbdr.domain.recipient.repository.RecipientRepository;
+import dbdr.domain.guardian.repository.GuardianRepository;
import dbdr.domain.recipient.service.RecipientService;
+import dbdr.openai.dto.response.SummaryResponse;
+import dbdr.openai.service.SummaryService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -31,142 +28,67 @@
public class AlarmService {
private final AlarmRepository alarmRepository;
private final CallSqsService callSqsService;
- private final RecipientRepository recipientRepository;
+ private final RecipientService recipientService;
+ private final CareworkerRepository careworkerRepository;
+ private final GuardianRepository guardianRepository;
+ private final ChartRepository chartRepository;
+ private final SummaryService summaryService;
- @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()
- );
+ @Transactional
+ public void sendAlarmToSqs(Alarm alarm, MessageChannel messageChannel, String name, String phoneNumber, String lineUserId) {
+ String alarmMessage = String.format(alarm.getMessage(), name);
+ callSqsService.sendMessage(new SqsMessageDto(messageChannel, lineUserId, alarmMessage, phoneNumber));
+ log.info("알림 메시지를 SQS로 전송했습니다. 이름 : {}, 메시지 : {}, 메세지 채널 : {}", name, alarmMessage, MessageChannel.valueOf(messageChannel.name()));
+ alarm.setSend(true);
+ alarm.setChannel(messageChannel);
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()
+ public Alarm getGuardianAlarmMessage(Long guardianId, LocalDateTime currentDateTime) {
+ // 어제 날짜로 차트 데이터가 있는지 확인하기
+ boolean isChartWrittenYesterday = recipientService.isChartWrittenYesterday(guardianId);
+ log.info("어제 날짜로 차트 데이터가 있는지 확인하기 : {}", isChartWrittenYesterday);
+ if (isChartWrittenYesterday) { // 어제자 차트 내용으로 알림 메시지 생성
+ Long chartId = recipientService.getChartIdByGuardianId(guardianId);
+ Chart chart = chartRepository.findById(chartId).orElseThrow(() -> new IllegalArgumentException("차트가 존재하지 않습니다."));
+ SummaryResponse summaryResponse = summaryService.getSummarization(chartId);
+ String message = MessageTemplate.CHART_UPDATED_MESSAGE.format(
+ guardianRepository.findById(guardianId).orElseThrow(() -> new IllegalArgumentException("보호자가 존재하지 않습니다.")).getName(),
+ summaryResponse.bodyManagement(),
+ summaryResponse.cognitiveManagement(),
+ summaryResponse.nursingManagement(),
+ summaryResponse.recoveryTraining()
);
- Alarm alarm = new Alarm(
- nextAlertTime,
- currentAlarm.getChannel(),
- currentAlarm.getChannelId(),
- MessageTemplate.CAREWORKER_ALARM_MESSAGE.getTemplate(),
- careworker.getPhone(),
- Role.CAREWORKER,
- careworker.getId()
+
+ return new Alarm(
+ currentDateTime,
+ message,
+ chart.getRecipient().getGuardian().getPhone(),
+ Role.GUARDIAN,
+ guardianId
);
- alarmRepository.save(alarm);
} else {
- log.warn("{} 요양보호사의 다음 근무일이 지정되지 않았습니다.", careworker.getName());
+ return new Alarm(
+ currentDateTime,
+ MessageTemplate.NO_CHART_MESSAGE.getTemplate(),
+ guardianRepository.findById(guardianId).orElseThrow(() -> new IllegalArgumentException("보호자가 존재하지 않습니다.")).getPhone(),
+ Role.GUARDIAN,
+ guardianId
+ );
}
}
- @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()
+ // 요양보호사 알림 메시지 생성
+ public Alarm getCareworkerAlarmMessage(Long careworkerId, LocalDateTime currentDateTime) {
+ return new Alarm(
+ currentDateTime,
+ MessageTemplate.CAREWORKER_ALARM_MESSAGE.getTemplate(),
+ careworkerRepository.findById(careworkerId).orElseThrow(() -> new IllegalArgumentException("요양보호사가 존재하지 않습니다.")).getPhone(),
+ Role.CAREWORKER,
+ careworkerId
);
- 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);
- }
- }
-
- @Transactional
- public void updateAlarmByLocalTime(LocalTime localTime, String phone) {
- Alarm alarm = alarmRepository.findByPhone(phone).orElse(null);
- if (alarm != null && !alarm.isSend()) {
- alarm.setAlertTime(localTime.atDate(alarm.getAlertTime().toLocalDate()));
- alarmRepository.save(alarm);
- }
- }
-
- @Transactional
- public void updateGuardianAlarmMessage(ChartDetailResponse chartDetailResponse) {
- Recipient recipient = recipientRepository.findById(chartDetailResponse.recipientId()).orElse(null);
- Alarm alarm = alarmRepository.findByPhone(recipient.getGuardian().getPhone()).orElse(null);
-
- if (alarm != null && !alarm.isSend() && alarm.getAlertTime().isAfter(LocalDateTime.now())) {
- // ChartDetailResponse의 데이터를 사용해 알림 메시지 생성
- String message = MessageTemplate.CHART_UPDATED_MESSAGE.format(
- recipient.getGuardian().getName(),
- chartDetailResponse.conditionDisease(),
- chartDetailResponse.bodyManagement().wash() ? "예" : "아니오",
- chartDetailResponse.bodyManagement().bath() ? "예" : "아니오",
- chartDetailResponse.bodyManagement().mealType(),
- chartDetailResponse.nursingManagement().systolic(),
- chartDetailResponse.nursingManagement().diastolic(),
- chartDetailResponse.nursingManagement().healthTemperature(),
- chartDetailResponse.cognitiveManagement().cognitiveHelp() ? "예" : "아니오",
- chartDetailResponse.recoveryTraining().recoveryProgram()
- );
-
- alarm.setMessage(message);
- alarmRepository.save(alarm);
- }
}
}
diff --git a/src/main/java/dbdr/domain/core/messaging/MessageTemplate.java b/src/main/java/dbdr/domain/core/messaging/MessageTemplate.java
index a3a1cd30..27df6a5c 100644
--- a/src/main/java/dbdr/domain/core/messaging/MessageTemplate.java
+++ b/src/main/java/dbdr/domain/core/messaging/MessageTemplate.java
@@ -2,21 +2,20 @@
public enum MessageTemplate {
- FOLLOW_MESSAGE("안녕하세요! 🌸\n 최고의 요양원 서비스 돌봄다리입니다. 🤗\n 서비스를 시작하려면 전화번호를 다음과 같은 형식으로 입력해주시기 바랍니다. 😄\n 예시 : 01012345678"),
- STRANGER_FOLLOW_MESSAGE("%s님, 안녕하세요! 🌸\n 저희 서비스는 보호자와 요양보호사를 위한 서비스입니다. \n 회원가입을 통해 이용해주시기 바랍니다. 😅"),
+ 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 돌봄다리 서비스의 마이페이지에서 알림 시간을 수정할 수 있습니다.\""),
+ 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이제 차트를 작성하실 시간입니다. 잊지 마시고 차트 작성 부탁드립니다! 📝"),
- CHART_UPDATED_MESSAGE("%s님, 안녕하세요! 😊 새로운 차트 정보가 업데이트되었습니다. 📋\n" +
- "- 상태/질환: %s\n" +
- "- 신체 관리: 세수 - %s, 목욕 - %s, 식사 - %s\n" +
- "- 간호 관리: 혈압 - %s/%s, 체온 - %s도\n" +
- "- 인지 관리: 인지 도움 제공 - %s\n" +
- "- 회복 훈련 프로그램: %s\n\n" +
- "더 자세한 내용은 돌봄다리에서 확인하세요. 감사합니다! 🙏");
+ CAREWORKER_ALARM_MESSAGE(" %s 요양보호사님, 오늘 하루는 어떠셨나요? 😊\n이제 차트를 작성하실 시간입니다. 잊지 마시고 차트 작성 부탁드립니다! 📝"),
+ CHART_UPDATED_MESSAGE(" %s님, 안녕하세요! 😊 돌봄대상자분의 새로운 차트 정보가 업데이트되었습니다. 📋\n\n" +
+ "(1) 신체 활동 지원 : %s\n" +
+ "(2) 인지관리 및 의사소통 : %s\n" +
+ "(3) 건강 및 간호 관리 : %s\n" +
+ "(4) 기능 회복 훈련 : %s\n\n" +
+ " 더 자세한 내용은 돌봄다리에서 확인하세요. 감사합니다! 🙏");
private final String template;
MessageTemplate(String template) {
diff --git a/src/main/java/dbdr/domain/core/messaging/dto/SqsMessageDto.java b/src/main/java/dbdr/domain/core/messaging/dto/SqsMessageDto.java
index 534699f5..38c56193 100644
--- a/src/main/java/dbdr/domain/core/messaging/dto/SqsMessageDto.java
+++ b/src/main/java/dbdr/domain/core/messaging/dto/SqsMessageDto.java
@@ -1,5 +1,6 @@
package dbdr.domain.core.messaging.dto;
+import dbdr.domain.core.messaging.MessageChannel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@@ -8,6 +9,8 @@
@NoArgsConstructor
@AllArgsConstructor
public class SqsMessageDto {
+ private MessageChannel messageChannel;
private String userId;
private String message;
+ private String phoneNumber;
}
diff --git a/src/main/java/dbdr/domain/core/messaging/service/CallSqsService.java b/src/main/java/dbdr/domain/core/messaging/service/CallSqsService.java
index 6ab9ecb5..5092426d 100644
--- a/src/main/java/dbdr/domain/core/messaging/service/CallSqsService.java
+++ b/src/main/java/dbdr/domain/core/messaging/service/CallSqsService.java
@@ -5,6 +5,7 @@
import com.fasterxml.jackson.databind.ObjectMapper;
+import dbdr.domain.core.messaging.MessageChannel;
import dbdr.domain.core.messaging.dto.SqsMessageDto;
import io.awspring.cloud.sqs.annotation.SqsListener;
import io.awspring.cloud.sqs.operations.SendResult;
@@ -19,6 +20,7 @@ public class CallSqsService {
private final SqsTemplate queueMessagingTemplate;
private final LineMessagingService lineMessagingService;
+ private final SmsMessagingService smsMessagingService;
private final ObjectMapper objectMapper; // JSON 변환용 ObjectMapper
@Value("${cloud.aws.sqs.queue-name}")
@@ -46,8 +48,13 @@ public void receiveMessage(String messageJson) {
SqsMessageDto messageDto = objectMapper.readValue(messageJson, SqsMessageDto.class);
log.info("Received message from SQS: {}", messageDto);
- // LineMessagingService를 통해 사용자에게 메시지 전송
- lineMessagingService.pushAlarmMessage(messageDto.getUserId(), messageDto.getMessage());
+ if (messageDto.getMessageChannel() != null && messageDto.getMessageChannel().equals(MessageChannel.LINE)) { // LineMessagingService를 통해 사용자에게 메시지 전송
+ log.info("라인 메시지 전송! 아이디 : {}, 메시지 : {} ", messageDto.getUserId(), messageDto.getMessage());
+ lineMessagingService.pushAlarmMessage(messageDto.getUserId(), messageDto.getMessage());
+ } else if (messageDto.getMessageChannel() != null && messageDto.getMessageChannel().equals(MessageChannel.SMS)) { // SMSMessagingService를 통해 사용자에게 메시지 전송
+ log.info("SMS 메시지 전송! 전화번호 : {}, 메시지 : {} ", messageDto.getPhoneNumber(), messageDto.getMessage());
+ smsMessagingService.sendMessageToUser(messageDto.getPhoneNumber(), 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 35c05d8e..d5806336 100644
--- a/src/main/java/dbdr/domain/core/messaging/service/LineMessagingService.java
+++ b/src/main/java/dbdr/domain/core/messaging/service/LineMessagingService.java
@@ -14,11 +14,10 @@
@Service
@RequiredArgsConstructor
@Slf4j
-public class LineMessagingService implements MessagingService{
+public class LineMessagingService {
private final LineMessagingClient lineMessagingClient;
// 사용자에게 메시지를 보내는 메서드
- @Override
public void sendMessageToUser(String userId, String message) {
TextMessage textMessage = new TextMessage(message);
PushMessage pushMessage = new PushMessage(userId, textMessage);
diff --git a/src/main/java/dbdr/domain/core/messaging/service/LineService.java b/src/main/java/dbdr/domain/core/messaging/service/LineService.java
index 0260e01f..738fb90d 100644
--- a/src/main/java/dbdr/domain/core/messaging/service/LineService.java
+++ b/src/main/java/dbdr/domain/core/messaging/service/LineService.java
@@ -95,11 +95,9 @@ private void receivePhoneNumber(String userId, String phoneNumber) {
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 {
// 보호자나 요양보호사가 아닌 경우
diff --git a/src/main/java/dbdr/domain/core/messaging/service/SmsMessagingService.java b/src/main/java/dbdr/domain/core/messaging/service/SmsMessagingService.java
index 8b165dba..0c3777ef 100644
--- a/src/main/java/dbdr/domain/core/messaging/service/SmsMessagingService.java
+++ b/src/main/java/dbdr/domain/core/messaging/service/SmsMessagingService.java
@@ -5,12 +5,15 @@
import net.nurigo.sdk.NurigoApp;
import net.nurigo.sdk.message.model.Message;
+import net.nurigo.sdk.message.model.MessageType;
import net.nurigo.sdk.message.request.SingleMessageSendingRequest;
import net.nurigo.sdk.message.response.SingleMessageSentResponse;
import net.nurigo.sdk.message.service.DefaultMessageService;
+import lombok.extern.slf4j.Slf4j;
@Service
-public class SmsMessagingService implements MessagingService {
+@Slf4j
+public class SmsMessagingService {
private final DefaultMessageService defaultMessageService;
@Value("${sms.from-number}")
@@ -22,14 +25,20 @@ public SmsMessagingService(@Value("${sms.api-key}") String apiKey,
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);
+ String subject = "안녕하세요! 돌봄다리 서비스입니다.";
+ log.info("Sending LMS to user : {}", userNumber);
+ log.info("Sending LMS subject : {}", subject);
+ log.info("Sending LMS message : {}", message);
- SingleMessageSentResponse response = defaultMessageService.sendOne(new SingleMessageSendingRequest(smsMessage));
+ Message lmsMessage = new Message();
+ lmsMessage.setFrom(fromNumber);
+ lmsMessage.setTo(userNumber);
+ lmsMessage.setText(message);
+ lmsMessage.setSubject(subject); // 제목 설정
+ lmsMessage.setType(MessageType.LMS); // LMS 타입 설정
+
+ SingleMessageSentResponse response = defaultMessageService.sendOne(new SingleMessageSendingRequest(lmsMessage));
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/util/MessagingScheduler.java b/src/main/java/dbdr/domain/core/messaging/util/MessagingScheduler.java
index 2d206daa..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;
@@ -26,44 +25,50 @@ public class MessagingScheduler {
private final CareworkerService careworkerService;
private final AlarmService alarmService;
- @Scheduled(cron = "0 0/1 * * * ?")
+ @Scheduled(cron = "0 0/1 * * * ?", zone = "Asia/Seoul")
public void sendChartUpdate() {
+ // 현재 시간 출력
+ log.info("현재 시간 : {}", LocalDateTime.now());
// 초와 나노초를 제거하고 분 단위로 비교하기 위해 현재 시간을 가져옴
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);
+ Alarm alarm = alarmService.getGuardianAlarmMessage(guardian.getId(), currentDateTime); // 보호자의 알람
String name = guardian.getName();
// (1) Line 채널 알림 보내기
- if (alarm != null && alarm.getChannel().equals(MessageChannel.LINE)) {
- log.info("알림 보낼 보호자 : {}", name);
- alarmService.sendAlarmToSqs(alarm, alarm.getChannelId(), name);
- alarmService.createGuardianNextDayAlarm(guardian);
+ if (alarm != null && guardian.isLineSubscription()) {
+ log.info("Line 알림 메세지를 받을 보호자 : {}", name);
+ alarmService.sendAlarmToSqs(alarm, MessageChannel.LINE, name, guardian.getPhone(), guardian.getLineUserId());
}
// (2) SMS 알림 보내기
+ if (alarm != null && guardian.isSmsSubscription()) {
+ log.info("SMS 문자 알림 메세지를 받을 보호자 : {}", name);
+ alarmService.sendAlarmToSqs(alarm, MessageChannel.SMS, name, guardian.getPhone(), guardian.getLineUserId());
+ }
}
// 요양보호사에게 알람 메시지를 SQS로 전송합니다.
for (Careworker careworker : careworkers) {
- String phone = careworker.getPhone();
- Alarm alarm = alarmService.getAlarmByPhoneAndAlertTime(phone, currentDateTime);
+ Alarm alarm = alarmService.getCareworkerAlarmMessage(careworker.getId(), currentDateTime); // 요양보호사의 알람
String name = careworker.getName();
+
// (1) Line 채널 알림 보내기
- if (alarm != null && alarm.getChannel().equals(MessageChannel.LINE) && !alarm.isSend()) {
- log.info("알림 보낼 요양보호사 : {}", name);
- alarmService.sendAlarmToSqs(alarm, alarm.getChannelId(), name);
- alarmService.createCareworkerNextWorkingdayAlarm(careworker);
+ 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() && 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/domain/excel/service/ExcelDownloadService.java b/src/main/java/dbdr/domain/excel/service/ExcelDownloadService.java
index c2f1940c..0ccfb831 100644
--- a/src/main/java/dbdr/domain/excel/service/ExcelDownloadService.java
+++ b/src/main/java/dbdr/domain/excel/service/ExcelDownloadService.java
@@ -25,7 +25,7 @@ public byte[] generateCareworkerTemplate() {
setCellStyleText(workbook, sheet, 2);
setCellStyleText(workbook, sheet, 3);
- String[] sampleData = {"1", "홍길동", "hong@example.com", "01012345678", "1234"};
+ String[] sampleData = {"홍길동", "hong@example.com", "01012345678", "1234"};
createSampleData(sheet, sampleData);
return convertWorkbookToByteArray(workbook);
diff --git a/src/main/java/dbdr/domain/excel/service/ExcelUploadService.java b/src/main/java/dbdr/domain/excel/service/ExcelUploadService.java
index fe5d59d1..38ecbb0e 100644
--- a/src/main/java/dbdr/domain/excel/service/ExcelUploadService.java
+++ b/src/main/java/dbdr/domain/excel/service/ExcelUploadService.java
@@ -153,17 +153,24 @@ private void processRecipientRow(Row row, List successLi
Long careworkerId = Long.valueOf(getCellValue(row.getCell(6)));
Long guardianId = Long.valueOf(getCellValue(row.getCell(7)));
-
Institution institution = institutionRepository.findById(institutionId)
.orElseThrow(() -> new ApplicationException(ApplicationError.INSTITUTION_NOT_FOUND));
- Careworker careworker = careworkerRepository.findById(careworkerId)
- .orElseThrow(() -> new ApplicationException(ApplicationError.CAREWORKER_NOT_FOUND));
-
- Guardian guardian = guardianRepository.findById(guardianId)
- .orElseThrow(() -> new ApplicationException(ApplicationError.GUARDIAN_NOT_FOUND));
+ Optional careworkerOpt = careworkerRepository.findById(careworkerId);
+ Optional guardianOpt = guardianRepository.findById(guardianId);
try {
+ // 요양보호사나 보호자가 없을 경우 예외 대신 실패 목록에 추가하고 리턴
+ if (careworkerOpt.isEmpty() || guardianOpt.isEmpty()) {
+ failedList.add(new ExcelRecipientResponse(
+ null, name, LocalDate.parse(birth), gender, careLevel, careNumber, LocalDate.parse(startDate),
+ institution.getId(), careworkerOpt.map(Careworker::getId).orElse(null), guardianOpt.map(Guardian::getId).orElse(null)));
+ return;
+ }
+
+ Careworker careworker = careworkerOpt.get();
+ Guardian guardian = guardianOpt.get();
+
checkDuplicate(seenCareNumbers, careNumber, ApplicationError.DUPLICATE_CARE_NUMBER);
validateCareNumber(careNumber, recipientRepository.existsByCareNumber(careNumber));
seenCareNumbers.add(careNumber);
@@ -185,7 +192,7 @@ private void processRecipientRow(Row row, List successLi
recipient.getId(), name, LocalDate.parse(birth), gender, careLevel, careNumber, LocalDate.parse(startDate), institution.getId(), careworker.getId(), guardian.getId()));
} catch (ApplicationException e) {
failedList.add(new ExcelRecipientResponse(
- null, name, LocalDate.parse(birth), gender, careLevel, careNumber, LocalDate.parse(startDate), institution.getId(), careworker.getId(), guardian.getId()));
+ null, name, LocalDate.parse(birth), gender, careLevel, careNumber, LocalDate.parse(startDate), institution.getId(), careworkerOpt.map(Careworker::getId).orElse(null), guardianOpt.map(Guardian::getId).orElse(null)));
}
}
@@ -244,4 +251,4 @@ private String getCellValue(Cell cell) {
private interface RowProcessor {
void process(Row row);
}
-}
+}
\ 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 353d383e..533e5160 100644
--- a/src/main/java/dbdr/domain/guardian/controller/GuardianController.java
+++ b/src/main/java/dbdr/domain/guardian/controller/GuardianController.java
@@ -1,6 +1,6 @@
package dbdr.domain.guardian.controller;
-import dbdr.domain.guardian.dto.request.GuardianAlertTimeRequest;
+import dbdr.domain.guardian.dto.request.GuardianMyPageRequest;
import dbdr.domain.guardian.dto.response.GuardianMyPageResponse;
import dbdr.domain.guardian.entity.Guardian;
import dbdr.domain.guardian.service.GuardianService;
@@ -47,10 +47,10 @@ public ResponseEntity> showGuardianIn
@PutMapping
@DbdrAuth(targetRole = Role.GUARDIAN, authParam = AuthParam.LOGIN_GUARDIAN)
public ResponseEntity> updateGuardianInfo(
- @Valid @RequestBody GuardianAlertTimeRequest guardianAlertTimeRequest,
+ @Valid @RequestBody GuardianMyPageRequest guardianMyPageRequest,
@Parameter(hidden = true) @LoginGuardian Guardian guardian) {
- GuardianMyPageResponse guardianMyPageResponse = guardianService.updateAlertTime(guardian.getId(),
- guardianAlertTimeRequest);
+ GuardianMyPageResponse guardianMyPageResponse =
+ guardianService.updateMyPageInfo(guardian.getId(), guardianMyPageRequest);
return ResponseEntity.ok(ApiUtils.success(guardianMyPageResponse));
}
}
diff --git a/src/main/java/dbdr/domain/guardian/dto/request/GuardianMyPageRequest.java b/src/main/java/dbdr/domain/guardian/dto/request/GuardianMyPageRequest.java
new file mode 100644
index 00000000..af272547
--- /dev/null
+++ b/src/main/java/dbdr/domain/guardian/dto/request/GuardianMyPageRequest.java
@@ -0,0 +1,16 @@
+package dbdr.domain.guardian.dto.request;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import java.time.LocalTime;
+
+public record GuardianMyPageRequest(
+ @Schema(description = "보호자의 알림 시간", example = "09:00:00")
+ LocalTime alertTime,
+
+ @Schema(description = "SMS 수신 동의 여부", example = "true")
+ boolean smsSubscription,
+
+ @Schema(description = "LINE 수신 동의 여부", example = "true")
+ boolean lineSubscription
+) {
+}
diff --git a/src/main/java/dbdr/domain/guardian/dto/response/GuardianMyPageResponse.java b/src/main/java/dbdr/domain/guardian/dto/response/GuardianMyPageResponse.java
index 062c32ee..1bc0b3ef 100644
--- a/src/main/java/dbdr/domain/guardian/dto/response/GuardianMyPageResponse.java
+++ b/src/main/java/dbdr/domain/guardian/dto/response/GuardianMyPageResponse.java
@@ -2,7 +2,11 @@
import java.time.LocalTime;
-public record GuardianMyPageResponse(String name, String phone,
- LocalTime alertTime) {
-
+public record GuardianMyPageResponse(
+ String name,
+ String phone,
+ LocalTime alertTime,
+ boolean smsSubscription,
+ boolean lineSubscription
+) {
}
diff --git a/src/main/java/dbdr/domain/guardian/entity/Guardian.java b/src/main/java/dbdr/domain/guardian/entity/Guardian.java
index f687f941..fa35e450 100644
--- a/src/main/java/dbdr/domain/guardian/entity/Guardian.java
+++ b/src/main/java/dbdr/domain/guardian/entity/Guardian.java
@@ -1,7 +1,6 @@
package dbdr.domain.guardian.entity;
import dbdr.domain.institution.entity.Institution;
-import dbdr.domain.recipient.entity.Recipient;
import jakarta.persistence.FetchType;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
@@ -39,9 +38,15 @@ public class Guardian extends BaseEntity {
@Column(nullable = true)
private String lineUserId;
- @Column(nullable = true)
+ @Column(nullable = false)
private LocalTime alertTime = LocalTime.of(9, 0); // 오전 9시로 초기화
+ @Column(nullable = false)
+ private boolean smsSubscription = false;
+
+ @Column(nullable = false)
+ private boolean lineSubscription = false;
+
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "institution_id")
private Institution institution;
@@ -60,9 +65,7 @@ public void updateGuardian(String phone, String name) {
this.name = name;
}
- public void updateAlertTime(String name, String phone, LocalTime alertTime) {
- this.phone = phone;
- this.name = name;
+ public void updateAlertTime(LocalTime alertTime) {
this.alertTime = alertTime;
}
@@ -70,7 +73,8 @@ public void updateLineUserId(String lineUserId) {
this.lineUserId = lineUserId;
}
- public void updateAlertTime(LocalTime alertTime) {
- this.alertTime = alertTime;
+ public void updateSubscriptions(boolean smsSubscription, boolean lineSubscription) {
+ this.smsSubscription = smsSubscription;
+ this.lineSubscription = lineSubscription;
}
}
diff --git a/src/main/java/dbdr/domain/guardian/service/GuardianService.java b/src/main/java/dbdr/domain/guardian/service/GuardianService.java
index c15e2feb..0e1be411 100644
--- a/src/main/java/dbdr/domain/guardian/service/GuardianService.java
+++ b/src/main/java/dbdr/domain/guardian/service/GuardianService.java
@@ -1,8 +1,6 @@
package dbdr.domain.guardian.service;
-import dbdr.domain.core.alarm.service.AlarmService;
-import dbdr.domain.core.alarm.service.AlarmService;
-import dbdr.domain.guardian.dto.request.GuardianAlertTimeRequest;
+import dbdr.domain.guardian.dto.request.GuardianMyPageRequest;
import dbdr.domain.guardian.dto.request.GuardianUpdateRequest;
import dbdr.domain.guardian.dto.response.GuardianMyPageResponse;
import dbdr.domain.guardian.entity.Guardian;
@@ -28,7 +26,6 @@ public class GuardianService {
private final GuardianRepository guardianRepository;
private final InstitutionRepository institutionRepository;
- private final AlarmService alarmService;
@Autowired
PasswordEncoder passwordEncoder;
@@ -42,20 +39,19 @@ public GuardianResponse getGuardianById(Long guardianId) {
public GuardianMyPageResponse getMyPageGuardianInfo(Long guardianId) {
Guardian guardian = findGuardianById(guardianId);
- return new GuardianMyPageResponse(guardian.getName(), guardian.getPhone(),
- guardian.getAlertTime());
+ return new GuardianMyPageResponse(guardian.getName(), guardian.getPhone(), guardian.getAlertTime(), guardian.isSmsSubscription(), guardian.isLineSubscription());
}
@Transactional
- public GuardianMyPageResponse updateAlertTime(Long guardianId,
- GuardianAlertTimeRequest request) {
- ensureUniquePhoneButNotId(request.phone(), guardianId);
+ public GuardianMyPageResponse updateMyPageInfo(Long guardianId, GuardianMyPageRequest request) {
Guardian guardian = findGuardianById(guardianId);
- guardian.updateAlertTime(request.name(), request.phone(), request.alertTime());
+ if (request.alertTime() != null) {
+ guardian.updateAlertTime(request.alertTime());
+ }
+ guardian.updateSubscriptions(request.smsSubscription(), request.lineSubscription());
guardianRepository.save(guardian);
- alarmService.updateAlarmByLocalTime(request.alertTime(), request.phone());
return new GuardianMyPageResponse(guardian.getName(), guardian.getPhone(),
- guardian.getAlertTime());
+ guardian.getAlertTime(), guardian.isSmsSubscription(), guardian.isLineSubscription());
}
@Transactional
@@ -109,7 +105,6 @@ public GuardianResponse addGuardian(GuardianRequest guardianRequest) {
.institution(institution)
.build();
guardian = guardianRepository.save(guardian);
- alarmService.createGuardianAlarm(guardian);
return new GuardianResponse(guardian.getId(), guardian.getPhone(), guardian.getName(),
guardian.getInstitution().getId(), guardian.isActive());
}
@@ -128,7 +123,6 @@ public GuardianResponse addGuardianByInstitution(GuardianRequest guardianRequest
.institution(institution)
.build();
guardian = guardianRepository.save(guardian);
- alarmService.createGuardianAlarm(guardian);
return new GuardianResponse(guardian.getId(), guardian.getPhone(), guardian.getName(),
guardian.getInstitution().getId(), guardian.isActive());
}
@@ -157,14 +151,9 @@ private void ensureUniquePhoneButNotId(String phone, Long id) {
}
}
- public Guardian findByLineUserId(String userId) {
- return guardianRepository.findByLineUserId(userId)
- .orElse(null);
- }
-
public Guardian findByPhone(String phone) {
return guardianRepository.findByPhone(phone)
- .orElse(null);
+ .orElseThrow(() -> new ApplicationException(ApplicationError.GUARDIAN_NOT_FOUND));
}
@Transactional
@@ -174,7 +163,7 @@ public void updateLineUserId(String userId, String phoneNumber) {
guardianRepository.save(guardian);
}
- public List findByAlertTime(LocalTime currentTime) {
+ public List findByAlertTime(LocalTime currentTime) {
return guardianRepository.findByAlertTime(currentTime);
- }
+ }
}
diff --git a/src/main/java/dbdr/domain/recipient/repository/RecipientRepository.java b/src/main/java/dbdr/domain/recipient/repository/RecipientRepository.java
index 4dbd0b48..0f91f262 100644
--- a/src/main/java/dbdr/domain/recipient/repository/RecipientRepository.java
+++ b/src/main/java/dbdr/domain/recipient/repository/RecipientRepository.java
@@ -10,6 +10,8 @@ public interface RecipientRepository extends JpaRepository {
boolean existsByCareNumber(String careNumber);
+ boolean existsByCareNumberAndIdNot(String careNumber, Long id);
+
List findByCareworkerId(Long careworkerId);
List findByInstitutionId(Long institutionId);
@@ -21,4 +23,6 @@ public interface RecipientRepository extends JpaRepository {
Optional findByIdAndInstitutionId(Long recipientId, Long institutionId);
Optional findByIdAndGuardianId(Long recipientId, Long guardianId);
+
+ Optional findByGuardianId(Long guardianId);
}
diff --git a/src/main/java/dbdr/domain/recipient/service/RecipientService.java b/src/main/java/dbdr/domain/recipient/service/RecipientService.java
index 36be3a4f..5e73bd97 100644
--- a/src/main/java/dbdr/domain/recipient/service/RecipientService.java
+++ b/src/main/java/dbdr/domain/recipient/service/RecipientService.java
@@ -1,6 +1,9 @@
package dbdr.domain.recipient.service;
import dbdr.domain.careworker.entity.Careworker;
+import dbdr.domain.chart.entity.Chart;
+import dbdr.domain.chart.repository.ChartRepository;
+import dbdr.domain.chart.service.ChartService;
import dbdr.domain.guardian.entity.Guardian;
import dbdr.domain.guardian.service.GuardianService;
import dbdr.domain.institution.entity.Institution;
@@ -18,6 +21,8 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
import java.util.List;
@Service
@@ -28,6 +33,7 @@ public class RecipientService {
private final CareworkerService careworkerService;
private final InstitutionService institutionService;
private final GuardianService guardianService;
+ private final ChartRepository chartRepository;
// 전체 돌봄대상자 목록 조회 (관리자용)
public List getAllRecipients() {
@@ -68,6 +74,7 @@ public RecipientResponse createRecipient(RecipientRequest recipientDTO) {
//관리자용
@Transactional
public RecipientResponse updateRecipientForAdmin(Long recipientId, RecipientRequest recipientDTO) {
+ ensureUniqueCareNumberNotId(recipientDTO.getCareNumber(), recipientId);
Recipient recipient = findRecipientById(recipientId);
Institution institution = institutionService.getInstitutionById(recipientDTO.getInstitutionId());
if (institution == null) {
@@ -190,6 +197,7 @@ public RecipientResponse createRecipientForInstitution(RecipientRequest recipien
//요양보호사가 담당하는 돌봄대상자 정보 수정
@Transactional
public RecipientResponse updateRecipientForCareworker(Long recipientId, RecipientUpdateCareworkerRequest recipientDTO, Long careworkerId) {
+ ensureUniqueCareNumberNotId(recipientDTO.getCareNumber(), recipientId);
Careworker careworker = careworkerService.getCareworkerById(careworkerId);
Recipient recipient = findRecipientByIdAndCareworker(recipientId, careworkerId);
@@ -204,6 +212,7 @@ public RecipientResponse updateRecipientForCareworker(Long recipientId, Recipien
// 요양원에 속한 돌봄대상자 정보 수정
@Transactional
public RecipientResponse updateRecipientForInstitution(Long recipientId, RecipientUpdateInstitutionRequest recipientDTO, Long institutionId) {
+ ensureUniqueCareNumberNotId(recipientDTO.getCareNumber(), recipientId);
Recipient recipient = findRecipientByIdAndInstitution(recipientId, institutionId);
Careworker careworker = careworkerService.getCareworkerById(recipientDTO.getCareworkerId());
@@ -273,4 +282,35 @@ private RecipientResponse toResponse(Recipient recipient) {
recipient.getGuardian() != null ? recipient.getGuardian().getId() : null
);
}
+
+ // 어제 날짜의 차트가 작성되었는지 확인
+ public boolean isChartWrittenYesterday(Long guardianId) {
+ Recipient recipient = recipientRepository.findByGuardianId(
+ guardianId).orElseThrow(() -> new ApplicationException(ApplicationError.RECIPIENT_NOT_FOUND
+ ));
+ Chart chart = getChartByRecipientIdAndDate(recipient.getId(), LocalDate.now().minusDays(1));
+ return chart != null;
+ }
+
+ // 보호자 ID로 어제 차트 ID 조회
+ public Long getChartIdByGuardianId(Long guardianId) {
+ Recipient recipient = recipientRepository.findByGuardianId(guardianId)
+ .orElseThrow(() -> new ApplicationException(ApplicationError.RECIPIENT_NOT_FOUND));
+ Chart chart = getChartByRecipientIdAndDate(recipient.getId(), LocalDate.now().minusDays(1));
+ return chart.getId();
+ }
+
+ public Chart getChartByRecipientIdAndDate(Long recipientId, LocalDate date) {
+ LocalDateTime startDateTime = date.atStartOfDay();
+ LocalDateTime endDateTime = date.atTime(23, 59, 59);
+
+ return chartRepository.findByRecipientIdAndCreatedAtBetween(recipientId, startDateTime, endDateTime)
+ .orElse(null);
+ }
+
+ private void ensureUniqueCareNumberNotId(String careNumber, Long id) {
+ if (recipientRepository.existsByCareNumberAndIdNot(careNumber, id)) {
+ throw new ApplicationException(ApplicationError.DUPLICATE_CARE_NUMBER);
+ }
+ }
}
diff --git a/src/main/java/dbdr/dto/request/CareworkerRequestDTO.java b/src/main/java/dbdr/dto/request/CareworkerRequestDTO.java
deleted file mode 100644
index 0c7efa83..00000000
--- a/src/main/java/dbdr/dto/request/CareworkerRequestDTO.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package dbdr.dto.request;
-
-import jakarta.validation.constraints.Email;
-import jakarta.validation.constraints.NotBlank;
-import jakarta.validation.constraints.NotNull;
-import jakarta.validation.constraints.Pattern;
-import lombok.AllArgsConstructor;
-import lombok.Getter;
-import lombok.NoArgsConstructor;
-
-@Getter
-@NoArgsConstructor
-@AllArgsConstructor
-public class CareworkerRequestDTO {
-
- @NotNull
- private Long institutionId;
-
- @NotBlank(message = "이름은 필수 항목입니다.")
- private String name;
-
- @NotBlank(message = "이메일은 필수 항목입니다.")
- @Email(message = "올바르지 않은 형식입니다.")
- private String email;
-
- @NotBlank(message = "휴대폰 번호는 필수 항목입니다.")
- @Pattern(regexp = "010\\d{8}", message = "010XXXXXXXX형식으로 입력해주세요.")
- private String phone;
-}
diff --git a/src/main/java/dbdr/dto/request/GuardiansRequest.java b/src/main/java/dbdr/dto/request/GuardiansRequest.java
deleted file mode 100644
index 98983466..00000000
--- a/src/main/java/dbdr/dto/request/GuardiansRequest.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package dbdr.dto.request;
-
-import jakarta.validation.constraints.NotBlank;
-import jakarta.validation.constraints.Pattern;
-
-public record GuardiansRequest(
- @NotBlank(message = "전화번호를 입력하세요.")
- @Pattern(regexp = "010\\d{8}", message = "010XXXXXXXX형식으로 입력해주세요.")
- String phone,
- @NotBlank(message = "이름을 입력하세요.")
- String name) {
-
-}
diff --git a/src/main/java/dbdr/dto/request/RecipientRequestDTO.java b/src/main/java/dbdr/dto/request/RecipientRequestDTO.java
deleted file mode 100644
index d5e34365..00000000
--- a/src/main/java/dbdr/dto/request/RecipientRequestDTO.java
+++ /dev/null
@@ -1,45 +0,0 @@
-package dbdr.dto.request;
-
-import jakarta.validation.constraints.NotBlank;
-import jakarta.validation.constraints.NotNull;
-import jakarta.validation.constraints.Pattern;
-import lombok.AllArgsConstructor;
-import lombok.Getter;
-import lombok.NoArgsConstructor;
-
-import java.time.LocalDate;
-
-@Getter
-@NoArgsConstructor
-@AllArgsConstructor
-public class RecipientRequestDTO {
-
- @NotBlank(message = "이름은 필수 항목입니다.")
- private String name;
-
- @NotNull(message = "생년월일은 필수 항목입니다.")
- private LocalDate birth;
-
- @NotBlank(message = "성별은 필수 항목입니다.")
- @Pattern(regexp = "^(남|여)$")
- private String gender;
-
- @NotBlank(message = "장기요양등급은 필수 항목입니다.")
- private String careLevel;
-
- @NotBlank(message = "장기요양번호는 필수 항목입니다.")
- @Pattern(regexp = "^[A-Z0-9-]+$", message = "올바르지 않은 형식입니다.")
- private String careNumber;
-
- @NotNull(message = "입소일은 필수 항목입니다.")
- private LocalDate startDate;
-
- @NotBlank(message = "요양기관이름은 필수 항목입니다.")
- private String institution;
-
- @NotNull(message = "요양기관번호는 필수 항목입니다.")
- private Long institutionNumber;
-
- @NotNull(message = "요양보호사 ID는 필수 항목입니다.")
- private Long careworkerId;
-}
diff --git a/src/main/java/dbdr/dto/response/CareworkerResponseDTO.java b/src/main/java/dbdr/dto/response/CareworkerResponseDTO.java
deleted file mode 100644
index 5313ec70..00000000
--- a/src/main/java/dbdr/dto/response/CareworkerResponseDTO.java
+++ /dev/null
@@ -1,17 +0,0 @@
-package dbdr.dto.response;
-
-import lombok.AllArgsConstructor;
-import lombok.Getter;
-import lombok.NoArgsConstructor;
-
-@Getter
-@NoArgsConstructor
-@AllArgsConstructor
-public class CareworkerResponseDTO {
-
- private Long id;
- private Long institutionId;
- private String name;
- private String email;
- private String phone;
-}
diff --git a/src/main/java/dbdr/dto/response/GuardiansResponse.java b/src/main/java/dbdr/dto/response/GuardiansResponse.java
deleted file mode 100644
index e2cbdcf6..00000000
--- a/src/main/java/dbdr/dto/response/GuardiansResponse.java
+++ /dev/null
@@ -1,14 +0,0 @@
-package dbdr.dto.response;
-
-import jakarta.validation.constraints.NotBlank;
-import jakarta.validation.constraints.Pattern;
-
-public record GuardiansResponse(
- @NotBlank(message = "전화번호를 입력하세요.")
- @Pattern(regexp = "010\\d{8}", message = "010XXXXXXXX형식으로 입력해주세요.")
- String phone,
- @NotBlank(message = "이름을 입력하세요.")
- String name,
- boolean isActive) {
-
-}
diff --git a/src/main/java/dbdr/dto/response/RecipientResponseDTO.java b/src/main/java/dbdr/dto/response/RecipientResponseDTO.java
deleted file mode 100644
index 1bc03dc5..00000000
--- a/src/main/java/dbdr/dto/response/RecipientResponseDTO.java
+++ /dev/null
@@ -1,24 +0,0 @@
-package dbdr.dto.response;
-
-import lombok.AllArgsConstructor;
-import lombok.Getter;
-import lombok.NoArgsConstructor;
-
-import java.time.LocalDate;
-
-@Getter
-@NoArgsConstructor
-@AllArgsConstructor
-public class RecipientResponseDTO {
-
- private Long id;
- private String name;
- private LocalDate birth;
- private String gender;
- private String careLevel;
- private String careNumber;
- private LocalDate startDate;
- private String institution;
- private Long institutionNumber;
- private Long careworkerId;
-}
diff --git a/src/main/java/dbdr/global/exception/ApplicationError.java b/src/main/java/dbdr/global/exception/ApplicationError.java
index 6ff98e27..8c57011c 100644
--- a/src/main/java/dbdr/global/exception/ApplicationError.java
+++ b/src/main/java/dbdr/global/exception/ApplicationError.java
@@ -66,7 +66,10 @@ public enum ApplicationError {
FILE_DOWNLOAD_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "파일 다운로드 중 오류가 발생했습니다."),
EMPTY_FILE(HttpStatus.BAD_REQUEST, "파일이 비어 있습니다."),
FILE_SIZE_EXCEEDED(HttpStatus.BAD_REQUEST, "파일 크기는 5MB를 초과할 수 없습니다."),
- INVALID_FILE(HttpStatus.BAD_REQUEST, "유효하지 않은 파일입니다. 엑셀 파일 (.xlsx)만 업로드 가능합니다.");
+ INVALID_FILE(HttpStatus.BAD_REQUEST, "유효하지 않은 파일입니다. 엑셀 파일 (.xlsx)만 업로드 가능합니다."),
+
+ //openAI
+ OPEN_AI_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "요약 중 오류가 발생했습니다.");
private final HttpStatus status;
private final String message;
diff --git a/src/main/java/dbdr/openai/service/SummaryService.java b/src/main/java/dbdr/openai/service/SummaryService.java
index 2dcee8ee..6113b5c5 100644
--- a/src/main/java/dbdr/openai/service/SummaryService.java
+++ b/src/main/java/dbdr/openai/service/SummaryService.java
@@ -11,7 +11,6 @@
import dbdr.openai.repository.SummaryRepository;
import java.time.LocalDate;
import java.time.LocalDateTime;
-import java.util.Date;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@@ -32,7 +31,7 @@ public SummaryApiFinalResponse getFinalSummary(Long chartId) {
institutionName);
}
- private SummaryResponse getSummarization(Long chartId) {
+ public SummaryResponse getSummarization(Long chartId) {
Summary summary = summaryRepository.findByChartId(chartId);
return new SummaryResponse(summary.getConditionDisease(), summary.getBodyManagement(),
summary.getNursingManagement(), summary.getCognitiveManagement(),
diff --git a/src/main/java/dbdr/repository/CareworkerRepository.java b/src/main/java/dbdr/repository/CareworkerRepository.java
deleted file mode 100644
index ab4c3a1b..00000000
--- a/src/main/java/dbdr/repository/CareworkerRepository.java
+++ /dev/null
@@ -1,11 +0,0 @@
-package dbdr.repository;
-
-import dbdr.domain.Careworker;
-import org.springframework.data.jpa.repository.JpaRepository;
-
-import java.util.List;
-
-public interface CareworkerRepository extends JpaRepository {
-
- List findByInstitutionId(Long institutionId);
-}
diff --git a/src/main/java/dbdr/repository/GuardiansRepository.java b/src/main/java/dbdr/repository/GuardiansRepository.java
deleted file mode 100644
index 0df4b8f7..00000000
--- a/src/main/java/dbdr/repository/GuardiansRepository.java
+++ /dev/null
@@ -1,9 +0,0 @@
-package dbdr.repository;
-
-import dbdr.domain.Guardians;
-import org.springframework.data.jpa.repository.JpaRepository;
-
-public interface GuardiansRepository extends JpaRepository {
-
- boolean existsByPhone(String phone);
-}
diff --git a/src/main/java/dbdr/repository/RecipientRepository.java b/src/main/java/dbdr/repository/RecipientRepository.java
deleted file mode 100644
index 8bbc26ea..00000000
--- a/src/main/java/dbdr/repository/RecipientRepository.java
+++ /dev/null
@@ -1,8 +0,0 @@
-package dbdr.repository;
-
-import dbdr.domain.Recipient;
-import org.springframework.data.jpa.repository.JpaRepository;
-
-public interface RecipientRepository extends JpaRepository {
-
-}
\ No newline at end of file
diff --git a/src/main/java/dbdr/security/authflow.md b/src/main/java/dbdr/security/authflow.md
new file mode 100644
index 00000000..f4d48d6b
--- /dev/null
+++ b/src/main/java/dbdr/security/authflow.md
@@ -0,0 +1,82 @@
+# 인증과정
+
+# 1. 필터에 걸렸다…!
+
+```java
+//SecurityConfig.java
+
+http.addFilterBefore(new JwtFilter(jwtProvider), UsernamePasswordAuthenticationFilter.class) //jwt 필터 추가
+ .authenticationProvider(baseAuthenticationProvider()) //인증 프로바이더 추가
+ .authorizeHttpRequests((authorize) -> {
+ authorize
+ .requestMatchers("/*/login/*").permitAll()
+ .anyRequest().authenticated();
+ });
+```
+
+- 님 코드에는 SecurityConfig는 http.csrf().cors(). …. 인데 왜 여기는 http.addFilterBefore 임요?
+
+ 각 메소드에서 계속해서 HttpSecurity 객체를 반환하고 있습니다!
+
+
+여기서 anyRequest() 에 대해서는 authenticated(); 를 요구하고 있습니다.
+
+즉, 로그인을 제외한 요청에 대해서는 인증된 사용자만 가능한 것입니다!
+
+(추후 admin은 localhost에서만 접속해서 추가할 수 있게 하기 or 따로 루트를 열어둬야할 것 같습니다)
+
+그럼 인증은 어디서 진행되는가? → addFilterBefore의 JwtFilter에서 인증을 진행하게 됩니다.
+
+이는 UsernamePasswordAuthenticationFilter 전에 추가되어 request에서 전달된 정보를 통해서 인증에 성공할 경우 SecurityContext 에 인증된 객체를 추가하게 됩니다.
+
+# 2. 필터 내부의 처리
+
+```java
+//JwtFilter.java
+
+String token = request.getHeader("Authorization");
+
+if (token != null) {
+ try {
+ jwtProvider.validateToken(token);
+ Authentication authentication = jwtProvider.getAuthentication(token);
+ SecurityContextHolder.getContext().setAuthentication(authentication);
+ } catch (Exception e) {
+ log.debug("토큰 유저 정보 추출 실패 : {}", e.getMessage());
+ }
+ }
+```
+
+필터 내부는 다음과 같습니다.
+
+request의 header에서 key가 “Authorization”인 필드를 추출합니다.
+
+(클라이언트가 헤더에 해당 이름으로 JWT를 넣어주어야합니닷!)
+
+이후 jwtProvider에게 토큰을 검사하도록 합니다. (accessToken이 맞는지, 만료된 것은 아닌지 등등…)
+
+성공한다면? → SecurityContext에 Authentication 객체를 등록하게 됩니다.
+
+```java
+//JwtProvider.java
+public Authentication getAuthentication(String token) {
+ BaseUserDetails userDetails = baseUserDetailsService.loadUserByUsernameAndRole(
+ getUserName(token), Role.valueOf(getRole(token)));
+ return new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(),
+ userDetails.getAuthorities());
+ }
+```
+
+이전 로그인 편에서 보시듯, Authentication객체의 구현체인 UsernamePasswordAuthenticationToken에 Authorities를 포함한 생성자를 이용해 인증된 객체로 반환하게 됩니다.
+
+---
+
+### 스프링 시큐리티에서 Context 관리 전략
+
+저희는 stateless 서버를 지향합니다.
+
+스프링에서 하나의 요청에 대해서 서버의 pool에서 스레드를 가져오게 되고 해당 스레드의 라이프 사이클은 해당 요청에 대한 응답이 끝날때 까지 입니다.
+
+스프링 시큐리티는 스레드마다 Context를 유지하고, 요청이 끝나면 해당 Context 를 clear하게 됩니다.(로그찍으면 볼 수 있어요!) → 따라서 stateless
+
+저희 서비스에서는 SecurityContext는 요청이 들어오면 JWTFilter에서 jwt를 통해 인증이 되면 SecurityContext에 해당 정보를 저장하게 되고, 해당 Context는 요청 → 응답 까지만 유효하게 됩니다.
\ No newline at end of file
diff --git a/src/main/java/dbdr/security/loginflow.md b/src/main/java/dbdr/security/loginflow.md
new file mode 100644
index 00000000..c06be210
--- /dev/null
+++ b/src/main/java/dbdr/security/loginflow.md
@@ -0,0 +1,303 @@
+# 로그인
+
+# 1. 회원의 로그인 시도
+
+회원은 로그인과 관련된 정보를 request body에 실어서 컨트롤러로 보냅니다.
+
+```java
+//request 객체
+public record LoginRequest(@NotBlank(message = "유저 ID는 필수 항목입니다.") String userId,
+ @NotBlank(message = "비밀번호는 필수 항목입니다.") String password) {
+
+}
+```
+
+<컨트롤러>
+
+```java
+
+//LoginController.java 내부
+
+@PostMapping("/{role}") //1번줄
+ public ResponseEntity login(@PathVariable("role") String role,
+ @RequestBody @Valid LoginRequest loginRequest) {
+ Role roleEnum = roleCheck(role);
+ String token = loginService.login(roleEnum, loginRequest);
+ return ResponseEntity.ok().header(authHeader, token).build();
+ }
+```
+
+### 컨트롤러 내부 1번줄
+
+이떄, URL을 기반으로 Role을 구분할 수 있도록 하였습니다.
+
+예를 들어, Guardian의 경우 URL은 “http:// 우리의주소/v1/login/guardian”
+
+으로 요청을 보내게 됩니다.
+
+해당 요청은 아무 사용자나 보낼 수 있습니다. - 필터를 그냥 쑥 통과해서요. 왜냐?!
+
+- 필터가 뭔데요
+
+
+ 스프링 시큐리티의 동작방식을 가볍게 설명해보면, 스프링 시큐리티는 서블릿 필터에서 동작하게 됩니다.
+
+ 서블릿 필터는 스프링의 디스패쳐 서블릿으로 가기 전 request를 가로채 필요한 작업을 진행하게되고, 우리는 스프링 시큐리티를 이용해서 해당 서블릿 필터에 필요한 작업을 추가하게 됩니다.
+
+ ![스크린샷 2024-10-11 오후 1.35.29.png](https://prod-files-secure.s3.us-west-2.amazonaws.com/8dc8dc5a-e7b1-4d8f-b454-1657415061ea/e79a38a8-da08-4879-8404-651962e6c0ec/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2024-10-11_%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE_1.35.29.png)
+
+ 이때 스프링빈으로 등록한 시큐리티를 위한 서비스들은 필터단계에서는 동작을 할 수 없는데요 (아직 오지 않았음으로)
+
+ 스프링 시큐리티는 DelegatingFilterProxy를 이용해 해당 빈을 가져와 의존성을 주입받습니다.
+
+
+```java
+//SecurityConfig.java
+
+http.authorizeHttpRequests((authorize) -> {
+ authorize
+ .requestMatchers("/*/login/*").permitAll()
+ .anyRequest().authenticated();
+ });
+```
+
+이 부분은 스프링 시큐리티 설정파일이 위치한 부분입니다.
+
+여기 requestMatchers( ) 에서 authorize가 없어도 permitAll()을 해주고 있기에 해당 url은 접근 가능해집니다.
+
+# 2. login service로 가자!
+
+request는 로그인 서비스로 오게 됩니다.
+
+```java
+ ///LoginService.java
+
+ @Transactional
+ public String login(Role role, LoginRequest loginRequest) {
+ BaseUserDetails userDetails = BaseUserDetails.builder()
+ .userLoginId(loginRequest.userId())
+ .password(loginRequest.password())
+ .role(role.name())
+ .build(); //1번줄
+
+ //2번줄
+ UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, loginRequest.password());
+ Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
+
+ return jwtProvider.createAccessToken(authentication.getName(), role.name(), jwtExpiration);
+ }
+```
+
+### 1번줄
+
+처음에는 BaseUserDetails 를 생성하고 있습니다.
+
+BaseUserDetails는 UserDetails 라고하는 인터페이스를 구현한 객체입니다.
+
+UserDetails는 스프링 시큐리티에서 기본적으로 제공하는 “회원”에 대한 객체이며,
+
+저희도 이를 기반으로 동작하고자 구현하였습니다.
+
+### 2번줄
+
+```java
+UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, loginRequest.password());
+Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
+```
+
+이 부분에서는 AuthenticationToken을 만들어서 회원에 대해 아직 인증되지않은 객체를 SecurityContextHolder 로 보내는 작업을 하고 있습니다.
+
+- 왜 UsernamePasswordAuthenticatonToken을 만들어야해요?
+
+ AuthenticationToken ( 구현체 → UsernamePasswordAuthenticationToken) 은 SeucrityContextHolder 에서 사용하는 인증을 위한 객체로 사용됩니다.
+
+ 하나의 요청이 있으면 해당 요청에 대한 작업이 끝날때까지 인증에 대한 정보를 보관해야합니다. (저희는 세션을 사용하지 않는 stateless 방식이므로 - 사용자에 대한 정보를 서버에서 보관하지 않는다는 뜻 - DB서버이야기 아닙니다!)
+
+ 이 때 SeucrityContextHolder가 해당 정보를 보관하고 있으며 우리는 컨트롤러 혹은 서비스 어디에서든지 해당 Context에서 요청자에 대한 정보를 얻을 수 있게 됩니다.
+
+ ```java
+ //스프링 빈 어디에서든지 인증정보를 꺼내볼 수 있어요
+
+ SecurityContextHolder.getContext().getAuthentication();
+ // Autentication 객체가 반환됩니다.
+ ```
+
+ 근데 서비스의 구현에 따라서 인증된 사용자를 구분하는데 방식이 바뀔 수 있으므로 스프링시큐리티는 AuthenticationToken이라는 추상클래스를 통해서 자율성을 부여해주고 있습니다.
+
+
+### 3. AuthenticationManager
+
+대부분의 애매함은 여기서 나올 것이라 추측됩니다.
+
+```java
+Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
+
+return jwtProvider.createAccessToken(authentication.getName(), role.name(), jwtExpiration);
+```
+
+진솔님이 말씀하신 로그인 실패 로직이라던지… 그런 부분이 여기에는 전혀 없는 모습으로 보이는데요
+
+저도 한 3일정도를 스프링시큐리티해석을 위해 공부하고도 ‘어 왜 없지? 내가 실수했나보군’ 하면서 다시 공부해서 알아낼정도로 은밀하게 되어있더군요
+
+위에 설명에서 “인증되지 않은 객체”라고 표현하였습니다.
+
+이는 Authentication 객체는 `authentication.isAuthenticated()` 라는 메소드를 가지고 있고, 인증과정이 따로 있다는 뜻이지요.
+
+# 3. 인증과정
+
+### 1. 너말고 매니저 불러와
+
+`authenticationManagerBuilder.getObject()` 에서 인증 매니저를 가져오고
+`.authenticate(authenticationToken)` 를 통해 인증되지않은 객체를 인증시켜달라고 위임합니다.
+
+### 2. 매니저 : 인증 담당자 나와라그래
+
+스프링시큐리티에 대해 이거 관련된 설정 어디 있지? 생각된다면 → SecurityConfig 를 참고하시면 되는데요
+
+`.authenticationProvider(baseAuthenticationProvider())`
+
+여기를 보니면 인증담당자(공급자)로 커스텀한 Provider를 구현한 모습을 볼 수 있습니다.
+
+즉, AuthenticationManager는 SecurityConfig를 통해서AuthenticationProvider를 포함하고 있고, 인증되지않은 객체에 대해 인증과정을 해당 provider에게 위임하게 되는 겁니다. (매니저는 관리직이래요)
+
+- 그럼 꼭 Provider를 구현해야하나요?
+
+ 기본적으로 따로 authenticationProvider를 등록하지않는다면 UsernamePasswordToken에 대해서 스프링 시큐리티가 기본으로 제공하는 DaoAuthenticationProvider(이하 Dao) 가 그 역할을 하게 됩니다.
+
+ Dao는 UserDetailsService이 구현된 Bean을 찾아서 @Override된 loadUserbyname() 메소드를 호출해서 인증을 제공합니다.
+
+ 저희는 Role에 따라 username이 같아질 수 있는데, 기본 DaoAuthenticationProvider 내부 코드를 보시면
+
+ `UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);`
+
+ 해당 방식으로 UserDetailsService에서 username을 검증하게되고, 이는 Careworker와 Guardian이 같은 번호를 쓰는 경우 충돌하게되는 문제가 생겨 직접 구현하게 되었습니다.
+
+
+### 3. 제가 인증담당자입니다.
+
+```java
+public class BaseAuthenticationProvider implements AuthenticationProvider {
+
+ private final BaseUserDetailsService baseUserDetailsService;
+
+ private final PasswordEncoder passwordEncoder;
+
+ @Override
+ public Authentication authenticate(Authentication authentication) throws AuthenticationException {
+
+ BaseUserDetails unAuthUser = (BaseUserDetails) authentication.getPrincipal();
+ BaseUserDetails authUser = baseUserDetailsService.loadUserByUsernameAndRole(unAuthUser.getUserLoginId(), unAuthUser.getRole());
+
+ if (!passwordEncoder.matches(unAuthUser.getPassword(), authUser.getPassword())) {
+ throw new ApplicationException(ApplicationError.PASSWORD_NOT_MATCH);
+ }
+ return new UsernamePasswordAuthenticationToken(authUser, authUser.getPassword(), authUser.getAuthorities());
+ }
+
+ @Override
+ public boolean supports(Class> authentication) {
+ log.debug("supports : {}", authentication.equals(UsernamePasswordAuthenticationToken.class));
+ return authentication.equals(UsernamePasswordAuthenticationToken.class);
+ }
+}
+```
+
+여기를 보면 authenticate 가 Override 된 모습을 볼 수 있습니다.
+
+즉, 위의 서비스에서 getObject().authenticate( ) 에서 해당 코드가 호출되는 것이지요.
+
+### 한줄씩 보기
+
+`BaseUserDetails unAuthUser = (BaseUserDetails)authentication.getPrincipal();`
+
+Authentication 객체에서 Principal()을 얻어내고 있는데요, principal는 user에 대한 구분할 수 있는 정보를 담고 있는 객체입니다.
+
+Object 타입으로 반환되고, 이를 casting 해주었습니다.
+
+- Princi…pal? 시…팔? 이 어디서 들어갔는데요?
+
+ `UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, loginRequest.password());`
+
+ 여기 LoginService에서 생성자에 userDetails 이 principal로 등록되었습니다!
+
+
+`BaseUserDetails authUser = baseUserDetailsService.loadUserByUsernameAndRole(unAuthUser.getUserLoginId(), unAuthUser.getRole());`
+
+여기서는 위에서 얻어낸 인증되지않은 user의 정보를 이용해 baseUserDetailsService를 통해서 DB에서 회원의 정보가 있는 지 조회하게 됩니다.
+
+```java
+ //BaseUserDetailsService.java
+ public BaseUserDetails loadUserByUsernameAndRole(String username, Role role) {
+
+ if (role == Role.GUARDIAN) {
+ return getGuadianDetails(username);
+ }
+ if (role == Role.CAREWORKER) {
+ return getCareWorkerDetails(username);
+ }
+ if (role == Role.INSTITUTION) {
+ return getInstitutionDetails(username);
+ }
+ if (role == Role.ADMIN) {
+ return getAdminDetails(username);
+ }
+
+ throw new ApplicationException(ApplicationError.ROLE_NOT_FOUND);
+ }
+```
+
+참고 : 메서드명은 기존은 Dao와 비슷하게 짓기 위해서 저렇게 하였습니다.
+
+그리고 비밀번호 일치를 확인한 뒤 (이때 DB에 저장된 비밀번호는 암호화된 상태로 저장되어있고, passwordEncoder를 사용해서 암호화된 값이 같은 지 검증하게 됩니다.)
+
+`return new UsernamePasswordAuthenticationToken(authUser, authUser.getPassword(), authUser.getAuthorities());`
+
+인증된 토큰을 만들어서 반환하게 됩니다.
+
+- 서비스에서 만들어준 UsernamePasswrdAuthenticationToken과 같은 것 같은데 인증된 객체와 인증되지않은 객체는 어떻게 비교하나요?
+
+ ```java
+ public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
+ super((Collection)null);
+ this.principal = principal;
+ this.credentials = credentials;
+ this.setAuthenticated(false);
+ }
+
+ public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection extends GrantedAuthority> authorities) {
+ super(authorities);
+ this.principal = principal;
+ this.credentials = credentials;
+ super.setAuthenticated(true);
+ }
+
+ //생성자에서 authorites를 넣고 안넣고에 따라서 구분됩니다!
+ ```
+
+
+- Override 된 supports 메소드는 뭐임요?
+
+ 인증 토큰의 종류에 따라서 Provider가 해당 토큰을 지원하는 지 유무를 체크할 수 있게 해줍니다. 이를 통해 OAuth와 같은 다양한 인증 토큰을 통한 로그인을 지원할 수 있지 않을까요…? (추측)
+
+
+### JWT 반환하기
+
+로그인이 실패한다면? → authenticate() 메소드 내부에서 처리하게 됩니다. → 이는 SecurityConfig에서
+
+```java
+.exceptionHandling((exception) -> exception
+ .accessDeniedHandler((request, response, accessDeniedException) -> {
+ response.sendError(HttpServletResponse.SC_FORBIDDEN, "접근 거부");
+ })
+ .authenticationEntryPoint((request, response, authException) -> {
+ response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "인증 실패");
+ }));
+```
+
+이렇게 핸들링하게 됩니다. (추후 AOP처리된 global exception handle로 넘길까 합니다!)
+
+로그인이 성공한다면? → jwtProvider를 통해서 토큰을 만들어 반환하게 됩니다.
+
+(아마 refresh 토큰을 도입 시, 해당 부분의 return을 access token과 refresh token을 합친 DTO를 만들어서 반환하게 될 것 같습니다.)
+
+### 끝.
\ No newline at end of file
diff --git a/src/main/java/dbdr/service/CareworkerService.java b/src/main/java/dbdr/service/CareworkerService.java
deleted file mode 100644
index cd15cf02..00000000
--- a/src/main/java/dbdr/service/CareworkerService.java
+++ /dev/null
@@ -1,83 +0,0 @@
-package dbdr.service;
-
-import dbdr.domain.Careworker;
-import dbdr.dto.request.CareworkerRequestDTO;
-import dbdr.dto.response.CareworkerResponseDTO;
-import dbdr.repository.CareworkerRepository;
-import org.springframework.stereotype.Service;
-import org.springframework.transaction.annotation.Transactional;
-
-import java.util.List;
-import java.util.stream.Collectors;
-
-@Service
-public class CareworkerService {
-
- private final CareworkerRepository careworkerRepository;
-
- public CareworkerService(CareworkerRepository careworkerRepository) {
- this.careworkerRepository = careworkerRepository;
- }
-
- @Transactional(readOnly = true)
- public List getAllCareworkers() {
- return careworkerRepository.findAll().stream()
- .map(this::toResponseDTO)
- .collect(Collectors.toList());
- }
-
- @Transactional(readOnly = true)
- public List getCareworkersByInstitution(Long institutionId) {
- return careworkerRepository.findByInstitutionId(institutionId).stream()
- .map(this::toResponseDTO)
- .collect(Collectors.toList());
- }
-
- @Transactional(readOnly = true)
- public CareworkerResponseDTO getCareworkerById(Long id) {
- Careworker careworker = careworkerRepository.findById(id)
- .orElseThrow(() -> new IllegalArgumentException("요양보호사를 찾을 수 없습니다."));
- return toResponseDTO(careworker);
- }
-
- @Transactional
- public CareworkerResponseDTO createCareworker(CareworkerRequestDTO careworkerRequestDTO) {
- Careworker careworker = new Careworker(
- careworkerRequestDTO.getInstitutionId(),
- careworkerRequestDTO.getName(),
- careworkerRequestDTO.getEmail(),
- careworkerRequestDTO.getPhone()
- );
- careworkerRepository.save(careworker);
- return toResponseDTO(careworker);
- }
-
- @Transactional
- public CareworkerResponseDTO updateCareworker(Long id, CareworkerRequestDTO careworkerRequestDTO) {
- Careworker careworker = careworkerRepository.findById(id)
- .orElseThrow(() -> new IllegalArgumentException("요양보호사를 찾을 수 없습니다."));
-
- careworker.updateCareworker(careworkerRequestDTO);
- careworkerRepository.save(careworker);
-
- return toResponseDTO(careworker);
- }
-
- @Transactional
- public void deleteCareworker(Long id) {
- Careworker careworker = careworkerRepository.findById(id)
- .orElseThrow(() -> new IllegalArgumentException("요양보호사를 찾을 수 없습니다."));
- careworker.deactivate();
- careworkerRepository.delete(careworker);
- }
-
- private CareworkerResponseDTO toResponseDTO(Careworker careworker) {
- return new CareworkerResponseDTO(
- careworker.getId(),
- careworker.getInstitutionId(),
- careworker.getName(),
- careworker.getEmail(),
- careworker.getPhone()
- );
- }
-}
diff --git a/src/main/java/dbdr/service/GuardiansService.java b/src/main/java/dbdr/service/GuardiansService.java
deleted file mode 100644
index 1d194387..00000000
--- a/src/main/java/dbdr/service/GuardiansService.java
+++ /dev/null
@@ -1,57 +0,0 @@
-package dbdr.service;
-
-import dbdr.domain.Guardians;
-import dbdr.dto.request.GuardiansRequest;
-import dbdr.dto.response.GuardiansResponse;
-import dbdr.repository.GuardiansRepository;
-import java.util.List;
-import org.springframework.stereotype.Service;
-
-@Service
-public class GuardiansService {
-
- private final GuardiansRepository guardiansRepository;
-
- public GuardiansService(GuardiansRepository guardiansRepository) {
- this.guardiansRepository = guardiansRepository;
- }
-
- public GuardiansResponse getGuardianById(Long guardianId) {
- Guardians guardian = guardiansRepository.findById(guardianId).orElseThrow();
- return new GuardiansResponse(guardian.getPhone(), guardian.getName(), guardian.isActive());
- }
-
- public GuardiansResponse updateGuardianById(Long guardianId,
- GuardiansRequest guardiansRequest) {
- Guardians guardian = guardiansRepository.findById(guardianId).orElseThrow();
- guardian.updateGuardian(guardiansRequest.phone(), guardiansRequest.name());
- guardiansRepository.save(guardian);
- return new GuardiansResponse(guardiansRequest.phone(), guardiansRequest.name(), guardian.isActive());
- }
-
- public List getAllGuardians() {
- List guardians = guardiansRepository.findAll();
- return guardians.stream()
- .map(guardian -> new GuardiansResponse(guardian.getPhone(), guardian.getName(), guardian.isActive()))
- .toList();
- }
-
- public GuardiansResponse addGuardian(GuardiansRequest guardiansRequest) {
- phoneNumberExists(guardiansRequest.phone());
- Guardians guardian = new Guardians(guardiansRequest.phone(), guardiansRequest.name());
- guardian = guardiansRepository.save(guardian);
- return new GuardiansResponse(guardian.getPhone(), guardian.getName(), guardian.isActive());
- }
-
- public void deleteGuardianById(Long guardianId) {
- Guardians guardians = guardiansRepository.findById(guardianId).orElseThrow();
- guardians.deactivate();
- guardiansRepository.save(guardians);
- }
-
- private void phoneNumberExists(String phone) {
- if(guardiansRepository.existsByPhone(phone)){
- throw new IllegalArgumentException("존재하는 전화번호입니다.");
- }
- }
-}
diff --git a/src/main/java/dbdr/service/RecipientService.java b/src/main/java/dbdr/service/RecipientService.java
deleted file mode 100644
index 948f84ed..00000000
--- a/src/main/java/dbdr/service/RecipientService.java
+++ /dev/null
@@ -1,91 +0,0 @@
-package dbdr.service;
-
-import dbdr.domain.Recipient;
-import dbdr.dto.request.RecipientRequestDTO;
-import dbdr.dto.response.RecipientResponseDTO;
-import dbdr.repository.RecipientRepository;
-import dbdr.repository.CareworkerRepository;
-import org.springframework.stereotype.Service;
-import org.springframework.transaction.annotation.Transactional;
-
-import java.util.List;
-import java.util.stream.Collectors;
-
-@Service
-public class RecipientService {
-
- private final RecipientRepository recipientRepository;
- private final CareworkerRepository careworkerRepository;
-
- public RecipientService(RecipientRepository recipientRepository, CareworkerRepository careworkerRepository) {
- this.recipientRepository = recipientRepository;
- this.careworkerRepository = careworkerRepository;
- }
-
- @Transactional(readOnly = true)
- public List getAllRecipients() {
- return recipientRepository.findAll().stream()
- .map(this::toResponseDTO)
- .collect(Collectors.toList());
- }
-
- @Transactional(readOnly = true)
- public RecipientResponseDTO getRecipientById(Long id) {
- Recipient recipient = recipientRepository.findById(id)
- .orElseThrow(() -> new IllegalArgumentException("돌봄대상자를 찾을 수 없습니다."));
- return toResponseDTO(recipient);
- }
-
- @Transactional
- public RecipientResponseDTO createRecipient(RecipientRequestDTO recipientRequestDTO) {
- Recipient recipient = new Recipient(
- recipientRequestDTO.getName(),
- recipientRequestDTO.getBirth(),
- recipientRequestDTO.getGender(),
- recipientRequestDTO.getCareLevel(),
- recipientRequestDTO.getCareNumber(),
- recipientRequestDTO.getStartDate(),
- recipientRequestDTO.getInstitution(),
- recipientRequestDTO.getInstitutionNumber(),
- careworkerRepository.findById(recipientRequestDTO.getCareworkerId())
- .orElseThrow(() -> new IllegalArgumentException("요양보호사를 찾을 수 없습니다."))
- );
- recipientRepository.save(recipient);
- return toResponseDTO(recipient);
- }
-
- @Transactional
- public RecipientResponseDTO updateRecipient(Long id, RecipientRequestDTO recipientRequestDTO) {
- Recipient recipient = recipientRepository.findById(id)
- .orElseThrow(() -> new IllegalArgumentException("돌봄대상자를 찾을 수 없습니다."));
-
- recipient.updateRecipient(recipientRequestDTO);
- recipientRepository.save(recipient);
-
- return toResponseDTO(recipient);
- }
-
- @Transactional
- public void deleteRecipient(Long id) {
- Recipient recipient = recipientRepository.findById(id)
- .orElseThrow(() -> new IllegalArgumentException("돌봄대상자를 찾을 수 없습니다."));
- recipient.deactivate();
- recipientRepository.delete(recipient);
- }
-
- private RecipientResponseDTO toResponseDTO(Recipient recipient) {
- return new RecipientResponseDTO(
- recipient.getId(),
- recipient.getName(),
- recipient.getBirth(),
- recipient.getGender(),
- recipient.getCareLevel(),
- recipient.getCareNumber(),
- recipient.getStartDate(),
- recipient.getInstitution(),
- recipient.getInstitutionNumber(),
- recipient.getCareworker() != null ? recipient.getCareworker().getId() : null
- );
- }
-}
-
diff --git a/src/test/java/dbdr/careworker/CareworkerMapperTest.java b/src/test/java/dbdr/careworker/CareworkerMapperTest.java
index 6eecc5fc..b8a875d2 100644
--- a/src/test/java/dbdr/careworker/CareworkerMapperTest.java
+++ b/src/test/java/dbdr/careworker/CareworkerMapperTest.java
@@ -15,9 +15,9 @@
import org.mapstruct.factory.Mappers;
import org.mockito.InjectMocks;
import org.mockito.Mock;
-import org.springframework.boot.test.context.SpringBootTest;
+import org.mockito.MockitoAnnotations;
+import org.springframework.security.crypto.password.PasswordEncoder;
-@SpringBootTest
public class CareworkerMapperTest {
@InjectMocks
@@ -26,36 +26,42 @@ public class CareworkerMapperTest {
@Mock
private InstitutionService institutionService;
+ @Mock
+ private PasswordEncoder passwordEncoder;
+
private Institution institution;
@BeforeEach
void setUp() throws NoSuchFieldException, IllegalAccessException {
+
+ MockitoAnnotations.openMocks(this);
+
+ Field passwordEncoderField = CareworkerMapper.class.getDeclaredField("passwordEncoder");
+ passwordEncoderField.setAccessible(true);
+ passwordEncoderField.set(mapper, passwordEncoder);
+
institution = Institution.builder()
- .institutionNumber(100L)
- .institutionName("Test Institution")
- .build();
+ .institutionNumber(100L)
+ .institutionName("Test Institution")
+ .build();
- // 리플렉션을 사용하여 ID 값을 설정
setId(institution, 1L);
}
@Test
void testToResponse() throws NoSuchFieldException, IllegalAccessException {
- // Given
+
Careworker careworker = Careworker.builder()
- .institution(institution)
- .name("John Doe")
- .email("johndoe@example.com")
- .phone("01012345678")
- .build();
+ .institution(institution)
+ .name("John Doe")
+ .email("johndoe@example.com")
+ .phone("01012345678")
+ .build();
- // 리플렉션을 사용하여 Careworker ID 설정
setId(careworker, 1L);
- // When
CareworkerResponse responseDTO = mapper.toResponse(careworker);
- // Then
assertEquals(careworker.getId(), responseDTO.getId());
assertEquals(careworker.getInstitution().getId(), responseDTO.getInstitutionId());
assertEquals(careworker.getName(), responseDTO.getName());
@@ -65,32 +71,30 @@ void testToResponse() throws NoSuchFieldException, IllegalAccessException {
@Test
void testToEntity() {
- // Given
+
CareworkerRequest requestDTO = new CareworkerRequest(
- 1L,
- "John Doe",
- "johndoe@example.com",
- "01012345678",
- "password"
+ 1L,
+ "John Doe",
+ "johndoe@example.com",
+ "01012345678",
+ "password"
);
- // Mock InstitutionService response
when(institutionService.getInstitutionById(requestDTO.getInstitutionId())).thenReturn(institution);
+ when(passwordEncoder.encode("password")).thenReturn("encodedPassword");
- // When
Careworker careworker = mapper.toEntity(requestDTO);
- // Then
assertEquals(requestDTO.getName(), careworker.getName());
assertEquals(requestDTO.getEmail(), careworker.getEmail());
assertEquals(requestDTO.getPhone(), careworker.getPhone());
+ assertEquals("encodedPassword", careworker.getLoginPassword());
assertEquals(institution, careworker.getInstitution());
}
- // 리플렉션을 사용하여 엔티티 ID 값을 설정
private void setId(Object entity, Long idValue) throws NoSuchFieldException, IllegalAccessException {
Field idField = entity.getClass().getSuperclass().getDeclaredField("id");
idField.setAccessible(true);
idField.set(entity, idValue);
}
-}
\ No newline at end of file
+}
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/e2etest/doAdmin/GuardianCrudTest.java b/src/test/java/dbdr/e2etest/doAdmin/GuardianCrudTest.java
index e112e85c..f6191516 100644
--- a/src/test/java/dbdr/e2etest/doAdmin/GuardianCrudTest.java
+++ b/src/test/java/dbdr/e2etest/doAdmin/GuardianCrudTest.java
@@ -1,5 +1,358 @@
package dbdr.e2etest.doAdmin;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import dbdr.domain.admin.entity.Admin;
+import dbdr.domain.admin.service.AdminService;
+import dbdr.domain.guardian.dto.request.GuardianRequest;
+import dbdr.domain.guardian.dto.request.GuardianUpdateRequest;
+import dbdr.domain.guardian.dto.response.GuardianResponse;
+import dbdr.domain.institution.dto.request.InstitutionRequest;
+import dbdr.domain.institution.dto.response.InstitutionResponse;
+import dbdr.global.exception.ApplicationError;
+import dbdr.global.exception.ApplicationException;
+import dbdr.global.util.api.ApiUtils.ApiError;
+import dbdr.global.util.api.ApiUtils.ApiResult;
+import dbdr.security.model.Role;
+import dbdr.testhelper.TestHelper;
+import java.util.List;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.web.server.LocalServerPort;
+import org.springframework.core.ParameterizedTypeReference;
+import org.springframework.http.HttpStatus;
+
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class GuardianCrudTest {
+
+ @Autowired
+ AdminService adminService;
+
+ @LocalServerPort
+ private int port;
+
+ private final long testAlpha_number = 3;
+
+ /*
+ 사용 번호 c1~
+ */
+
+ private void addAdmin(String adminId, String adminpassword) {
+ Admin admin = Admin.builder()
+ .loginId(adminId)
+ .loginPassword(adminpassword)
+ .build();
+ adminService.addAdmin(admin);
+ }
+
+ private Long addInstitutionRequestLogic(String adminId, String adminpassword,
+ String testAlpha, String testNumber) {
+ Long institutionNumber = 1000L * testAlpha_number + Long.parseLong(testNumber) * 100;
+ String institutionName = "institution_" + testAlpha + testNumber;
+ String institutionLoginId = "institution_" + testAlpha + testNumber;
+ String institutionLoginPassword = "institution_" + testAlpha + testNumber;
+
+ InstitutionRequest institutionRequest = new InstitutionRequest(institutionNumber,
+ institutionName, institutionLoginId, institutionLoginPassword);
+
+ TestHelper testHelper = new TestHelper(port);
+ return testHelper.user(Role.ADMIN, adminId, adminpassword)
+ .uri("/admin/institution")
+ .requestBody(institutionRequest)
+ .post()
+ .toEntity(new ParameterizedTypeReference>() {
+ })
+ .getBody().response().id();
+ }
+
+ @Test
+ @DisplayName("서버 관리자가 요양원에 보호자 추가")
+ void test_c1() {
+ String adminId = "admin_c1";
+ String adminpassword = "admin_c1";
+ addAdmin(adminId, adminpassword);
+
+ String testNumber = "1";
+ Long institutionId = addInstitutionRequestLogic(adminId, adminpassword, "c", testNumber);
+
+ TestHelper testHelper = new TestHelper(port);
+
+ //given
+ String guardianName = "guardian_c1";
+ String guardianPhone = "01033333334";
+ String guardianLoginPassword = "guardian_c1";
+
+ GuardianRequest guardianRequest = new GuardianRequest(guardianPhone, guardianName,
+ guardianLoginPassword,institutionId);
+
+ //when
+ var response = testHelper.user(Role.ADMIN, adminId, adminpassword)
+ .uri("/admin/guardian")
+ .requestBody(guardianRequest)
+ .post()
+ .toEntity(new ParameterizedTypeReference>() {
+ })
+ .getBody();
+
+ //then
+ assertThat(response).isNotNull();
+ assertThat(response.success()).isTrue();
+ assertThat(response.response().name()).isEqualTo(guardianName);
+ assertThat(response.response().phone()).isEqualTo(guardianPhone);
+
+ }
+
+ @Test
+ @DisplayName("서버관리자가 요양원이 없는 경우 보호자 추가 불가")
+ void test_c2() {
+ String adminId = "admin_c2";
+ String adminpassword = "admin_c2";
+ addAdmin(adminId, adminpassword);
+
+ TestHelper testHelper = new TestHelper(port);
+
+ //given
+ String guardianName = "guardian_c2";
+ String guardianPhone = "01033333335";
+ String guardianLoginPassword = "guardian_c2";
+
+ GuardianRequest guardianRequest = new GuardianRequest(guardianPhone, guardianName,
+ guardianLoginPassword, 0L);
+
+ //when
+ assertThatThrownBy(() -> testHelper.user(Role.ADMIN, adminId, adminpassword)
+ .uri("/admin/guardian")
+ .requestBody(guardianRequest)
+ .post()
+ .toEntity(new ParameterizedTypeReference>() {
+ })).hasMessageContaining("404");
+ }
+
+ @Test
+ @DisplayName("서버 관리자가 요양원에 보호자 추가 후 수정")
+ void test_c3(){
+ String adminId = "admin_c3";
+ String adminpassword = "admin_c3";
+ addAdmin(adminId, adminpassword);
+
+ String testNumber = "3";
+ Long institutionId = addInstitutionRequestLogic(adminId, adminpassword, "c", testNumber);
+
+ TestHelper testHelper = new TestHelper(port);
+
+ //given
+ String guardianName = "guardian_c3";
+ String guardianPhone = "01033333336";
+ String guardianLoginPassword = "guardian_c3";
+
+ GuardianRequest guardianRequest = new GuardianRequest(guardianPhone, guardianName,
+ guardianLoginPassword,institutionId);
+
+ //when
+ var response = testHelper.user(Role.ADMIN, adminId, adminpassword)
+ .uri("/admin/guardian")
+ .requestBody(guardianRequest)
+ .post()
+ .toEntity(new ParameterizedTypeReference>() {
+ })
+ .getBody();
+
+ //then
+ assertThat(response).isNotNull();
+ assertThat(response.success()).isTrue();
+ assertThat(response.response().name()).isEqualTo(guardianName);
+ assertThat(response.response().phone()).isEqualTo(guardianPhone);
+
+ // given
+ //
+ //
+ // 기존 보호자 정보 수정하기
+
+ long id = response.response().id();
+ String updateGuardianName = "update_guardian_c3";
+ String updateGuardianPhone = "01033333337";
+ String updateGuardianLoginPassword = "update_guardian_c3";
+
+ GuardianUpdateRequest guardianUpdateRequest = new GuardianUpdateRequest(updateGuardianPhone, updateGuardianName);
+
+ //when
+ var updateResponse = testHelper.user(Role.ADMIN, adminId, adminpassword)
+ .uri("/admin/guardian/" + id)
+ .requestBody(guardianUpdateRequest)
+ .put()
+ .toEntity(new ParameterizedTypeReference>() {
+ })
+ .getBody();
+
+ //then
+ assertThat(updateResponse).isNotNull();
+ assertThat(updateResponse.success()).isTrue();
+ assertThat(updateResponse.response().name()).isEqualTo(updateGuardianName);
+ assertThat(updateResponse.response().phone()).isEqualTo(updateGuardianPhone);
+ }
+
+ @Test
+ @DisplayName("서버 관리자가 요양원의 보호자 삭제")
+ void test_c4(){
+ String adminId = "admin_c4";
+ String adminpassword = "admin_c4";
+ addAdmin(adminId, adminpassword);
+
+ String testNumber = "4";
+ Long institutionId = addInstitutionRequestLogic(adminId, adminpassword, "c", testNumber);
+
+ TestHelper testHelper = new TestHelper(port);
+
+ //given
+ String guardianName = "guardian_c4";
+ String guardianPhone = "01033333338";
+ String guardianLoginPassword = "guardian_c4";
+
+ GuardianRequest guardianRequest = new GuardianRequest(guardianPhone, guardianName,
+ guardianLoginPassword,institutionId);
+
+ //when
+ var response = testHelper.user(Role.ADMIN, adminId, adminpassword)
+ .uri("/admin/guardian")
+ .requestBody(guardianRequest)
+ .post()
+ .toEntity(new ParameterizedTypeReference>() {
+ })
+ .getBody();
+
+ //then
+ assertThat(response).isNotNull();
+ assertThat(response.success()).isTrue();
+ assertThat(response.response().name()).isEqualTo(guardianName);
+ assertThat(response.response().phone()).isEqualTo(guardianPhone);
+
+ // given
+ //
+ //
+ // 기존 보호자 정보 삭제하기
+
+ long id = response.response().id();
+
+ //when
+ var deleteResponse = testHelper.user(Role.ADMIN, adminId, adminpassword)
+ .uri("/admin/guardian/" + id)
+ .delete()
+ .toEntity(new ParameterizedTypeReference>() {
+ });
+
+ //then
+ assertThat(deleteResponse).isNotNull();
+ assertThat(deleteResponse.getStatusCode().is2xxSuccessful()).isTrue();
+ }
+
+ @Test
+ @DisplayName("서버관리자가 다수의 요양원에 있는 모든 보호자 조회")
+ void test_c5678(){
+ String adminId = "admin_c5";
+ String adminpassword = "admin_c5";
+ addAdmin(adminId, adminpassword);
+
+ String testNumber = "5";
+ Long institutionId = addInstitutionRequestLogic(adminId, adminpassword, "c", testNumber);
+ Long institutionId2 = addInstitutionRequestLogic(adminId, adminpassword, "c", "6");
+ Long institutionId3 = addInstitutionRequestLogic(adminId, adminpassword, "c", "7");
+ Long institutionId4 = addInstitutionRequestLogic(adminId, adminpassword, "c", "8");
+
+ TestHelper testHelper = new TestHelper(port);
+
+ //given
+ String guardianName = "guardian_c5";
+ String guardianPhone = "01033333339";
+ String guardianLoginPassword = "guardian_c5";
+
+ GuardianRequest guardianRequest = new GuardianRequest(guardianPhone, guardianName,
+ guardianLoginPassword,institutionId);
+
+ String guardianName2 = "guardian_c6";
+ String guardianPhone2 = "01033333340";
+ String guardianLoginPassword2 = "guardian_c6";
+
+ GuardianRequest guardianRequest2 = new GuardianRequest(guardianPhone2, guardianName2,
+ guardianLoginPassword2,institutionId2);
+
+ String guardianName3 = "guardian_c7";
+ String guardianPhone3 = "01033333341";
+ String guardianLoginPassword3 = "guardian_c7";
+
+ GuardianRequest guardianRequest3 = new GuardianRequest(guardianPhone3, guardianName3,
+ guardianLoginPassword3,institutionId3);
+
+ String guardianName4 = "guardian_c8";
+ String guardianPhone4 = "01033333342";
+ String guardianLoginPassword4 = "guard";
+
+ GuardianRequest guardianRequest4 = new GuardianRequest(guardianPhone4, guardianName4,
+ guardianLoginPassword4,institutionId4);
+
+
+
+ //when
+ testHelper.user(Role.ADMIN, adminId, adminpassword)
+ .uri("/admin/guardian")
+ .requestBody(guardianRequest)
+ .post()
+ .toEntity(new ParameterizedTypeReference>() {
+ })
+ .getBody();
+
+ testHelper.user(Role.ADMIN, adminId, adminpassword)
+ .uri("/admin/guardian")
+ .requestBody(guardianRequest2)
+ .post()
+ .toEntity(new ParameterizedTypeReference>() {
+ })
+ .getBody();
+
+ testHelper.user(Role.ADMIN, adminId, adminpassword)
+ .uri("/admin/guardian")
+ .requestBody(guardianRequest3)
+ .post()
+ .toEntity(new ParameterizedTypeReference>() {
+ })
+ .getBody();
+
+ testHelper.user(Role.ADMIN, adminId, adminpassword)
+ .uri("/admin/guardian")
+ .requestBody(guardianRequest4)
+ .post()
+ .toEntity(new ParameterizedTypeReference>() {
+ })
+ .getBody();
+
+
+
+ // given
+ //
+ //
+ // 모든 보호자 조회하기
+
+ //when
+ var allGuardianResponse = testHelper.user(Role.ADMIN, adminId, adminpassword)
+ .uri("/admin/guardian")
+ .get()
+ .toEntity(new ParameterizedTypeReference>>() {
+ })
+ .getBody();
+
+ //then
+ assertThat(allGuardianResponse).isNotNull();
+ assertThat(allGuardianResponse.success()).isTrue();
+ assertThat(allGuardianResponse.response().size()).isGreaterThan(0);
+ assertThat(allGuardianResponse.response().stream().anyMatch(guardianResponse -> guardianResponse.name().equals(guardianName))).isTrue();
+ assertThat(allGuardianResponse.response().stream().anyMatch(guardianResponse -> guardianResponse.name().equals(guardianName2))).isTrue();
+ assertThat(allGuardianResponse.response().stream().anyMatch(guardianResponse -> guardianResponse.name().equals(guardianName3))).isTrue();
+ assertThat(allGuardianResponse.response().stream().anyMatch(guardianResponse -> guardianResponse.name().equals(guardianName4))).isTrue();
+
+ }
+
+
}
diff --git a/src/test/java/dbdr/e2etest/doAdmin/RecipientCrudTest.java b/src/test/java/dbdr/e2etest/doAdmin/RecipientCrudTest.java
new file mode 100644
index 00000000..e0b3b171
--- /dev/null
+++ b/src/test/java/dbdr/e2etest/doAdmin/RecipientCrudTest.java
@@ -0,0 +1,204 @@
+package dbdr.e2etest.doAdmin;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import dbdr.domain.admin.entity.Admin;
+import dbdr.domain.admin.service.AdminService;
+import dbdr.domain.careworker.dto.request.CareworkerRequest;
+import dbdr.domain.careworker.dto.response.CareworkerResponse;
+import dbdr.domain.guardian.dto.request.GuardianRequest;
+import dbdr.domain.guardian.dto.response.GuardianResponse;
+import dbdr.domain.institution.dto.request.InstitutionRequest;
+import dbdr.domain.institution.dto.response.InstitutionResponse;
+import dbdr.domain.recipient.dto.request.RecipientRequest;
+import dbdr.domain.recipient.dto.response.RecipientResponse;
+import dbdr.global.util.api.ApiUtils.ApiResult;
+import dbdr.security.model.Role;
+import dbdr.testhelper.TestHelper;
+import java.time.LocalDate;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.web.server.LocalServerPort;
+import org.springframework.core.ParameterizedTypeReference;
+
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+public class RecipientCrudTest {
+
+ @Autowired
+ AdminService adminService;
+
+ @LocalServerPort
+ private int port;
+
+ private final long testAlpha_number = 4L;
+
+ /*
+ 사용Number : d1~
+ */
+
+ private void addAdmin(String adminId, String adminpassword) {
+ Admin admin = Admin.builder()
+ .loginId(adminId)
+ .loginPassword(adminpassword)
+ .build();
+ adminService.addAdmin(admin);
+ }
+
+ private Long addInstitutionRequestLogic(String adminId, String adminpassword,
+ String testAlpha, String testNumber) {
+ Long institutionNumber = 1000L * testAlpha_number + Long.parseLong(testNumber) * 100;
+ String institutionName = "institution_" + testAlpha + testNumber;
+ String institutionLoginId = "institution_" + testAlpha + testNumber;
+ String institutionLoginPassword = "institution_" + testAlpha + testNumber;
+
+ InstitutionRequest institutionRequest = new InstitutionRequest(institutionNumber,
+ institutionName, institutionLoginId, institutionLoginPassword);
+
+ TestHelper testHelper = new TestHelper(port);
+ return testHelper.user(Role.ADMIN, adminId, adminpassword)
+ .uri("/admin/institution")
+ .requestBody(institutionRequest)
+ .post()
+ .toEntity(new ParameterizedTypeReference>() {
+ })
+ .getBody().response().id();
+ }
+
+ private Long addGuardianRequestLogic(String adminId, String adminpassword,
+ String testAlpha, String testNumber, Long institutionId, String phoneNumber) {
+
+ String guardianName = "guardian_" + testAlpha + testNumber;
+ String guardianLoginPassword = "guardian_" + testAlpha + testNumber;
+
+ GuardianRequest guardianRequest = new GuardianRequest(phoneNumber,
+ guardianName, guardianLoginPassword, institutionId);
+
+ TestHelper testHelper = new TestHelper(port);
+ return testHelper.user(Role.ADMIN, adminId, adminpassword)
+ .uri("/admin/guardian")
+ .requestBody(guardianRequest)
+ .post()
+ .toEntity(new ParameterizedTypeReference>() {
+ })
+ .getBody().response().id();
+ }
+
+ private Long addCareworkerRequestLogic(String adminId, String adminpassword,
+ String testAlpha, String testNumber, Long institutionId, String phoneNumber) {
+
+ String careworkerName = "careworker_" + testAlpha + testNumber;
+ String careworkerLoginPassword = "careworker_" + testAlpha + testNumber;
+ String careworkerEmail = "careworker_" + testAlpha + testNumber + "@naver.com";
+
+ CareworkerRequest careworkerRequest = new CareworkerRequest(institutionId, careworkerName,
+ careworkerEmail, phoneNumber, careworkerLoginPassword);
+
+ TestHelper testHelper = new TestHelper(port);
+ return testHelper.user(Role.ADMIN, adminId, adminpassword)
+ .uri("/admin/careworker")
+ .requestBody(careworkerRequest)
+ .post()
+ .toEntity(new ParameterizedTypeReference>() {
+ })
+ .getBody().response().getId();
+ }
+
+ @Test
+ @DisplayName("서버관리자가 돌봄대상자 추가")
+ void test_d1() {
+ //given
+ String adminId = "admin1";
+ String adminpassword = "admin1";
+ addAdmin(adminId, adminpassword);
+
+ //요양원 추가
+ Long institutionId = addInstitutionRequestLogic(adminId, adminpassword, "d", "1");
+
+ //보호자 추가
+ String phoneNumber = "01044123212";
+ Long guardianId = addGuardianRequestLogic(adminId, adminpassword, "d", "1", institutionId,
+ phoneNumber);
+
+ //요양보호사 추가
+ phoneNumber = "01044123213";
+ Long careworkerId = addCareworkerRequestLogic(adminId, adminpassword, "d", "1",
+ institutionId, phoneNumber);
+
+ //돌봄대상자 정보
+ String recipientName = "recipient_d1";
+ LocalDate birth = LocalDate.of(1981, 8, 1);
+ String gender = "남";
+ String careLevel = "2등급";
+ String careNumber = "L0000000230-300";
+ LocalDate startDate = LocalDate.of(2024, 1, 1);
+ String institution = "institution_d1";
+ Long institutionNumber = 777999L;
+
+ RecipientRequest recipientRequest = new RecipientRequest(recipientName, birth, gender,
+ careLevel, careNumber, startDate
+ , institution, institutionNumber, institutionId, careworkerId, guardianId);
+
+ //when
+
+ var response = new TestHelper(port)
+ .user(Role.ADMIN, adminId, adminpassword)
+ .uri("/admin/recipient")
+ .requestBody(recipientRequest)
+ .post()
+ .toEntity(new ParameterizedTypeReference>() {
+ }).getBody();
+
+ //then
+ assertThat(response.success()).isTrue();
+ assertThat(response.response().getName()).isEqualTo(recipientName);
+ assertThat(response.response().getBirth()).isEqualTo(birth);
+ assertThat(response.response().getGender()).isEqualTo(gender);
+ }
+
+ @Test
+ @DisplayName("보호자가 존재하지않는 경우 실패")
+ void test_d2() {
+ //given
+ String adminId = "admin_d1";
+ String adminpassword = "admin_d1";
+ addAdmin(adminId, adminpassword);
+
+ //요양원 추가
+ Long institutionId = addInstitutionRequestLogic(adminId, adminpassword, "d", "2");
+
+ //요양보호사 추가
+ String phoneNumber = "01043213213";
+ Long careworkerId = addCareworkerRequestLogic(adminId, adminpassword, "d", "2",
+ institutionId, phoneNumber);
+
+ //돌봄대상자 정보
+ String recipientName = "recipient_d2";
+ LocalDate birth = LocalDate.of(1981, 8, 1);
+ String gender = "남";
+ String careLevel = "2등급";
+ String careNumber = "L0000032230-300";
+ LocalDate startDate = LocalDate.of(2024, 1, 1);
+ String institution = "institution_d1";
+ Long institutionNumber = 777999L;
+
+ Long guardianId = 100000L;
+
+ RecipientRequest recipientRequest = new RecipientRequest(recipientName, birth, gender,
+ careLevel, careNumber, startDate
+ , institution, institutionNumber, institutionId, careworkerId, guardianId);
+
+ //when
+
+ assertThatThrownBy(() -> new TestHelper(port)
+ .user(Role.ADMIN, adminId, adminpassword)
+ .uri("/admin/recipient")
+ .requestBody(recipientRequest)
+ .post()
+ .toEntity(new ParameterizedTypeReference>() {
+ })).hasMessageContaining("404");
+
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/dbdr/e2etest/e2eInfo.md b/src/test/java/dbdr/e2etest/e2eInfo.md
new file mode 100644
index 00000000..358271b4
--- /dev/null
+++ b/src/test/java/dbdr/e2etest/e2eInfo.md
@@ -0,0 +1,70 @@
+# 테스트 코드 작성 요령
+
+## 1. Test Helper 사용법
+
+### 1. 'TestHelper' 클래스는 RestClient를 이용하여 통합테스트를 돕기 위해 작성됨.
+
+### 2. Test 클래스 내부 필요 어노테이션
+
+- @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+
+- Admin Service : 관리자 생성 기능은 내부의 서비스 클래스를 호출하여 사용
+ @Autowired
+ AdminService adminService;
+
+- 모든 Spring Bean을 주입받아 사용할 수 있도록 설정
+ @LocalServerPort
+ private int port;
+
+### 3. TestHelper 기본 사용법
+
+```java
+ import dbdr.testhelper.TestHelper;
+
+@Test
+public void test() {
+ // init : 이 떄 각 테스트마다 서버 관리자의 ID가 중복 불가능
+ String loginId = "admin";
+ String password = "admin";
+ Admin admin = new Admin(loginId, password);
+ adminService.addAdmin(admin);
+
+ // given
+
+ //Request 객체를 생성합니다.
+
+ // when
+ TestHelper testHelper = new TestHelper(port);
+ var response = testHelper.user(Role,loginId,password).uri("").requestBody(request).post();
+}
+```
+
+user() 메소드 내부에 접근하고 하는 사용자의 역할과 로그인 아이디 비밀번호를 입력하면, testHelper에서 login이후
+JWT를 가져와 자동으로 header에 넣어줍니다.
+
+reqeustBody()는 특정 requestbody가 필요 시 적용할 수 있습니다.
+
+uri()는 기본 도메인주소와 현재 api 버전까지가 포함되어있으며, 실질적으로 테스트하고자하는 주소를 입력하면 됩니다.
+
+### 4. 반환 타입의 처리
+
+컨트롤러에서 반환 타입은 ApiUtils를 통해서 처리됩니다.
+
+- 성공 응답에 대한 assertThat
+
+import dbdr.global.util.api.ApiUtils.ApiResult;
+import org.springframework.core.ParameterizedTypeReference;
+
+var body = response.toEntity(new ParameterizedTypeReference>(){}).getBody();
+
+예를 들어, 만약 GuardianResponse를 반환하는 주소로 요청을 보낸다면, "원하는 Response"에 GuardianResponse를 넣어주면 됩니다.
+
+
+- 실패 응답에 대한 assertThatThrownBy
+
+assertThatThrownBy(() -> testHelper.user().uri("").requestBody(request).post())
+ .toEntity(new ParameterizedTypeReference>(){})
+ .hasMessageContaining("원하는 에러 메시지");
+
+이를 통해 응답에 대한 테스트를 진행할 수 있습니다.
+
diff --git a/src/test/java/dbdr/excel/ExcelTest.java b/src/test/java/dbdr/excel/ExcelTest.java
new file mode 100644
index 00000000..fb39c59d
--- /dev/null
+++ b/src/test/java/dbdr/excel/ExcelTest.java
@@ -0,0 +1,240 @@
+package dbdr.excel;
+
+import dbdr.domain.excel.service.ExcelUploadService;
+import dbdr.domain.excel.dto.*;
+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 dbdr.domain.institution.entity.Institution;
+import dbdr.domain.institution.repository.InstitutionRepository;
+import dbdr.domain.recipient.repository.RecipientRepository;
+import org.apache.poi.ss.usermodel.Row;
+import org.apache.poi.ss.usermodel.Sheet;
+import org.apache.poi.ss.usermodel.Workbook;
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
+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 org.springframework.mock.web.MockMultipartFile;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+public class ExcelTest {
+
+ @InjectMocks
+ private ExcelUploadService excelUploadService;
+
+ @Mock
+ private CareworkerRepository careworkerRepository;
+
+ @Mock
+ private GuardianRepository guardianRepository;
+
+ @Mock
+ private RecipientRepository recipientRepository;
+
+ @Mock
+ private InstitutionRepository institutionRepository;
+
+ @Mock
+ private PasswordEncoder passwordEncoder;
+
+ private Institution institution;
+ private Careworker careworker;
+ private Guardian guardian;
+
+ @BeforeEach
+ public void setUp() throws NoSuchFieldException, IllegalAccessException {
+ MockitoAnnotations.openMocks(this);
+
+ // Institution 객체 생성 및 ID 설정
+ institution = Institution.builder()
+ .institutionNumber(100L)
+ .institutionName("Test Institution")
+ .build();
+ setId(institution, 1L);
+ when(institutionRepository.findById(1L)).thenReturn(Optional.of(institution));
+
+ // Careworker와 Guardian 생성 후 ID 설정
+ careworker = Careworker.builder()
+ .name("김요양")
+ .institution(institution)
+ .build();
+ setId(careworker, 10L);
+ when(careworkerRepository.findById(10L)).thenReturn(Optional.of(careworker));
+
+ guardian = Guardian.builder()
+ .name("이보호")
+ .institution(institution)
+ .build();
+ setId(guardian, 20L);
+ when(guardianRepository.findById(20L)).thenReturn(Optional.of(guardian));
+ }
+
+ @Test
+ void testUploadCareworkerExcel_WithFailedEntries() throws IOException {
+ Workbook workbook = new XSSFWorkbook();
+ Sheet sheet = workbook.createSheet();
+
+ // 첫 번째 행 - 정상 데이터
+ Row row1 = sheet.createRow(1);
+ row1.createCell(0).setCellValue("John Doe");
+ row1.createCell(1).setCellValue("johndoe@example.com");
+ row1.createCell(2).setCellValue("01012345678");
+ row1.createCell(3).setCellValue("password");
+
+ // 두 번째 행 - 중복된 전화번호
+ Row row2 = sheet.createRow(2);
+ row2.createCell(0).setCellValue("Jane Doe");
+ row2.createCell(1).setCellValue("janedoe@example.com");
+ row2.createCell(2).setCellValue("01012345678"); // 중복 번호
+ row2.createCell(3).setCellValue("password2");
+
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ workbook.write(outputStream);
+ MockMultipartFile file = new MockMultipartFile("file", "careworker.xlsx",
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", outputStream.toByteArray());
+
+ // Mock 설정
+ when(passwordEncoder.encode("password")).thenReturn("encodedPassword");
+ when(passwordEncoder.encode("password2")).thenReturn("encodedPassword2");
+ when(careworkerRepository.existsByPhone("01012345678")).thenReturn(false, true); // 첫 번째는 false, 두 번째는 true
+ when(careworkerRepository.existsByEmail("johndoe@example.com")).thenReturn(false);
+ when(careworkerRepository.existsByEmail("janedoe@example.com")).thenReturn(false);
+
+ Careworker mockCareworker = Careworker.builder()
+ .institution(institution)
+ .name("John Doe")
+ .email("johndoe@example.com")
+ .phone("01012345678")
+ .loginPassword("encodedPassword")
+ .build();
+ when(careworkerRepository.save(any(Careworker.class))).thenReturn(mockCareworker);
+
+ CareworkerFileUploadResponse response = excelUploadService.uploadCareworkerExcel(file, institution.getId());
+
+ // 검증 - 성공 항목과 실패 항목의 개수 확인
+ assertEquals(1, response.uploadedCareworkers().size());
+ assertEquals(1, response.failedCareworkers().size());
+
+ ExcelCareworkerResponse uploadedCareworker = response.uploadedCareworkers().get(0);
+ assertEquals("John Doe", uploadedCareworker.getName());
+ ExcelCareworkerResponse failedCareworker = response.failedCareworkers().get(0);
+ assertEquals("Jane Doe", failedCareworker.getName());
+ }
+
+ @Test
+ void testUploadGuardianExcel_WithFailedEntries() throws IOException {
+ Workbook workbook = new XSSFWorkbook();
+ Sheet sheet = workbook.createSheet();
+
+ // 첫 번째 행 - 정상 데이터
+ Row row1 = sheet.createRow(1);
+ row1.createCell(0).setCellValue("Jane Guardian");
+ row1.createCell(1).setCellValue("01098765432");
+ row1.createCell(2).setCellValue("password");
+
+ // 두 번째 행 - 중복된 전화번호
+ Row row2 = sheet.createRow(2);
+ row2.createCell(0).setCellValue("Jack Guardian");
+ row2.createCell(1).setCellValue("01098765432"); // 중복 번호
+ row2.createCell(2).setCellValue("password2");
+
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ workbook.write(outputStream);
+ MockMultipartFile file = new MockMultipartFile("file", "guardian.xlsx",
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", outputStream.toByteArray());
+
+ // Mock 설정
+ when(passwordEncoder.encode("password")).thenReturn("encodedPassword");
+ when(passwordEncoder.encode("password2")).thenReturn("encodedPassword2");
+ when(guardianRepository.existsByPhone("01098765432")).thenReturn(false, true);
+
+ Guardian mockGuardian = Guardian.builder()
+ .institution(institution)
+ .name("Jane Guardian")
+ .phone("01098765432")
+ .loginPassword("encodedPassword")
+ .build();
+ when(guardianRepository.save(any(Guardian.class))).thenReturn(mockGuardian);
+
+ GuardianFileUploadResponse response = excelUploadService.uploadGuardianExcel(file, institution.getId());
+
+ // 검증 - 성공 항목과 실패 항목의 개수 확인
+ assertEquals(1, response.uploadedGuardians().size());
+ assertEquals(1, response.failedGuardians().size());
+
+ ExcelGuardianResponse uploadedGuardian = response.uploadedGuardians().get(0);
+ assertEquals("Jane Guardian", uploadedGuardian.getName());
+ ExcelGuardianResponse failedGuardian = response.failedGuardians().get(0);
+ assertEquals("Jack Guardian", failedGuardian.getName());
+ }
+
+ @Test
+ void testUploadRecipientExcel_WithFailedEntries() throws IOException {
+ Workbook workbook = new XSSFWorkbook();
+ Sheet sheet = workbook.createSheet();
+
+ // 첫 번째 행 - 정상 데이터
+ Row row1 = sheet.createRow(1);
+ row1.createCell(0).setCellValue("Valid Recipient");
+ row1.createCell(1).setCellValue("2000-01-01");
+ row1.createCell(2).setCellValue("M");
+ row1.createCell(3).setCellValue("3");
+ row1.createCell(4).setCellValue("R12345");
+ row1.createCell(5).setCellValue("2022-01-01");
+ row1.createCell(6).setCellValue("10"); // Careworker ID
+ row1.createCell(7).setCellValue("20"); // Guardian ID
+
+ // 두 번째 행 - 중복된 care number
+ Row row2 = sheet.createRow(2);
+ row2.createCell(0).setCellValue("Duplicate Recipient");
+ row2.createCell(1).setCellValue("2000-01-01");
+ row2.createCell(2).setCellValue("M");
+ row2.createCell(3).setCellValue("3");
+ row2.createCell(4).setCellValue("R12345"); // 중복 care number
+ row2.createCell(5).setCellValue("2022-01-01");
+ row2.createCell(6).setCellValue("10"); // Careworker ID
+ row2.createCell(7).setCellValue("20"); // Guardian ID
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ workbook.write(outputStream);
+ MockMultipartFile file = new MockMultipartFile("file", "recipient.xlsx",
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", outputStream.toByteArray());
+
+ // 중복 확인에 대한 Mock 설정
+ when(recipientRepository.existsByCareNumber("R12345")).thenReturn(false, true);
+
+
+ RecipientFileUploadResponse response = excelUploadService.uploadRecipientExcel(file, institution.getId());
+
+ // 검증 - 성공 항목과 실패 항목의 개수 확인
+ assertEquals(1, response.uploadedRecipients().size());
+ assertEquals(1, response.failedRecipients().size());
+
+ // 성공적으로 업로드된 항목 검증
+ ExcelRecipientResponse successRecipient = response.uploadedRecipients().get(0);
+ assertEquals("Valid Recipient", successRecipient.getName());
+ assertEquals("R12345", successRecipient.getCareNumber());
+
+ // 실패한 항목 검증
+ ExcelRecipientResponse failedRecipient = response.failedRecipients().get(0);
+ assertEquals("Duplicate Recipient", failedRecipient.getName());
+ assertEquals("R12345", failedRecipient.getCareNumber());
+ }
+
+ // 리플렉션을 사용하여 엔티티의 ID 값을 설정
+ private void setId(Object entity, Long idValue) throws NoSuchFieldException, IllegalAccessException {
+ Field idField = entity.getClass().getSuperclass().getDeclaredField("id");
+ idField.setAccessible(true);
+ idField.set(entity, idValue);
+ }
+}
\ No newline at end of file
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());
+ }
+}
diff --git a/src/test/java/dbdr/openAi/OpenAiTest.java b/src/test/java/dbdr/openAi/OpenAiTest.java
new file mode 100644
index 00000000..b2abbcff
--- /dev/null
+++ b/src/test/java/dbdr/openAi/OpenAiTest.java
@@ -0,0 +1,112 @@
+package dbdr.openAi;
+
+import dbdr.domain.chart.service.ChartService;
+import dbdr.global.configuration.OpenAiSummarizationConfig;
+import dbdr.openai.dto.etc.Choice;
+import dbdr.openai.dto.etc.CompletionTokensDetails;
+import dbdr.openai.dto.etc.Message;
+import dbdr.openai.dto.etc.Usage;
+import dbdr.openai.dto.response.OpenAiSummaryResponse;
+import dbdr.global.exception.ApplicationError;
+import dbdr.global.exception.ApplicationException;
+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 org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.client.RestTemplate;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import java.util.List;
+
+public class OpenAiTest {
+
+ @InjectMocks
+ private ChartService chartService;
+
+ @Mock
+ private OpenAiSummarizationConfig summarizationConfig;
+
+ @Mock
+ private RestTemplate restTemplate;
+
+ @Value("${openai.chat-completions}")
+ private String chatUrl;
+
+ @BeforeEach
+ public void setUp() {
+ MockitoAnnotations.openMocks(this);
+ when(summarizationConfig.restTemplate()).thenReturn(restTemplate);
+ }
+
+ @Test
+ void testOpenAiResponse_SuccessfulResponse() {
+ String testInput = "Sample text";
+ String tempModel = "test-model";
+ String expectedContent = "Test summary response";
+
+ HttpHeaders headers = new HttpHeaders();
+ when(summarizationConfig.httpHeaders()).thenReturn(headers);
+
+ Message message = new Message("user", expectedContent);
+ Choice choice = new Choice(0, message, null, "stop");
+
+ CompletionTokensDetails completionTokensDetails = new CompletionTokensDetails(5);
+ Usage usage = new Usage(10, 10, 20, completionTokensDetails);
+ OpenAiSummaryResponse mockResponse = new OpenAiSummaryResponse(
+ "id", "object", System.currentTimeMillis(), "model", "fingerprint", List.of(choice), usage
+ );
+
+ ResponseEntity responseEntity = new ResponseEntity<>(mockResponse, HttpStatus.OK);
+
+ when(restTemplate.exchange(
+ eq(chatUrl),
+ eq(HttpMethod.POST),
+ any(HttpEntity.class),
+ eq(OpenAiSummaryResponse.class)
+ )).thenReturn(responseEntity);
+
+ OpenAiSummaryResponse result = chartService.openAiResponse(testInput, tempModel);
+
+ assertNotNull(result);
+ assertEquals(expectedContent, result.choices().get(0).message().content());
+ assertEquals("user", result.choices().get(0).message().role());
+ assertEquals("stop", result.choices().get(0).finishReason());
+ }
+
+ @Test
+ void testOpenAiResponse_FailureAndRetry() {
+ String testInput = "Sample text for OpenAI";
+ String tempModel = "test-model";
+
+ // Mock headers
+ HttpHeaders headers = new HttpHeaders();
+ when(summarizationConfig.httpHeaders()).thenReturn(headers);
+
+ // Simulate failure response
+ when(restTemplate.exchange(
+ anyString(),
+ eq(HttpMethod.POST),
+ any(HttpEntity.class),
+ eq(OpenAiSummaryResponse.class)
+ )).thenReturn(
+ new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR),
+ new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR),
+ new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR)
+ );
+
+ ApplicationException exception = assertThrows(ApplicationException.class, () ->
+ chartService.openAiResponse(testInput, tempModel)
+ );
+
+ assertEquals(ApplicationError.OPEN_AI_ERROR, exception.getApplicationError());
+ }
+}
diff --git a/src/test/resources/logging.yml b/src/test/resources/logging.yml
index 8c511b99..da27c30d 100644
--- a/src/test/resources/logging.yml
+++ b/src/test/resources/logging.yml
@@ -3,3 +3,5 @@ logging:
root: info
org.springframework.web: debug # Spring Web 관련 로그
org.springframework.security: debug # Spring Security 관련 로그
+ pattern:
+ dateformat: yyyy-MM-dd HH:mm:ss.SSSz,Asia/Seoul