diff --git a/.github/workflows/backend_dev_cd.yml b/.github/workflows/backend_dev_cd.yml new file mode 100644 index 00000000..ade0b4b1 --- /dev/null +++ b/.github/workflows/backend_dev_cd.yml @@ -0,0 +1,180 @@ +name: Backend Dev CD + +on: + workflow_dispatch: + push: + branches: + - dev + paths: + - backend/** + +jobs: + build: + name: 🏗️ Build Jar and Upload Docker Image + environment: dev + runs-on: ubuntu-latest + defaults: + run: + working-directory: backend + + steps: + - uses: actions/checkout@v4 + with: + submodules: true + token: ${{ secrets.SUBMODULE_GITHUB_TOKEN }} + + - name: 🏗️ Set up JDK 21 + uses: actions/setup-java@v4 + with: + distribution: 'corretto' + java-version: 21 + + - name: 🏗️ Set up Gradle + uses: gradle/actions/setup-gradle@v3 + + - name: 🏗️ Build with Gradle + run: ./gradlew clean bootJar + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Docker Image Build and Push + uses: docker/build-push-action@v6 + with: + context: ./backend + push: true + tags: ${{ secrets.DOCKER_REPOSITORY_NAME }}:${{ github.sha }} + platforms: linux/arm64 + + deploy: + name: 🚀 Server Deployment + environment: dev + needs: build + runs-on: [ self-hosted, develup-dev ] + defaults: + run: + working-directory: backend + + env: + BACKEND_APP_IMAGE_NAME: ${{ secrets.DOCKER_REPOSITORY_NAME }}:${{ github.sha }} + MYSQL_DATABASE: ${{ secrets.MYSQL_DATABASE }} + MYSQL_ROOT_PASSWORD: ${{ secrets.MYSQL_ROOT_PASSWORD }} + MYSQL_ROOT_HOST: ${{ secrets.MYSQL_ROOT_HOST }} + + steps: + - uses: actions/checkout@v4 + with: + submodules: true + token: ${{ secrets.SUBMODULE_GITHUB_TOKEN }} + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Docker Compose up + run: docker compose -f compose.dev.yml up -d + + - name: Clean Unused Image + run: docker image prune -af + + slack-notify_success: + runs-on: ubuntu-latest + environment: dev + needs: + - build + - deploy + if: success() + steps: + - name: Extract Commit Title + run: | + COMMIT_TITLE=$(echo "${{ github.event.head_commit.message }}" | head -n 1) + echo "COMMIT_TITLE=$COMMIT_TITLE" >> $GITHUB_ENV + + - name: Build and Deploy Success + uses: slackapi/slack-github-action@v1.24.0 + with: + channel-id: ${{ secrets.ISSUE_CHANNEL }} + payload: | + { + "text": "Build and Deploy Status", + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": " \n 📣 Server Build & Deploy 결과를 안내 드립니다. 📣 \n\t • 🚀 Build Success \n\t • 🟢 Deploy Success \n\t • 🏷️ 관련 Commit: <${{ github.event.head_commit.url }}|${{ env.COMMIT_TITLE }}>" + } + } + ] + } + env: + SLACK_BOT_TOKEN: ${{ secrets.BOT_TOKEN }} + + slack-notify_build-fail: + runs-on: ubuntu-latest + environment: dev + needs: + - build + if: failure() + steps: + - name: Extract Commit Title + run: | + COMMIT_TITLE=$(echo "${{ github.event.head_commit.message }}" | head -n 1) + echo "COMMIT_TITLE=$COMMIT_TITLE" >> $GITHUB_ENV + + - name: Build Fail + uses: slackapi/slack-github-action@v1.24.0 + with: + channel-id: ${{ secrets.ISSUE_CHANNEL }} + payload: | + { + "text": "Build and Deploy Status", + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": " \n 📣 Server Build & Deploy 결과를 안내 드립니다. 📣 \n\t • 🔴 Build Fail \n\t • 🏷️ 관련 Commit: <${{ github.event.head_commit.url }}|${{ env.COMMIT_TITLE }}>" + } + } + ] + } + env: + SLACK_BOT_TOKEN: ${{ secrets.BOT_TOKEN }} + + slack-notify_deploy-fail: + runs-on: ubuntu-latest + environment: dev + needs: + - deploy + if: failure() + steps: + - name: Extract Commit Title + run: | + COMMIT_TITLE=$(echo "${{ github.event.head_commit.message }}" | head -n 1) + echo "COMMIT_TITLE=$COMMIT_TITLE" >> $GITHUB_ENV + + - name: Deploy Fail + uses: slackapi/slack-github-action@v1.24.0 + with: + channel-id: ${{ secrets.ISSUE_CHANNEL }} + payload: | + { + "text": "Build and Deploy Status", + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": " \n 📣 Server Build & Deploy 결과를 안내 드립니다. 📣 \n\t • 🚀Build Success \n\t • 🔴Deploy Fail \n\t • 🏷️ 관련 Commit: <${{ github.event.head_commit.url }}|${{ env.COMMIT_TITLE }}>" + } + } + ] + } + env: + SLACK_BOT_TOKEN: ${{ secrets.BOT_TOKEN }} diff --git a/.github/workflows/backend_dev_ci.yml b/.github/workflows/backend_dev_ci.yml new file mode 100644 index 00000000..1c1f119a --- /dev/null +++ b/.github/workflows/backend_dev_ci.yml @@ -0,0 +1,95 @@ +name: 🚛 Backend Dev CI + +on: + pull_request: + types: [ opened, reopened, synchronize ] + branches: + - dev + +jobs: + PATH_CHANGES: + runs-on: ubuntu-latest + permissions: + pull-requests: read + outputs: + backend: ${{ steps.changes.outputs.backend }} + + steps: + - uses: dorny/paths-filter@v3 + id: changes + with: + filters: | + backend: + - 'backend/**' + + BE_CI: + environment: dev + runs-on: ubuntu-latest + needs: PATH_CHANGES + if: ${{ needs.PATH_CHANGES.outputs.backend == 'true' }} + + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.ref }} + + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: 'corretto' + java-version: '21' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + + - name: Test with Gradle + id: gradle_test + working-directory: backend + run: ./gradlew test + + BE_SLACK_MESSAGE: + environment: dev + runs-on: ubuntu-latest + needs: BE_CI + if: ${{!cancelled() && needs.BE_CI.result != 'skipped'}} + + env: + lilychoibb: ${{secrets.LILYCHOIBB_SLACK_ID}} + robinjoon: ${{secrets.ROBINJOON_SLACK_ID}} + brgndyy: ${{secrets.BRGNDYY_SLACK_ID}} + chosim-dvlpr: ${{secrets.CHOSIM_DVLPR_SLACK_ID}} + Minjoo522: ${{secrets.MINJOO522_SLACK_ID}} + alstn113: ${{secrets.ALSTN113_SLACK_ID}} + le2sky: ${{secrets.LE2SKY_SLACK_ID}} + Parkhanyoung: ${{secrets.PARKHANYOUNG_SLACK_ID}} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.ref }} + + - name: Get teamMember + id: teamMember + run: | + echo "SENDER_SLACK_ID=${{ env[github.event.sender.login] }}" >> $GITHUB_ENV + + - name: Slack mention + uses: slackapi/slack-github-action@v1.24.0 + with: + channel-id: ${{ secrets.ISSUE_CHANNEL }} + payload: | + { + "text": "pr 테스트 결과", + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "pr 테스트 ${{ needs.BE_CI.result }} \n • 링크: <${{ github.event.pull_request.html_url }}|${{ github.event.pull_request.title }}> \n • pr 담당자: <@${{ env.SENDER_SLACK_ID }}>" + } + } + ] + } + env: + SLACK_BOT_TOKEN: ${{ secrets.BOT_TOKEN }} diff --git a/.github/workflows/frontend_dev_ci.yml b/.github/workflows/frontend_dev_ci.yml new file mode 100644 index 00000000..19acf1b0 --- /dev/null +++ b/.github/workflows/frontend_dev_ci.yml @@ -0,0 +1,138 @@ +name: 🎨 Frontend Dev CI + +on: + pull_request: + types: [opened, reopened, synchronize] + branches: + - dev + +env: + lilychoibb: ${{secrets.LILYCHOIBB_SLACK_ID}} + robinjoon: ${{secrets.ROBINJOON_SLACK_ID}} + brgndyy: ${{secrets.BRGNDYY_SLACK_ID}} + chosim-dvlpr: ${{secrets.CHOSIM_DVLPR_SLACK_ID}} + Minjoo522: ${{secrets.MINJOO522_SLACK_ID}} + alstn113: ${{secrets.ALSTN113_SLACK_ID}} + le2sky: ${{secrets.LE2SKY_SLACK_ID}} + Parkhanyoung: ${{secrets.PARKHANYOUNG_SLACK_ID}} + +jobs: + PATH_CHANGES: + runs-on: ubuntu-latest + permissions: + pull-requests: read + outputs: + frontend: ${{ steps.changes.outputs.frontend }} + + steps: + - uses: dorny/paths-filter@v3 + id: changes + with: + filters: | + frontend: + - 'frontend/**' + + FE_CI: + runs-on: ubuntu-latest + needs: PATH_CHANGES + if: ${{ needs.PATH_CHANGES.outputs.frontend == 'true' }} + permissions: + contents: read + packages: write + actions: write + + outputs: + lint: ${{ steps.npm_run_lint_result.outputs.result }} + build: ${{ steps.npm_run_build_result.outputs.result }} + test: ${{ steps.npm_run_test_result.outputs.result }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.ref }} + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.15.1' + + - name: Install dependencies + run: | + cd frontend + npm install + + - name: Run lint + continue-on-error: true + id: npm_run_lint + run: | + cd frontend + npm run lint + + - name: Save Run lint result + continue-on-error: true + id: npm_run_lint_result + run: | + echo "result=${{steps.npm_run_lint.outcome}}" >> $GITHUB_OUTPUT + + - name: Run build + continue-on-error: true + id: npm_run_build + run: | + pwd + cd frontend + npm run build + + - name: Save Run build result + continue-on-error: true + id: npm_run_build_result + run: | + echo "result=${{steps.npm_run_build.outcome}}" >> $GITHUB_OUTPUT + + - name: Run test + continue-on-error: true + id: npm_run_test + run: | + cd frontend + npm run test + + - name: Save Run test result + continue-on-error: true + id: npm_run_test_result + run: | + echo "result=${{steps.npm_run_test.outcome}}" >> $GITHUB_OUTPUT + + FE_SLACK_MESSAGE: + runs-on: ubuntu-latest + needs: FE_CI + if: ${{needs.FE_CI.result != 'skipped'}} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.ref }} + + - name: Get teamMember + id: teamMember + run: | + echo "SENDER_SLACK_ID=${{ env[github.event.sender.login] }}" >> $GITHUB_ENV + + - name: Slack mention + uses: slackapi/slack-github-action@v1.24.0 + with: + channel-id: ${{ secrets.ISSUE_CHANNEL }} + payload: | + { + "text": "pr 테스트 결과", + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "pr 테스트 결과\n lint : ${{ needs.FE_CI.outputs.lint }} \n build : ${{ needs.FE_CI.outputs.build }} \n test : ${{ needs.FE_CI.outputs.test }} \n • 링크: <${{ github.event.pull_request.html_url }}|${{ github.event.pull_request.title }}> \n • pr 담당자: <@${{ env.SENDER_SLACK_ID }}>" + } + } + ] + } + env: + SLACK_BOT_TOKEN: ${{ secrets.BOT_TOKEN }} diff --git a/backend/build.gradle b/backend/build.gradle index 0672379d..795c743f 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -27,6 +27,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-validation' runtimeOnly 'com.h2database:h2' + runtimeOnly 'com.mysql:mysql-connector-j' // jwt implementation "io.jsonwebtoken:jjwt-api:${JJWT_VERSION}" diff --git a/backend/compose.dev.yml b/backend/compose.dev.yml new file mode 100644 index 00000000..6c2b4483 --- /dev/null +++ b/backend/compose.dev.yml @@ -0,0 +1,45 @@ +services: + nginx: + image: nginx + depends_on: + - application + networks: + - nginx-app-net + ports: + - "80:80" + - "443:443" + volumes: + - /home/ubuntu/custom.conf:/etc/nginx/conf.d/default.conf + - /etc/letsencrypt/live/dev.api.devel-up.co.kr/fullchain.pem:/etc/letsencrypt/live/dev.api.devel-up.co.kr/fullchain.pem + - /etc/letsencrypt/live/dev.api.devel-up.co.kr/privkey.pem:/etc/letsencrypt/live/dev.api.devel-up.co.kr/privkey.pem + + application: + image: ${BACKEND_APP_IMAGE_NAME} + depends_on: + - mysql + networks: + - nginx-app-net + - app-mysql-net + ports: + - "8080:8080" + environment: + TZ: "Asia/Seoul" + SPRING_PROFILE: dev + restart: always + container_name: develup-app + + mysql: + image: mysql + networks: + - app-mysql-net + ports: + - "3306:3306" + environment: + MYSQL_DATABASE: ${MYSQL_DATABASE} + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} + MYSQL_ROOT_HOST: ${MYSQL_ROOT_HOST} + container_name: develup-db-dev + +networks: + nginx-app-net: + app-mysql-net: diff --git a/backend/compose.yml b/backend/compose.yml index 35b50b89..8ca024d1 100644 --- a/backend/compose.yml +++ b/backend/compose.yml @@ -22,7 +22,7 @@ services: - "8082:8082" environment: TZ: "Asia/Seoul" - SPRING_PROFILE: dev + SPRING_PROFILE: prod restart: always container_name: develup-app diff --git a/backend/secrets b/backend/secrets index 8ad132f7..f4a6bb66 160000 --- a/backend/secrets +++ b/backend/secrets @@ -1 +1 @@ -Subproject commit 8ad132f73273f382bf81c26505565780266ed303 +Subproject commit f4a6bb669d397a13757622aa1970db8712a033e7 diff --git a/backend/src/main/java/develup/api/config/WebConfig.java b/backend/src/main/java/develup/api/config/WebConfig.java index feb8ed04..5cea1da2 100644 --- a/backend/src/main/java/develup/api/config/WebConfig.java +++ b/backend/src/main/java/develup/api/config/WebConfig.java @@ -2,6 +2,7 @@ import java.util.List; import develup.api.auth.AuthArgumentResolver; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.CorsRegistry; @@ -10,6 +11,11 @@ @Configuration public class WebConfig implements WebMvcConfigurer { + @Value("${client-host}") + private String clientHost; + @Value("${api-host}") + private String apiHost; + private final AuthArgumentResolver authArgumentResolver; public WebConfig(AuthArgumentResolver authArgumentResolver) { @@ -19,9 +25,8 @@ public WebConfig(AuthArgumentResolver authArgumentResolver) { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") - .allowedOrigins("http://localhost:3000", "http://3.38.11.109", "https://www.devel-up.co.kr", - "https://devel-up.co.kr") - .allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH") + .allowedOrigins(clientHost, apiHost) + .allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS") .allowCredentials(true); } diff --git a/backend/src/main/resources/data.sql b/backend/src/main/resources/data.sql index be8486af..8f92ee6c 100644 --- a/backend/src/main/resources/data.sql +++ b/backend/src/main/resources/data.sql @@ -7,33 +7,44 @@ VALUES ('test1@gmail.com', 'GITHUB', '1234', '구름', 'https://avatars.githubus '2024-08-16 13:40:00'); INSERT INTO mission (title, thumbnail, summary, url) -VALUES ('루터회관 흡연 단속', 'https://raw.githubusercontent.com/develup-mission/docs/main/image/java-smoking.png', - '담배피다 걸린 행성이를 위한 벌금 계산 미션', 'https://github.com/develup-mission/java-smoking'), +VALUES ('주문', 'https://raw.githubusercontent.com/develup-mission/docs/main/image/java-order.png', + '배달 주문을 받아보자', 'https://github.com/develup-mission/java-order'), ('숫자 맞추기 게임', 'https://raw.githubusercontent.com/develup-mission/docs/main/image/java-guessing-number.png', - '숫자를 맞춰보자', 'https://github.com/develup-mission/java-guessing-number'); + '숫자를 맞춰보자', 'https://github.com/develup-mission/java-guessing-number'), + ('미로 탈출', 'https://raw.githubusercontent.com/develup-mission/docs/main/image/java-maze.png', + '미노타우로스를 피해 미로에서 탈출하세요!', 'https://github.com/develup-mission/java-maze'), + ('엘리베이터 시뮬레이션', 'https://raw.githubusercontent.com/develup-mission/docs/main/image/java-elevator.png', + '엘리베이터를 만들어봐요.', 'https://github.com/develup-mission/java-elevator'), + ('단어 퍼즐 게임', 'https://raw.githubusercontent.com/develup-mission/docs/main/image/java-word-puzzle.png', + '단어의 퍼즐들을 맞춰주세요!', 'https://github.com/develup-mission/java-word-puzzle'), + ('리액트 회원가입/로그인 폼', 'https://raw.githubusercontent.com/develup-mission/docs/main/image/react-auth-form.png', + '리액트 회원가입/로그인 폼을 구현해봐요.', 'https://github.com/develup-mission/react-auth-form'); INSERT INTO hash_tag (name) VALUES ('JAVA'), ('객체지향'), - ('TDD'), ('클린코드'), - ('레벨1'), - ('잠실캠퍼스'), - ('선릉캠퍼스'); + ('TYPESCRIPT'), + ('REACT'); INSERT INTO mission_hash_tag (mission_id, hash_tag_id) VALUES (1, 1), (1, 2), (1, 3), - (1, 4), - (1, 5), - (1, 6), (2, 1), (2, 2), (2, 3), - (2, 4), - (2, 5), - (2, 7); + (3, 1), + (3, 2), + (3, 3), + (4, 1), + (4, 2), + (4, 3), + (5, 1), + (5, 2), + (5, 3), + (6, 4), + (6, 5); INSERT INTO solution (mission_id, member_id, title, description, url, status, created_at) VALUES (1, 1, '릴리 미션 제출합니다.', '안녕하세요. 잘 부탁 드립니다.', 'https://github.com/develup/mission/pull/1', 'COMPLETED', diff --git a/backend/src/test/java/develup/api/SolutionCommentApiTest.java b/backend/src/test/java/develup/api/SolutionCommentApiTest.java index 9e613c51..95724f6a 100644 --- a/backend/src/test/java/develup/api/SolutionCommentApiTest.java +++ b/backend/src/test/java/develup/api/SolutionCommentApiTest.java @@ -65,7 +65,7 @@ void getMyComments() throws Exception { .andExpect(jsonPath("$.data[0].id", equalTo(1))) .andExpect(jsonPath("$.data[0].solutionId", equalTo(1))) .andExpect(jsonPath("$.data[0].content", equalTo("댓글 내용"))) - .andExpect(jsonPath("$.data[0].createdAt", equalTo(now.toString()))) + .andExpect(jsonPath("$.data[0].createdAt").exists()) .andExpect(jsonPath("$.data[0].solutionTitle", equalTo("솔루션 제목"))) .andExpect(jsonPath("$.data[0].solutionCommentCount", equalTo(123))); } diff --git a/frontend/index.html b/frontend/index.html index e858b214..ea1fa9d3 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,29 +1,26 @@ - -
- - - - - - + -