Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
kwaksj329 committed Nov 28, 2024
2 parents 9fae19e + 931952c commit e85a4e8
Show file tree
Hide file tree
Showing 20 changed files with 351 additions and 102 deletions.
38 changes: 27 additions & 11 deletions .github/workflows/client-ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ on:
- 'client/**'
- 'core/**'
- '.github/workflows/client-ci-cd.yml'
- 'nginx.conf'

jobs:
ci:
Expand Down Expand Up @@ -39,17 +40,33 @@ jobs:
working-directory: ./client
run: pnpm test | true

- name: Build Client
working-directory: ./client
env:
VITE_API_URL: ${{secrets.VITE_API_URL}}
VITE_SOCKET_URL: ${{secrets.VITE_SOCKET_URL}}
run: pnpm build

deploy:
docker-deploy:
needs: ci
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Docker Setup
uses: docker/setup-buildx-action@v3

- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: Build and Push Docker Image
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile.nginx
push: true
tags: ${{ secrets.DOCKERHUB_USERNAME }}/troublepainter-nginx:latest
build-args: |
VITE_API_URL=${{secrets.VITE_API_URL}}
VITE_SOCKET_URL=${{secrets.VITE_SOCKET_URL}}
- name: Deploy to Server
uses: appleboy/[email protected]
with:
Expand All @@ -58,7 +75,6 @@ jobs:
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd /home/mira/web30-stop-troublepainter
git pull origin develop
docker compose build nginx
export DOCKERHUB_USERNAME=${{ secrets.DOCKERHUB_USERNAME }}
docker pull ${{ secrets.DOCKERHUB_USERNAME }}/troublepainter-nginx:latest
docker compose up -d nginx
15 changes: 5 additions & 10 deletions .github/workflows/server-ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,9 @@ jobs:
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile.nestjs
file: ./Dockerfile.api
push: true
tags: ${{ secrets.DOCKERHUB_USERNAME }}/troublepainter:latest
tags: ${{ secrets.DOCKERHUB_USERNAME }}/troublepainter-api:latest
build-args: |
REDIS_HOST=${{ secrets.REDIS_HOST }}
REDIS_PORT=${{ secrets.REDIS_PORT }}
Expand All @@ -75,11 +75,6 @@ jobs:
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd /home/mira/web30-stop-troublepainter
git pull origin develop
echo "REDIS_HOST=${{ secrets.REDIS_HOST }}" > .env
echo "REDIS_PORT=${{ secrets.REDIS_PORT }}" >> .env
echo "CLOVA_API_KEY=${{ secrets.CLOVA_API_KEY }}" >> .env
echo "CLOVA_GATEWAY_KEY=${{ secrets.CLOVA_GATEWAY_KEY }}" >> .env
docker compose up -d
export DOCKERHUB_USERNAME=${{ secrets.DOCKERHUB_USERNAME }}
docker pull ${{ secrets.DOCKERHUB_USERNAME }}/troublepainter-api:latest
docker compose up -d api
5 changes: 0 additions & 5 deletions Dockerfile.nestjs → Dockerfile.api
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,8 @@ RUN corepack enable && corepack prepare [email protected] --activate && cd server && p

WORKDIR /app/server

ARG REDIS_HOST
ARG REDIS_PORT

ENV NODE_ENV=production
ENV PORT=3000
ENV REDIS_HOST=$REDIS_HOST
ENV REDIS_PORT=$REDIS_PORT

EXPOSE 3000

Expand Down
13 changes: 11 additions & 2 deletions Dockerfile.nginx
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,24 @@ RUN corepack enable && corepack prepare [email protected] --activate

WORKDIR /app

COPY pnpm-workspace.yaml package.json pnpm-lock.yaml ./
COPY core/package.json ./core/
COPY client/package.json ./client/

RUN pnpm install --frozen-lockfile

COPY . .

RUN pnpm install --frozen-lockfile && pnpm build
ARG VITE_API_URL
ARG VITE_SOCKET_URL

RUN echo "VITE_API_URL=$VITE_API_URL" > client/.env && echo "VITE_SOCKET_URL=$VITE_SOCKET_URL" >> client/.env && pnpm --filter @troublepainter/core build && pnpm --filter client build

FROM nginx:alpine

COPY nginx.conf /etc/nginx/templates/default.conf.template
COPY --from=builder /app/client/dist /usr/share/nginx/html

EXPOSE 80
EXPOSE 80 443

CMD ["nginx", "-g", "daemon off;"]
4 changes: 2 additions & 2 deletions client/src/api/api.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const API_CONFIG = {
BASE_URL,
ENDPOINTS: {
GAME: {
CREATE_ROOM: '/game/rooms',
CREATE_ROOM: '/api/game/rooms',
},
},
OPTIONS: {
Expand Down Expand Up @@ -92,7 +92,7 @@ export class ApiError extends Error {
* };
*/
export async function fetchApi<T>(endpoint: string, options?: RequestInit): Promise<T> {
const response = await fetch(`${API_CONFIG.BASE_URL}${endpoint}`, {
const response = await fetch(`${endpoint}`, {
...API_CONFIG.OPTIONS,
...options,
headers: {
Expand Down
66 changes: 66 additions & 0 deletions client/src/components/modal/NavigationModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { KeyboardEvent, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { Button } from '@/components/ui/Button';
import { Modal } from '@/components/ui/Modal';
import { useNavigationModalStore } from '@/stores/navigationModal.store';

export const NavigationModal = () => {
const navigate = useNavigate();
const { isOpen, actions } = useNavigationModalStore();

const handleConfirmExit = () => {
actions.closeModal();
navigate('/', { replace: true });
};

const handleKeyDown = useCallback(
(e: KeyboardEvent<Element>) => {
switch (e.key) {
case 'Enter':
handleConfirmExit();
break;
case 'Escape':
actions.closeModal();
break;
}
},
[actions, navigate],
);

return (
<Modal
title="게임 나가기"
isModalOpened={isOpen}
closeModal={actions.closeModal}
className="min-w-72 max-w-sm lg:max-w-md xl:max-w-lg"
handleKeyDown={handleKeyDown}
aria-label="게임 나가기 확인 모달"
>
<div className="flex flex-col gap-4">
<p className="text-center text-violet-950 lg:text-lg xl:text-xl">
정말 게임을 나가실거에요...??
<br />
퇴장하면 다시 돌아오기 힘들어요! 🥺💔
</p>
<div className="flex gap-4" role="group" aria-label="게임 나가기 선택">
<Button
onClick={actions.closeModal}
variant="primary"
className="h-12 flex-1 text-base md:text-lg"
aria-label="게임에 머물기"
>
안나갈래요!
</Button>
<Button
onClick={handleConfirmExit}
variant="primary"
className="h-12 flex-1 bg-red-800 text-base hover:bg-red-600 md:text-lg"
aria-label="게임 나가기 확인"
>
나갈래요..
</Button>
</div>
</div>
</Modal>
);
};
10 changes: 9 additions & 1 deletion client/src/components/ui/Modal.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { HTMLAttributes, KeyboardEvent, PropsWithChildren } from 'react';
import { HTMLAttributes, KeyboardEvent, PropsWithChildren, useEffect, useRef } from 'react';
import ReactDOM from 'react-dom'; // Import ReactDOM explicitly
import { cn } from '@/utils/cn';

Expand All @@ -11,11 +11,19 @@ export interface ModalProps extends PropsWithChildren<HTMLAttributes<HTMLDivElem

const Modal = ({ className, handleKeyDown, closeModal, isModalOpened, title, children, ...props }: ModalProps) => {
const modalRoot = document.getElementById('modal-root');
const modalRef = useRef<HTMLDivElement>(null);

if (!modalRoot) return null;

useEffect(() => {
if (isModalOpened && modalRef.current) {
modalRef.current.focus();
}
}, [isModalOpened]);

return ReactDOM.createPortal(
<div
ref={modalRef}
className={cn(
'fixed left-0 top-0 flex h-full w-full items-center justify-center',
isModalOpened ? 'pointer-events-auto' : 'pointer-events-none',
Expand Down
4 changes: 2 additions & 2 deletions client/src/hooks/socket/useGameSocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ export const useGameSocket = () => {
if (word) gameActions.updateCurrentWord(word);
gameActions.updateTimer(TimerType.DRAWING, drawTime);
gameActions.updateRoomStatus(RoomStatus.DRAWING);
navigate(`/game/${roomId}`);
navigate(`/game/${roomId}`, { replace: true }); // replace: true로 설정, 히스토리에서 대기방 제거
},

guesserRoundStarted: (response: RoundStartResponse) => {
Expand All @@ -164,7 +164,7 @@ export const useGameSocket = () => {
guessers?.forEach((playerId) => gameActions.updatePlayerRole(playerId, PlayerRole.GUESSER));
gameActions.updateTimer(TimerType.DRAWING, drawTime);
gameActions.updateRoomStatus(RoomStatus.DRAWING);
navigate(`/game/${roomId}`);
navigate(`/game/${roomId}`, { replace: true });
},

timerSync: (response: TimerSyncResponse) => {
Expand Down
48 changes: 48 additions & 0 deletions client/src/index.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,51 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer components {
/* 스크롤바 hide 기능 */
/* Hide scrollbar for Chrome, Safari and Opera */
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
/* Hide scrollbar for IE, Edge and Firefox */
.scrollbar-hide {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}

.scrollbar-custom {
scrollbar-width: thin; /* Firefox */
}

/* ===== Scrollbar CSS ===== */
/* Firefox */
* {
scrollbar-width: auto;
scrollbar-color: rgb(67, 79, 115) rgba(178, 199, 222, 0.5);
}
/* Chrome, Safari and Opera etc. */
*::-webkit-scrollbar {
width: 6px;
/* height: 6px; */
}
*::-webkit-scrollbar-track {
background-color: rgba(178, 199, 222, 0.5); /* eastbay-500 with opacity */
border-radius: 9999px;
}
*::-webkit-scrollbar-thumb {
background-color: rgb(67, 79, 115); /* eastbay-900 */
border-radius: 9999px;
}
*::-webkit-scrollbar-thumb:hover {
background-color: rgb(84, 103, 161); /* eastbay-700 */
}

/* 가로 스크롤바 변형 */
.scrollbar-custom.horizontal::-webkit-scrollbar-track {
background-color: rgba(178, 199, 222, 0.5);
}
.scrollbar-custom.horizontal::-webkit-scrollbar-thumb {
background-color: rgb(67, 79, 115);
}
}
56 changes: 56 additions & 0 deletions client/src/layouts/BrowserNavigationGuard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { useEffect } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { useNavigationModalStore } from '@/stores/navigationModal.store';

const BrowserNavigationGuard = () => {
const navigate = useNavigate();
const location = useLocation();
const { actions: modalActions } = useNavigationModalStore();

useEffect(() => {
// 새로고침, beforeunload 이벤트 핸들러
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
// 브라우저 기본 경고 메시지 표시
e.preventDefault();
e.returnValue = ''; // 레거시 브라우저 지원

// 새로고침 시 메인으로 이동하도록 로컬스토리지에 플래그 저장
localStorage.setItem('shouldRedirect', 'true');

// 사용자 정의 메시지 반환 (일부 브라우저에서는 무시될 수 있음)
return '게임을 종료하시겠습니까? 현재 진행 상태가 저장되지 않을 수 있습니다.';
};

// popstate 이벤트 핸들러 (브라우저 뒤로가기/앞으로가기)
const handlePopState = (e: PopStateEvent) => {
e.preventDefault(); // 기본 동작 중단
modalActions.openModal();

// 취소 시 현재 URL 유지를 위해 history stack에 다시 추가하도록 조작
window.history.pushState(null, '', location.pathname);
};

// 초기 진입 시 history stack에 현재 상태 추가
window.history.pushState(null, '', location.pathname);

// 이벤트 리스너 등록
window.addEventListener('beforeunload', handleBeforeUnload);
window.addEventListener('popstate', handlePopState);

// 새로고침 후 리다이렉트 체크
const shouldRedirect = localStorage.getItem('shouldRedirect');
if (shouldRedirect === 'true' && location.pathname !== '/') {
navigate('/', { replace: true });
localStorage.removeItem('shouldRedirect');
}

return () => {
window.removeEventListener('beforeunload', handleBeforeUnload);
window.removeEventListener('popstate', handlePopState);
};
}, [navigate, location.pathname]);

return null;
};

export default BrowserNavigationGuard;
19 changes: 19 additions & 0 deletions client/src/layouts/GameHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Logo } from '@/components/ui/Logo';
import { useNavigationModalStore } from '@/stores/navigationModal.store';

const GameHeader = () => {
const { actions } = useNavigationModalStore();

return (
<header className="flex items-center justify-center">
<button
onClick={actions.openModal}
className="transition-transform hover:scale-105 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-violet-500 focus-visible:ring-offset-2"
>
<Logo variant="side" />
</button>
</header>
);
};

export default GameHeader;
Loading

0 comments on commit e85a4e8

Please sign in to comment.