diff --git a/.github/workflows/preview-cleanup.yml b/.github/workflows/preview-cleanup.yml
new file mode 100644
index 00000000..11a4cc78
--- /dev/null
+++ b/.github/workflows/preview-cleanup.yml
@@ -0,0 +1,34 @@
+name: PR Preview Cleanup (S3 Cleanup + CloudFront 무효화)
+
+on:
+ pull_request:
+ types: [closed]
+
+jobs:
+ cleanup-preview:
+ runs-on: ubuntu-latest
+
+ env:
+ BUCKET_NAME: starsync
+
+ steps:
+ - name: AWS 인증 정보 설정
+ uses: aws-actions/configure-aws-credentials@v4
+ with:
+ aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
+ aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
+ aws-region: ${{ secrets.AWS_REGION }}
+
+ - name: Preview S3 파일 삭제 (PR 닫힐 때)
+ run: |
+ PREVIEW_PATH="preview/pr-${{ github.event.pull_request.number }}"
+ echo "🧹 S3 경로 삭제 중: s3://$BUCKET_NAME/$PREVIEW_PATH/"
+ aws s3 rm s3://$BUCKET_NAME/$PREVIEW_PATH/ --recursive
+
+ - name: CloudFront 캐시 무효화 (Preview PR 경로)
+ run: |
+ INVALIDATION_PATH="/preview/pr-${{ github.event.pull_request.number }}/*"
+ echo "🌀 캐시 무효화 대상: $INVALIDATION_PATH"
+ aws cloudfront create-invalidation \
+ --distribution-id ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }} \
+ --paths "$INVALIDATION_PATH"
diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml
new file mode 100644
index 00000000..b5b748dc
--- /dev/null
+++ b/.github/workflows/preview.yml
@@ -0,0 +1,54 @@
+name: PR Preview Deploy to S3 and CloudFront
+
+on:
+ pull_request:
+ branches:
+ - develop
+ - main
+
+permissions:
+ pull-requests: write
+ contents: read
+
+jobs:
+ Deploy:
+ runs-on: ubuntu-latest
+
+ env:
+ BUCKET_NAME: starsync
+ VITE_BASE_URL: ${{ secrets.VITE_BASE_URL }}
+
+ steps:
+ - name: Github Repository 파일 불러오기
+ uses: actions/checkout@v4
+
+ - name: Node.js 환경 설정
+ uses: actions/setup-node@v4
+ with:
+ node-version: 22.14.0
+
+ - name: 의존성 설치 (npm ci)
+ run: npm ci
+
+ - name: 프로젝트 빌드 (Vite)
+ run: npm run build -- --mode production
+
+ - name: AWS 인증 정보 설정
+ uses: aws-actions/configure-aws-credentials@v4
+ with:
+ aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
+ aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
+ aws-region: ${{ secrets.AWS_REGION }}
+
+ - name: Preview S3 업로드 (PR 번호 기준)
+ run: |
+ PREVIEW_PATH="preview/pr-${{ github.event.pull_request.number }}"
+ echo "Preview 경로: s3://$BUCKET_NAME/$PREVIEW_PATH/"
+ aws s3 sync dist/ s3://$BUCKET_NAME/$PREVIEW_PATH/ --delete
+
+ - name: PR에 Preview 링크 남기기
+ uses: marocchino/sticky-pull-request-comment@v2
+ with:
+ message: |
+ 🚀 **PR Preview 배포 완료!**
+ 🔗 [Preview 확인하기](https://preview.starsync.wiki/preview/pr-${{ github.event.pull_request.number }})
\ No newline at end of file
diff --git a/biome.json b/biome.json
index 66653488..d0c51b37 100644
--- a/biome.json
+++ b/biome.json
@@ -18,7 +18,7 @@
"attributePosition": "auto"
},
"organizeImports": {
- "enabled": true
+ "enabled": false
},
"linter": {
"enabled": true,
diff --git a/src/App.jsx b/src/App.jsx
index 32ad1446..35a48ff6 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -1,6 +1,7 @@
import { useEffect, useState } from 'react';
import { RouterProvider } from 'react-router-dom';
-import { SplashScreen } from './components/loadingStatus/splashScreen';
+import { AlertManager } from './components/alertManager';
+import { SplashScreen } from './components/loadingStatus';
export default function App({ router }) {
const [showSplashScreen, setShowSplashScreen] = useState(true);
@@ -19,5 +20,10 @@ export default function App({ router }) {
() => clearInterval(splashScreenInterval);
}, []);
- return <>{showSplashScreen ?
{content}
diff --git a/src/components/alert/alert.styles.js b/src/components/alert/alert.styles.js index 7c4a0dc4..53340371 100644 --- a/src/components/alert/alert.styles.js +++ b/src/components/alert/alert.styles.js @@ -1,5 +1,5 @@ -import media from '@/styles/responsive'; import { css, keyframes } from '@emotion/react'; +import media from '@/styles/responsive'; const bounceIn = keyframes` 0% { @@ -21,17 +21,25 @@ const bounceIn = keyframes` export const alertWrapper = css` display: flex; position: fixed; + top: 8vh; + left: 35%; z-index: 1111; - width: 30.7rem; + width: 30%; height: 6rem; - margin: auto; padding: 2.7rem ; border-radius: 3.6rem; color: var(--brown-dark); background-color: var(--pink-soft); - ${media({ - top: ['10rem', '10rem', '10rem', '2rem', '2rem'], - })} + ${media({ + width: [ + '50%', + '50%', // 모바일 (375px 이하) + '50%', // 작은 태블릿 (744px 이하) + '30%', // 노트북 (1280px 이하) + '30%', // 데스크탑 (1920px 이하) + ], + left: ['25%', '25%', '25%', '35%', '35%'], + })} animation: ${bounceIn} 0.5s ease forwards; `; diff --git a/src/components/alertManager/AlertManager.jsx b/src/components/alertManager/AlertManager.jsx new file mode 100644 index 00000000..d5c7d802 --- /dev/null +++ b/src/components/alertManager/AlertManager.jsx @@ -0,0 +1,43 @@ +import { useEffect, useRef, useState } from 'react'; +import { Alert } from '@/components/alert'; +import { registerAlertTrigger } from '@/utils/alert'; + +const AlertManager = () => { + const [visible, setVisible] = useState(false); + const [content, setContent] = useState(''); + const [type, setType] = useState('warning'); + const [customStyle, setCustomStyle] = useState({}); + const timeoutRef = useRef(null); + const triggerAlert = (message, type = 'warning', duration = 2000, style = {}) => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + setVisible(true); + setContent(message); + setType(type); + setCustomStyle(style); + timeoutRef.current = setTimeout(() => { + setVisible(false); + timeoutRef.current = null; + }, duration); + }; + + // biome-ignore lint/correctness/useExhaustiveDependencies:{addCommas(data.receivedDonations)}
+{`${addCommas(data.receivedDonations)} / ${addCommas(data.targetDonation)}`}
{daysLeft > 1 ? `D-${daysLeft}` : daysLeft === 1 ? '오늘 마감' : '마감 완료'}
갖고 있는 크레딧보다 더 많이 후원할 수 없어요
} + {isInvalidNumber ? ( +1 이상의 숫자만 입력할 수 있어요
+ ) : hasNoMoney ? ( +갖고 있는 크레딧보다 더 많이 후원할 수 없어요
+ ) : null}투표하는 데 1000 크레딧이 소요됩니다.
- {showAlert &&{uiMessage}
- -{uiMessage}
-{idol.group}
-