Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FE][Chore] #1 - React Icons 추가 및 Tailwind css prettier 설정 #146

Merged
merged 17 commits into from
Nov 12, 2024
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
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];
});
});
};
8 changes: 5 additions & 3 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,10 @@ export default [
'no-undef': 'off',
'import/extensions': [
'error',
'ignorePackages',
'always',
{
js: 'never',
jsx: 'never',
js: 'always',
jsx: 'always',
ts: 'never',
tsx: 'never',
},
Expand Down Expand Up @@ -111,6 +111,8 @@ export default [
'import/prefer-default-export': 'off',
'import/no-unresolved': 'warn',
'no-console': 'off',
'consistent-return': 'off',
'import/extensions': '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
5 changes: 3 additions & 2 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@
"dependencies": {
"@fontsource/pretendard": "^5.1.0",
"react": "^18.3.1",
"react-dom": "^18.3.1"
"react-dom": "^18.3.1",
"react-icons": "^5.3.0",
"react-router-dom": "^6.28.0"
},
"devDependencies": {
"@chromatic-com/storybook": "^3.2.2",
Expand All @@ -47,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.

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>
);
};
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: '',
},
};
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,18 +55,17 @@
"jsdoc": "^4.0.4",
"lint-staged": "^15.2.10",
"prettier": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.8",
"typedoc": "^0.26.11",
"typescript": "~5.6.2",
"typescript-eslint": "^8.11.0",
"vitest": "^2.1.4"
},

"lint-staged": {
"**/*.{js,jsx,ts,tsx}": [
"eslint --fix",
"git add"
],
"!docs/**/*.{js,jsx,ts,tsx}": "eslint --fix"
}

}
Loading
Loading