diff --git a/.github/workflows/cleanup-preview.yml b/.github/workflows/cleanup-preview.yml
new file mode 100644
index 00000000..54d810df
--- /dev/null
+++ b/.github/workflows/cleanup-preview.yml
@@ -0,0 +1,36 @@
+name: Cleanup-Preview
+
+on:
+ pull_request:
+ types: [closed]
+ branches: ['develop']
+
+jobs:
+ cleanup:
+ runs-on: ubuntu-latest
+ environment: PREVIEW_ENV
+ permissions:
+ pull-requests: write
+
+ steps:
+ - name: Configure AWS Credentials
+ uses: aws-actions/configure-aws-credentials@v4
+ with:
+ aws-access-key-id: ${{ secrets.AWS_PREVIEW_ACCESS_KEY_ID }}
+ aws-secret-access-key: ${{ secrets.AWS_PREVIEW_SECRET_ACCESS_KEY }}
+ aws-region: ap-northeast-2
+
+ - name: Delete Preview from S3
+ run: |
+ aws s3 rm s3://${{ secrets.AWS_PREVIEW_BUCKET_NAME }}/pr-${{ github.event.pull_request.number }}/ --recursive
+
+ - name: Comment Cleanup Notice
+ uses: actions/github-script@v7
+ with:
+ script: |
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.payload.pull_request.number,
+ body: '๐งน Preview ๋ฐฐํฌ๊ฐ ์ ๋ฆฌ๋์์ต๋๋ค.'
+ });
diff --git a/.github/workflows/deploy-preview.yml b/.github/workflows/deploy-preview.yml
new file mode 100644
index 00000000..3d8274ef
--- /dev/null
+++ b/.github/workflows/deploy-preview.yml
@@ -0,0 +1,119 @@
+name: Deploy-Preview
+
+on:
+ pull_request:
+ types: [opened, synchronize, reopened]
+ branches: ['develop']
+
+concurrency:
+ group: preview-${{ github.event.pull_request.number }}
+ cancel-in-progress: true
+
+jobs:
+ deploy-preview:
+ runs-on: ubuntu-latest
+ environment: PREVIEW_ENV
+ permissions:
+ pull-requests: write
+ contents: read
+
+ steps:
+ - name: Setup NodeJS
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20
+
+ - name: Checkout Code
+ uses: actions/checkout@v4
+
+ - name: Setup .env
+ run: |
+ echo "${{ vars.ENV }}" > .env
+ echo "${{ secrets.ENV }}" >> .env
+ echo "VITE_BASE_PATH=/pr-${{ github.event.pull_request.number }}" >> .env
+
+ - name: Install Dependencies
+ run: npm ci
+
+ - name: Build Preview
+ run: npm run build
+
+ - name: Configure AWS Credentials
+ uses: aws-actions/configure-aws-credentials@v4
+ with:
+ aws-access-key-id: ${{ secrets.AWS_PREVIEW_ACCESS_KEY_ID }}
+ aws-secret-access-key: ${{ secrets.AWS_PREVIEW_SECRET_ACCESS_KEY }}
+ aws-region: ap-northeast-2
+
+ - name: Deploy to S3 Preview Bucket
+ run: |
+ aws s3 sync ./dist s3://${{ secrets.AWS_PREVIEW_BUCKET_NAME }}/pr-${{ github.event.pull_request.number }}/ --delete
+
+ - name: Deploy OAuth Redirect Handler
+ run: |
+ cat > /tmp/oauth-handler.html << 'OAUTH_EOF'
+
+
+
Redirecting...
+
+
+
+
+ OAUTH_EOF
+ aws s3 cp /tmp/oauth-handler.html s3://${{ secrets.AWS_PREVIEW_BUCKET_NAME }}/oauth --content-type "text/html"
+
+ - name: Invalidate CloudFront Cache
+ run: |
+ aws cloudfront create-invalidation \
+ --distribution-id ${{ secrets.AWS_PREVIEW_CLOUDFRONT_ID }} \
+ --paths "/pr-${{ github.event.pull_request.number }}/*" "/oauth"
+
+ - name: Comment Preview URL on PR
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const prNumber = context.payload.pull_request.number;
+ const url = `https://preview.debate-timer.com/pr-${prNumber}/`;
+
+ const body = `## ๐ Preview ๋ฐฐํฌ ์๋ฃ!
+
+ | ํ๊ฒฝ | URL |
+ |-----|-----|
+ | Preview | [์ด๊ธฐ](${url}) |
+ | API | Dev ํ๊ฒฝ |
+
+ > PR์ด ๋ซํ๋ฉด ์๋์ผ๋ก ์ ๋ฆฌ๋ฉ๋๋ค.`;
+
+ const { data: comments } = await github.rest.issues.listComments({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: prNumber,
+ });
+
+ const existing = comments.find(c =>
+ c.user.login === 'github-actions[bot]' &&
+ c.body.includes('Preview ๋ฐฐํฌ ์๋ฃ')
+ );
+
+ if (existing) {
+ await github.rest.issues.updateComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ comment_id: existing.id,
+ body
+ });
+ } else {
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: prNumber,
+ body
+ });
+ }
diff --git a/src/constants/debate_template.ts b/src/constants/debate_template.ts
index bac13ddf..961c0bce 100644
--- a/src/constants/debate_template.ts
+++ b/src/constants/debate_template.ts
@@ -16,7 +16,9 @@ import { DebateTemplate } from '../type/type';
function createTableShareUrl(encodeData: string): string {
const baseUrl = import.meta.env.VITE_SHARE_BASE_URL || window.location.origin;
const normalizedBaseUrl = baseUrl.replace(/\/+$/, '');
- return `${normalizedBaseUrl}/share?data=${encodeData}`;
+ const basePath = import.meta.env.VITE_BASE_PATH;
+ const pathPrefix = basePath && basePath !== '/' ? basePath : '';
+ return `${normalizedBaseUrl}${pathPrefix}/share?data=${encodeData}`;
}
interface DebateTemplateList {
ONE: DebateTemplate[];
diff --git a/src/i18n.ts b/src/i18n.ts
index a7da32f7..a11585d5 100644
--- a/src/i18n.ts
+++ b/src/i18n.ts
@@ -19,7 +19,7 @@ i18n
// ๋ฒ์ญ ํ์ผ์ ๋ถ๋ฌ์ฌ ์์น
backend: {
- loadPath: '/locales/{{lng}}/translation.json',
+ loadPath: `${import.meta.env.VITE_BASE_PATH || ''}/locales/{{lng}}/translation.json`,
},
// React์ ํจ๊ป ์ฌ์ฉํ ๋์ ์ต์
diff --git a/src/page/DebateVotePage/DebateVotePage.tsx b/src/page/DebateVotePage/DebateVotePage.tsx
index 4f7924da..d1deff7f 100644
--- a/src/page/DebateVotePage/DebateVotePage.tsx
+++ b/src/page/DebateVotePage/DebateVotePage.tsx
@@ -33,7 +33,9 @@ export default function DebateVotePage() {
const isArgsValid = isPollIdValid && isTableIdValid;
const voteUrl = useMemo(() => {
- return `${baseUrl}/vote/${pollId}`;
+ const basePath = import.meta.env.VITE_BASE_PATH;
+ const pathPrefix = basePath && basePath !== '/' ? basePath : '';
+ return `${baseUrl}${pathPrefix}/vote/${pollId}`;
}, [baseUrl, pollId]);
const handleGoToResult = () => {
diff --git a/src/routes/routes.tsx b/src/routes/routes.tsx
index e19ae4ee..d6c9a899 100644
--- a/src/routes/routes.tsx
+++ b/src/routes/routes.tsx
@@ -102,28 +102,33 @@ const protectedAppRoutes = appRoutes.map((route) => ({
),
}));
-const router = createBrowserRouter([
+const router = createBrowserRouter(
+ [
+ {
+ element: (
+ <>
+
+
+ >
+ ),
+ children: [
+ {
+ path: '/',
+ element: ,
+ children: protectedAppRoutes, // ๊ธฐ๋ณธ ์ธ์ด(ko) ๋ผ์ฐํธ
+ },
+ {
+ path: ':lang', // ๋ค๋ฅธ ์ธ์ด ๋ผ์ฐํธ
+ element: ,
+ children: protectedAppRoutes,
+ },
+ ],
+ },
+ ],
{
- element: (
- <>
-
-
- >
- ),
- children: [
- {
- path: '/',
- element: ,
- children: protectedAppRoutes, // ๊ธฐ๋ณธ ์ธ์ด(ko) ๋ผ์ฐํธ
- },
- {
- path: ':lang', // ๋ค๋ฅธ ์ธ์ด ๋ผ์ฐํธ
- element: ,
- children: protectedAppRoutes,
- },
- ],
+ basename: import.meta.env.VITE_BASE_PATH || '/',
},
-]);
+);
// ๋ผ์ฐํธ ๋ณ๊ฒฝ ์ Google Analytics ์ด๋ฒคํธ ์ ์ก
router.subscribe(({ location }) => {
diff --git a/src/util/arrayEncoding.ts b/src/util/arrayEncoding.ts
index 515d4db1..a96c30bc 100644
--- a/src/util/arrayEncoding.ts
+++ b/src/util/arrayEncoding.ts
@@ -28,7 +28,9 @@ export function createTableShareUrl(
const resolvedBaseUrl =
baseUrl && baseUrl.trim() !== '' ? baseUrl : window.location.origin;
const normalizedBaseUrl = resolvedBaseUrl.replace(/\/+$/, '');
- return `${normalizedBaseUrl}/share?data=${encoded}`;
+ const basePath = import.meta.env.VITE_BASE_PATH;
+ const pathPrefix = basePath && basePath !== '/' ? basePath : '';
+ return `${normalizedBaseUrl}${pathPrefix}/share?data=${encoded}`;
}
export function extractTableShareUrl(url: string): DebateTableData | null {
diff --git a/src/util/googleAuth.ts b/src/util/googleAuth.ts
index 1d4104c9..5d3ee844 100644
--- a/src/util/googleAuth.ts
+++ b/src/util/googleAuth.ts
@@ -7,12 +7,18 @@ export const oAuthLogin = () => {
throw new Error('OAuth ์ ๋ณด๊ฐ ์์ต๋๋ค.');
}
- const params = {
+ const params: Record = {
client_id: import.meta.env.VITE_GOOGLE_O_AUTH_CLIENT_ID,
redirect_uri: import.meta.env.VITE_GOOGLE_O_AUTH_REDIRECT_URI,
response_type: 'code',
scope: 'openid profile email',
};
+
+ const basePath = import.meta.env.VITE_BASE_PATH;
+ if (basePath && basePath !== '/') {
+ params.state = basePath;
+ }
+
const queryString = new URLSearchParams(params).toString();
const googleOAuthUrl = `${import.meta.env.VITE_GOOGLE_O_AUTH_REQUEST_URL}?${queryString}`;
diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts
index bd3d95eb..17433776 100644
--- a/src/vite-env.d.ts
+++ b/src/vite-env.d.ts
@@ -2,6 +2,7 @@
interface ImportMetaEnv {
readonly VITE_MOCK_API: string;
+ readonly VITE_BASE_PATH: string;
}
interface ImportMeta {
diff --git a/vite.config.ts b/vite.config.ts
index ce11360d..4489551d 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -6,6 +6,7 @@ const viteConfig = defineViteConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '');
return {
+ base: env.VITE_BASE_PATH || '/',
plugins: [react()],
server: {
proxy: {