Skip to content

Commit

Permalink
Merge branch 'feature/fe/#145-FloatingButton' of https://github.com/b…
Browse files Browse the repository at this point in the history
…oostcampwm-2024/web28-DDara into feature/fe/#145-FloatingButton
  • Loading branch information
leedongyull committed Nov 12, 2024
2 parents 97ba860 + e343842 commit 4a0192d
Show file tree
Hide file tree
Showing 28 changed files with 700 additions and 23 deletions.
6 changes: 5 additions & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,14 @@
"author": "",
"license": "ISC",
"dependencies": {
"bcrypt": "^5.1.1",
"dotenv": "^16.4.5",
"express": "^4.21.1",
"express-validator": "^7.2.0",
"jsonwebtoken": "^9.0.2",
"pg": "^8.13.1",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.1"
"swagger-ui-express": "^5.0.1",
"ws": "^8.11.0"
}
}
1 change: 1 addition & 0 deletions backend/src/constants/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const PORT = 3001;
16 changes: 16 additions & 0 deletions backend/src/controllers/authController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { loginUser } from '../services/authService.js';

export const login = async (req, res) => {
const { id, password } = req.body;

try {
const token = await loginUser(id, password);
if (!token) {
return res.status(401).json({ message: 'Invalid ID or password' });
}
return res.status(200).json({ token });
} catch (error) {
console.error('Login error:', error);
return res.status(500).json({ message: 'Server error occurred' });
}
};
File renamed without changes.
26 changes: 21 additions & 5 deletions backend/src/index.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import express from 'express';
import swaggerUi from 'swagger-ui-express';
import { specs } from '../swaggerConfig';
import { pool } from './db';
import http from 'http';
import { specs } from '../swaggerConfig.js';
import { pool } from './db/db.js';
import { PORT } from './constants/constants.js';
import { initializeWebSocketServer } from './websocketServer.js';
import { authRouter } from './routes/authRouter.js';

const app = express();
app.use(express.json());
const port = 3001;

app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(specs));

app.use('/api/auth', authRouter);

// TODO: 데이터베이스에서 데이터 가져오기 예시
app.get('/guests', async (req, res) => {
try {
Expand All @@ -25,6 +30,17 @@ app.get('/example', (req, res) => {
res.send('Hello World');
});

app.listen(port, () => {
console.log(`Server is running on http://localhost:${port}`);
// HTTP 서버 생성
const server = http.createServer(app);

// WebSocket 서버 초기화
try {
initializeWebSocketServer(server);
console.log('WebSocket server initialized successfully.');
} catch (error) {
console.error('Failed to initialize WebSocket server:', error);
}

app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
9 changes: 9 additions & 0 deletions backend/src/middleware/validationMiddleware.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { validationResult } from 'express-validator';

export const validationMiddleware = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
next();
};
6 changes: 6 additions & 0 deletions backend/src/repositories/userRepository.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { pool } from '../db/db.js';

export const findUserById = async id => {
const result = await pool.query('SELECT * FROM "main"."user" WHERE id = $1', [id]);
return result.rows[0];
};
18 changes: 18 additions & 0 deletions backend/src/routes/authRouter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import express from 'express';
import { body } from 'express-validator';
import { login } from '../controllers/authController.js';
import { validationMiddleware } from '../middleware/validationMiddleware.js';

export const authRouter = express.Router();

authRouter.post(
'/login',
[
body('id').notEmpty().withMessage('ID is required'),
body('password')
.isLength({ min: 6 })
.withMessage('Password must be at least 6 characters long'),
],
validationMiddleware,
login,
);
19 changes: 19 additions & 0 deletions backend/src/services/authService.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
import { findUserById } from '../repositories/userRepository.js';

export const loginUser = async (id, password) => {
const user = await findUserById(id);
if (!user) {
throw new Error('User not found');
}

const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) {
throw new Error('Invalid password');
}

// JWT 생성
const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET, { expiresIn: '1h' });
return { token, userId: user.id };
};
41 changes: 41 additions & 0 deletions backend/src/websocketServer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { WebSocketServer } from 'ws';

const activeConnections = {}; // token별로 연결을 관리하기 위한 객체

export const initializeWebSocketServer = server => {
const wss = new WebSocketServer({ server });

wss.on('connection', (ws, req) => {
// URL에서 token 추출
// TODO: 프론트 라우터 및 token 설정 완료 후 테스트
const url = new URL(req.url, `http://${req.headers.host}`);
const token = url.searchParams.get('token');

if (!token) {
ws.close(4001, 'Token is required');
return;
}

// 동일한 token으로 이미 연결된 클라이언트가 있으면 이전 연결을 강제로 종료
if (activeConnections[token]) {
activeConnections[token].close(4000, 'Duplicate connection');
}

// 새로운 연결을 활성화된 연결 목록에 저장
activeConnections[token] = ws;

console.log(`Client connected with token: ${token}`);

// 클라이언트로부터 메시지 받았을 때의 이벤트 처리
ws.on('message', message => {
console.log(`Received from ${token}:`, message);
});

// 클라이언트 연결 종료 시
ws.on('close', (code, reason) => {
console.log(`Client disconnected with token: ${token}, Code: ${code}, Reason: ${reason}`);
// 연결이 종료되면 activeConnections에서 해당 token 제거
delete activeConnections[token];
});
});
};
15 changes: 11 additions & 4 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,16 @@ export default [
'prettier/prettier': 'error',
'no-underscore-dangle': 'warn',
'no-undef': 'off',
'import/extensions': 'off',
'import/extensions': [
'error',
'always',
{
js: 'always',
jsx: 'always',
ts: 'never',
tsx: 'never',
},
],
},
settings: {
'import/resolver': {
Expand Down Expand Up @@ -102,10 +111,8 @@ export default [
'import/prefer-default-export': 'off',
'import/no-unresolved': 'warn',
'no-console': 'off',
'consistent-return': 'off',
'import/extensions': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/consistent-type-definitions': 'off',
'@typescript-eslint/naming-convention': 'off',
},
},

Expand Down
11 changes: 11 additions & 0 deletions frontend/.storybook/preview.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
import type { Preview } from '@storybook/react';
import '../src/index.css';
// 우선은 폰트 다 포함시켰는데, 나중에 사용할 것들만 따로 뺴자.
import '@fontsource/pretendard/100.css';
import '@fontsource/pretendard/200.css';
import '@fontsource/pretendard/300.css';
import '@fontsource/pretendard/400.css';
import '@fontsource/pretendard/500.css';
import '@fontsource/pretendard/600.css';
import '@fontsource/pretendard/700.css';
import '@fontsource/pretendard/800.css';
import '@fontsource/pretendard/900.css';

const preview: Preview = {
parameters: {
Expand Down
4 changes: 2 additions & 2 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
"@fontsource/pretendard": "^5.1.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-icons": "^5.3.0"
"react-icons": "^5.3.0",
"react-router-dom": "^6.28.0"
},
"devDependencies": {
"@chromatic-com/storybook": "^3.2.2",
Expand All @@ -48,7 +49,6 @@
"eslint-plugin-storybook": "^0.11.0",
"globals": "^15.11.0",
"postcss": "^8.4.47",
"react-router-dom": "^6.28.0",
"storybook": "^8.4.2",
"tailwindcss": "^3.4.14",
"typescript": "~5.6.2",
Expand Down
1 change: 0 additions & 1 deletion frontend/public/vite.svg

This file was deleted.

29 changes: 25 additions & 4 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,26 @@
import { Map } from '@/component/maps/Map.tsx';
import { Route, Routes } from 'react-router-dom';
import { Main } from '@/pages/Main';
import { Register } from '@/pages/Register';
import { AddChannel } from '@/pages/AddChannel';
import { UserRoute } from '@/pages/UserRoute';
import { DrawRoute } from '@/pages/DrawRoute';
import { HostView } from '@/pages/HostView';
import { GuestView } from '@/pages/GuestView';

export const App = () => {
return <Map lat={37.3595704} lng={127.105399} type="naver" />;
};
const ChannelRoutes = () => (
<Routes>
<Route path="host" element={<HostView />} />
<Route path="guest/:guestId" element={<GuestView />} />
</Routes>
);

export const App = () => (
<Routes>
<Route path="/" element={<Main />} />
<Route path="/register" element={<Register />} />
<Route path="/add-channel" element={<AddChannel />} />
<Route path="/add-channel/:user" element={<UserRoute />} />
<Route path="/add-channel/:user/draw" element={<DrawRoute />} />
<Route path="/channel/:channelId/*" element={<ChannelRoutes />} />
</Routes>
);
27 changes: 27 additions & 0 deletions frontend/src/component/common/dropdown/DropdownButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { ReactNode } from 'react';
import classNames from 'classnames';

interface IDropdownButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
children: ReactNode;
className?: string;
}

export const DropdownButton = (props: IDropdownButtonProps) => {
return (
<button
type={props.type ?? 'button'}
className={classNames(
'flex',
'justify-center',
'items-center',
'bg-transparent',
'w-6',
'h-6',
props.className,
)}
{...props}
>
{props.children}
</button>
);
};
5 changes: 4 additions & 1 deletion frontend/src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import './index.css';
import { App } from './App';
// 우선은 폰트 다 포함시켰는데, 나중에 사용할 것들만 따로 뺴자.
Expand All @@ -15,6 +16,8 @@ import '@fontsource/pretendard/900.css';

createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
<BrowserRouter>
<App />
</BrowserRouter>
</StrictMode>,
);
1 change: 1 addition & 0 deletions frontend/src/pages/AddChannel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const AddChannel = () => <>Hello</>;
1 change: 1 addition & 0 deletions frontend/src/pages/DrawRoute.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const DrawRoute = () => <>Hello</>;
1 change: 1 addition & 0 deletions frontend/src/pages/GuestView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const GuestView = () => <>Hello</>;
1 change: 1 addition & 0 deletions frontend/src/pages/HostView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const HostView = () => <>Hello</>;
1 change: 1 addition & 0 deletions frontend/src/pages/Main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const Main = () => <>Hello</>;
1 change: 1 addition & 0 deletions frontend/src/pages/Register.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const Register = () => <>Hello</>;
1 change: 1 addition & 0 deletions frontend/src/pages/UserRoute.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const UserRoute = () => <>Hello</>;
46 changes: 46 additions & 0 deletions frontend/src/stories/DropdownButton.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { Meta, StoryObj } from '@storybook/react';
import { fn } from '@storybook/test';

import { DropdownButton } from '@/component/common/dropdown/DropdownButton.tsx';

import { MdDensityMedium } from 'react-icons/md';

// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
const meta = {
title: 'Dropdown/Button',
component: DropdownButton,
parameters: {
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
layout: 'centered',
},
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
tags: ['autodocs'],
// More on argTypes: https://storybook.js.org/docs/api/argtypes
argTypes: {
children: {
control: 'object',
description: '자식 컴포넌트로 항상 리액트 노드를 넘겨준다.',
table: {
type: { summary: 'ReactNode' },
},
required: true, // 설명 목적으로 required 여부는 table 필드로 작성함
},
className: {
control: 'text',
description: '테일 윈드 기반의 클래스 이름을 넘겨준다.',
},
},
// Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args
args: { onClick: fn() },
} satisfies Meta<typeof DropdownButton>;

export default meta;
type Story = StoryObj<typeof meta>;

// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
export const Default: Story = {
args: {
children: <MdDensityMedium />,
className: '',
},
};
Loading

0 comments on commit 4a0192d

Please sign in to comment.