diff --git a/.github/workflows/boolock-dev-cicd.yml b/.github/workflows/boolock-dev-cicd.yml index 51cd35b..c703073 100644 --- a/.github/workflows/boolock-dev-cicd.yml +++ b/.github/workflows/boolock-dev-cicd.yml @@ -1,4 +1,4 @@ -name: ci/cd action +name: dev ci/cd action on: push: @@ -10,110 +10,90 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: '20.x' - - - name: Install Pnpm - run: npm install -g pnpm - - name: Package install with pnpm - run: pnpm install:all + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + with: + buildkitd-flags: --debug - name: Set BE .env run: | - echo "MONGO_URI=${{ secrets.MONGO_URI }}" > apps/server/.env - echo "SSH_HOST=${{ secrets.SSH_HOST }}" >> apps/server/.env - echo "SSH_PORT=${{ secrets.SSH_PORT }}" >> apps/server/.env - echo "SSH_USER=${{ secrets.SSH_USER }}" >> apps/server/.env - echo "SSH_PASSWORD=${{ secrets.SSH_PASSWORD }}" >> apps/server/.env - echo "SSH_DATABASE_HOST=${{ secrets.SSH_DATABASE_HOST }}" >> apps/server/.env - echo "SSH_DATABASE_PORT=${{ secrets.SSH_DATABASE_PORT }}" >> apps/server/.env - echo "LOCAL_PORT=${{ secrets.LOCAL_PORT }}" >> apps/server/.env - echo "IS_LOCAL=true" >> apps/server/.env - echo "SERVER_CORS_ACCEPT=http://localhost:5173" >> apps/server/.env + echo "MONGO_URI=${{ secrets.TEST_MONGO_URI }}" > apps/server/.env + echo "IS_LOCAL=false" >> apps/server/.env + echo "SERVER_CORS_ACCEPT=${{ secrets.TEST_SERVER_CORS_ACCEPT }}" >> apps/server/.env echo "S3_ACCESS_KEY=${{ secrets.S3_ACCESS_KEY }}" >> apps/server/.env echo "S3_SECRET_KEY=${{ secrets.S3_SECRET_KEY }}" >> apps/server/.env echo "S3_BUCKET_NAME=${{ secrets.S3_BUCKET_NAME }}" >> apps/server/.env - + echo "NODE_ENV=production" >> apps/server/.env + - name: Set FE .env run: | - echo "VITE_SERVER_URL=http://localhost:3000" > apps/client/.env + echo "VITE_SERVER_URL=${{ secrets.DEPLOY_VITE_SERVER_URL }}" > apps/client/.env echo "VITE_MIXPANEL_TOKEN=${{ secrets.VITE_MIXPANEL_TOKEN }}" >> apps/client/.env echo "VITE_STATIC_STORAGE_URL=${{ secrets.VITE_STATIC_STORAGE_URL }}" >> apps/client/.env - - name: Set Nginx SSL files - run: | - mkdir -p apps/client/ssl - echo "${{ secrets.SSL_FULLCHAIN }}" > apps/client/ssl/fullchain.pem - echo "${{ secrets.SSL_PRIVKEY }}" > apps/client/ssl/privkey.pem - - - name: Buld Frontend - run: | - cd apps/client - pnpm run build - - - name: Build Backend - run: | - cd apps/server - pnpm run swagger-auto - pnpm run build - - - name: Build base image - run: | - docker build . --file Dockerfile.base --tag ${{ secrets.DOCKERHUB_USERNAME }}/boolock_base_test:1.0 - docker tag ${{ secrets.DOCKERHUB_USERNAME }}/boolock_base_test:1.0 base-image - - - name: Build frontend and backend images - run: | - docker build . --file apps/client/Dockerfile --tag ${{ secrets.DOCKERHUB_USERNAME }}/boolock_client_test:1.0 - docker build . --file apps/server/Dockerfile --tag ${{ secrets.DOCKERHUB_USERNAME }}/boolock_server_test:1.0 - - name: Login to Docker Hub uses: docker/login-action@v2 with: username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_PASSWORD }} + password: ${{ secrets.DOCKERHUB_ACCESS_TOKEN }} - - name: Docker Hub push - run: | - docker push ${{ secrets.DOCKERHUB_USERNAME }}/boolock_base_test:1.0 - docker push ${{ secrets.DOCKERHUB_USERNAME }}/boolock_client_test:1.0 - docker push ${{ secrets.DOCKERHUB_USERNAME }}/boolock_server_test:1.0 + - name: Build and Push Base Image + uses: docker/build-push-action@v4 + with: + context: . + file: Dockerfile.base + push: true + tags: ${{ secrets.DOCKERHUB_USERNAME }}/base-image-test:latest + cache-from: type=gha,scope=base-image-test + cache-to: type=gha,mode=max,scope=base-image-test + + - name: Build and Push Frontend Image + uses: docker/build-push-action@v4 + with: + context: . + file: apps/client/Dockerfile.test + push: true + tags: ${{ secrets.DOCKERHUB_USERNAME }}/boolock_client_test:latest + cache-from: type=gha,scope=frontend-test + cache-to: type=gha,mode=max,scope=frontend-test + build-args: | + DOCKERHUB_USERNAME=${{ secrets.DOCKERHUB_USERNAME }} + TYPE=test + + - name: Build and Push Backend Image + uses: docker/build-push-action@v4 + with: + context: . + file: apps/server/Dockerfile + push: true + tags: ${{ secrets.DOCKERHUB_USERNAME }}/boolock_server_test:latest + cache-from: type=gha,scope=backend-test + cache-to: type=gha,mode=max,scope=backend-test + build-args: | + DOCKERHUB_USERNAME=${{ secrets.DOCKERHUB_USERNAME }} + TYPE=test deploy: + if: github.event_name == 'push' needs: build runs-on: ubuntu-latest steps: - - name: Login to Docker Hub - run: sudo docker login -u ${{ secrets.DOCKERHUB_USERNAME }} -p ${{ secrets.DOCKERHUB_PASSWORD }} - - name: Deploy with docker uses: appleboy/ssh-action@v0.1.6 with: - host: ${{ secrets.SSH_HOST }} - username: ${{ secrets.SSH_USER }} - password: ${{ secrets.SSH_PASSWORD }} - port: ${{ secrets.SSH_PORT }} + host: ${{ secrets.TEST_SSH_HOST }} + username: ${{ secrets.TEST_SSH_USER }} + password: ${{ secrets.TEST_SSH_PASSWORD }} + port: ${{ secrets.TEST_SSH_PORT }} script: | cd boolock/refactor-web31-BooLock - git checkout main - git pull origin main - - echo "MONGO_URI=${{ secrets.DEPLOY_MONGO_URI }}" > apps/server/.env - echo "IS_LOCAL=false" >> apps/server/.env - echo "SERVER_CORS_ACCEPT=${{ secrets.DEPLOY_SERVER_CORS_ACCEPT }}" >> apps/server/.env - echo "S3_ACCESS_KEY=${{ secrets.S3_ACCESS_KEY }}" >> apps/server/.env - echo "S3_SECRET_KEY=${{ secrets.S3_SECRET_KEY }}" >> apps/server/.env - echo "S3_BUCKET_NAME=${{ secrets.S3_BUCKET_NAME }}" >> apps/server/.env - echo "NODE_ENV=production" >> apps/server/.env + git fetch origin + git checkout dev + git pull origin dev - echo "VITE_SERVER_URL=${{ secrets.DEPLOY_VITE_SERVER_URL }}" > apps/client/.env - echo "VITE_MIXPANEL_TOKEN=${{ secrets.VITE_MIXPANEL_TOKEN }}" >> apps/client/.env - echo "VITE_STATIC_STORAGE_URL=${{ secrets.VITE_STATIC_STORAGE_URL }}" >> apps/client/.env + echo "DOCKERHUB_USERNAME=${{ secrets.DOCKERHUB_USERNAME }}" > .env + echo "${{ secrets.DOCKERHUB_ACCESS_TOKEN }}" | docker login -u ${{ secrets.DOCKERHUB_USERNAME }} --password-stdin - sudo docker compose pull - sudo docker compose down - sudo docker compose up -d --build - sudo docker image prune -f + sudo docker compose -f docker-compose.test.yml pull + sudo docker compose -f docker-compose.test.yml up -d --force-recreate --remove-orphans diff --git a/.github/workflows/boolock-main-cicd.yml b/.github/workflows/boolock-main-cicd.yml new file mode 100644 index 0000000..44508d7 --- /dev/null +++ b/.github/workflows/boolock-main-cicd.yml @@ -0,0 +1,107 @@ +name: main ci/cd action + +on: + push: + branches: ['main'] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + with: + buildkitd-flags: --debug + + - name: Set BE .env + run: | + echo "MONGO_URI=${{ secrets.DEPLOY_MONGO_URI }}" > apps/server/.env + echo "IS_LOCAL=false" >> apps/server/.env + echo "SERVER_CORS_ACCEPT=${{ secrets.DEPLOY_SERVER_CORS_ACCEPT }}" >> apps/server/.env + echo "S3_ACCESS_KEY=${{ secrets.S3_ACCESS_KEY }}" >> apps/server/.env + echo "S3_SECRET_KEY=${{ secrets.S3_SECRET_KEY }}" >> apps/server/.env + echo "S3_BUCKET_NAME=${{ secrets.S3_BUCKET_NAME }}" >> apps/server/.env + echo "NODE_ENV=production" >> apps/server/.env + + - name: Set FE .env + run: | + echo "VITE_SERVER_URL=${{ secrets.DEPLOY_VITE_SERVER_URL }}" > apps/client/.env + echo "VITE_MIXPANEL_TOKEN=${{ secrets.VITE_MIXPANEL_TOKEN }}" >> apps/client/.env + echo "VITE_STATIC_STORAGE_URL=${{ secrets.VITE_STATIC_STORAGE_URL }}" >> apps/client/.env + + - name: Set Nginx SSL files + run: | + mkdir -p apps/client/ssl + echo "${{ secrets.SSL_FULLCHAIN }}" > apps/client/ssl/fullchain.pem + echo "${{ secrets.SSL_PRIVKEY }}" > apps/client/ssl/privkey.pem + + - name: Login to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_ACCESS_TOKEN }} + + - name: Build and Push Base Image + uses: docker/build-push-action@v4 + with: + context: . + file: Dockerfile.base + push: true + tags: ${{ secrets.DOCKERHUB_USERNAME }}/base-image-prod:latest + cache-from: type=gha,scope=base-image-prod + cache-to: type=gha,mode=max,scope=base-image-prod + + - name: Build and Push Frontend Image + uses: docker/build-push-action@v4 + with: + context: . + file: apps/client/Dockerfile + push: true + tags: ${{ secrets.DOCKERHUB_USERNAME }}/boolock_client_prod:latest + cache-from: type=gha,scope=frontend + cache-to: type=gha,mode=max,scope=frontend + build-args: | + DOCKERHUB_USERNAME=${{ secrets.DOCKERHUB_USERNAME }} + TYPE=prod + + - name: Build and Push Backend Image + uses: docker/build-push-action@v4 + with: + context: . + file: apps/server/Dockerfile + push: true + tags: ${{ secrets.DOCKERHUB_USERNAME }}/boolock_server_prod:latest + cache-from: type=gha,scope=backend + cache-to: type=gha,mode=max,scope=backend + build-args: | + DOCKERHUB_USERNAME=${{ secrets.DOCKERHUB_USERNAME }} + TYPE=prod + + deploy: + needs: build + runs-on: ubuntu-latest + steps: + - name: Login to Docker Hub + run: sudo docker login -u ${{ secrets.DOCKERHUB_USERNAME }} -p ${{ secrets.DOCKERHUB_PASSWORD }} + + - name: Deploy with docker + uses: appleboy/ssh-action@v0.1.6 + with: + host: ${{ secrets.SSH_HOST }} + username: ${{ secrets.SSH_USER }} + password: ${{ secrets.SSH_PASSWORD }} + port: ${{ secrets.SSH_PORT }} + script: | + cd boolock/refactor-web31-BooLock + git fetch origin + git checkout main + git pull origin main + + echo "DOCKERHUB_USERNAME=${{ secrets.DOCKERHUB_USERNAME }}" > .env + echo "${{ secrets.DOCKERHUB_ACCESS_TOKEN }}" | docker login -u ${{ secrets.DOCKERHUB_USERNAME }} --password-stdin + + sudo docker compose pull + sudo docker compose up -d --force-recreate --remove-orphans \ No newline at end of file diff --git a/README.md b/README.md index 8d7ef2a..7d868ea 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,11 @@

BooLock

-Frame 2016 - - +![image](https://github.com/user-attachments/assets/c58af651-8a5d-4b09-a98e-04ce50f27fca)


- 웹 개발, 블록으로 쉽고 즐겁게!
+ 웹 개발, 블록 조립으로 쉽고 재밌게!
HTML과 CSS를 배우는 블록코딩 플랫폼

나만의 창작물을 완성하고, 새로운 웹 개발의 가능성을 경험해보세요. ♥️

👇 배포 사이트
diff --git a/apps/client/Dockerfile b/apps/client/Dockerfile index 6f9915d..c234586 100644 --- a/apps/client/Dockerfile +++ b/apps/client/Dockerfile @@ -1,4 +1,7 @@ -FROM base-image AS frontend-build + +ARG DOCKERHUB_USERNAME +ARG TYPE +FROM ${DOCKERHUB_USERNAME}/base-image-${TYPE} AS frontend-build WORKDIR /app/apps/client COPY ./apps/client . @@ -7,7 +10,7 @@ RUN pnpm install --offline --frozen-lockfile &&\ FROM nginx:alpine AS frontend -COPY /apps/client/nginx.conf /etc/nginx/conf.d/default.conf +COPY /apps/client/production/nginx.conf /etc/nginx/conf.d/default.conf COPY /apps/client/ssl /etc/nginx/ssl RUN chmod 644 /etc/nginx/ssl/fullchain.pem &&\ chmod 600 /etc/nginx/ssl/privkey.pem &&\ diff --git a/apps/client/Dockerfile.test b/apps/client/Dockerfile.test new file mode 100644 index 0000000..1270603 --- /dev/null +++ b/apps/client/Dockerfile.test @@ -0,0 +1,19 @@ +ARG DOCKERHUB_USERNAME +ARG TYPE +FROM ${DOCKERHUB_USERNAME}/base-image-${TYPE} AS frontend-build + +WORKDIR /app/apps/client +COPY ./apps/client . +RUN pnpm install --offline --frozen-lockfile &&\ + pnpm run build + +FROM nginx:alpine AS frontend + +COPY /apps/client/test/nginx.conf /etc/nginx/conf.d/default.conf +RUN chown -R nginx:nginx /usr/share/nginx/html + +COPY --from=frontend-build /app/apps/client/dist /usr/share/nginx/html/ +RUN chmod -R 755 /usr/share/nginx/html + +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/apps/client/nginx.conf b/apps/client/production/nginx.conf similarity index 87% rename from apps/client/nginx.conf rename to apps/client/production/nginx.conf index 3bb2383..50b2bcb 100644 --- a/apps/client/nginx.conf +++ b/apps/client/production/nginx.conf @@ -9,10 +9,18 @@ server { server { listen 443 ssl; + http2 on; + http3 on; + quic_retry off; + server_name boolock.site; ssl_certificate /etc/nginx/ssl/fullchain.pem; ssl_certificate_key /etc/nginx/ssl/privkey.pem; + ssl_protocols TLSv1.3; + ssl_prefer_server_ciphers off; + + add_header Alt-Svc 'h3-23=":443"; ma=86400'; location / { root /usr/share/nginx/html; diff --git a/apps/client/src/entities/index.ts b/apps/client/src/entities/index.ts index adb8eeb..7d89b56 100644 --- a/apps/client/src/entities/index.ts +++ b/apps/client/src/entities/index.ts @@ -19,3 +19,4 @@ export { ImageTagModalImg } from './workspace/ImageTagModalImg/ImageTagModalImg' export { ImageTagModalButton } from './workspace/ImageTagModalButton/ImageTagModalButton'; export { ImageTagModalListItem } from './workspace/ImageTagModalListItem/ImageTagModalListItem'; export { CodeExportButton } from './workspace/CodeExportButton/CodeExportButton'; +export { HelpButton } from './workspace/HelpButton/HelpButton'; diff --git a/apps/client/src/entities/workspace/CodeExportButton/CodeExportButton.tsx b/apps/client/src/entities/workspace/CodeExportButton/CodeExportButton.tsx index f407d8e..112c3c0 100644 --- a/apps/client/src/entities/workspace/CodeExportButton/CodeExportButton.tsx +++ b/apps/client/src/entities/workspace/CodeExportButton/CodeExportButton.tsx @@ -1,9 +1,9 @@ import { Spinner } from '@/shared/ui'; import { exportPreviewHtml } from '@/shared/utils'; import toast from 'react-hot-toast'; -import { useState } from 'react'; +import { memo, useState } from 'react'; -export const CodeExportButton = () => { +export const CodeExportButton = memo(() => { const [isLoading, setIsLoading] = useState(false); const handleClick = () => { @@ -32,4 +32,4 @@ export const CodeExportButton = () => { )} ); -}; +}); diff --git a/apps/client/src/entities/workspace/CssCategoryButton/CssCategoryButton.tsx b/apps/client/src/entities/workspace/CssCategoryButton/CssCategoryButton.tsx index 2a94421..f0ec73e 100644 --- a/apps/client/src/entities/workspace/CssCategoryButton/CssCategoryButton.tsx +++ b/apps/client/src/entities/workspace/CssCategoryButton/CssCategoryButton.tsx @@ -1,6 +1,5 @@ import { TCssCategory } from '@/shared/types'; import { useCssPropsStore } from '@/shared/store'; - type CssCategoryButtonProps = { cssCategory: TCssCategory; }; @@ -11,7 +10,8 @@ type CssCategoryButtonProps = { * CSS 카테고리를 선택할 수 있는 버튼 컴포넌트 */ export const CssCategoryButton = ({ cssCategory }: CssCategoryButtonProps) => { - const { selectedCssCategory, setSelectedCssCategory } = useCssPropsStore(); + const selectedCssCategory = useCssPropsStore((state) => state.selectedCssCategory); + const setSelectedCssCategory = useCssPropsStore((state) => state.setSelectedCssCategory); return ( + ); +}); diff --git a/apps/client/src/entities/workspace/RedoButton/RedoButton.tsx b/apps/client/src/entities/workspace/RedoButton/RedoButton.tsx index 6c708e0..5807cc7 100644 --- a/apps/client/src/entities/workspace/RedoButton/RedoButton.tsx +++ b/apps/client/src/entities/workspace/RedoButton/RedoButton.tsx @@ -1,12 +1,13 @@ import { CircleButton } from '@/shared/ui'; import RightArrow from '@/shared/assets/arrow_right.svg?react'; import { useWorkspaceStore } from '@/shared/store'; +import { memo } from 'react'; /** * @description * 워크스페이스 캔버스에서 redo 기능을 실행시키는 버튼입니다. */ -export const RedoButton = () => { +export const RedoButton = memo(() => { const { workspace } = useWorkspaceStore(); const handleRedo = () => { @@ -20,4 +21,4 @@ export const RedoButton = () => { ); -}; +}); diff --git a/apps/client/src/entities/workspace/SaveButton/SaveButton.tsx b/apps/client/src/entities/workspace/SaveButton/SaveButton.tsx index 3a8dda9..dca5134 100644 --- a/apps/client/src/entities/workspace/SaveButton/SaveButton.tsx +++ b/apps/client/src/entities/workspace/SaveButton/SaveButton.tsx @@ -1,14 +1,5 @@ -import * as Blockly from 'blockly/core'; - -import { useCssPropsStore, useResetCssStore, useWorkspaceStore } from '@/shared/store'; - +import { useWorkspaceSave } from '@/shared/hooks/useWorkspaceSave'; import { Spinner } from '@/shared/ui'; -import { capturePreview, trackEvent } from '@/shared/utils'; -import { cssStyleToolboxConfig } from '@/shared/blockly'; -import toast from 'react-hot-toast'; -import { useParams } from 'react-router-dom'; -import { useSaveWorkspace } from '@/shared/hooks'; -import { useState } from 'react'; /** * @@ -17,40 +8,13 @@ import { useState } from 'react'; * 저장 항목 : css 속성, 캔버스 블록 상태, css class 블록, css 리셋 여부, 미리보기 썸네일 */ export const SaveButton = () => { - const workspaceId = useParams().workspaceId as string; - const { mutate: saveWorkspace, isPending } = useSaveWorkspace(workspaceId); - const { totalCssPropertyObj } = useCssPropsStore(); - const { workspace } = useWorkspaceStore(); - const { isResetCssChecked } = useResetCssStore(); - const [isCapture, setIsCapture] = useState(false); - - const handleClick = async () => { - trackEvent('workspace_saved'); - try { - const canvas = Blockly.serialization.workspaces.save(workspace!) as any; - setIsCapture(true); - const thumbnail = await capturePreview(); - saveWorkspace({ - totalCssPropertyObj, - canvas, - classBlockList: cssStyleToolboxConfig.contents, - cssResetStatus: isResetCssChecked, - thumbnail, - }); - setIsCapture(false); - } catch (error) { - if (error instanceof Error) { - toast.error(error.message); - } - } finally { - setIsCapture(false); - } - }; + const { handleSave, isCapture, isPending } = useWorkspaceSave(); return ( <> +

{isPending || isCapture ? '저장 중' : '저장 완료'}