diff --git a/.github/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml index 733db28..5afe25f 100644 --- a/.github/workflows/ci-docker.yml +++ b/.github/workflows/ci-docker.yml @@ -1,11 +1,11 @@ # 워크플로우의 이름 -name: IssueDive CI +name: IssueDive Docker CI # 워크플로우가 언제 실행될지 정의 on: push: branches: [ "dev" ] # dev 브랜치에 push될 때 실행 - pull_request: + pull_request_target: # Fork에서 보낸 PR도 공용 저장소의 Secrets에 접근할 수 있게 pull_request_target으로 변경함 branches: [ "dev" ] # dev 브랜치로 Pull Request가 생성될 때 실행 # 실행될 작업(Job)들을 정의 @@ -14,23 +14,87 @@ jobs: name: Build and Test # 작업의 이름 (GitHub Actions UI에 표시됨) runs-on: ubuntu-latest # 작업을 실행할 가상 머신 환경 (Ubuntu 최신 버전) + # 0. DB 서비스를 여기에 정의합니다. + # 이 작업(Job)이 실행되는 동안 MySQL 컨테이너를 함께 실행합니다. + services: + mysql: + image: mysql:8 + # GitHub Secrets를 사용하여 DB를 설정합니다. + env: + MYSQL_ROOT_PASSWORD: ${{ secrets.DB_PASSWORD }} + MYSQL_DATABASE: issue_dive + ports: + - 3306:3306 # Runner의 3306 포트와 컨테이너의 3306 포트를 연결 + options: >- + --health-cmd "mysqladmin ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + # 작업의 단계(Step)들을 정의 steps: # 1. 소스 코드 체크아웃 + # GitHub Actions Runner가 우리 저장소의 코드에 접근할 수 있도록 내려받는 단계 - name: Checkout source code uses: actions/checkout@v4 - # 2. JDK 17 설치 - - name: Set up JDK 17 - uses: actions/setup-java@v4 + # 2. Docker Buildx 설정 + # Docker 빌드 성능을 향상시키고 다양한 빌드 기능을 활성화하는 단계 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + # 'pull_request_target'을 사용할 때는 어떤 코드를 체크아웃할지 명시해야 합니다. with: - java-version: '17' - distribution: 'temurin' + ref: ${{ github.event.pull_request.head.sha }} + + # 3. Docker 이미지 빌드 + # Dockerfile을 기반으로 애플리케이션을 빌드하여 Docker 이미지를 생성. + # 이 단계에서 Gradle 빌드가 컨테이너 안에서 먼저 실행됨. + - name: Build Docker image + # GitHub Secrets를 환경 변수로 설정하고, + # docker build 명령어에 --build-arg로 전달합니다. + env: + DB_URL: ${{ secrets.DB_URL }} + DB_USER: ${{ secrets.DB_USER }} + DB_PASSWORD: ${{ secrets.DB_PASSWORD }} + run: | + docker build -t issue-dive-app \ + --build-arg DB_URL="$DB_URL" \ + --build-arg DB_USER="$DB_USER" \ + --build-arg DB_PASSWORD="$DB_PASSWORD" \ + . + # 4. Docker 컨테이너 안에서 테스트 실행 + - name: Run tests inside Docker container + # GitHub Secrets를 다시 한번 환경 변수로 가져옵니다. + env: + # Spring Boot는 이 환경 변수들을 자동으로 인식하여 DB 설정으로 사용합니다. + # URL - localhost: GitHub Actions가 services에 설정된 MySQL 컨테이너를 localhost로 연결 + SPRING_DATASOURCE_URL: jdbc:mysql://localhost:3306/issue_dive?useSSL=false + SPRING_DATASOURCE_USERNAME: ${{ secrets.DB_USER }} + SPRING_DATASOURCE_PASSWORD: ${{ secrets.DB_PASSWORD }} + run: | + docker run --rm \ + --network host \ + -e SPRING_DATASOURCE_URL=$SPRING_DATASOURCE_URL \ + -e SPRING_DATASOURCE_USERNAME=$SPRING_DATASOURCE_USERNAME \ + -e SPRING_DATASOURCE_PASSWORD=$SPRING_DATASOURCE_PASSWORD \ + issue-dive-app ./gradlew test + +# --- 이전 방식 (더 이상 필요 없음) --- +# 이제 모든 빌드와 테스트가 Docker 컨테이너 안에서 이루어지므로, +# GitHub Actions Runner에 직접 Java를 설치하거나 gradlew에 실행 권한을 줄 필요가 없습니다. +# # 2. JDK 17 설치 +# - name: Set up JDK 17 +# uses: actions/setup-java@v4 +# with: +# java-version: '17' +# distribution: 'temurin' + +# # 3. gradlew 파일에 실행 권한 부여 +# - name: Grant execute permission for gradlew +# run: chmod +x ./gradlew - # 3. gradlew 파일에 실행 권한 부여 - - name: Grant execute permission for gradlew - run: chmod +x ./gradlew +# # 4. Gradle로 빌드 및 테스트 실행 +# - name: Build with Gradle +# run: ./gradlew build --no-daemon - # 4. Gradle로 빌드 및 테스트 실행 - - name: Build with Gradle - run: ./gradlew build --no-daemon \ No newline at end of file +# re-run test용 주석 추가 \ No newline at end of file diff --git a/.gitignore b/.gitignore index ade270d..7515a1b 100644 --- a/.gitignore +++ b/.gitignore @@ -43,4 +43,5 @@ application-dev.properties .env .java-version -logs/ \ No newline at end of file +logs/ +gradle.properties \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index f4069d5..e2626bc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,6 +3,17 @@ FROM gradle:8.14.3-jdk17 AS builder WORKDIR /app +# build-arg를 받기 위한 ARG 선언을 추가합니다. +ARG DB_URL +ARG DB_USER +ARG DB_PASSWORD + +# gradle.properties 파일을 동적으로 생성합니다. +RUN echo "dbUrl=${DB_URL}" >> gradle.properties +RUN echo "dbUser=${DB_USER}" >> gradle.properties +RUN echo "dbPassword=${DB_PASSWORD}" >> gradle.properties + + # Gradle 캐시 최적화를 위해 build.gradle과 settings.gradle 먼저 복사 COPY build.gradle settings.gradle gradlew ./ COPY gradle ./gradle @@ -12,7 +23,6 @@ RUN ./gradlew --no-daemon dependencies # 나머지 소스 복사 및 빌드 COPY src ./src -# RUN ./gradlew build --no-daemon -Dspring.profiles.active=test RUN ./gradlew assemble --no-daemon -Dspring.profiles.active=test @@ -26,3 +36,4 @@ COPY --from=builder /app/build/libs/*.jar app.jar # 컨테이너 실행 시 ENTRYPOINT ["java", "-jar", "app.jar"] + diff --git a/build.gradle b/build.gradle index 287fb56..ebd8998 100644 --- a/build.gradle +++ b/build.gradle @@ -1,9 +1,26 @@ +// buildscript 블록 +// 플러그인 자체와, 플러그인이 사용할 MySQL 라이브러리를 함께 정의. +buildscript { + repositories { + gradlePluginPortal() + } + dependencies { + def flywayVersion = "11.11.2" + classpath "org.flywaydb:flyway-gradle-plugin:${flywayVersion}" + classpath "org.flywaydb:flyway-mysql:${flywayVersion}" + } +} + plugins { id 'java' id 'org.springframework.boot' version '3.5.5' id 'io.spring.dependency-management' version '1.1.7' +// id 'org.flywaydb.flyway' version "11.11.2" } +// buildscript에 정의한 플러그인 적용. +apply plugin: 'org.flywaydb.flyway' + group = 'com.issueDive' version = '0.0.1-SNAPSHOT' description = 'issueDive' @@ -41,6 +58,7 @@ dependencies { testImplementation 'org.testcontainers:junit-jupiter' testImplementation 'org.testcontainers:mysql' testImplementation 'com.h2database:h2' +// flywayPlugin 'org.flywaydb:flyway-mysql' // flywayClean 같은 명령어 직접 사용 시 필요. 라이브러리 지정. // QueryDsl implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' @@ -62,4 +80,12 @@ clean{ tasks.named('test') { useJUnitPlatform() jvmArgs '-Xshare:off' +} + +// Flyway 설정 +flyway { + url = dbUrl + user = dbUser + password = dbPassword + cleanDisabled = false } \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 1b44657..85f78df 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,10 +7,6 @@ services: environment: MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} MYSQL_DATABASE: issue_dive - # 일반 사용자 추가하지 않거나 별도의 사용자명으로 변경 - # 예를 들어 USER는 issueuser 등으로 변경 가능 - # MYSQL_USER: issueuser - # MYSQL_PASSWORD: user_password ports: - "3306:3306" volumes: @@ -22,7 +18,12 @@ services: retries: 5 app: - build: . + build: + context: . + args: + - DB_URL=${SPRING_DATASOURCE_URL} + - DB_USER=${SPRING_DATASOURCE_USERNAME} + - DB_PASSWORD=${SPRING_DATASOURCE_PASSWORD} container_name: issueDive-app depends_on: mysql: @@ -34,8 +35,6 @@ services: ports: - "8080:8080" command: ./gradlew bootRun -# volumes: -# - ./:/app tty: true volumes: diff --git a/settings.gradle b/settings.gradle index 1f19b78..128f36c 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1,8 @@ +pluginManagement { + repositories { + gradlePluginPortal() + mavenCentral() + } +} + rootProject.name = 'issueDive' diff --git a/src/main/java/com/issueDive/config/SecurityConfig.java b/src/main/java/com/issueDive/config/SecurityConfig.java index efae866..c07fcce 100644 --- a/src/main/java/com/issueDive/config/SecurityConfig.java +++ b/src/main/java/com/issueDive/config/SecurityConfig.java @@ -4,6 +4,7 @@ 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.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; @@ -34,8 +35,12 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti http .csrf(csrf -> csrf.disable()) .cors(cors -> cors.configurationSource(corsConfigurationSource())) // CORS 설정 + .csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests(authorize -> authorize + // 모든 요청(/**)을 허용 (임시로) .anyRequest().permitAll() // 개발 단계에서는 전체 허용 +// .requestMatchers(PUBLIC_URLS).permitAll() // 공개 URL은 모두 허용 +// .anyRequest().authenticated() // 나머지는 인증 필요 ) .formLogin(formLogin -> formLogin.disable()) // 폼 로그인 비활성화 (필요 시) .logout(logout -> logout.disable()); // 로그아웃 비활성화 (필요 시) @@ -47,11 +52,13 @@ public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); // 프론트엔드 서버 주소 허용 - configuration.setAllowedOrigins(Arrays.asList("http://localhost:5173")); + configuration.setAllowedOrigins(Arrays.asList("http://localhost:5173", "http://localhost:5174")); +// configuration.setAllowedOrigins(Arrays.asList("http://localhost:5174")); // 모든 HTTP 메서드 허용 configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")); // 모든 헤더 허용 - configuration.setAllowedHeaders(Arrays.asList("*")); +// configuration.setAllowedHeaders(Arrays.asList("Origin", "Content-Type", "Accept", "Authorization")); + configuration.addAllowedHeader("*"); // 자격 증명(쿠키 등) 허용 configuration.setAllowCredentials(true); diff --git a/src/main/java/com/issueDive/dto/CreateIssueRequest.java b/src/main/java/com/issueDive/dto/CreateIssueRequest.java index 6338f83..dae02d0 100644 --- a/src/main/java/com/issueDive/dto/CreateIssueRequest.java +++ b/src/main/java/com/issueDive/dto/CreateIssueRequest.java @@ -1,8 +1,11 @@ package com.issueDive.dto; +import java.util.List; + public record CreateIssueRequest( String title, String description, - Long assigneeId + Long assigneeId, + List labels ) { } diff --git a/src/main/java/com/issueDive/entity/Issue.java b/src/main/java/com/issueDive/entity/Issue.java index 97e658e..f61aa9a 100644 --- a/src/main/java/com/issueDive/entity/Issue.java +++ b/src/main/java/com/issueDive/entity/Issue.java @@ -22,7 +22,7 @@ public class Issue { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(nullable = false) + @Column(nullable = false, length = 255) private String title; @Column(columnDefinition = "TEXT") @@ -30,7 +30,7 @@ public class Issue { @Builder.Default @Enumerated(EnumType.STRING) - @Column(nullable = false) + @Column(name = "_status", nullable = false) private IssueStatus status = IssueStatus.OPEN; @ManyToOne(fetch = FetchType.LAZY) @@ -57,4 +57,17 @@ public class Issue { inverseJoinColumns = @JoinColumn(name = "label_id") ) private Set