Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions apps/client/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# shadcn/ui 컴포넌트 폴더 무시
src/components/ui/*

# node_modules는 기본적으로 무시되지만, 명시적으로 추가할 수도 있습니다
node_modules/

# 다른 무시하고 싶은 파일/폴더들
dist/
build/
44 changes: 35 additions & 9 deletions apps/client/.eslintrc
Original file line number Diff line number Diff line change
@@ -1,35 +1,45 @@
{
"parser": "@typescript-eslint/parser",

"parserOptions": {
"project": ["./apps/client/tsconfig.json"],
"project": ["./tsconfig.json"],
"ecmaVersion": 12,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
}
},

"env": {
"browser": true,
"es2021": true
},
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"plugin:@typescript-eslint/recommended",
"prettier"
],

"extends": ["airbnb", "airbnb/hooks", "plugin:@typescript-eslint/recommended", "prettier"],

"settings": {
"react": {
"version": "detect"
}
},

"plugins": ["prettier"],

"rules": {
// React 관련 규칙
"react/react-in-jsx-scope": "off",
"react/no-unescaped-entities": "off",
"react/prop-types": "off",
"react-hooks/exhaustive-deps": "warn",
"react/jsx-filename-extension": [
"warn",
{
"extensions": [".js", ".jsx", ".ts", ".tsx"]
}
],
"react/require-default-props": "off",
"react/jsx-props-no-spreading": "off",

// TypeScript 관련 규칙
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-unused-vars": [
"error",
Expand All @@ -38,6 +48,22 @@
"varsIgnorePattern": "^_", // _ 로 시작하는 변수는 무시
"ignoreRestSiblings": true
}
],

// Import/Export 관련 규칙
"import/no-unresolved": "off",
"import/extensions": ["off"],
"import/prefer-default-export": "off",

// 접근성 관련 규칙
"jsx-a11y/media-has-caption": "off",

// 기타 규칙
"no-param-reassign": [
"warn",
{
"props": false
}
]
}
}
7 changes: 7 additions & 0 deletions apps/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,24 @@
"@types/node": "^20.3.1",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"@vitejs/plugin-react-swc": "^3.5.0",
"autoprefixer": "^10.4.20",
"eslint": "*",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^18.0.0",
"eslint-config-prettier": "*",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-prettier": "*",
"eslint-plugin-react": "^7.37.2",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.14",
"postcss": "^8.4.47",
"prettier": "*",
"tailwindcss": "^3.4.14",
"typescript": "*",
"vite": "^5.4.10"
}
}
2 changes: 1 addition & 1 deletion apps/client/src/Router.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { createBrowserRouter } from 'react-router-dom';
import App from './App';
import Home from '@pages/Home';
import Profile from '@pages/Profile';
import Live from '@pages/Live';
import Broadcast from '@pages/Broadcast';
import Auth from '@pages/Auth';
import Record from '@pages/Record';
import App from './App';
import ProtectedRoute from './ProtectedRoute';

const routerOptions = {
Expand Down
6 changes: 3 additions & 3 deletions apps/client/src/components/ChatContainer/ChatEndModal.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import Modal from '@components/Modal';
import { useNavigate } from 'react-router-dom';

interface ChatEndModalProps {
type ChatEndModalProps = {
setShowModal: (b: boolean) => void;
}
};

function ChatEndModal({ setShowModal }: ChatEndModalProps) {
const navigate = useNavigate();
Expand All @@ -17,7 +17,7 @@ function ChatEndModal({ setShowModal }: ChatEndModalProps) {
<Modal modalClassName="h-32 w-1/3" setShowModal={setShowModal}>
<div className="flex flex-col items-center gap-3">
<p className="text-text-strong text-display-medium16">방송이 종료되었습니다.</p>
<button onClick={handleClick} className="underline">
<button type="button" onClick={handleClick} className="underline">
홈으로 이동하기
</button>
</div>
Expand Down
84 changes: 47 additions & 37 deletions apps/client/src/components/ChatContainer/index.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,26 @@
import { useState, useEffect, useRef, useContext } from 'react';
import { Card, CardHeader, CardTitle, CardContent, CardFooter } from '@components/ui/card';
import { Input } from '@components/ui/input';
import { SmileIcon } from '@/components/Icons';
import { useSocket } from '@hooks/useSocket';
import ErrorCharacter from '@components/ErrorCharacter';
import { AuthContext } from '@/contexts/AuthContext';
import { createPortal } from 'react-dom';
import { AuthContext } from '@/contexts/AuthContext';
import { SmileIcon } from '@/components/Icons';
import ChatEndModal from './ChatEndModal';

interface Chat {
type Chat = {
chatId?: string;
camperId: string;
name: string;
message: string;
}
};

const chatServerUrl = import.meta.env.VITE_CHAT_SERVER_URL;

const ChatContainer = ({ roomId, isProducer }: { roomId: string; isProducer: boolean }) => {
function ChatContainer({ roomId, isProducer }: { roomId: string; isProducer: boolean }) {
const { isLoggedIn } = useContext(AuthContext);
// 채팅 방 입장
const [isJoinedRoom, setIsJoinedRoom] = useState(false);
const isJoinedRoomRef = useRef(false);
// 채팅 전송
const { socket, isConnected, socketError } = useSocket(chatServerUrl);
const [chattings, setChattings] = useState<Chat[]>([]);
Expand All @@ -35,57 +36,65 @@ const ChatContainer = ({ roomId, isProducer }: { roomId: string; isProducer: boo
// 채팅 종료
const [showModal, setShowModal] = useState(false);

const setUpRoom = async (isProducer: boolean) => {
if (isProducer) {
socket?.emit('createRoom', { roomId: roomId });
} else {
// 채팅방 입장
socket?.emit('joinRoom', { roomId: roomId }, () => {});
// 채팅방 종료 이벤트
socket?.on('chatClosed', () => {
setShowModal(true);
});
}
setIsJoinedRoom(true);
};

const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(e.target.value);
};

const hanldeKeyDownEnter = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (isComposing) return;
if (e.key === 'Enter') {
handleSendChat();
}
};

const handleSendChat = () => {
if (inputValue.trim() && socket) {
socket.emit('chat', { roomId: roomId, message: inputValue });
socket.emit('chat', { roomId, message: inputValue });
}
setInputValue('');
};

const handleReceiveChat = (response: Chat) => {
const { camperId, name, message } = response;
setChattings(prev => [...prev, { camperId, name, message }]);
const hanldeKeyDownEnter = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (isComposing) return;
if (e.key === 'Enter') {
handleSendChat();
}
};

const handleClickEmoticon = () => {
alert('구현 예정');
};
// 채팅방 입장
useEffect(() => {
if (!isConnected || !socket || !roomId || isJoinedRoomRef.current) return;

const setUpRoom = async () => {
if (isProducer) {
socket?.emit('createRoom', { roomId });
} else {
// 채팅방 입장
socket?.emit('joinRoom', { roomId }, () => {});
// 채팅방 종료 이벤트
}
isJoinedRoomRef.current = true;
};
setUpRoom();
}, [isConnected, socket, roomId, isProducer]);

// 채팅 이벤트 등록/해제
useEffect(() => {
if (!isConnected || !socket || !roomId || isJoinedRoom) return;
setUpRoom(isProducer);
if (!socket || !isConnected) return () => {};

const handleReceiveChat = (response: Chat) => {
const { camperId, name, message } = response;
setChattings(prev => [...prev, { chatId: `${Date.now()}-${camperId}`, camperId, name, message }]);
};

const handleChatClosed = () => {
setShowModal(true);
};

socket?.on('chat', handleReceiveChat);
socket?.on('chatClosed', handleChatClosed);

return () => {
socket?.off('chat', handleReceiveChat);
socket?.off('chatClosed');
};
}, [isConnected, roomId, socket]);
}, [socket, isConnected]);

// 자동 스크롤
useEffect(() => {
Expand All @@ -106,8 +115,8 @@ const ChatContainer = ({ roomId, isProducer }: { roomId: string; isProducer: boo
<>
<CardContent ref={scrollAreaRef} className="flex flex-1 px-6 pb-2 overflow-y-auto flex-col-reverse">
<div className="w-full flex flex-col space-y-1">
{chattings.map((chat, index) => (
<div key={index}>
{chattings.map((chat: Chat) => (
<div key={chat.chatId}>
<span className="font-medium text-display-medium16 text-text-weak">{chat.camperId} </span>
<span className="font-medium text-display-medium14 text-text-strong">{chat.message}</span>
</div>
Expand All @@ -128,6 +137,7 @@ const ChatContainer = ({ roomId, isProducer }: { roomId: string; isProducer: boo
disabled={!isLoggedIn}
/>
<button
type="button"
onClick={handleClickEmoticon}
className="ml-2 p-2 rounded-full text-text-default"
disabled={!isLoggedIn}
Expand All @@ -142,6 +152,6 @@ const ChatContainer = ({ roomId, isProducer }: { roomId: string; isProducer: boo
{showModal && createPortal(<ChatEndModal setShowModal={setShowModal} />, document.body)}
</>
);
};
}

export default ChatContainer;
8 changes: 4 additions & 4 deletions apps/client/src/components/ErrorCharacter/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
interface ErrorCharacterProps {
type ErrorCharacterProps = {
size?: number;
message?: string;
}
};

const ErrorCharacter = ({ size = 300, message = 'Error' }: ErrorCharacterProps): JSX.Element => {
function ErrorCharacter({ size = 300, message = 'Error' }: ErrorCharacterProps): JSX.Element {
return (
<div style={{ width: size, height: size }} className="flex flex-col items-center">
<svg viewBox="0 0 200 200" className="w-full h-full" xmlns="http://www.w3.org/2000/svg">
Expand Down Expand Up @@ -82,6 +82,6 @@ const ErrorCharacter = ({ size = 300, message = 'Error' }: ErrorCharacterProps):
<p className="text-[#EF4444] font-bold mt-4">{message}</p>
</div>
);
};
}

export default ErrorCharacter;
6 changes: 4 additions & 2 deletions apps/client/src/components/Header/LogInButton.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { useState } from 'react';
import WelcomeCharacter from '@components/WelcomeCharacter';
import { useAuth } from '@/hooks/useAuth';
import { Button } from '@components/ui/button';
import { createPortal } from 'react-dom';
import Modal from '@components/Modal';
import { GithubIcon, GoogleIcon } from '@components/Icons';
import { useAuth } from '@/hooks/useAuth';
import axiosInstance from '@/services/axios';

function LogInButton() {
Expand Down Expand Up @@ -38,6 +38,7 @@ function LogInButton() {
</div>
<div className="flex flex-row md:flex-col h-full justify-around items-center gap-3 p-4">
<button
type="button"
onClick={() => {
requestLogIn('github');
}}
Expand All @@ -49,6 +50,7 @@ function LogInButton() {
</span>
</button>
<button
type="button"
onClick={() => {
requestLogIn('google');
}}
Expand All @@ -59,7 +61,7 @@ function LogInButton() {
Google로 로그인하기
</span>
</button>
<button className="border-none underline" onClick={handleGuestLogIn}>
<button type="button" className="border-none underline" onClick={handleGuestLogIn}>
게스트로 로그인하기
</button>
</div>
Expand Down
6 changes: 3 additions & 3 deletions apps/client/src/components/Header/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { useContext, useEffect, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Character, Logo } from '@components/Icons';
import { Avatar, AvatarFallback, AvatarImage } from '@components/ui/avatar';
import { cn } from '@utils/utils';
import { AuthContext } from '@/contexts/AuthContext';
import axiosInstance from '@/services/axios';
import { cn } from '@utils/utils';
import LogInButton from './LogInButton';
import { Button } from '../ui/button';
import { useAuth } from '@/hooks/useAuth';
Expand Down Expand Up @@ -72,10 +72,10 @@ function Header() {

return (
<header className="fixed top-0 left-0 h-fit w-full px-10 py-3 flex justify-between z-10 bg-surface-default">
<div className="flex flex-row gap-2 hover:cursor-pointer" onClick={handleLogoClick}>
<button type="button" className="flex flex-row gap-2 hover:cursor-pointer" onClick={handleLogoClick}>
<Character size={48} />
<Logo width={109} height={50} className="text-text-strong" />
</div>
</button>
<div className="flex items-center">
{isLoggedIn ? (
<div className="flex gap-2 items-center">
Expand Down
Loading