Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 82 additions & 31 deletions .github/workflows/release_cd_workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,18 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4 # 저장소 코드를 가져옴
- uses: actions/checkout@v4

- name: Set up JDK 17 # 자바 설치
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'adopt'

- name: Grant execute permission for gradlew
run: chmod +x gradlew # gradlew 실행 권한 부여
run: chmod +x gradlew

- name: Gradle Caching # Gradle 빌드 캐싱을 통해 속도 향상
- name: Gradle Caching
uses: actions/cache@v3
with:
path: |
Expand All @@ -31,26 +31,24 @@ jobs:
${{ runner.os }}-gradle-

- name: Build jar
run: ./gradlew --info clean bootJar -x test # 테스트는 생략하고 bootJar로 .jar 파일 생성
run: ./gradlew --info clean bootJar -x test

- name: docker login # Docker Hub 로그인
- name: docker login
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}

- name: docker build and push #Docker 이미지 빌드 & 푸시
- name: docker build and push
uses: docker/build-push-action@v5.1.0
with:
context: .
push: true
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/${{ secrets.DOCKER_HUB_REPOSITORY }}:latest
# cache-from: type=registry,ref=${{ secrets.DOCKER_HUB_USERNAME }}/${{ secrets.DOCKER_HUB_REPOSITORY }}:latest
# cache-to: type=inline # 빌드 시 캐시 활용이 필요하면 추가(깃허브 액션)

deploy: # SSH 접속 방식 (Self-hosted Runner 방식X) # 배포 자동화
deploy:
runs-on: ubuntu-latest
needs: build-and-push-image # build가 끝난 후 실행됨
needs: build-and-push-image

steps:
- name: SSH into EC2 and deploy
Expand All @@ -60,32 +58,85 @@ jobs:
username: ${{ secrets.EC2_USER }}
key: ${{ secrets.EC2_SSH_KEY }}
script: |
# docker-compose 설치 여부 확인
if ! command -v docker-compose &> /dev/null
then
sudo apt-get update
sudo apt-get install -y docker-compose-plugin
fi

# Docker 이미지 정리
docker system prune -af || true

# 최신 이미지 pull
docker pull ${{ secrets.DOCKER_HUB_USERNAME }}/${{ secrets.DOCKER_HUB_REPOSITORY }}:latest

docker stop app || true
docker rm app || true

docker run -d \
--name app \
--restart unless-stopped \
-p 8080:8080 \
--env SPRING_PROFILES_ACTIVE=prod \
--env RDS_HOST=${{ secrets.RDS_HOST }} \
--env RDS_PORT=${{ secrets.RDS_PORT }} \
--env RDS_DB=${{ secrets.RDS_DB }} \
--env RDS_USERNAME=${{ secrets.RDS_USERNAME }} \
--env RDS_PASSWORD=${{ secrets.RDS_PASSWORD }} \
--env OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY }} \
--env JWT_SECRET=${{ secrets.JWT_SECRET }} \
${{ secrets.DOCKER_HUB_USERNAME }}/${{ secrets.DOCKER_HUB_REPOSITORY }}:latest

# 디스코드 배포 알림
# docker-compose.yml 생성
cat <<EOF > /home/ubuntu/docker-compose.yml
version: '3'
services:
app:
image: ${{ secrets.DOCKER_HUB_USERNAME }}/${{ secrets.DOCKER_HUB_REPOSITORY }}:latest
container_name: app
restart: always
ports:
- '8080:8080'
env_file:
- .env
read_only: true
tmpfs:
- /tmp
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
interval: 30s
timeout: 5s
retries: 3
EOF

# .env 파일 생성
cat <<EOF > /home/ubuntu/.env
SPRING_PROFILES_ACTIVE=prod
RDS_HOST=${{ secrets.RDS_HOST }}
RDS_PORT=${{ secrets.RDS_PORT }}
RDS_DB=${{ secrets.RDS_DB }}
RDS_USERNAME=${{ secrets.RDS_USERNAME }}
RDS_PASSWORD=${{ secrets.RDS_PASSWORD }}
JWT_SECRET=${{ secrets.JWT_SECRET }}
OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY }}
EOF

# .env 파일 권한 설정
chmod 600 /home/ubuntu/.env

# docker-compose pull
docker compose -f /home/ubuntu/docker-compose.yml pull

# docker-compose 재배포
docker compose -f /home/ubuntu/docker-compose.yml down || true
docker compose -f /home/ubuntu/docker-compose.yml up -d

- name: Send Discord Notification
if: always()
run: |
STATUS="${{ job.status }}"
MESSAGE="🚀 **Ouch 배포 완료**\n배포 상태: $STATUS\n🔗 프로젝트: ${{ github.repository }}\n👤 커밋: ${{ github.actor }}"
if [ "$STATUS" == "success" ]; then
EMOJI="✅"
else
EMOJI="❌"
fi

# 서버 Healthcheck
sleep 5
HEALTH=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/actuator/health || true)
if [ "$HEALTH" == "200" ]; then
HEALTH_STATUS="✅ 서버 정상 작동"
else
HEALTH_STATUS="❌ 서버 비정상 작동"
fi

MESSAGE="$EMOJI **Ouch 배포 결과**\\n상태: $STATUS\\n$HEALTH_STATUS\\n🔗 프로젝트: ${{ github.repository }}\\n👤 커밋: ${{ github.actor }}"

curl -H "Content-Type: application/json" \
-X POST \
-d "{\"content\": \"$MESSAGE\"}" \
${{ secrets.DISCORD_WEBHOOK_URL }}
${{ secrets.DISCORD_WEBHOOK_URL }} || true
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ dependencies {
// 스프링 시큐리티
implementation "org.springframework.boot:spring-boot-starter-security"

// 도커 헬스체크용
implementation 'org.springframework.boot:spring-boot-starter-actuator'

//jwt 토큰
implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.onebridge.ouch.apiPayload.code.error;

import org.springframework.http.HttpStatus;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public enum DiagnosisErrorCode implements ErrorCode {

SYMPTOM_NOT_FOUND(HttpStatus.NOT_FOUND, "DIAGNOSIS401", "입력한 증상이 존재하지 않습니다."),
DIAGNOSIS_NOT_FOUND(HttpStatus.NOT_FOUND, "DIAGNOSIS402", "자가진단표가 존재하지 않습니다."),
SYMPTOM_ALREADY_ADDED(HttpStatus.BAD_REQUEST, "DIAGNOSIS403", "해당 증상이 이미 존재합니다."),
;

private final HttpStatus httpStatus;
private final String code;
private final String message;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.onebridge.ouch.apiPayload.code.error;

import org.springframework.http.HttpStatus;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public enum HealthStatusErrorCode implements ErrorCode {

HEALTH_STATUS_NOT_FOUND(HttpStatus.NOT_FOUND, "HEALTH-STATUS401", "건강상태가 존재하지 않습니다.");

private final HttpStatus httpStatus;
private final String code;
private final String message;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.onebridge.ouch.apiPayload.code.error;

import org.springframework.http.HttpStatus;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public enum MedicalRecordErrorCode implements ErrorCode {

MEDICAL_RECORD_NOT_FOUND(HttpStatus.NOT_FOUND, "MEDICAL-RECORD401", "의료기록이 존재하지 않습니다."),
HOSPITAL_NOT_FOUND(HttpStatus.NOT_FOUND, "MEDICAL-RECORD402", "입력한 병원이 존재하지 않습니다."),
DEPARTMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "MEDICAL-RECORD403", "입력한 진료 과가 존재하지 않습니다."),
MEDICAL_RECORD_USER_NOT_MATCH(HttpStatus.FORBIDDEN, "MEDICAL-RECORD404", "해당 사용자의 의료기록이 아닙니다."),
;

private final HttpStatus httpStatus;
private final String code;
private final String message;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package com.onebridge.ouch.controller.healthStatus;

import java.util.List;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
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;

import com.onebridge.ouch.apiPayload.ApiResponse;
import com.onebridge.ouch.dto.healthStatus.request.HealthStatusCreateRequest;
import com.onebridge.ouch.dto.healthStatus.request.HealthStatusUpdateRequest;
import com.onebridge.ouch.dto.healthStatus.response.DateAndDisease;
import com.onebridge.ouch.dto.healthStatus.response.GetHealthStatusResponse;
import com.onebridge.ouch.security.authorization.UserId;
import com.onebridge.ouch.service.healthStatus.HealthStatusService;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;

@Tag(name = "건강 상태 API", description = "건강 상태 API")
@RestController
@RequestMapping("/health-status")
@RequiredArgsConstructor
public class HealthStatusController {

private final HealthStatusService healthStatusService;

//건강상태 생성
@Operation(summary = "건강상태 생성 API", description = "건강상태 생성 API 입니다.")
@PostMapping
public ResponseEntity<ApiResponse<Void>> createHealthStatus(
@RequestBody @Valid HealthStatusCreateRequest request,
@UserId Long userId
) {
healthStatusService.createHealthStatus(request, userId);
return ResponseEntity.ok(ApiResponse.successWithNoData());
}

//특정 건강상태 조회
@Operation(summary = "건강상태 조회 API", description = "건강상태 조회 API 입니다.")
@GetMapping("/{healthStatusId}")
public ResponseEntity<ApiResponse<GetHealthStatusResponse>> getHealthStatus(@PathVariable Long healthStatusId,
@UserId Long userId
) {
GetHealthStatusResponse response = healthStatusService.getHealthStatus(healthStatusId, userId);
return ResponseEntity.ok(ApiResponse.success(response));
}

/*
//특정 사용자의 모든 건강상태 조회
@Operation(summary = "특정 사용자의 모든 건강상태 조회 API", description = "특정 사용자의 모든 건강상태 조회 API 입니다.")
@GetMapping
public ResponseEntity<ApiResponse<List<DateAndDisease>>> getUsersAllHealthStatus(
@UserId Long userId
) {
List<DateAndDisease> list = healthStatusService.getUsersAllHealthStatus(userId);
return ResponseEntity.ok(ApiResponse.success(list));
}
*/ // 건강 상태 한 개만 보유

//특정 건강상태 수정
@Operation(summary = "건강상태 수정 API", description = "건강상태 수정 API 입니다.")
@PutMapping("/{healthStatusId}")
public ResponseEntity<ApiResponse<Void>> updateHealthStatus(
@RequestBody @Valid HealthStatusUpdateRequest request,
@PathVariable Long healthStatusId,
@UserId Long userId
) {
healthStatusService.updateHealthStatus(request, healthStatusId, userId);
return ResponseEntity.ok(ApiResponse.successWithNoData());
}

//특정 건강상태 삭제
@Operation(summary = "건강상태 삭제 API", description = "건강상태 삭제 API 입니다.")
@DeleteMapping("/{healthStatusId}")
public ResponseEntity<ApiResponse<Void>> deleteHealthStatus(@PathVariable Long healthStatusId,
@UserId Long userId
) {
healthStatusService.deleteHealthStatus(healthStatusId, userId);
return ResponseEntity.ok(ApiResponse.successWithNoData());
}
}
Loading