diff --git a/.github/workflows/release_cd_workflow.yml b/.github/workflows/release_cd_workflow.yml index 8da8f59..871e938 100644 --- a/.github/workflows/release_cd_workflow.yml +++ b/.github/workflows/release_cd_workflow.yml @@ -20,18 +20,24 @@ jobs: - name: Grant execute permission for gradlew run: chmod +x gradlew - - name: Gradle Caching - uses: actions/cache@v3 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - restore-keys: | - ${{ runner.os }}-gradle- +# - name: Gradle Caching +# uses: actions/cache@v3 +# with: +# path: | +# ~/.gradle/caches +# ~/.gradle/wrapper +# key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} +# restore-keys: | +# ${{ runner.os }}-gradle- - name: Build jar - run: ./gradlew --info clean bootJar -x test + run: ./gradlew clean --refresh-dependencies --no-build-cache --no-daemon bootJar -x test #./gradlew --info clean bootJar -x test + + # Actuator 포함 여부 검증 + - name: Assert actuator present + run: | + jar tf build/libs/ouch.jar | grep spring-boot-actuator || { + echo "❌ Actuator not in JAR"; exit 1; } - name: docker login uses: docker/login-action@v3 @@ -45,6 +51,8 @@ jobs: context: . push: true tags: ${{ secrets.DOCKER_HUB_USERNAME }}/${{ secrets.DOCKER_HUB_REPOSITORY }}:latest + no-cache: true # 💡 매 빌드마다 새 이미지 + pull: true # 💡 베이스 이미지도 최신으로 deploy: runs-on: ubuntu-latest @@ -58,16 +66,10 @@ jobs: username: ${{ secrets.EC2_USER }} key: ${{ secrets.EC2_SSH_KEY }} script: | - # secrets를 미리 bash 변수에 담는다 - DOCKER_USERNAME=${{ secrets.DOCKER_HUB_USERNAME }} - DOCKER_REPO=${{ secrets.DOCKER_HUB_REPOSITORY }} - 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 }} + # 디스크 청소 두 줄 / Docker 이미지, 로그 정리 + docker system prune -af || true + docker builder prune -af + docker volume prune -f # docker-compose 설치 여부 확인, 없으면 설치 if ! command -v docker-compose &> /dev/null @@ -76,55 +78,51 @@ jobs: sudo apt-get install -y docker-compose-plugin fi - # Docker 이미지 정리 - docker system prune -af || true - # 최신 이미지 pull - docker pull $DOCKER_USERNAME/$DOCKER_REPO:latest + docker pull ${{ secrets.DOCKER_HUB_USERNAME }}/${{ secrets.DOCKER_HUB_REPOSITORY }}:latest # docker-compose.yml 생성 - cat < /home/ubuntu/docker-compose.yml -version: '3' -services: - app: - image: $DOCKER_USERNAME/$DOCKER_REPO: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 - + echo "version: '3'" > /home/ubuntu/docker-compose.yml + echo "services:" >> /home/ubuntu/docker-compose.yml + echo " app:" >> /home/ubuntu/docker-compose.yml + echo " image: ${{ secrets.DOCKER_HUB_USERNAME }}/${{ secrets.DOCKER_HUB_REPOSITORY }}:latest" >> /home/ubuntu/docker-compose.yml + echo " container_name: app" >> /home/ubuntu/docker-compose.yml + echo " restart: always" >> /home/ubuntu/docker-compose.yml + echo " ports:" >> /home/ubuntu/docker-compose.yml + echo " - '8080:8080'" >> /home/ubuntu/docker-compose.yml + echo " env_file:" >> /home/ubuntu/docker-compose.yml + echo " - .env" >> /home/ubuntu/docker-compose.yml + echo " read_only: true" >> /home/ubuntu/docker-compose.yml + echo " tmpfs:" >> /home/ubuntu/docker-compose.yml + echo " - /tmp" >> /home/ubuntu/docker-compose.yml + echo " healthcheck:" >> /home/ubuntu/docker-compose.yml + echo " test: [\"CMD\",\"curl\",\"-f\",\"http://localhost:8080/actuator/health\"]" >> /home/ubuntu/docker-compose.yml + echo " interval: 30s" >> /home/ubuntu/docker-compose.yml + echo " timeout: 5s" >> /home/ubuntu/docker-compose.yml + echo " retries: 5" >> /home/ubuntu/docker-compose.yml + echo " start_period: 60s" >> /home/ubuntu/docker-compose.yml + # .env 파일 생성 - cat < /home/ubuntu/.env -SPRING_PROFILES_ACTIVE=prod -RDS_HOST=$RDS_HOST -RDS_PORT=$RDS_PORT -RDS_DB=$RDS_DB -RDS_USERNAME=$RDS_USERNAME -RDS_PASSWORD=$RDS_PASSWORD -JWT_SECRET=$JWT_SECRET -OPENAI_API_KEY=$OPENAI_API_KEY -EOF + echo "SPRING_PROFILES_ACTIVE=prod" > /home/ubuntu/.env + echo "RDS_HOST=${{ secrets.RDS_HOST }}" >> /home/ubuntu/.env + echo "RDS_PORT=${{ secrets.RDS_PORT }}" >> /home/ubuntu/.env + echo "RDS_DB=${{ secrets.RDS_DB }}" >> /home/ubuntu/.env + echo "RDS_USERNAME=${{ secrets.RDS_USERNAME }}" >> /home/ubuntu/.env + echo "RDS_PASSWORD=${{ secrets.RDS_PASSWORD }}" >> /home/ubuntu/.env + echo "JWT_SECRET=${{ secrets.JWT_SECRET }}" >> /home/ubuntu/.env + echo "OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY }}" >> /home/ubuntu/.env # .env 파일 권한 설정 chmod 600 /home/ubuntu/.env - + + # 기존 app 컨테이너 강제 제거 (없어도 에러 무시) + docker rm -f app || true + # 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 + docker compose -f /home/ubuntu/docker-compose.yml up -d --force-recreate --remove-orphans - name: Send Discord Notification if: always() @@ -136,14 +134,15 @@ EOF 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 + # 반복 폴링 헬스체크 + HEALTH_STATUS="❌ 서버 비정상 작동" + for i in {1..30}; do + if curl -sSf https://ouchapi.duckdns.org/actuator/health >/dev/null; then + HEALTH_STATUS="✅ 서버 정상 작동" + break + fi + sleep 5 + done MESSAGE="$EMOJI **Ouch 배포 결과**\\n상태: $STATUS\\n$HEALTH_STATUS\\n🔗 프로젝트: ${{ github.repository }}\\n👤 커밋: ${{ github.actor }}" diff --git a/Dockerfile b/Dockerfile index 110620b..8d51d82 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,13 @@ -FROM openjdk:17 +FROM openjdk:17-slim -ARG JAR_FILE=build/libs/*.jar +# curl 설치 (헬스체크용) openjdk:17 (slim버전이 아닌 full 버전)로 설치하고 RUN줄 삭제 해도 됨 +RUN apt-get update && apt-get install -y curl + +ARG JAR_FILE=build/libs/ouch.jar COPY ${JAR_FILE} ouch.jar ENTRYPOINT ["java", "-jar", "ouch.jar"] -HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ - CMD curl -f http://localhost:8080/actuator/health || exit 1 +HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=5 \ + CMD status=$(curl -s -o /dev/null -w '%{http_code}' http://localhost:8080/actuator/health) && [ "$status" -eq 200 ] diff --git a/src/main/java/com/onebridge/ouch/converter/SelfDiagnosisConverter.java b/src/main/java/com/onebridge/ouch/converter/SelfDiagnosisConverter.java index a866d9d..ec22dc0 100644 --- a/src/main/java/com/onebridge/ouch/converter/SelfDiagnosisConverter.java +++ b/src/main/java/com/onebridge/ouch/converter/SelfDiagnosisConverter.java @@ -11,7 +11,6 @@ import com.onebridge.ouch.domain.mapping.DiagnosisSymptom; import com.onebridge.ouch.dto.selfDiagnosis.request.DiagnosisCreateRequest; import com.onebridge.ouch.dto.selfDiagnosis.request.DiagnosisUpdateRequest; -import com.onebridge.ouch.dto.selfDiagnosis.response.DiagnosisUpdateResponse; import com.onebridge.ouch.dto.selfDiagnosis.response.GetDiagnosisByUserIdResponse; import com.onebridge.ouch.dto.selfDiagnosis.response.GetDiagnosisResponse; import com.onebridge.ouch.dto.selfDiagnosis.response.GetSymptomsOfDiagnosisResponse; @@ -19,13 +18,6 @@ @Component public class SelfDiagnosisConverter { - public DiagnosisUpdateResponse diagnosisToDiagnosisUpdateResponse(SelfDiagnosis updatedDiagnosis) { - List symptoms = symptomListForResponseDto(updatedDiagnosis); - return new DiagnosisUpdateResponse(updatedDiagnosis.getId(), updatedDiagnosis.getVisitType(), symptoms, - updatedDiagnosis.getDuration(), updatedDiagnosis.getPainSeverity(), updatedDiagnosis.getAdditionalNote(), - updatedDiagnosis.getCreatedAt().toString()); - } - public GetDiagnosisResponse diagnosisToGetDiagnosisResponse(SelfDiagnosis diagnosis) { List symptoms = symptomListForResponseDto(diagnosis); return new GetDiagnosisResponse(diagnosis.getUser().getId(), diagnosis.getVisitType(), symptoms, diff --git a/src/main/java/com/onebridge/ouch/dto/selfDiagnosis/request/DiagnosisCreateRequest.java b/src/main/java/com/onebridge/ouch/dto/selfDiagnosis/request/DiagnosisCreateRequest.java index 72ab9cd..142964b 100644 --- a/src/main/java/com/onebridge/ouch/dto/selfDiagnosis/request/DiagnosisCreateRequest.java +++ b/src/main/java/com/onebridge/ouch/dto/selfDiagnosis/request/DiagnosisCreateRequest.java @@ -14,9 +14,6 @@ @Getter public class DiagnosisCreateRequest { - // @NotNull(message = "User Id is required.") - // private Long userId; - @NotNull(message = "Visit type is required.") private VisitType visitType; diff --git a/src/main/java/com/onebridge/ouch/dto/selfDiagnosis/response/DiagnosisUpdateResponse.java b/src/main/java/com/onebridge/ouch/dto/selfDiagnosis/response/DiagnosisUpdateResponse.java deleted file mode 100644 index d599a4b..0000000 --- a/src/main/java/com/onebridge/ouch/dto/selfDiagnosis/response/DiagnosisUpdateResponse.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.onebridge.ouch.dto.selfDiagnosis.response; - -import java.util.List; - -import com.onebridge.ouch.domain.enums.SymptomDuration; -import com.onebridge.ouch.domain.enums.VisitType; - -import lombok.AllArgsConstructor; -import lombok.Getter; - -@Getter -@AllArgsConstructor -public class DiagnosisUpdateResponse { - - private Long id; - private VisitType visitType; - private List symptoms; - private SymptomDuration duration; - private Integer painSeverity; - private String additionalNote; - private String createdAt; -} diff --git a/src/main/java/com/onebridge/ouch/security/SecurityConfig.java b/src/main/java/com/onebridge/ouch/security/SecurityConfig.java index 1e69bfb..69be5e3 100644 --- a/src/main/java/com/onebridge/ouch/security/SecurityConfig.java +++ b/src/main/java/com/onebridge/ouch/security/SecurityConfig.java @@ -2,19 +2,18 @@ import java.util.List; -import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; -import org.springframework.web.filter.CorsFilter; import com.onebridge.ouch.security.filter.JwtAuthenticationFilter; import com.onebridge.ouch.security.tokenManger.TokenManager; @@ -35,14 +34,15 @@ public SecurityFilterChain publicResourceConfig(HttpSecurity http) throws Except http.cors( cors -> cors.configurationSource(corsConfigurationSource()) ); + http.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); // 세션 사용 안함 http.addFilterAt(new JwtAuthenticationFilter(tokenManager), BasicAuthenticationFilter.class); http.authorizeHttpRequests( (authorizeRequests) - -> authorizeRequests.requestMatchers("/users/login", "/users/signup/**").permitAll() // 로그인, 회원가입 페이지는 모두 허용 - .anyRequest().authenticated() // 그 외의 요청은 인증 필요 - // -> authorizeRequests.anyRequest().permitAll() // 모든 사용자 접근 가능 - // -> authorizeRequests.anyRequest().authenticated() // 로그인한 사용자만 접근 가능 - ); + -> authorizeRequests.anyRequest().permitAll()); + // -> authorizeRequests.requestMatchers("/users/login", "/users/signup/**", "/actuator/health", "/health", + // "/swagger-ui/**", "/v3/api-docs/**").permitAll() // 로그인, 회원가입 페이지는 모두 허용 + // .anyRequest().authenticated() // 그 외의 요청은 인증 필요 + // ); return http.build(); } @@ -56,7 +56,10 @@ public CorsConfigurationSource corsConfigurationSource() { configuration.addAllowedHeader("*"); configuration.addAllowedMethod("*"); - configuration.setAllowCredentials(true); + configuration.setAllowCredentials(false); + + // ***응답 헤더 노출*** + configuration.setExposedHeaders(List.of("Authorization", "Refresh")); // 필요하면 추가 UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration); diff --git a/src/main/java/com/onebridge/ouch/service/selfDiagnosis/SelfDiagnosisService.java b/src/main/java/com/onebridge/ouch/service/selfDiagnosis/SelfDiagnosisService.java index b4b3903..274a626 100644 --- a/src/main/java/com/onebridge/ouch/service/selfDiagnosis/SelfDiagnosisService.java +++ b/src/main/java/com/onebridge/ouch/service/selfDiagnosis/SelfDiagnosisService.java @@ -19,7 +19,6 @@ import com.onebridge.ouch.dto.selfDiagnosis.request.AddSymptomsToDiagnosisRequest; import com.onebridge.ouch.dto.selfDiagnosis.request.DiagnosisCreateRequest; import com.onebridge.ouch.dto.selfDiagnosis.request.DiagnosisUpdateRequest; -import com.onebridge.ouch.dto.selfDiagnosis.response.DiagnosisUpdateResponse; import com.onebridge.ouch.dto.selfDiagnosis.response.GetDiagnosisByUserIdResponse; import com.onebridge.ouch.dto.selfDiagnosis.response.GetDiagnosisResponse; import com.onebridge.ouch.dto.selfDiagnosis.response.GetSymptomsOfDiagnosisResponse; @@ -110,7 +109,7 @@ public GetSymptomsOfDiagnosisResponse getSymptomsOfDiagnosis(Long diagnosisId, L //자가진단표 수정 @Transactional - public DiagnosisUpdateResponse updateDiagnosis(Long diagnosisId, Long userId, DiagnosisUpdateRequest request) { + public void updateDiagnosis(Long diagnosisId, Long userId, DiagnosisUpdateRequest request) { SelfDiagnosis diagnosis = selfDiagnosisRepository.findByIdAndUserId(diagnosisId, userId) .orElseThrow(() -> new OuchException(DiagnosisErrorCode.DIAGNOSIS_NOT_FOUND)); @@ -139,8 +138,6 @@ public DiagnosisUpdateResponse updateDiagnosis(Long diagnosisId, Long userId, Di } selfDiagnosisRepository.save(updatedDiagnosis); - - return selfDiagnosisConverter.diagnosisToDiagnosisUpdateResponse(updatedDiagnosis); } //특정 자가진단표에 증상 추가 diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 68a72f6..69072eb 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -11,7 +11,7 @@ spring: driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: - ddl-auto: create + ddl-auto: update show-sql: true properties: hibernate: