diff --git a/backend/package.json b/backend/package.json index 06c7da8..0290606 100644 --- a/backend/package.json +++ b/backend/package.json @@ -4,7 +4,9 @@ "private": true, "scripts": { "start": "nodemon --exec ts-node -r tsconfig-paths/register src/index.ts", - "lint": "eslint src" + "lint": "eslint src", + "api-docs": "swagger-cli bundle src/swagger/openapi.yaml --outfile src/swagger/swagger.yaml --type yaml", + "prestart": "yarn run api-docs" }, "dependencies": { "bcrypt": "^5.0.1", @@ -24,8 +26,11 @@ "redis": "^3.1.2", "reflect-metadata": "^0.1.13", "socket.io": "^4.3.1", + "swagger-cli": "^4.0.4", + "swagger-ui-express": "^4.1.6", "typeorm": "^0.2.38", - "uuid": "^8.3.2" + "uuid": "^8.3.2", + "yamljs": "^0.3.0" }, "devDependencies": { "@types/bcrypt": "^5.0.0", @@ -37,7 +42,9 @@ "@types/passport-github": "^1.1.6", "@types/passport-local": "^1.0.34", "@types/redis": "^2.8.32", + "@types/swagger-ui-express": "^4.1.3", "@types/uuid": "^8.3.1", + "@types/yamljs": "^0.2.31", "@typescript-eslint/eslint-plugin": "^5.2.0", "@typescript-eslint/parser": "^5.2.0", "dotenv": "^10.0.0", diff --git a/backend/src/controllers/chat-controller.ts b/backend/src/controllers/chat-controller.ts index 8b35005..0f23c9a 100644 --- a/backend/src/controllers/chat-controller.ts +++ b/backend/src/controllers/chat-controller.ts @@ -1,5 +1,5 @@ import { Request, Response } from 'express'; -import ChatRoomService from '@src/services/chat-room-service'; +import ChatRoomService from '@services/chat-room-service'; const ChatController = { async createChatRoom(req: Request, res: Response) { @@ -21,6 +21,48 @@ const ChatController = { } catch (err) { res.sendStatus(409); } + }, + + async updateChatRoomName(req: Request, res: Response) { + try { + const { chatRoomId } = req.params; + const chatRoomName = req.body.chat_room_name; + await ChatRoomService.getInstance().updateChatRoomName(Number(chatRoomId), chatRoomName); + res.sendStatus(201); + } catch (err) { + res.sendStatus(409); + } + }, + + async getChatRoomUsers(req: Request, res: Response) { + try { + const { chatRoomId } = req.params; + const chatRoomInfo = await ChatRoomService.getInstance().getChatRoomUsers(Number(chatRoomId)); + res.status(200).json(chatRoomInfo); + } catch (err) { + res.sendStatus(409); + } + }, + + async addChatRoomUsers(req: Request, res: Response) { + try { + const { chatRoomId } = req.params; + const { user_list } = req.body; + await ChatRoomService.getInstance().addChatRoomUsers(Number(chatRoomId), user_list); + res.sendStatus(201); + } catch (err) { + res.sendStatus(409); + } + }, + + async deleteChatRoomUser(req: Request, res: Response) { + try { + const { chatRoomId, userId } = req.params; + await ChatRoomService.getInstance().deleteChatRoomUser(Number(chatRoomId), Number(userId)); + res.sendStatus(204); + } catch (err) { + res.sendStatus(409); + } } }; diff --git a/backend/src/controllers/team-contorller.ts b/backend/src/controllers/team-contorller.ts index 93568bf..a7fc8d1 100644 --- a/backend/src/controllers/team-contorller.ts +++ b/backend/src/controllers/team-contorller.ts @@ -4,82 +4,87 @@ import TeamService from '@services/team-service'; import UserService from '@services/user-service'; const TeamController = { - async read(req: any, res: Response) { + + async create(req: any, res: Response) { try { const userId = req.user_id; - const teams = await TeamUserService.getInstance().read(userId); - res.status(200).send(teams); + const teamId = await TeamService.getInstance().create(req.body); + await TeamUserService.getInstance().create(userId, teamId); + res.sendStatus(201); } catch (err) { - res.status(404).send(err); + res.sendStatus(409); } }, - async readTeamInfo(req: any, res: Response) { + async read(req: any, res: Response) { try { - const team = await TeamService.getInstance().read(req.params.id); - res.status(200).send(team); + const userId = req.user_id; + const teams = await TeamUserService.getInstance().read(userId); + res.status(200).send(teams); } catch (err) { - res.status(404).send(err); + res.status(409).send(err); } }, - async readTeamUsers(req: any, res: Response) { + async update(req: any, res: Response) { try { - const teams = await TeamUserService.getInstance().readAllUsers(req.params.id); - res.status(200).send(teams); + const teamId = req.params.teamId; + await TeamService.getInstance().update(teamId, req.body); + res.sendStatus(201); } catch (err) { - res.status(404).send(err); + res.sendStatus(409); } }, - async create(req: any, res: Response) { + async delete(req: any, res: Response) { try { - const userId = req.user_id; - const teamId = await TeamService.getInstance().create(req.body); - await TeamUserService.getInstance().create(userId, teamId); + const teamId = req.params.teamId; + await TeamService.getInstance().delete(teamId); res.sendStatus(204); } catch (err) { res.sendStatus(409); } }, - async delete(req: any, res: Response) { + async readTeamInfo(req: any, res: Response) { try { - const teamId = req.body.team_id; - await TeamService.getInstance().delete(teamId); - res.sendStatus(204); + const team = await TeamService.getInstance().read(req.params.teamId); + res.status(200).send(team[0]); } catch (err) { - res.sendStatus(409); + res.sendStatus(409) } }, - async update(req: any, res: Response) { + async readTeamUsers(req: any, res: Response) { try { - await TeamService.getInstance().update(req.body); - res.sendStatus(204); + const teamId = req.params.teamId; + const teams = await TeamUserService.getInstance().readAllUsers(teamId); + res.status(200).send(teams); } catch (err) { - res.sendStatus(409); + res.status(409).send(err); } }, async invite(req: any, res: Response) { try { - const { user_email, team_id } = req.body; - const userInfo = await UserService.getInstance().getUserByEmail(user_email); + const teamId = req.params.teamId; + const userName = req.body.userName; + const userInfo = await UserService.getInstance().getUserByUserName(userName); + if(!userInfo) res.sendStatus(404); const userId = userInfo.user_id; - await TeamUserService.getInstance().invite(userId, team_id); + await TeamUserService.getInstance().invite(userId, teamId); res.sendStatus(201); } catch (err) { - res.sendStatus(204); + res.sendStatus(409); } }, async acceptInvitation(req: any, res: Response) { try { const userId = req.user_id; - const teamId = req.body.team_id; + const teamId = req.params.teamId; await TeamUserService.getInstance().update(userId, teamId); - res.sendStatus(204); + res.sendStatus(201); } catch (err) { res.sendStatus(409); } @@ -88,7 +93,7 @@ const TeamController = { async declineInvitation(req: any, res: Response) { try { const userId = req.user_id; - const teamId = req.body.team_id; + const teamId = req.params.teamId; await TeamUserService.getInstance().delete(userId, teamId); res.sendStatus(204); } catch (err) { @@ -98,13 +103,25 @@ const TeamController = { async kickOut(req: any, res: Response) { try { - const userId = req.params.id; - const teamId = req.body.team_id; + const userId = req.params.userId; + const teamId = req.params.teamId; await TeamUserService.getInstance().delete(userId, teamId); res.sendStatus(204); } catch (err) { res.sendStatus(409); } + }, + + async changeRole(req: any, res: Response) { + try { + const teamId = req.params.teamId; + const userId = req.params.userId; + const role = req.body.role; + await TeamUserService.getInstance().changeRole(userId, teamId, role); + res.sendStatus(201); + } catch (err) { + res.sendStatus(409); + } } }; diff --git a/backend/src/controllers/user-controller.ts b/backend/src/controllers/user-controller.ts index 84490f9..971b26f 100644 --- a/backend/src/controllers/user-controller.ts +++ b/backend/src/controllers/user-controller.ts @@ -39,8 +39,8 @@ const UserController = { const existUser = await UserService.getInstance().getUserByName(req.body.newName); if (existUser) return res.sendStatus(409); const newUser = await UserService.getInstance().updateUserToName(req.user_id, req.body.newName); - if (!newUser) return res.sendStatus(401); - res.sendStatus(204); + if (!newUser) return res.sendStatus(409); + res.status(201).send(newUser); } catch (err) { res.sendStatus(409); } diff --git a/backend/src/entities/chat_room-user.ts b/backend/src/entities/chat_room-user.ts index 072a296..da92918 100644 --- a/backend/src/entities/chat_room-user.ts +++ b/backend/src/entities/chat_room-user.ts @@ -17,7 +17,7 @@ export class ChatRoomUser { @JoinColumn({ name: 'user_id' }) user: User; - @ManyToOne(() => ChatRoom, (chatRoom) => chatRoom.chat_room_id) + @ManyToOne(() => ChatRoom, (chatRoom) => chatRoom.chat_room_id, { onDelete: 'CASCADE' }) @JoinColumn({ name: 'chat_room_id' }) chat_room: ChatRoom; } diff --git a/backend/src/entities/chat_room.ts b/backend/src/entities/chat_room.ts index 142078b..f0934fd 100644 --- a/backend/src/entities/chat_room.ts +++ b/backend/src/entities/chat_room.ts @@ -1,6 +1,5 @@ import { Entity, PrimaryGeneratedColumn, ManyToOne, JoinColumn, Column, OneToMany } from 'typeorm'; import { ChatRoomUser } from './chat_room-user'; -import { Message } from './message'; import { Team } from './team'; @Entity({ name: 'chat_room' }) @@ -18,9 +17,6 @@ export class ChatRoom { @Column() chat_room_name: string; - @OneToMany(() => ChatRoomUser, (chatRoomUser) => chatRoomUser.chat_room) + @OneToMany(() => ChatRoomUser, (chatRoomUser) => chatRoomUser.chat_room, { cascade: true }) chat_room_users: ChatRoomUser[]; - - @OneToMany(() => Message, (message) => message.chat_room) - messages: Message[]; } diff --git a/backend/src/entities/message.ts b/backend/src/entities/message.ts deleted file mode 100644 index 5496a8e..0000000 --- a/backend/src/entities/message.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Entity, PrimaryGeneratedColumn, ManyToOne, JoinColumn, OneToMany, Column, CreateDateColumn } from 'typeorm'; -import { ChatRoom } from './chat_room'; -import { Reaction } from './reaction'; -import { User } from './user'; - -@Entity({ name: 'message' }) -export class Message { - @PrimaryGeneratedColumn() - message_id!: number; - - @ManyToOne(() => ChatRoom, (chatRoom) => chatRoom.chat_room_id) - @JoinColumn({ name: 'chat_room_id' }) - chat_room: ChatRoom; - - @ManyToOne(() => User, (user) => user.user_id) - @JoinColumn({ name: 'user_id' }) - user: User; - - @Column() - content: string; - - @CreateDateColumn() - created_at: Date; - - @OneToMany(() => Reaction, (reaction) => reaction.message_id) - reactions: Reaction[]; -} diff --git a/backend/src/entities/postit.ts b/backend/src/entities/postit.ts deleted file mode 100644 index 5b7b6bb..0000000 --- a/backend/src/entities/postit.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - JoinColumn -} from 'typeorm'; -import { User } from './user'; -import { Team } from './team'; - -@Entity({ name: 'postit' }) -export class Postit { - @PrimaryGeneratedColumn() - postit_id!: number; - - @ManyToOne(() => User, (user) => user.user_id) - @JoinColumn({ name: 'created_by' }) - user: User; - - @ManyToOne(() => User, (user) => user.user_id) - @JoinColumn({ name: 'updated_by' }) - user2: User; - - @ManyToOne(() => Team, (team) => team.team_id, { onDelete: 'CASCADE' }) - @JoinColumn({ name: 'team_id' }) - team: Team; - - @CreateDateColumn() - created_at: Date; - - @UpdateDateColumn() - updated_at: Date; - - @Column() - title: string; - - @Column() - content: string; - - @Column() - x: number; - - @Column() - y: number; - - @Column() - color: number; -} diff --git a/backend/src/entities/reaction.ts b/backend/src/entities/reaction.ts deleted file mode 100644 index 4e1f36f..0000000 --- a/backend/src/entities/reaction.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Entity, PrimaryGeneratedColumn, ManyToOne, Column } from 'typeorm'; -import { Message } from './message'; -import { User } from './user'; - -@Entity({ name: 'reaction' }) -export class Reaction { - @PrimaryGeneratedColumn() - reaction_id: number; - - @ManyToOne(() => Message, (message) => message.message_id) - message_id: Message; - - @ManyToOne(() => User, (user) => user.user_id) - user_id: User; - - @Column() - emoji_code: number; -} diff --git a/backend/src/entities/team.ts b/backend/src/entities/team.ts index bd6c3b1..fd6940f 100644 --- a/backend/src/entities/team.ts +++ b/backend/src/entities/team.ts @@ -1,6 +1,5 @@ import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm'; import { ChatRoom } from './chat_room'; -import { Postit } from './postit'; import { Schedule } from './schedule'; import { TeamUser } from './team-user'; @@ -23,7 +22,4 @@ export class Team { @OneToMany(() => ChatRoom, (chatRoom) => chatRoom.team, { cascade: true }) chat_rooms: ChatRoom[]; - - @OneToMany(() => Postit, (postit) => postit.team, { cascade: true }) - postits: Postit[]; } diff --git a/backend/src/entities/user.ts b/backend/src/entities/user.ts index eb2f6dc..b1738e4 100644 --- a/backend/src/entities/user.ts +++ b/backend/src/entities/user.ts @@ -1,5 +1,4 @@ import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm'; -import { Postit } from './postit'; import { TeamUser } from './team-user'; import { ChatRoomUser } from './chat_room-user'; @@ -31,10 +30,4 @@ export class User { @OneToMany(() => ChatRoomUser, (chatRoomUser) => chatRoomUser.user) chat_room_users: ChatRoomUser[]; - - @OneToMany(() => Postit, (postit) => postit.user) - postits: Postit[]; - - @OneToMany(() => Postit, (postit) => postit.user2) - postit2: Postit[]; } diff --git a/backend/src/index.ts b/backend/src/index.ts index 6293ac0..b5cbbbd 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -13,20 +13,26 @@ import { initStrategy } from './passport'; import { Namespace, Server } from 'socket.io'; import socketInit from './sockets'; +import swaggerUi from 'swagger-ui-express'; +import YAML from 'yamljs'; + import userRouter from '@routes/user-router'; import authRouter from '@routes/auth-router'; import scheduleRouter from '@routes/schedule-router'; import teamRouter from '@routes/team-router'; import chatRouter from '@routes/chat-router'; +import path from 'path'; class App { app: express.Application; server: any; // Server from http? https? port: string; + swaggerSpec: any; constructor() { this.app = express(); this.port = process.env.PORT || '4000'; + this.swaggerSpec = YAML.load(path.join(__dirname, './swagger/swagger.yaml')) this.config(); this.middleware(); this.route(); @@ -54,11 +60,12 @@ class App { } private route() { - this.app.use('/api/user', userRouter); + this.app.use('/api/users', userRouter); this.app.use('/api/auth', authRouter); - this.app.use('/api/schedule', scheduleRouter); - this.app.use('/api/team', teamRouter); - this.app.use('/api/chat', chatRouter); + this.app.use('/api/schedules', scheduleRouter); + this.app.use('/api/teams', teamRouter); + this.app.use('/api/chats', chatRouter); + this.app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(this.swaggerSpec)) } listen() { diff --git a/backend/src/middlewares/user.ts b/backend/src/middlewares/user.ts index 5fecbe5..cac1b29 100644 --- a/backend/src/middlewares/user.ts +++ b/backend/src/middlewares/user.ts @@ -1,5 +1,6 @@ import { Response, NextFunction } from 'express'; import UserService from '@services/user-service'; +import TeamUserService from '@src/services/team-user-service'; export const getUserInfo = async (req: any, res: Response, next: NextFunction) => { const user_id = req.user_id; @@ -8,3 +9,19 @@ export const getUserInfo = async (req: any, res: Response, next: NextFunction) = req.user = user; next(); }; + +export const checkTeamUser = async (req: any, res: Response, next: NextFunction) => { + const userId = req.user_id; + const teamId = req.params.teamId; + const teamUser = await TeamUserService.getInstance().checkTeamUser(teamId, userId); + if(!teamUser) res.status(403).send({ msg: 'you are not member!' }); + next(); +}; + +export const checkIsManager = async (req: any, res: Response, next: NextFunction) => { + const userId = req.user_id; + const teamId = req.params.teamId; + const teamUser = await TeamUserService.getInstance().checkTeamUser(teamId, userId); + if(!teamUser || teamUser.role !== 0) res.status(403).send({ msg: 'you are not manager!' }); + next(); +}; diff --git a/backend/src/redis/index.ts b/backend/src/redis/index.ts index 3d1d6ef..c32e677 100644 --- a/backend/src/redis/index.ts +++ b/backend/src/redis/index.ts @@ -17,10 +17,74 @@ export default class Redis { } return Redis.instance; } - get(key: string) { - console.log('redis get'); + + get(key: string, field: string) { + return new Promise((resolve, reject) => { + Redis.client.hget(key, field, async (err, searchResult) => { + if (err) return reject(err); + if (searchResult === null) { + Redis.client.hset(key, field, JSON.stringify([])); + const previousIndex = await Redis.getNextId(key); + if (!previousIndex) Redis.client.hset(key, 'nextId', '0'); + return resolve([]); + } else { + return resolve(JSON.parse(searchResult)); + } + }); + }); + } + + set(key: string, field: string, value: any) { + return new Promise((resolve, reject) => { + Redis.client.hget(key, field, async (err, searchResult) => { + if (err) return reject(err); + if (searchResult === null) { + Redis.client.hset(key, field, JSON.stringify([value])); + return resolve(value); + } + + const storedDataList = JSON.parse(searchResult); + const oldDataIdx = storedDataList.findIndex((data) => Number(data.id) === Number(value.id)); + // update + if (oldDataIdx !== -1) { + const updatedData = { ...storedDataList[oldDataIdx], ...value }; + storedDataList.splice(oldDataIdx, 1, updatedData); + Redis.client.hset(key, field, JSON.stringify(storedDataList)); + return resolve(updatedData); + } + // create + else { + Redis.client.hset(key, field, JSON.stringify([...storedDataList, value])); + await Redis.setNextId(key); + return resolve(value); + } + }); + }); } - set(key: string, value: any) { - console.log('redis set'); + + delete(key: string, field: string, targetId: number) { + return new Promise((resolve, reject) => { + Redis.client.hget(key, field, (err, searchResult) => { + if (err) return reject(err); + const storedDataList = JSON.parse(searchResult); + const updatedDataList = storedDataList.filter((data) => Number(data.id) !== Number(targetId)); + Redis.client.hset(key, field, JSON.stringify(updatedDataList)); + return resolve(updatedDataList); + }); + }); + } + + static getNextId(key: string, field = 'nextId') { + return new Promise((resolve) => { + Redis.client.hget(key, field, (err, nextId) => resolve(Number(nextId))); + }); + } + + static async setNextId(key: string, field = 'nextId') { + const presentId = await Redis.getNextId(key); + return new Promise((resolve) => { + Redis.client.hset(key, field, String(presentId + 1)); + return resolve(null); + }); } } diff --git a/backend/src/repositories/message-repository.ts b/backend/src/repositories/message-repository.ts deleted file mode 100644 index 062467b..0000000 --- a/backend/src/repositories/message-repository.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { EntityRepository, Repository } from 'typeorm'; -import { Message } from '@entities/message'; - -@EntityRepository(Message) -export default class MessageRepository extends Repository {} diff --git a/backend/src/repositories/postit-repository.ts b/backend/src/repositories/postit-repository.ts deleted file mode 100644 index 1253d03..0000000 --- a/backend/src/repositories/postit-repository.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { EntityRepository, Repository } from 'typeorm'; -import { Postit } from '@entities/postit'; - -@EntityRepository(Postit) -export default class PostitRepository extends Repository {} diff --git a/backend/src/repositories/reaction-repository.ts b/backend/src/repositories/reaction-repository.ts deleted file mode 100644 index fe40017..0000000 --- a/backend/src/repositories/reaction-repository.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { EntityRepository, Repository } from 'typeorm'; -import { Reaction } from '@entities/reaction'; - -@EntityRepository(Reaction) -export default class ReactionRepository extends Repository {} diff --git a/backend/src/routes/chat-router.ts b/backend/src/routes/chat-router.ts index 91c0079..aff04d0 100644 --- a/backend/src/routes/chat-router.ts +++ b/backend/src/routes/chat-router.ts @@ -3,7 +3,12 @@ import ChatController from '@controllers/chat-controller'; const router = express.Router(); -router.post('/room', ChatController.createChatRoom); -router.get('/room', ChatController.getChatRooms); +router.post('/rooms', ChatController.createChatRoom); +router.get('/rooms', ChatController.getChatRooms); +router.patch('/rooms/:chatRoomId', ChatController.updateChatRoomName); + +router.get('/rooms/:chatRoomId/users', ChatController.getChatRoomUsers); +router.post('/rooms/:chatRoomId/users', ChatController.addChatRoomUsers); +router.delete('/rooms/:chatRoomId/users/:userId', ChatController.deleteChatRoomUser); export default router; diff --git a/backend/src/routes/schedule-router.ts b/backend/src/routes/schedule-router.ts index ef8944f..7488490 100644 --- a/backend/src/routes/schedule-router.ts +++ b/backend/src/routes/schedule-router.ts @@ -1,11 +1,12 @@ import express from 'express'; import ScheduleController from '@controllers/schedule-controller'; +import { checkTeamUser } from '@src/middlewares/user'; const router = express.Router(); -router.post('/:teamId', ScheduleController.createSchedule); -router.get('/:teamId', ScheduleController.getSchedule); -router.put('/:scheduleId', ScheduleController.updateRepeatSchedule); -router.delete('/:scheduleId', ScheduleController.deleteSchedule); +router.post('/:teamId', checkTeamUser, ScheduleController.createSchedule); +router.get('/:teamId', checkTeamUser, ScheduleController.getSchedule); +router.put('/:scheduleId', checkTeamUser, ScheduleController.updateRepeatSchedule); +router.delete('/:scheduleId', checkTeamUser, ScheduleController.deleteSchedule); export default router; diff --git a/backend/src/routes/team-router.ts b/backend/src/routes/team-router.ts index 6cefee8..a5a6d80 100644 --- a/backend/src/routes/team-router.ts +++ b/backend/src/routes/team-router.ts @@ -1,20 +1,22 @@ import express from 'express'; import TeamController from '@controllers/team-contorller'; import { authenticateToken } from '@middlewares/token'; +import { checkTeamUser, checkIsManager } from '@src/middlewares/user'; const router = express.Router(); -router.post('/create', authenticateToken, TeamController.create); // 팀 생성, [team, team-user] -router.delete('/', authenticateToken, TeamController.delete); // 팀 삭제, [team] -router.put('/', authenticateToken, TeamController.update); // 팀 수정 [team] +router.post('/', authenticateToken, TeamController.create); // 팀 생성, [team, team-user] router.get('/', authenticateToken, TeamController.read); // 팀, 초대 목록 [team-user] +router.put('/:teamId', authenticateToken, checkIsManager, TeamController.update); // 팀 수정 [team] +router.delete('/:teamId', authenticateToken, checkIsManager, TeamController.delete); // 팀 삭제, [team] -router.get('/users/:id', authenticateToken, TeamController.readTeamUsers); // 팀의 모든 유저 리스트 -router.get('/:id', authenticateToken, TeamController.readTeamInfo); // 팀 정보 (이름, 팀 desc ..) +router.get('/:teamId', authenticateToken, checkTeamUser, TeamController.readTeamInfo); // 팀 정보 (이름, 팀 desc ..) +router.get('/:teamId/users', authenticateToken, checkTeamUser, TeamController.readTeamUsers); // 팀의 모든 유저 리스트 -router.post('/invite', authenticateToken, TeamController.invite); // 초대 전송 [team-user] -router.post('/invite/response', authenticateToken, TeamController.acceptInvitation); // 초대 수락 [team-user] -router.delete('/invite/response', authenticateToken, TeamController.declineInvitation); // 초대 거절 [team-user] +router.delete('/:teamId/users/:userId', authenticateToken, checkIsManager, TeamController.kickOut); // 유저 강퇴 +router.patch('/:teamId/users/:userId', authenticateToken, checkIsManager, TeamController.changeRole); // 권한 변경 -router.delete('/:id', authenticateToken, TeamController.kickOut); // 유저 강퇴 +router.post('/:teamId/invitations', authenticateToken, checkIsManager, TeamController.invite); // 초대 전송 [team-user] +router.patch('/:teamId/invitations', authenticateToken, TeamController.acceptInvitation); // 초대 수락 [team-user] +router.delete('/:teamId/invitations', authenticateToken, TeamController.declineInvitation); // 초대 거절 [team-user] export default router; diff --git a/backend/src/routes/user-router.ts b/backend/src/routes/user-router.ts index 9eb7d4e..68a288b 100644 --- a/backend/src/routes/user-router.ts +++ b/backend/src/routes/user-router.ts @@ -4,6 +4,6 @@ import { authenticateToken } from '@middlewares/token'; const router = express.Router(); -router.patch('/name', authenticateToken, UserController.updateUser); +router.patch('/', authenticateToken, UserController.updateUser); export default router; diff --git a/backend/src/services/chat-room-service.ts b/backend/src/services/chat-room-service.ts index 6d95e23..18a9618 100644 --- a/backend/src/services/chat-room-service.ts +++ b/backend/src/services/chat-room-service.ts @@ -20,7 +20,7 @@ class ChatRoomService { } async createChatRoom(chatRoomInfo: ChatRoomInfoType) { - const { team_id, chat_room_name, user_id_list } = chatRoomInfo; + const { team_id, chat_room_name, user_list } = chatRoomInfo; const newChatRoom = await this.chatRoomRepository .createQueryBuilder() .insert() @@ -30,7 +30,7 @@ class ChatRoomService { if (!newChatRoom) throw new Error('채팅방 생성 오류'); const chat_room_id = newChatRoom.raw.insertId; - const chatUsers = user_id_list.map((user) => { + const chatUsers = user_list.map((user) => { return { user_id: user.user_id, chat_room_id }; }); const addUserResult = await this.chatRoomUserRepository @@ -40,18 +40,66 @@ class ChatRoomService { .values(chatUsers) .execute(); if (!addUserResult) throw new Error('채팅방 유저 초대 오류'); - return { chat_room_id, chat_room_name, user_id_list }; + return { chat_room_id, chat_room_name, user_list }; } async getChatRooms(teamId: number, userId: number) { - const chatRoomsResult = await this.chatRoomRepository + const chatRooms = await this.chatRoomRepository .createQueryBuilder('chat_room') + .select('chat_room.chat_room_id') + .addSelect('chat_room.chat_room_name') .innerJoin('chat_room.chat_room_users', 'chat_room_user') .where('chat_room.team_id = :teamId', { teamId }) .andWhere('chat_room_user.user_id =:userId', { userId }) .getMany(); - if (!chatRoomsResult) throw new Error('채팅방 목록 불러오기 오류'); - return { chat_rooms: chatRoomsResult }; + if (!chatRooms) throw new Error('채팅방 목록 불러오기 오류'); + return { chat_rooms: chatRooms }; + } + + async updateChatRoomName(chatRoomId: number, chatRoomName: string) { + const updatedChatRoom = await this.chatRoomRepository + .createQueryBuilder() + .update('chat_room') + .set({ chat_room_name: chatRoomName }) + .where('chat_room_id = :chatRoomId', { chatRoomId }) + .execute(); + if (!updatedChatRoom) throw new Error('채팅방 이름 변경 오류'); + } + + async getChatRoomUsers(chatRoomId: number) { + const chatRoomInfo = await this.chatRoomRepository + .createQueryBuilder('chat_room') + .select('chat_room.chat_room_id') + .addSelect('chat_room_user.user_id') + .innerJoin('chat_room.chat_room_users', 'chat_room_user') + .where('chat_room.chat_room_id = :chatRoomId', { chatRoomId }) + .getOne(); + if (!chatRoomInfo) throw new Error('채팅방 유저 가져오기 오류'); + return chatRoomInfo; + } + + async addChatRoomUsers(chatRoomId: number, userList: UserIdType[]) { + const chatUsers = userList.map((user) => { + return { user_id: user.user_id, chat_room_id: chatRoomId }; + }); + const addedUsers = await this.chatRoomUserRepository + .createQueryBuilder() + .insert() + .into('chat_room_user') + .values(chatUsers) + .execute(); + if (!addedUsers) throw new Error('채팅방 유저 추가 오류'); + } + + async deleteChatRoomUser(chatRoomId: number, userId: number) { + const deletedUser = await this.chatRoomUserRepository + .createQueryBuilder() + .delete() + .from('chat_room_user') + .where('chat_room_id = :chatRoomId', { chatRoomId }) + .andWhere('user_id = :userId', { userId }) + .execute(); + if (!deletedUser) throw new Error('채팅방 유저 삭제 오류'); } } @@ -62,7 +110,7 @@ interface UserIdType { interface ChatRoomInfoType { team_id: number; chat_room_name: string; - user_id_list: UserIdType[]; + user_list: UserIdType[]; } export default ChatRoomService; diff --git a/backend/src/services/team-service.ts b/backend/src/services/team-service.ts index 50c8f3f..ba36eae 100644 --- a/backend/src/services/team-service.ts +++ b/backend/src/services/team-service.ts @@ -31,17 +31,22 @@ export default class TeamService { return result; } - async update(teamData: TeamData) { + async update(teamId: number, teamData: TeamData) { await this.teamRepository .createQueryBuilder() .update('team') .set(teamData) - .where('team_id=:id', { id: teamData.team_id }) + .where('team_id=:id', { id: teamId }) .execute(); } async delete(teamId: number) { - await this.teamRepository.createQueryBuilder().delete().from('team').where('team_id=:id', { id: teamId }).execute(); + await this.teamRepository + .createQueryBuilder() + .delete() + .from('team') + .where('team_id=:id', { id: teamId }) + .execute(); } } diff --git a/backend/src/services/team-user-service.ts b/backend/src/services/team-user-service.ts index 179d09c..38a24da 100644 --- a/backend/src/services/team-user-service.ts +++ b/backend/src/services/team-user-service.ts @@ -30,6 +30,14 @@ export default class TeamUserService { .execute(); } + async checkTeamUser(teamId: number, userId: number) { + return await this.teamUserRepository + .createQueryBuilder('team_user') + .where('team_user.user = :userId', { userId }) + .where('team_user.team = :teamId', { teamId }) + .getOne(); + } + async create(userId: number, teamId: number) { await this.teamUserRepository .createQueryBuilder() @@ -55,8 +63,14 @@ export default class TeamUserService { async readAllUsers(teamId: number) { return await this.teamUserRepository .createQueryBuilder('team_user') - .leftJoinAndSelect('team_user.user', 'user') + .select('team_user.role') + .addSelect('user.user_id') + .addSelect('user.user_name') + .addSelect('user.user_color') + .addSelect('user.user_email') + .innerJoin('team_user.user', 'user') .where('team_user.team = :teamId', { teamId }) + .andWhere('team_user.state =:state', { state: true }) .getMany(); } @@ -79,4 +93,14 @@ export default class TeamUserService { .andWhere('user = :userId', { userId }) .execute(); } + + async changeRole(userId: number, teamId: number, role: number) { + return await this.teamUserRepository + .createQueryBuilder() + .update() + .set({ role }) + .where('team = :teamId', { teamId }) + .andWhere('user = :userId', { userId }) + .execute(); + } } diff --git a/backend/src/services/user-service.ts b/backend/src/services/user-service.ts index 307539a..99f8ee2 100644 --- a/backend/src/services/user-service.ts +++ b/backend/src/services/user-service.ts @@ -68,6 +68,13 @@ class UserService { return user; } + async getUserByUserName(user_name: string) { + const user = await this.userRepository.findOne({ + where: { user_name } + }); + return user; + } + async getUserByName(user_name: string) { const users = await this.userRepository.find({ where: { diff --git a/backend/src/sockets/chat.ts b/backend/src/sockets/chat.ts new file mode 100644 index 0000000..a66fdeb --- /dev/null +++ b/backend/src/sockets/chat.ts @@ -0,0 +1,103 @@ +import { Namespace, Socket } from 'socket.io'; +import { onlineUsersInfo } from './store'; +import Redis from '@redis/index'; + +const initChat = (socket: Socket, namespace: Namespace) => { + const redisClient = new Redis(); + + socket.on('enter chat rooms', async ({ chatRooms }: { chatRooms: { chatRoomId: number }[] }) => { + chatRooms.forEach(({ chatRoomId }) => { + socket.join(`chat-${chatRoomId}`); + }); + const chatRoomMessages = await Promise.all( + chatRooms.map(async ({ chatRoomId }) => redisClient.get('message', chatRoomId.toString())) + ); + const lastMessages = {}; + chatRoomMessages.forEach((messages: MessageType[]) => { + if (messages.length !== 0) { + const lastMessage = messages[messages.length - 1]; + lastMessages[lastMessage.chatRoomId] = lastMessage; + } + }); + socket.emit('receive last messages', lastMessages); + }); + + socket.on('leave chat rooms', ({ chatRooms }: { chatRooms: { chatRoomId: number }[] }) => { + chatRooms.forEach(({ chatRoomId }) => { + socket.leave(`chat-${chatRoomId}`); + }); + }); + + socket.on('get message list', async ({ chatRoomId }) => { + const messageList = await redisClient.get('message', chatRoomId); + socket.emit('receive message list', { chatRoomId, messageList }); + }); + + socket.on('send message', async (messageData: MessageReqType) => { + const chatRoomId = messageData.chatRoomId.toString(); + const newMessage = await makeMessageObj(messageData); + await redisClient.set('message', chatRoomId, newMessage); + namespace.in(`chat-${messageData.chatRoomId}`).emit('receive message', newMessage); + }); + + socket.on('update chat room name', ({ chatRoomId }) => { + namespace.to(`chat-${chatRoomId}`).emit('refresh chat rooms'); + }); + + socket.on('create chat room', ({ chatRoomId, userList, teamId }) => { + socket.join(`chat-${chatRoomId}`); + userList.forEach((user: { userId: number }) => { + const onlineInvitedUser = Object.keys(onlineUsersInfo).find((socketId) => { + return onlineUsersInfo[socketId].userId === user.userId && onlineUsersInfo[socketId].teamId === teamId; + }); + if (onlineUsersInfo[onlineInvitedUser] && onlineUsersInfo[onlineInvitedUser].socket) { + onlineUsersInfo[onlineInvitedUser].socket.join(`chat-${chatRoomId}`); + socket.to(onlineInvitedUser).emit('refresh chat rooms'); + } + }); + }); + + socket.on('invite users', ({ chatRoomId, userList, teamId }) => { + userList.forEach((user: { userId: number }) => { + const onlineInvitedUser = Object.keys(onlineUsersInfo).find((socketId) => { + return onlineUsersInfo[socketId].userId === user.userId && onlineUsersInfo[socketId].teamId === teamId; + }); + if (onlineUsersInfo[onlineInvitedUser] && onlineUsersInfo[onlineInvitedUser].socket) { + onlineUsersInfo[onlineInvitedUser].socket.join(`chat-${chatRoomId}`); + socket.to(onlineInvitedUser).emit('refresh chat rooms'); + } + }); + }); + + socket.on('exit chat room', ({ chatRoomId }) => { + namespace.to(`chat-${chatRoomId}`).emit('refresh chat room users', { chatRoomId }); + socket.leave(`chat-${chatRoomId}`); + }); +}; + +const makeMessageObj = async (messageData: MessageReqType) => { + const id = await Redis.getNextId('message'); + return { + messageId: id, + content: messageData.content, + createdAt: new Date(), + userId: messageData.userId, + chatRoomId: messageData.chatRoomId + }; +}; + +export interface MessageReqType { + content: string; + userId: number; + chatRoomId: number; +} + +export interface MessageType { + messageId: number; + content: string; + createdAt: Date; + userId: number; + chatRoomId: number; +} + +export default initChat; diff --git a/backend/src/sockets/index.ts b/backend/src/sockets/index.ts index 590af4b..cd3b453 100644 --- a/backend/src/sockets/index.ts +++ b/backend/src/sockets/index.ts @@ -1,10 +1,12 @@ import { Namespace, Socket } from 'socket.io'; import initTeam from './team'; import initTeamBoard from './teamBoard'; +import initChat from './chat'; const socketInit = (namespace: Namespace): void => { namespace.on('connect', (socket: Socket) => { initTeam(socket); initTeamBoard(socket); + initChat(socket, namespace); }); }; diff --git a/backend/src/sockets/store.ts b/backend/src/sockets/store.ts index 84c4ae7..ba1686c 100644 --- a/backend/src/sockets/store.ts +++ b/backend/src/sockets/store.ts @@ -1,5 +1,5 @@ // {teamId1: [{userId}, {userId}], teamId2: [{userId}]} export const onlineUsersByTeam = {}; -// {socketId: {currTeamId, userId}, socketId: {currTeamId, userId}} +// {socketId: {teamId(현재팀), userId, socket}, socketId: {teamId(현재팀), userId, socket}} export const onlineUsersInfo = {}; diff --git a/backend/src/sockets/team.ts b/backend/src/sockets/team.ts index fabc851..f378e9d 100644 --- a/backend/src/sockets/team.ts +++ b/backend/src/sockets/team.ts @@ -5,21 +5,21 @@ interface UserType { userId: number; } -const initTeam = (socket: Socket): void => { - const setUserStatusToOnline = (teamId: number, userId: number, socketId: string): void => { - if (!onlineUsersByTeam[teamId]) onlineUsersByTeam[teamId] = [{ userId }]; - else { - const users = onlineUsersByTeam[teamId].filter((user: UserType) => user.userId !== userId); - onlineUsersByTeam[teamId] = [...users, { userId }]; - } - onlineUsersInfo[socketId] = { teamId, userId }; - }; +const setUserStatusToOnline = (teamId: number, userId: number, socket: Socket): void => { + if (!onlineUsersByTeam[teamId]) onlineUsersByTeam[teamId] = [{ userId }]; + else { + const users = onlineUsersByTeam[teamId].filter((user: UserType) => user.userId !== userId); + onlineUsersByTeam[teamId] = [...users, { userId }]; + } + onlineUsersInfo[socket.id] = { teamId: Number(teamId), userId, socket }; +}; - const setUserStatusToOffline = (teamId: number, userId: number, socketId: string): void => { - onlineUsersByTeam[teamId] = onlineUsersByTeam[teamId].filter((user: UserType) => user.userId !== userId); - delete onlineUsersInfo[socketId]; - }; +const setUserStatusToOffline = (teamId: number, userId: number, socketId: string): void => { + onlineUsersByTeam[teamId] = onlineUsersByTeam[teamId].filter((user: UserType) => user.userId !== userId); + delete onlineUsersInfo[socketId]; +}; +const initTeam = (socket: Socket): void => { const sendOnlineUsers = (socket: Socket, teamId: number): void => { socket.emit('online users', { onlineUsers: onlineUsersByTeam[teamId] }); }; @@ -28,22 +28,23 @@ const initTeam = (socket: Socket): void => { socket.to('users').emit('online users', { onlineUsers: onlineUsersByTeam[teamId] }); }; + socket.on('enter users room', () => { + const { teamId } = onlineUsersInfo[socket.id]; + socket.join('users'); + sendOnlineUsers(socket, teamId); + }); + socket.on('leave users room', () => { socket.leave('users'); }); socket.on('change status to online', ({ teamId, userId }: { teamId: number; userId: number }) => { - setUserStatusToOnline(teamId, userId, socket.id); + setUserStatusToOnline(teamId, userId, socket); sendOnlineUsersToRoom(socket, teamId); }); - socket.on('enter users room', () => { - const { teamId } = onlineUsersInfo[socket.id]; - socket.join('users'); - sendOnlineUsers(socket, teamId); - }); - socket.on('disconnect', () => { + if (!onlineUsersInfo[socket.id]) return; const { teamId, userId } = onlineUsersInfo[socket.id]; setUserStatusToOffline(teamId, userId, socket.id); sendOnlineUsersToRoom(socket, teamId); diff --git a/backend/src/sockets/teamBoard.ts b/backend/src/sockets/teamBoard.ts index ca3df29..b43a895 100644 --- a/backend/src/sockets/teamBoard.ts +++ b/backend/src/sockets/teamBoard.ts @@ -1,58 +1,93 @@ import { Socket } from 'socket.io'; import { onlineUsersInfo } from '@sockets/store'; +import Redis from '@redis/index'; const initTeamBoard = (socket: Socket) => { - socket.on('join board page', () => { - const teamId = Number(onlineUsersInfo[socket.id].teamId); - socket.join('board'); - socket.emit('join board page', dummyPostit[teamId]); + const redisClient = new Redis(); + + socket.on('join board page', async () => { + try { + socket.join('board'); + const teamId = onlineUsersInfo[socket.id].teamId; + const postitList = await redisClient.get('board', teamId); + socket.emit('join board page', postitList); + } catch (err) { + socket.emit('team board error', '포스트잇을 불러오는데 실패했습니다!'); + } }); socket.on('leave board page', () => socket.leave('board')); - socket.on('create new postit', (postit) => { - const teamId = Number(onlineUsersInfo[socket.id].teamId); - const newPostit = makePostit(postit); - if (!dummyPostit[teamId] || dummyPostit[teamId] === []) { - dummyPostit[teamId] = []; + socket.on('create new postit', async (postit) => { + try { + const teamId = onlineUsersInfo[socket.id].teamId; + const newPostit = await makePostitObj(postit); + await redisClient.set('board', teamId, newPostit); + socket.emit('create new postit', newPostit); + socket.broadcast.to('board').emit('create new postit', newPostit); + } catch (err) { + socket.emit('team board error', '새로운 포스트잇 생성 실패!'); + } + }); + + socket.on('update start postit', async (newPostit) => { + try { + const teamId = onlineUsersInfo[socket.id].teamId; + const updatedPostit = await redisClient.set('board', teamId, newPostit); + socket.broadcast.to('board').emit('update start postit', updatedPostit); + } catch (err) { + socket.emit('team board error', '포스트잇을 업데이트 할 수 없습니다!'); + } + }); + + socket.on('update end postit', async (newPostit) => { + try { + const teamId = onlineUsersInfo[socket.id].teamId; + const updatedPostit = await redisClient.set('board', teamId, newPostit); + socket.broadcast.to('board').emit('update end postit', updatedPostit); + } catch (err) { + socket.emit('team board error', '포스트잇 업데이트 실패!'); } - dummyPostit[teamId].push(newPostit); - socket.emit('create new postit', dummyPostit[teamId]); - socket.broadcast.to('board').emit('create new postit', dummyPostit[teamId]); }); - socket.on('update postit', (newPostit) => { - const teamId = Number(onlineUsersInfo[socket.id].teamId); - const targetPostit = dummyPostit[teamId].find((postit) => Number(postit.id) === Number(newPostit.id)); - const updatedPostit = updatePostit(targetPostit, newPostit, 'update'); - dummyPostit[teamId] = dummyPostit[teamId].filter((postit) => Number(postit.id) !== Number(newPostit.id)); - dummyPostit[teamId].push(updatedPostit); - socket.broadcast.to('board').emit('drag postit', updatedPostit); + socket.on('drag postit', async (newPostit) => { + try { + const teamId = onlineUsersInfo[socket.id].teamId; + const draggedPostit = await redisClient.set('board', teamId, newPostit); + socket.broadcast.to('board').emit('drag postit', draggedPostit); + } catch (err) { + socket.emit('team board error'); + } }); - socket.on('drag postit', (newPostit) => { - const teamId = Number(onlineUsersInfo[socket.id].teamId); - const targetPostit = dummyPostit[teamId].find((postit) => Number(postit.id) === Number(newPostit.id)); - const updatedPostit = updatePostit(targetPostit, newPostit, 'drag'); - dummyPostit[teamId] = dummyPostit[teamId].filter((postit) => Number(postit.id) !== Number(newPostit.id)); - dummyPostit[teamId].push(updatedPostit); - socket.broadcast.to('board').emit('drag postit', updatedPostit); + socket.on('drag end postit', async (newPostit) => { + try { + const teamId = onlineUsersInfo[socket.id].teamId; + const draggedPostit = await redisClient.set('board', teamId, newPostit); + socket.broadcast.to('board').emit('drag end postit', draggedPostit); + } catch (err) { + socket.emit('team board error'); + } }); - socket.on('delete postit', (postitId) => { - const teamId = Number(onlineUsersInfo[socket.id].teamId); - dummyPostit[teamId] = dummyPostit[teamId].filter((postit) => postit.id !== postitId); - socket.emit('delete postit', dummyPostit[teamId]); - socket.broadcast.to('board').emit('delete postit', dummyPostit[teamId]); + socket.on('delete postit', async (postitId) => { + try { + const teamId = onlineUsersInfo[socket.id].teamId; + const updatedPostitList = await redisClient.delete('board', teamId, postitId); + socket.emit('delete postit', updatedPostitList); + socket.broadcast.to('board').emit('delete postit', updatedPostitList); + } catch (err) { + socket.emit('team board error', '포스트잇 삭제 실패!'); + } }); }; export default initTeamBoard; -const makePostit = (newData) => { - dummyPostit.numberOfPostit += 1; +const makePostitObj = async (newData) => { + const id = await Redis.getNextId('board'); return { - id: dummyPostit.numberOfPostit, + id: id, title: newData.title, content: newData.content, x: 0, @@ -61,19 +96,8 @@ const makePostit = (newData) => { updatedAt: new Date(), updatedBy: newData.updatedBy, createdAt: new Date(), - createdBy: newData.createdBy + createdBy: newData.createdBy, + whoIsDragging: -1, + whoIsUpdating: -1 }; }; - -const updatePostit = (targetPostit, newData, updateType) => { - const updatedPostit = targetPostit; - Object.keys(newData).forEach((key) => { - if (['id', 'x', 'y', 'color', 'updatedBy', 'createdBy'].includes(key)) updatedPostit[key] = Number(newData[key]); - }); - if (updateType === 'update') updatedPostit.updatedAt = new Date(); - return updatedPostit; -}; - -const dummyPostit = { - numberOfPostit: 0 -}; diff --git a/backend/src/swagger/components/chat.yaml b/backend/src/swagger/components/chat.yaml new file mode 100644 index 0000000..01e98ac --- /dev/null +++ b/backend/src/swagger/components/chat.yaml @@ -0,0 +1,72 @@ +components: + parameters: + GetChatRooms: + - name: teamId + in: query + required: true + schema: + type: integer + - name: userId + in: query + required: true + schema: + type: integer + ChatRoomInfo: + - name: chatRoomId + in: path + required: true + schema: + type: integer + schemas: + CreateChatRoomBody: + type: object + properties: + team_id: + type: integer + chat_room_name: + type: string + user_id_list: + type: array + items: + type: object + properties: + user_id: + type: integer + CreateChatRoomRes: + type: object + properties: + chat_room_id: + type: integer + chat_room_name: + type: string + user_list: + type: array + items: + type: object + properties: + user_id: + type: integer + GetChatRooms: + type: object + properties: + chat_rooms: + type: array + items: + type: object + properties: + chat_room_id: + type: integer + chat_room_name: + type: string + ChatRoomInfo: + type: object + properties: + chat_room_id: + type: integer + user_list: + type: array + items: + type: object + properties: + user_id: + type: integer diff --git a/backend/src/swagger/components/schedule.yaml b/backend/src/swagger/components/schedule.yaml new file mode 100644 index 0000000..f116585 --- /dev/null +++ b/backend/src/swagger/components/schedule.yaml @@ -0,0 +1,209 @@ +components: + parameters: + GetScheduleParam: + - name: team_id + in: path + required: true + description: 대상 팀 id + schema: + type: integer + - name: start_date + in: query + required: true + description: 시작 날짜 + schema: + type: string + - name: end_date + in: query + required: true + description: 끝 날짜 + schema: + type: string + ScheduleId: + - name: schdule_id + in: path + required: true + description: shedule id + schema: + type: integer + schemas: + CreateScheduleRes: + type: array + items: + type: object + properties: + title: + type: string + team_id: + type: integer + start_date: + type: string + end_date: + type: string + content: + type: string + color: + type: integer + repeat_id: + type: string + repeat_option: + type: integer + schedule_id: + type: integer + example: + - title: 매일 5번 반복 + team_id: 1 + start_date: Mon Nov 01 2021 12:00:00 GMT+0900 + end_date: Mon Nov 01 2021 13:00:00 GMT+0900 + content: 매일 5번 반복 + color: 2 + repeat_id: d4a826b3-719d-4954-a250-e772821dc897 + repeat_option: 1 + schedule_id: 157 + - title: 매일 5번 반복 + team_id: 1 + start_date: Tue Nov 02 2021 12:00:00 GMT+0900 + end_date: Tue Nov 02 2021 13:00:00 GMT+0900 + content: 매일 5번 반복 + color: 2 + repeat_id: d4a826b3-719d-4954-a250-e772821dc897 + repeat_option: 1 + schedule_id: 156 + - title: 매일 5번 반복 + team_id: 1 + start_date: Wed Nov 03 2021 12:00:00 GMT+0900 + end_date: Wed Nov 03 2021 13:00:00 GMT+0900 + content: 매일 5번 반복 + color: 2 + repeat_id: d4a826b3-719d-4954-a250-e772821dc897 + repeat_option: 1 + schedule_id: 158 + GetSchedule: + type: array + items: + type: object + properties: + title: + type: string + team_id: + type: integer + start_date: + type: string + end_date: + type: string + content: + type: string + color: + type: integer + repeat_id: + type: string + repeat_option: + type: integer + schedule_id: + type: integer + example: + - schedule_id: 135 + team_id: 1 + title: 회의 - 수정 + start_date: 2021-11-09T05:00:00.000Z + end_date: 2021-11-09T06:00:00.000Z + repeat_id: 837148eb-b3b6-45a7-8f86-0517f10ffe10 + repeat_option: 0 + content: 화요일 회의 - 수정 + color: 4 + - schedule_id: 139 + team_id: 1 + title: hi + start_date: 2021-11-09T12:45:00.000Z + end_date: 2021-11-09T13:30:00.000Z + repeat_id: 288d71e8-06a1-46c9-93fe-0299b949dbc6 + repeat_option: 0 + content: 22 + color: 0 + - schedule_id: 146 + team_id: 1 + title: hihi + start_date: 2021-11-11T09:30:00.000Z + end_date: 2021-11-11T10:00:00.000Z + repeat_id: 2069433b-3a19-47a2-b2f0-62008729a925 + repeat_option: 0 + content: hihi + color: 3 + CreateScheduleBody: + type: object + properties: + title: + type: string + start_date: + type: string + end_date: + type: string + repeat_option: + type: integer + repeat_count: + type: integer + content: + type: string + color: + type: integer + example: + title: 매일 5번 반복 + start_date: 2021-11-01T03:00:00.000Z + end_date: 2021-11-01T04:00:00.000Z + repeat_option: 1 + repeat_count: 5 + content: 매일 5번 반복 + color: 2 + UpdateScheduleBody: + type: object + properties: + title: + type: string + start_date: + type: string + end_date: + type: string + repeat_option: + type: integer + repeat_count: + type: integer + content: + type: string + color: + type: integer + example: + title: 매일 5번 반복 + start_date: 2021-11-01T03:00:00.000Z + end_date: 2021-11-01T04:00:00.000Z + repeat_option: 1 + repeat_count: 5 + content: 매일 5번 반복 + color: 2 + UpdateScheduleRes: + type: object + properties: + schedule_id: + type: integer + team_id: + type: integer + title: + type: string + start_date: + type: string + end_date: + type: string + repeat_id: + type: string + content: + type: string + color: + type: integer + example: + schedule_id: 135 + team_id: 1 + title: 회의 - 수정 + start_date: 2021-11-09T05:00:00.000Z + end_date: 2021-11-09T06:00:00.000Z + repeat_id: 837148eb-b3b6-45a7-8f86-0517f10ffe10 + content: 화요일 회의 - 수정 + color: 4 diff --git a/backend/src/swagger/components/team.yaml b/backend/src/swagger/components/team.yaml new file mode 100644 index 0000000..b1fe805 --- /dev/null +++ b/backend/src/swagger/components/team.yaml @@ -0,0 +1,83 @@ +components: + parameters: + UserId: + - name: userId + in: path + required: true + schema: + type: integer + TeamId: + - name: teamId + in: path + required: true + schema: + type: integer + schemas: + TeamList: + type: array + items: + type: object + properties: + team_user_id: + type: integer + state: + type: boolean + role: + type: integer + team: + type: object + properties: + team_id: + type: integer + team_name: + type: string + team_desc: + type: string + TeamInfo: + type: object + properties: + team_id: + type: integer + team_name: + type: string + team_desc: + type: string + TeamId: + type: object + properties: + team_id: + type: integer + Invite: + type: object + properties: + user_email: + type: string + team_id: + type: integer + CreateTeam: + type: object + properties: + team_name: + type: string + team_desc: + type: string + TeamUsers: + type: array + items: + type: object + properties: + team_user_id: + type: integer + state: + type: boolean + role: + type: integer + user: + type: object + properties: + user_id: + type: integer + user_name: + type: string + user_color: + type: integer diff --git a/backend/src/swagger/components/user.yaml b/backend/src/swagger/components/user.yaml new file mode 100644 index 0000000..4865d81 --- /dev/null +++ b/backend/src/swagger/components/user.yaml @@ -0,0 +1,8 @@ +components: + parameters: + schemas: + NewnameBody: + type: object + properties: + newName: + type: string diff --git a/backend/src/swagger/openapi.yaml b/backend/src/swagger/openapi.yaml new file mode 100644 index 0000000..766d8ae --- /dev/null +++ b/backend/src/swagger/openapi.yaml @@ -0,0 +1,59 @@ +openapi: 3.0.0 +info: + version: 1.0.0 + title: BoostTeams API docs + description: BoostTeams의 API 문서입니다 + license: + name: MIT +servers: + - url: http://localhost:4000/ + description: BoostTeams Server +tags: + - name: auth + description: Join / Login / Logout + - name: chat + description: Chat Page + - name: schedule + description: Calendar Page + - name: team + description: Team Page + - name: user + description: User Manage +paths: + # auth + /api/auth/login: + $ref: './paths/auth/login.yaml' + /api/auth/github: + $ref: './paths/auth/github.yaml' + /api/auth/github/callback: + $ref: './paths/auth/callback.yaml' + /api/auth/signup: + $ref: './paths/auth/signup.yaml' + /api/auth/info: + $ref: './paths/auth/info.yaml' + # schdule + /api/schedule/{team_id}: + $ref: './paths/schedule/scheduleByTeamId.yaml' + /api/schedule/{schedule_id}: + $ref: './paths/schedule/scheduleByScheduleId.yaml' + # chat + /api/chat/room: + $ref: './paths/chat/chat.yaml' + /api/chat/room/{chatRoomId}: + $ref: './paths/chat/chatRoom.yaml' + # user + /api/user/name: + $ref: './paths/user/user.yaml' + # team + /api/team: + $ref: './paths/team/team.yaml' + /api/team/{id}: + $ref: './paths/team/user.yaml' + /api/team/create: + $ref: './paths/team/createTeam.yaml' + /api/team/users/{id}: + $ref: './paths/team/users.yaml' + /api/team/invite: + $ref: './paths/team/invite.yaml' + /api/team/invite/response: + $ref: './paths/team/response.yaml' diff --git a/backend/src/swagger/paths/auth/callback.yaml b/backend/src/swagger/paths/auth/callback.yaml new file mode 100644 index 0000000..05414f1 --- /dev/null +++ b/backend/src/swagger/paths/auth/callback.yaml @@ -0,0 +1,8 @@ +get: + summary: github/callback + tags: [auth] + responses: + 200: + description: 성공 + 404: + description: error diff --git a/backend/src/swagger/paths/auth/github.yaml b/backend/src/swagger/paths/auth/github.yaml new file mode 100644 index 0000000..1f8feff --- /dev/null +++ b/backend/src/swagger/paths/auth/github.yaml @@ -0,0 +1,8 @@ +get: + summary: github + tags: [auth] + responses: + 200: + description: 성공 + 404: + description: error diff --git a/backend/src/swagger/paths/auth/info.yaml b/backend/src/swagger/paths/auth/info.yaml new file mode 100644 index 0000000..22222ea --- /dev/null +++ b/backend/src/swagger/paths/auth/info.yaml @@ -0,0 +1,8 @@ +get: + summary: 유저 정보 가져오기 + tags: [auth] + responses: + 200: + description: 성공 + 404: + description: error diff --git a/backend/src/swagger/paths/auth/login.yaml b/backend/src/swagger/paths/auth/login.yaml new file mode 100644 index 0000000..968b37d --- /dev/null +++ b/backend/src/swagger/paths/auth/login.yaml @@ -0,0 +1,8 @@ +post: + summary: 로그인하기 + tags: [auth] + responses: + 200: + description: 성공 + 404: + description: error diff --git a/backend/src/swagger/paths/auth/signup.yaml b/backend/src/swagger/paths/auth/signup.yaml new file mode 100644 index 0000000..61e6cad --- /dev/null +++ b/backend/src/swagger/paths/auth/signup.yaml @@ -0,0 +1,8 @@ +post: + summary: 회원가입하기 + tags: [auth] + responses: + 200: + description: 성공 + 404: + description: error diff --git a/backend/src/swagger/paths/chat/chat.yaml b/backend/src/swagger/paths/chat/chat.yaml new file mode 100644 index 0000000..fbe6c64 --- /dev/null +++ b/backend/src/swagger/paths/chat/chat.yaml @@ -0,0 +1,31 @@ +get: + summary: 채팅방 목록 가져오기 + tags: [chat] + parameters: + $ref: '../../components/chat.yaml#/components/parameters/GetChatRooms' + responses: + 200: + description: 성공 + content: + application/json: + schema: + $ref: '../../components/chat.yaml#/components/schemas/GetChatRooms' + 404: + description: error +post: + summary: 채팅방 생성하기 + tags: [chat] + requestBody: + content: + application/json: + schema: + $ref: '../../components/chat.yaml#/components/schemas/CreateChatRoomBody' + responses: + 200: + description: 성공 + content: + application/json: + schema: + $ref: '../../components/chat.yaml#/components/schemas/CreateChatRoomRes' + 409: + description: error diff --git a/backend/src/swagger/paths/chat/chatRoom.yaml b/backend/src/swagger/paths/chat/chatRoom.yaml new file mode 100644 index 0000000..e0acbef --- /dev/null +++ b/backend/src/swagger/paths/chat/chatRoom.yaml @@ -0,0 +1,14 @@ +get: + summary: 채팅방 정보 가져오기 + tags: [chat] + parameters: + $ref: '../../components/chat.yaml#/components/parameters/ChatRoomInfo' + responses: + 200: + description: 성공 + content: + application/json: + schema: + $ref: '../../components/chat.yaml#/components/schemas/ChatRoomInfo' + 404: + description: error diff --git a/backend/src/swagger/paths/schedule/scheduleByScheduleId.yaml b/backend/src/swagger/paths/schedule/scheduleByScheduleId.yaml new file mode 100644 index 0000000..8132a31 --- /dev/null +++ b/backend/src/swagger/paths/schedule/scheduleByScheduleId.yaml @@ -0,0 +1,29 @@ +delete: + summary: 일정 삭제하기 + tags: [schedule] + parameters: + $ref: '../../components/schedule.yaml#/components/parameters/ScheduleId' + responses: + 204: + description: 성공 + 409: + description: error +put: + summary: 일정 수정하기 + tags: [schedule] + parameters: + $ref: '../../components/schedule.yaml#/components/parameters/ScheduleId' + requestBody: + content: + application/json: + schema: + $ref: '../../components/schedule.yaml#/components/schemas/UpdateScheduleBody' + responses: + 200: + description: 성공 + content: + application/json: + schema: + $ref: '../../components/schedule.yaml#/components/schemas/UpdateScheduleRes' + 409: + description: error diff --git a/backend/src/swagger/paths/schedule/scheduleByTeamId.yaml b/backend/src/swagger/paths/schedule/scheduleByTeamId.yaml new file mode 100644 index 0000000..0b29c04 --- /dev/null +++ b/backend/src/swagger/paths/schedule/scheduleByTeamId.yaml @@ -0,0 +1,38 @@ +get: + summary: 일정 가져오기 + tags: [schedule] + parameters: + $ref: '../../components/schedule.yaml#/components/parameters/GetScheduleParam' + responses: + 200: + description: 성공 + content: + application/json: + schema: + $ref: '../../components/schedule.yaml#/components/schemas/GetSchedule' + 404: + description: error +post: + summary: 일정 추가하기 + tags: [schedule] + parameters: + - name: team_id + in: path + required: true + description: 대상 팀 id + schema: + type: integer + requestBody: + content: + application/json: + schema: + $ref: '../../components/schedule.yaml#/components/schemas/CreateScheduleBody' + responses: + 200: + description: 성공 + content: + application/json: + schema: + $ref: '../../components/schedule.yaml#/components/schemas/CreateScheduleRes' + 409: + description: error diff --git a/backend/src/swagger/paths/team/createTeam.yaml b/backend/src/swagger/paths/team/createTeam.yaml new file mode 100644 index 0000000..418eeb2 --- /dev/null +++ b/backend/src/swagger/paths/team/createTeam.yaml @@ -0,0 +1,13 @@ +post: + summary: 팀 생성, [team, team-user] + tags: [team] + requestBody: + content: + application/json: + schema: + $ref: '../../components/team.yaml#/components/schemas/CreateTeam' + responses: + 204: + description: 성공 + 409: + description: error diff --git a/backend/src/swagger/paths/team/invite.yaml b/backend/src/swagger/paths/team/invite.yaml new file mode 100644 index 0000000..5cdf51d --- /dev/null +++ b/backend/src/swagger/paths/team/invite.yaml @@ -0,0 +1,13 @@ +post: + summary: 초대 전송 [team-user] + tags: [team] + requestBody: + content: + application/json: + schema: + $ref: '../../components/team.yaml#/components/schemas/Invite' + responses: + 201: + description: 성공 + 404: + description: error diff --git a/backend/src/swagger/paths/team/response.yaml b/backend/src/swagger/paths/team/response.yaml new file mode 100644 index 0000000..5eaee5b --- /dev/null +++ b/backend/src/swagger/paths/team/response.yaml @@ -0,0 +1,26 @@ +post: + summary: 초대 수락 [team-user] + tags: [team] + requestBody: + content: + application/json: + schema: + $ref: '../../components/team.yaml#/components/schemas/TeamId' + responses: + 204: + description: 성공 + 409: + description: error +delete: + summary: 초대 거절 [team-user] + tags: [team] + requestBody: + content: + application/json: + schema: + $ref: '../../components/team.yaml#/components/schemas/TeamId' + responses: + 204: + description: 성공 + 409: + description: error diff --git a/backend/src/swagger/paths/team/team.yaml b/backend/src/swagger/paths/team/team.yaml new file mode 100644 index 0000000..0a0af0c --- /dev/null +++ b/backend/src/swagger/paths/team/team.yaml @@ -0,0 +1,38 @@ +get: + summary: 팀, 초대 목록 [team-user] + tags: [team] + responses: + 200: + description: 성공 + content: + application/json: + schema: + $ref: '../../components/team.yaml#/components/schemas/TeamList' + 404: + description: error +put: + summary: 팀 수정 [team] + tags: [team] + requestBody: + content: + application/json: + schema: + $ref: '../../components/team.yaml#/components/schemas/TeamInfo' + responses: + 200: + description: 성공 + 409: + description: error +delete: + summary: 팀 삭제, [team] + tags: [team] + requestBody: + content: + application/json: + schema: + $ref: '../../components/team.yaml#/components/schemas/TeamId' + responses: + 204: + description: 성공 + 409: + description: error diff --git a/backend/src/swagger/paths/team/user.yaml b/backend/src/swagger/paths/team/user.yaml new file mode 100644 index 0000000..bf8044a --- /dev/null +++ b/backend/src/swagger/paths/team/user.yaml @@ -0,0 +1,27 @@ +get: + summary: 팀 정보 가져오기 + tags: [team] + responses: + 200: + description: 성공 + content: + application/json: + schema: + $ref: '../../components/team.yaml#/components/schemas/TeamInfo' + 404: + description: error +delete: + summary: 유저 강퇴하기 + tags: [team] + parameters: + $ref: '../../components/team.yaml#/components/parameters/UserId' + requestBody: + content: + application/json: + schema: + $ref: '../../components/team.yaml#/components/schemas/TeamId' + responses: + 204: + description: 성공 + 409: + description: error diff --git a/backend/src/swagger/paths/team/users.yaml b/backend/src/swagger/paths/team/users.yaml new file mode 100644 index 0000000..ea5efba --- /dev/null +++ b/backend/src/swagger/paths/team/users.yaml @@ -0,0 +1,14 @@ +get: + summary: 팀의 모든 유저 리스트 가져오기 + tags: [team] + parameters: + $ref: '../../components/team.yaml#/components/parameters/TeamId' + responses: + 200: + description: 성공 + content: + application/json: + schema: + $ref: '../../components/team.yaml#/components/schemas/TeamUsers' + 404: + description: error diff --git a/backend/src/swagger/paths/user/user.yaml b/backend/src/swagger/paths/user/user.yaml new file mode 100644 index 0000000..538f896 --- /dev/null +++ b/backend/src/swagger/paths/user/user.yaml @@ -0,0 +1,15 @@ +patch: + summary: 유저 닉네임 변경하기 + tags: [user] + requestBody: + content: + application/json: + schema: + $ref: '../../components/user.yaml#/components/schemas/NewnameBody' + responses: + 204: + description: 성공 + 401: + description: 유저 정보 없음 + 409: + description: 이미 존재하는 닉네임 diff --git a/backend/src/swagger/swagger.yaml b/backend/src/swagger/swagger.yaml new file mode 100644 index 0000000..d8f501a --- /dev/null +++ b/backend/src/swagger/swagger.yaml @@ -0,0 +1,675 @@ +openapi: 3.0.0 +info: + version: 1.0.0 + title: BoostTeams API docs + description: BoostTeams의 API 문서입니다 + license: + name: MIT +servers: + - url: 'http://localhost:4000/' + description: BoostTeams Server +tags: + - name: auth + description: Join / Login / Logout + - name: chat + description: Chat Page + - name: schedule + description: Calendar Page + - name: team + description: Team Page + - name: user + description: User Manage +paths: + /api/auth/login: + post: + summary: 로그인하기 + tags: + - auth + responses: + '200': + description: 성공 + '404': + description: error + /api/auth/github: + get: + summary: github + tags: + - auth + responses: + '200': + description: 성공 + '404': + description: error + /api/auth/github/callback: + get: + summary: github/callback + tags: + - auth + responses: + '200': + description: 성공 + '404': + description: error + /api/auth/signup: + post: + summary: 회원가입하기 + tags: + - auth + responses: + '200': + description: 성공 + '404': + description: error + /api/auth/info: + get: + summary: 유저 정보 가져오기 + tags: + - auth + responses: + '200': + description: 성공 + '404': + description: error + '/api/schedule/{team_id}': + get: + summary: 일정 가져오기 + tags: + - schedule + parameters: + - name: team_id + in: path + required: true + description: 대상 팀 id + schema: + type: integer + - name: start_date + in: query + required: true + description: 시작 날짜 + schema: + type: string + - name: end_date + in: query + required: true + description: 끝 날짜 + schema: + type: string + responses: + '200': + description: 성공 + content: + application/json: + schema: + type: array + items: + type: object + properties: + title: + type: string + team_id: + type: integer + start_date: + type: string + end_date: + type: string + content: + type: string + color: + type: integer + repeat_id: + type: string + repeat_option: + type: integer + schedule_id: + type: integer + example: + - schedule_id: 135 + team_id: 1 + title: 회의 - 수정 + start_date: 2021-11-09T05:00:00.000Z + end_date: 2021-11-09T06:00:00.000Z + repeat_id: 837148eb-b3b6-45a7-8f86-0517f10ffe10 + repeat_option: 0 + content: 화요일 회의 - 수정 + color: 4 + - schedule_id: 139 + team_id: 1 + title: hi + start_date: 2021-11-09T12:45:00.000Z + end_date: 2021-11-09T13:30:00.000Z + repeat_id: 288d71e8-06a1-46c9-93fe-0299b949dbc6 + repeat_option: 0 + content: 22 + color: 0 + - schedule_id: 146 + team_id: 1 + title: hihi + start_date: 2021-11-11T09:30:00.000Z + end_date: 2021-11-11T10:00:00.000Z + repeat_id: 2069433b-3a19-47a2-b2f0-62008729a925 + repeat_option: 0 + content: hihi + color: 3 + '404': + description: error + post: + summary: 일정 추가하기 + tags: + - schedule + parameters: + - name: team_id + in: path + required: true + description: 대상 팀 id + schema: + type: integer + requestBody: + content: + application/json: + schema: + type: object + properties: + title: + type: string + start_date: + type: string + end_date: + type: string + repeat_option: + type: integer + repeat_count: + type: integer + content: + type: string + color: + type: integer + example: + title: 매일 5번 반복 + start_date: 2021-11-01T03:00:00.000Z + end_date: 2021-11-01T04:00:00.000Z + repeat_option: 1 + repeat_count: 5 + content: 매일 5번 반복 + color: 2 + responses: + '200': + description: 성공 + content: + application/json: + schema: + type: array + items: + type: object + properties: + title: + type: string + team_id: + type: integer + start_date: + type: string + end_date: + type: string + content: + type: string + color: + type: integer + repeat_id: + type: string + repeat_option: + type: integer + schedule_id: + type: integer + example: + - title: 매일 5번 반복 + team_id: 1 + start_date: 'Mon Nov 01 2021 12:00:00 GMT+0900' + end_date: 'Mon Nov 01 2021 13:00:00 GMT+0900' + content: 매일 5번 반복 + color: 2 + repeat_id: d4a826b3-719d-4954-a250-e772821dc897 + repeat_option: 1 + schedule_id: 157 + - title: 매일 5번 반복 + team_id: 1 + start_date: 'Tue Nov 02 2021 12:00:00 GMT+0900' + end_date: 'Tue Nov 02 2021 13:00:00 GMT+0900' + content: 매일 5번 반복 + color: 2 + repeat_id: d4a826b3-719d-4954-a250-e772821dc897 + repeat_option: 1 + schedule_id: 156 + - title: 매일 5번 반복 + team_id: 1 + start_date: 'Wed Nov 03 2021 12:00:00 GMT+0900' + end_date: 'Wed Nov 03 2021 13:00:00 GMT+0900' + content: 매일 5번 반복 + color: 2 + repeat_id: d4a826b3-719d-4954-a250-e772821dc897 + repeat_option: 1 + schedule_id: 158 + '409': + description: error + '/api/schedule/{schedule_id}': + delete: + summary: 일정 삭제하기 + tags: + - schedule + parameters: + $ref: '#/paths/~1api~1schedule~1%7Bschedule_id%7D/put/parameters' + responses: + '204': + description: 성공 + '409': + description: error + put: + summary: 일정 수정하기 + tags: + - schedule + parameters: + - name: schdule_id + in: path + required: true + description: shedule id + schema: + type: integer + requestBody: + content: + application/json: + schema: + type: object + properties: + title: + type: string + start_date: + type: string + end_date: + type: string + repeat_option: + type: integer + repeat_count: + type: integer + content: + type: string + color: + type: integer + example: + title: 매일 5번 반복 + start_date: 2021-11-01T03:00:00.000Z + end_date: 2021-11-01T04:00:00.000Z + repeat_option: 1 + repeat_count: 5 + content: 매일 5번 반복 + color: 2 + responses: + '200': + description: 성공 + content: + application/json: + schema: + type: object + properties: + schedule_id: + type: integer + team_id: + type: integer + title: + type: string + start_date: + type: string + end_date: + type: string + repeat_id: + type: string + content: + type: string + color: + type: integer + example: + schedule_id: 135 + team_id: 1 + title: 회의 - 수정 + start_date: 2021-11-09T05:00:00.000Z + end_date: 2021-11-09T06:00:00.000Z + repeat_id: 837148eb-b3b6-45a7-8f86-0517f10ffe10 + content: 화요일 회의 - 수정 + color: 4 + '409': + description: error + /api/chat/room: + get: + summary: 채팅방 목록 가져오기 + tags: + - chat + parameters: + - name: teamId + in: query + required: true + schema: + type: integer + - name: userId + in: query + required: true + schema: + type: integer + responses: + '200': + description: 성공 + content: + application/json: + schema: + type: object + properties: + chat_rooms: + type: array + items: + type: object + properties: + chat_room_id: + type: integer + chat_room_name: + type: string + '404': + description: error + post: + summary: 채팅방 생성하기 + tags: + - chat + requestBody: + content: + application/json: + schema: + type: object + properties: + team_id: + type: integer + chat_room_name: + type: string + user_id_list: + type: array + items: + type: object + properties: + user_id: + type: integer + responses: + '200': + description: 성공 + content: + application/json: + schema: + type: object + properties: + chat_room_id: + type: integer + chat_room_name: + type: string + user_list: + type: array + items: + type: object + properties: + user_id: + type: integer + '409': + description: error + '/api/chat/room/{chatRoomId}': + get: + summary: 채팅방 정보 가져오기 + tags: + - chat + parameters: + - name: chatRoomId + in: path + required: true + schema: + type: integer + responses: + '200': + description: 성공 + content: + application/json: + schema: + type: object + properties: + chat_room_id: + type: integer + user_list: + type: array + items: + type: object + properties: + user_id: + type: integer + '404': + description: error + /api/user/name: + patch: + summary: 유저 닉네임 변경하기 + tags: + - user + requestBody: + content: + application/json: + schema: + type: object + properties: + newName: + type: string + responses: + '204': + description: 성공 + '401': + description: 유저 정보 없음 + '409': + description: 이미 존재하는 닉네임 + /api/team: + get: + summary: '팀, 초대 목록 [team-user]' + tags: + - team + responses: + '200': + description: 성공 + content: + application/json: + schema: + type: array + items: + type: object + properties: + team_user_id: + type: integer + state: + type: boolean + role: + type: integer + team: + type: object + properties: + team_id: + type: integer + team_name: + type: string + team_desc: + type: string + '404': + description: error + put: + summary: '팀 수정 [team]' + tags: + - team + requestBody: + content: + application/json: + schema: + type: object + properties: + team_id: + type: integer + team_name: + type: string + team_desc: + type: string + responses: + '200': + description: 성공 + '409': + description: error + delete: + summary: '팀 삭제, [team]' + tags: + - team + requestBody: + content: + application/json: + schema: + type: object + properties: + team_id: + type: integer + responses: + '204': + description: 성공 + '409': + description: error + '/api/team/{id}': + get: + summary: 팀 정보 가져오기 + tags: + - team + responses: + '200': + description: 성공 + content: + application/json: + schema: + $ref: '#/paths/~1api~1team/put/requestBody/content/application~1json/schema' + '404': + description: error + delete: + summary: 유저 강퇴하기 + tags: + - team + parameters: + - name: userId + in: path + required: true + schema: + type: integer + requestBody: + content: + application/json: + schema: + $ref: '#/paths/~1api~1team/delete/requestBody/content/application~1json/schema' + responses: + '204': + description: 성공 + '409': + description: error + /api/team/create: + post: + summary: '팀 생성, [team, team-user]' + tags: + - team + requestBody: + content: + application/json: + schema: + type: object + properties: + team_name: + type: string + team_desc: + type: string + responses: + '204': + description: 성공 + '409': + description: error + '/api/team/users/{id}': + get: + summary: 팀의 모든 유저 리스트 가져오기 + tags: + - team + parameters: + - name: teamId + in: path + required: true + schema: + type: integer + responses: + '200': + description: 성공 + content: + application/json: + schema: + type: array + items: + type: object + properties: + team_user_id: + type: integer + state: + type: boolean + role: + type: integer + user: + type: object + properties: + user_id: + type: integer + user_name: + type: string + user_color: + type: integer + '404': + description: error + /api/team/invite: + post: + summary: '초대 전송 [team-user]' + tags: + - team + requestBody: + content: + application/json: + schema: + type: object + properties: + user_email: + type: string + team_id: + type: integer + responses: + '201': + description: 성공 + '404': + description: error + /api/team/invite/response: + post: + summary: '초대 수락 [team-user]' + tags: + - team + requestBody: + content: + application/json: + schema: + $ref: '#/paths/~1api~1team/delete/requestBody/content/application~1json/schema' + responses: + '204': + description: 성공 + '409': + description: error + delete: + summary: '초대 거절 [team-user]' + tags: + - team + requestBody: + content: + application/json: + schema: + $ref: '#/paths/~1api~1team/delete/requestBody/content/application~1json/schema' + responses: + '204': + description: 성공 + '409': + description: error diff --git a/backend/tsconfig.path.json b/backend/tsconfig.path.json index b07255b..3396bcb 100644 --- a/backend/tsconfig.path.json +++ b/backend/tsconfig.path.json @@ -10,7 +10,8 @@ "@repositories/*": ["src/repositories/*"], "@routes/*": ["src/routes/*"], "@services/*": ["src/services/*"], - "@sockets/*": ["src/sockets/*"] + "@sockets/*": ["src/sockets/*"], + "@redis/*": ["src/redis/*"] } } } diff --git a/backend/yarn.lock b/backend/yarn.lock index 08745a8..9046218 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -2,6 +2,48 @@ # yarn lockfile v1 +"@apidevtools/json-schema-ref-parser@^9.0.6": + version "9.0.9" + resolved "https://registry.yarnpkg.com/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.0.9.tgz#d720f9256e3609621280584f2b47ae165359268b" + integrity sha512-GBD2Le9w2+lVFoc4vswGI/TjkNIZSVp7+9xPf+X3uidBfWnAeUWmquteSyt0+VCrhNMWj/FTABISQrD3Z/YA+w== + dependencies: + "@jsdevtools/ono" "^7.1.3" + "@types/json-schema" "^7.0.6" + call-me-maybe "^1.0.1" + js-yaml "^4.1.0" + +"@apidevtools/openapi-schemas@^2.0.4": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz#9fa08017fb59d80538812f03fc7cac5992caaa17" + integrity sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ== + +"@apidevtools/swagger-cli@4.0.4": + version "4.0.4" + resolved "https://registry.yarnpkg.com/@apidevtools/swagger-cli/-/swagger-cli-4.0.4.tgz#c645c291f56e4add583111aca9edeee23d60fa10" + integrity sha512-hdDT3B6GLVovCsRZYDi3+wMcB1HfetTU20l2DC8zD3iFRNMC6QNAZG5fo/6PYeHWBEv7ri4MvnlKodhNB0nt7g== + dependencies: + "@apidevtools/swagger-parser" "^10.0.1" + chalk "^4.1.0" + js-yaml "^3.14.0" + yargs "^15.4.1" + +"@apidevtools/swagger-methods@^3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz#b789a362e055b0340d04712eafe7027ddc1ac267" + integrity sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg== + +"@apidevtools/swagger-parser@^10.0.1": + version "10.0.3" + resolved "https://registry.yarnpkg.com/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz#32057ae99487872c4dd96b314a1ab4b95d89eaf5" + integrity sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g== + dependencies: + "@apidevtools/json-schema-ref-parser" "^9.0.6" + "@apidevtools/openapi-schemas" "^2.0.4" + "@apidevtools/swagger-methods" "^3.0.2" + "@jsdevtools/ono" "^7.1.3" + call-me-maybe "^1.0.1" + z-schema "^5.0.1" + "@cspotcode/source-map-consumer@0.8.0": version "0.8.0" resolved "https://registry.yarnpkg.com/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz#33bf4b7b39c178821606f669bbc447a6a629786b" @@ -43,6 +85,11 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== +"@jsdevtools/ono@^7.1.3": + version "7.1.3" + resolved "https://registry.yarnpkg.com/@jsdevtools/ono/-/ono-7.1.3.tgz#9df03bbd7c696a5c58885c34aa06da41c8543796" + integrity sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg== + "@mapbox/node-pre-gyp@^1.0.0": version "1.0.6" resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.6.tgz#f859d601a210537e27530f363028cde56e0cf962" @@ -177,7 +224,7 @@ "@types/qs" "*" "@types/serve-static" "*" -"@types/json-schema@^7.0.9": +"@types/json-schema@^7.0.6", "@types/json-schema@^7.0.9": version "7.0.9" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d" integrity sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ== @@ -278,11 +325,24 @@ "@types/mime" "^1" "@types/node" "*" +"@types/swagger-ui-express@^4.1.3": + version "4.1.3" + resolved "https://registry.yarnpkg.com/@types/swagger-ui-express/-/swagger-ui-express-4.1.3.tgz#7adbbbf5343b45869debef1e9ff39c9ba73e380f" + integrity sha512-jqCjGU/tGEaqIplPy3WyQg+Nrp6y80DCFnDEAvVKWkJyv0VivSSDCChkppHRHAablvInZe6pijDFMnavtN0vqA== + dependencies: + "@types/express" "*" + "@types/serve-static" "*" + "@types/uuid@^8.3.1": version "8.3.1" resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.1.tgz#1a32969cf8f0364b3d8c8af9cc3555b7805df14f" integrity sha512-Y2mHTRAbqfFkpjldbkHGY8JIzRN6XqYRliG8/24FcHm2D2PwW24fl5xMRTVGdrb7iMrwCaIEbLWerGIkXuFWVg== +"@types/yamljs@^0.2.31": + version "0.2.31" + resolved "https://registry.yarnpkg.com/@types/yamljs/-/yamljs-0.2.31.tgz#b1a620b115c96db7b3bfdf0cf54aee0c57139245" + integrity sha512-QcJ5ZczaXAqbVD3o8mw/mEBhRvO5UAdTtbvgwL/OgoWubvNBh6/MxLBAigtcgIFaq3shon9m3POIxQaLQt4fxQ== + "@types/zen-observable@0.8.3": version "0.8.3" resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.8.3.tgz#781d360c282436494b32fe7d9f7f8e64b3118aa3" @@ -468,6 +528,13 @@ arg@^4.1.0: resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + argparse@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" @@ -632,11 +699,21 @@ call-bind@^1.0.0, call-bind@^1.0.2: function-bind "^1.1.1" get-intrinsic "^1.0.2" +call-me-maybe@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.1.tgz#26d208ea89e37b5cbde60250a15f031c16a4d66b" + integrity sha1-JtII6onje1y95gJQoV8DHBak1ms= + callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== +camelcase@^5.0.0: + version "5.3.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + camelcase@^6.2.0: version "6.2.1" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.1.tgz#250fd350cfd555d0d2160b1d51510eaf8326e86e" @@ -692,6 +769,15 @@ cli-highlight@^2.1.11: parse5-htmlparser2-tree-adapter "^6.0.0" yargs "^16.0.0" +cliui@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1" + integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^6.2.0" + cliui@^7.0.2: version "7.0.4" resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" @@ -725,6 +811,11 @@ color-support@^1.1.2: resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== +commander@^2.7.1: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + component-emitter@~1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" @@ -848,6 +939,11 @@ debug@^3.2.7: dependencies: ms "^2.1.1" +decamelize@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= + decompress-response@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3" @@ -1221,6 +1317,11 @@ espree@^9.0.0: acorn-jsx "^5.3.1" eslint-visitor-keys "^3.0.0" +esprima@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + esquery@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5" @@ -1363,6 +1464,14 @@ find-up@^2.1.0: dependencies: locate-path "^2.0.0" +find-up@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + flat-cache@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" @@ -1435,7 +1544,7 @@ generate-function@^2.3.1: dependencies: is-property "^1.0.2" -get-caller-file@^2.0.5: +get-caller-file@^2.0.1, get-caller-file@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== @@ -1485,7 +1594,7 @@ glob-parent@^6.0.1: dependencies: is-glob "^4.0.3" -glob@^7.1.3, glob@^7.1.6: +glob@^7.0.5, glob@^7.1.3, glob@^7.1.6: version "7.2.0" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== @@ -1875,6 +1984,14 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= +js-yaml@^3.14.0: + version "3.14.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" + integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + js-yaml@^4.0.0, js-yaml@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" @@ -1967,6 +2084,18 @@ locate-path@^2.0.0: p-locate "^2.0.0" path-exists "^3.0.0" +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + +lodash.get@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" + integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= + lodash.includes@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" @@ -1977,6 +2106,11 @@ lodash.isboolean@^3.0.3: resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" integrity sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY= +lodash.isequal@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" + integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA= + lodash.isinteger@^4.0.4: version "4.0.4" resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" @@ -2355,6 +2489,13 @@ p-limit@^1.1.0: dependencies: p-try "^1.0.0" +p-limit@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + p-locate@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" @@ -2362,11 +2503,23 @@ p-locate@^2.0.0: dependencies: p-limit "^1.1.0" +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + p-try@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M= +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + package-json@^6.3.0: version "6.5.0" resolved "https://registry.yarnpkg.com/package-json/-/package-json-6.5.0.tgz#6feedaca35e75725876d0b0e64974697fed145b0" @@ -2449,6 +2602,11 @@ path-exists@^3.0.0: resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" @@ -2663,6 +2821,11 @@ require-directory@^2.1.1: resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= +require-main-filename@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" + integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== + resolve-from@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" @@ -2855,6 +3018,11 @@ socket.io@^4.3.1: socket.io-adapter "~2.3.2" socket.io-parser "~4.0.4" +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= + sqlstring@^2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/sqlstring/-/sqlstring-2.3.2.tgz#cdae7169389a1375b18e885f2e60b3e460809514" @@ -2953,6 +3121,25 @@ supports-color@^7.1.0: dependencies: has-flag "^4.0.0" +swagger-cli@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/swagger-cli/-/swagger-cli-4.0.4.tgz#c3f0b94277073c776b9bcc3ae7507b372f3ff414" + integrity sha512-Cp8YYuLny3RJFQ4CvOBTaqmOOgYsem52dPx1xM5S4EUWFblIh2Q8atppMZvXKUr1e9xH5RwipYpmdUzdPcxWcA== + dependencies: + "@apidevtools/swagger-cli" "4.0.4" + +swagger-ui-dist@^3.18.1: + version "3.52.5" + resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-3.52.5.tgz#9aa8101a2be751f5145195b9e048bc21b12fac60" + integrity sha512-8z18eX8G/jbTXYzyNIaobrnD7PSN7yU/YkSasMmajrXtw0FGS64XjrKn5v37d36qmU3o1xLeuYnktshRr7uIFw== + +swagger-ui-express@^4.1.6: + version "4.1.6" + resolved "https://registry.yarnpkg.com/swagger-ui-express/-/swagger-ui-express-4.1.6.tgz#682294af3d5c70f74a1fa4d6a9b503a9ee55ea82" + integrity sha512-Xs2BGGudvDBtL7RXcYtNvHsFtP1DBFPMJFRxHe5ez/VG/rzVOEjazJOOSc/kSCyxreCTKfJrII6MJlL9a6t8vw== + dependencies: + swagger-ui-dist "^3.18.1" + tar@^6.1.11: version "6.1.11" resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621" @@ -3193,6 +3380,11 @@ v8-compile-cache@^2.0.3: resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== +validator@^13.7.0: + version "13.7.0" + resolved "https://registry.yarnpkg.com/validator/-/validator-13.7.0.tgz#4f9658ba13ba8f3d82ee881d3516489ea85c0857" + integrity sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw== + vary@^1, vary@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" @@ -3222,6 +3414,11 @@ which-boxed-primitive@^1.0.2: is-string "^1.0.5" is-symbol "^1.0.3" +which-module@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" + integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= + which@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" @@ -3248,6 +3445,15 @@ word-wrap@^1.2.3: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== +wrap-ansi@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" + integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" @@ -3295,6 +3501,11 @@ xmlbuilder@~11.0.0: resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3" integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA== +y18n@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf" + integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ== + y18n@^5.0.5: version "5.0.8" resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" @@ -3310,11 +3521,44 @@ yallist@^4.0.0: resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== +yamljs@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/yamljs/-/yamljs-0.3.0.tgz#dc060bf267447b39f7304e9b2bfbe8b5a7ddb03b" + integrity sha512-C/FsVVhht4iPQYXOInoxUM/1ELSf9EsgKH34FofQOp6hwCPrW4vG4w5++TED3xRUo8gD7l0P1J1dLlDYzODsTQ== + dependencies: + argparse "^1.0.7" + glob "^7.0.5" + +yargs-parser@^18.1.2: + version "18.1.3" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" + integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + yargs-parser@^20.2.2: version "20.2.9" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== +yargs@^15.4.1: + version "15.4.1" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" + integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A== + dependencies: + cliui "^6.0.0" + decamelize "^1.2.0" + find-up "^4.1.0" + get-caller-file "^2.0.1" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^4.2.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^18.1.2" + yargs@^16.0.0: version "16.2.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" @@ -3346,6 +3590,17 @@ yn@3.1.1: resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== +z-schema@^5.0.1: + version "5.0.2" + resolved "https://registry.yarnpkg.com/z-schema/-/z-schema-5.0.2.tgz#f410394b2c9fcb9edaf6a7511491c0bb4e89a504" + integrity sha512-40TH47ukMHq5HrzkeVE40Ad7eIDKaRV2b+Qpi2prLc9X9eFJFzV7tMe5aH12e6avaSS/u5l653EQOv+J9PirPw== + dependencies: + lodash.get "^4.4.2" + lodash.isequal "^4.5.0" + validator "^13.7.0" + optionalDependencies: + commander "^2.7.1" + zen-observable-ts@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-1.1.0.tgz#2d1aa9d79b87058e9b75698b92791c1838551f83" diff --git a/frontend/package.json b/frontend/package.json index 255212b..6b35608 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -15,11 +15,13 @@ "react-router": "^5.2.1", "react-router-dom": "^5.3.0", "react-scripts": "4.0.3", + "react-select": "^5.2.1", "react-toastify": "^8.0.3", "recoil": "^0.4.1", "socket.io-client": "^4.3.2", "styled-components": "^5.3.3", "styled-reset": "^4.3.4", + "use-image": "^1.0.8", "uuid": "^8.3.2", "web-vitals": "^1.0.1" }, diff --git a/frontend/public/images/pencil-square.svg b/frontend/public/images/pencil-square.svg new file mode 100644 index 0000000..b8c90d5 --- /dev/null +++ b/frontend/public/images/pencil-square.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/frontend/src/apis/chat.ts b/frontend/src/apis/chat.ts index 6edf691..7fce36d 100644 --- a/frontend/src/apis/chat.ts +++ b/frontend/src/apis/chat.ts @@ -4,20 +4,22 @@ import { ChatRoomReqType, ChatRoomResType, ChatRoomType, - ChatRoomInfoType, - MessageList, + ChatRoomUsersType, ChatRoomsType, + MessageListType, + UserListReqType, } from '@src/types/chat'; +import { UserIdType } from '@src/types/team'; +import { Socket } from 'socket.io-client'; export const createChatRoom = async (roomInfo: ChatRoomReqType): Promise => { try { - const res = await fetchApi.post('/api/chat/room', { ...roomInfo }); - if (res.status === 409) throw new Error(); + const res = await fetchApi.post('/api/chats/rooms', { ...roomInfo }); + if (res.status !== 201) throw new Error(); const data = await res.json(); return { chatRoomId: data.chat_room_id, chatRoomName: data.chat_room_name, - lastMessage: { messageId: 1, content: '메시지 내용', createdAt: new Date(), userId: 55 }, }; } catch (err) { toast.error('😣 채팅방 생성에 실패하였습니다!'); @@ -27,8 +29,8 @@ export const createChatRoom = async (roomInfo: ChatRoomReqType): Promise => { try { - const res = await fetchApi.get(`/api/chat/room?teamId=${teamId}&userId=${userId}`); - if (res.status === 409) throw new Error(); + const res = await fetchApi.get(`/api/chats/rooms?teamId=${teamId}&userId=${userId}`); + if (res.status !== 200) throw new Error(); const data = await res.json(); const entries = data.chat_rooms.map((chatRoom: ChatRoomResType) => { return [ @@ -36,7 +38,6 @@ export const getChatRooms = async (teamId: number, userId: number): Promise => { +export const updateChatRoomName = async (chatRoomId: number, chatRoomName: string): Promise => { try { - const res = await fetchApi.get(`/api/chat/room/${chatRoomId}`); - if (res.status === 409) throw new Error(); - const data = await res.json(); - return { chatRoomId: data.chat_room_id, userList: data.user_list }; + const res = await fetchApi.patch(`/api/chats/rooms/${chatRoomId}`, { chat_room_name: chatRoomName }); + if (res.status !== 201) throw new Error(); + return true; } catch (err) { - toast.error('😣 채팅방 정보 가져오기에 실패하였습니다!'); - return undefined; + toast.error('😣 채팅방 이름 변경에 실패하였습니다!'); + return false; } }; -export const getMessageList = async (chatRoomId: number): Promise => { +export const getChatRoomUsers = async (chatRoomId: number): Promise => { try { - const res = await fetchApi.get(`/api/chat/message?chatRoomId=${chatRoomId}`); // 스크롤 나중에 - if (res.status === 409) throw new Error(); + const res = await fetchApi.get(`/api/chats/rooms/${chatRoomId}/users`); + if (res.status !== 200) throw new Error(); const data = await res.json(); - return data.message_list; + const userList = data.chat_room_users.map((user: { user_id: number }) => { + return { userId: user.user_id }; + }); + return { userList }; } catch (err) { - toast.error('😣 메시지 가져오기에 실패하였습니다!'); - return []; + toast.error('😣 채팅방 유저 목록 가져오기에 실패하였습니다!'); + return { userList: [] }; + } +}; + +export const addChatRoomUsers = async (chatRoomId: number, userList: UserListReqType): Promise => { + try { + const res = await fetchApi.post(`/api/chats/rooms/${chatRoomId}/users`, { user_list: userList }); + if (res.status !== 201) throw new Error(); + return true; + } catch (err) { + toast.error('😣 유저 초대하기에 실패하였습니다!'); + return false; + } +}; + +export const deleteChatRoomUser = async (chatRoomId: number, userId: number): Promise => { + try { + const res = await fetchApi.delete(`/api/chats/rooms/${chatRoomId}/users/${userId}`); + if (res.status !== 204) throw new Error(); + return true; + } catch (err) { + toast.error('😣 채팅방 나가기에 실패하였습니다!'); + return false; } }; + +export const socketApi = { + enterChatRooms: (socket: Socket, chatRoomList: { chatRoomId: number }[]) => { + socket.emit('enter chat rooms', { chatRooms: chatRoomList }); + }, + leaveChatRooms: (socket: Socket, chatRoomList: { chatRoomId: number }[]) => { + socket.emit('refresh chat rooms', { chatRooms: chatRoomList }); + }, + createChatRoom: (socket: Socket, chatRoomId: number, userList: UserIdType[], teamId: number) => { + socket.emit('create chat room', { chatRoomId, userList, teamId }); + }, + inviteUsers: (socket: Socket, chatRoomId: number, userList: UserIdType[], teamId: number) => { + socket.emit('invite users', { chatRoomId, userList, teamId }); + }, + exitChatRoom: (socket: Socket, chatRoomId: number) => { + socket.emit('exit chat room', { chatRoomId }); + }, + getMessageList: (socket: Socket, chatRoomId: number) => { + socket.emit('get message list', { chatRoomId }); + }, + sendMessage: (socket: Socket, content: string, userId: number, chatRoomId: number) => { + socket.emit('send message', { content, userId, chatRoomId }); + }, + updateChatRoomName: (socket: Socket, chatRoomId: number) => { + socket.emit('update chat room name', { chatRoomId }); + }, +}; diff --git a/frontend/src/apis/schedule.ts b/frontend/src/apis/schedule.ts index 125910f..b4f8f89 100644 --- a/frontend/src/apis/schedule.ts +++ b/frontend/src/apis/schedule.ts @@ -15,24 +15,26 @@ export interface ScheduleReqType { export const getSchedules = async (teamId: number, firstDate: string, lastDate: string): Promise => { try { - const res = await fetchApi.get(`/api/schedule/${teamId}?start_date=${firstDate}&end_date=${lastDate}`); - if (res.status === 404) throw new Error(); + const res = await fetchApi.get(`/api/schedules/${teamId}?start_date=${firstDate}&end_date=${lastDate}`); + if (res.status === 404) throw new Error('😣 일정을 가져올 수 없습니다!'); + else if (res.status === 403) throw new Error('😣 권한이 없습니다!'); const data = await res.json(); return data; } catch (err) { - toast.error('😣 일정을 가져올 수 없습니다!'); + toast.error((err as Error).message); return []; } }; -export const createNewSchedule = async (team_id: number, newSchedule: ScheduleReqType): Promise => { +export const createNewSchedule = async (teamId: number, newSchedule: ScheduleReqType): Promise => { try { - const res = await fetchApi.post(`/api/schedule/${team_id}`, { ...newSchedule }); - if (res.status === 409) throw new Error(); + const res = await fetchApi.post(`/api/schedules/${teamId}`, { ...newSchedule }); + if (res.status === 409) throw new Error('😣 일정 추가에 실패하였습니다!'); + else if (res.status === 403) throw new Error('😣 권한이 없습니다!'); const data = await res.json(); return data; } catch (err) { - toast.error('😣 일정 추가에 실패하였습니다!'); + toast.error((err as Error).message); return []; } }; @@ -42,23 +44,25 @@ export const updateSchedule = async ( newSchedule: ScheduleReqType, ): Promise => { try { - const res = await fetchApi.put(`/api/schedule/${schedule_id}`, { ...newSchedule }); - if (res.status === 409) throw new Error(); + const res = await fetchApi.put(`/api/schedules/${schedule_id}`, { ...newSchedule }); + if (res.status === 409) throw new Error('😣 일정 수정에 실패하였습니다!'); + else if (res.status === 403) throw new Error('😣 권한이 없습니다!'); const data = await res.json(); return data; } catch (err) { - toast.error('😣 일정 수정에 실패하였습니다!'); + toast.error((err as Error).message); return undefined; } }; export const deleteSchedule = async (schedule_id: number): Promise => { try { - const res = await fetchApi.delete(`/api/schedule/${schedule_id}`); - if (res.status === 409) throw new Error(); + const res = await fetchApi.delete(`/api/schedules/${schedule_id}`); + if (res.status === 409) throw new Error('😣 일정 삭제에 실패하였습니다!'); + else if (res.status === 403) throw new Error('😣 권한이 없습니다!'); return true; } catch (err) { - toast.error('😣 일정 삭제에 실패하였습니다!'); + toast.error((err as Error).message); return false; } }; diff --git a/frontend/src/apis/team.ts b/frontend/src/apis/team.ts index cbec029..095de4d 100644 --- a/frontend/src/apis/team.ts +++ b/frontend/src/apis/team.ts @@ -1,12 +1,8 @@ +import { TeamUsersResType, TeamUsersType } from '@src/types/team'; +import { Role } from '@utils/constants'; import fetchApi from '@utils/fetch'; import { toast } from 'react-toastify'; -export const readMyTeam = async () => { - const res = await fetchApi.get(`/api/team`); - const data = await res.json(); - return data; -}; - interface teamData { team_id?: number; team_name: string; @@ -14,45 +10,154 @@ interface teamData { } export const create = async (setLoadTrigger: (param: any) => void, teamData: teamData) => { - await fetchApi.post('/api/team/create', { ...teamData }); - setLoadTrigger((prev: number) => prev + 1); + try { + const res = await fetchApi.post('/api/teams', { ...teamData }); + if (res.status === 409) throw new Error('😣 팀 생성에 실패했습니다!'); + setLoadTrigger((prev: number) => prev + 1); + } catch (err) { + toast.error((err as Error).message); + } }; -export const update = async (setLoadTrigger: (param: any) => void, teamData: teamData) => { - await fetchApi.put('/api/team', { ...teamData }); - setLoadTrigger((prev: number) => prev + 1); +export const readMyTeam = async () => { + try { + const res = await fetchApi.get(`/api/teams`); + if (res.status === 409) throw new Error('😣 팀 목록을 읽어오지 못했습니다!'); + const data = await res.json(); + return data; + } catch (err) { + toast.error((err as Error).message); + return err; + } }; -export const accept = async (setLoadTrigger: (param: any) => void, team_id: number) => { - await fetchApi.post('/api/team/invite/response', { team_id }); - setLoadTrigger((prev: number) => prev + 1); +export const update = async (setLoadTrigger: (param: any) => void, teamId: number, teamData: teamData) => { + try { + const res = await fetchApi.put(`/api/teams/${teamId}`, { ...teamData }); + if (res.status === 403) throw new Error('😣 권한이 없습니다!'); + else if (res.status === 409) throw new Error('😣 팀 업데이트에 실패했습니다!'); + setLoadTrigger((prev: number) => prev + 1); + } catch (err) { + toast.error((err as Error).message); + } }; -export const decline = async (setLoadTrigger: (param: any) => void, team_id: number) => { - await fetchApi.delete('/api/team/invite/response', { team_id }); - setLoadTrigger((prev: number) => prev + 1); +export const deleteTeam = async (setLoadTrigger: (param: any) => void, teamId: number) => { + try { + const res = await fetchApi.delete(`/api/teams/${teamId}`); + if (res.status === 403) throw new Error('😣 권한이 없습니다!'); + else if (res.status === 409) throw new Error('😣 팀 삭제에 실패했습니다!'); + setLoadTrigger((prev: number) => prev + 1); + } catch (err) { + toast.error((err as Error).message); + } }; -export const kickOut = async (setLoadTrigger: (param: any) => void, user_id: number, team_id: number) => { - await fetchApi.delete(`/api/team/${user_id}`, { team_id }); - setLoadTrigger((prev: number) => prev + 1); +export const inviteUser = async (teamId: number, userName: string) => { + try { + const res = await fetchApi.post(`/api/teams/${teamId}/invitations`, { teamId, userName }); + if (res.status === 404) throw new Error('😣 해당 유저가 존재하지 않습니다!'); + else if (res.status === 403) throw new Error('😣 권한이 없습니다!'); + else if (res.status === 409) throw new Error('😣 초대에 실패했습니다!'); + } catch (err) { + toast.error((err as Error).message); + } }; -export const leaveTeam = async (setLoadTrigger: (param: any) => void, team_id: number) => { - await fetchApi.delete('/api/team/invite/response', { team_id }); - setLoadTrigger((prev: number) => prev + 1); +export const accept = async (setLoadTrigger: (param: any) => void, teamId: number) => { + try { + const res = await fetchApi.patch(`/api/teams/${teamId}/invitations`, {}); + if (res.status === 403) throw new Error('😣 권한이 없습니다!'); + else if (res.status === 409) throw new Error('😣 초대를 수락하지 못했습니다!'); + setLoadTrigger((prev: number) => prev + 1); + } catch (err) { + toast.error((err as Error).message); + } }; -export const deleteTeam = async (setLoadTrigger: (param: any) => void, team_id: number) => { - await fetchApi.delete('/api/team', { team_id }); - setLoadTrigger((prev: number) => prev + 1); +export const decline = async (setLoadTrigger: (param: any) => void, teamId: number) => { + try { + const res = await fetchApi.delete(`/api/teams/${teamId}/invitations`); + if (res.status === 403) throw new Error('😣 권한이 없습니다!'); + else if (res.status === 409) throw new Error('😣 초대를 거절하지 못했습니다!'); + setLoadTrigger((prev: number) => prev + 1); + } catch (err) { + toast.error((err as Error).message); + } +}; + +export const kickOut = async (setLoadTrigger: (param: any) => void, userId: number, teamId: number) => { + try { + const res = await fetchApi.delete(`/api/teams/${teamId}/users/${userId}`); + if (res.status === 403) throw new Error('😣 권한이 없습니다!'); + else if (res.status === 409) throw new Error('😣 유저 강퇴에 실패했습니다!'); + setLoadTrigger((prev: number) => prev + 1); + } catch (err) { + toast.error((err as Error).message); + } +}; + +export const leaveTeam = async (setLoadTrigger: (param: any) => void, teamId: number) => { + try { + const res = await fetchApi.delete(`/api/teams/${teamId}/invitations`); + if (res.status === 409) throw new Error('😣 팀 탈퇴에 실패했습니다!'); + setLoadTrigger((prev: number) => prev + 1); + } catch (err) { + toast.error((err as Error).message); + } +}; + +export const readTeamInfo = async (id: number) => { + try { + const res = await fetchApi.get(`/api/teams/${id}`); + if (res.status === 403) throw new Error('😣 올바른 접근이 아닙니다!'); + else if (res.status === 403) throw new Error('😣 팀 정보를 읽어오지 못했습니다!'); + const data = await res.json(); + return data; + } catch (err) { + toast.error((err as Error).message); + return err; + } +}; + +export const readTeamUsers = async (id: number) => { + try { + const res = await fetchApi.get(`/api/teams/${id}/users`); + if (res.status === 403) throw new Error('😣 권한이 없습니다!'); + else if (res.status === 409) throw new Error('😣 유저 목록을 읽어오지 못했습니다!'); + const data = await res.json(); + const entries = data.map((el: TeamUsersResType) => { + return [ + el.user.user_id, + { + userId: el.user.user_id, + name: el.user.user_name, + email: el.user.user_email, + color: el.user.user_color, + role: Role[el.role], + }, + ]; + }); + const teamUsers: TeamUsersType = Object.fromEntries(entries); + return teamUsers; + } catch (err) { + toast.error((err as Error).message); + return {}; + } }; -export const inviteUser = async (team_id: number, user_email: string) => { +export const patchRole = async ( + setLoadTrigger: (param: any) => void, + userId: number, + teamId: number, + newRole: number, +) => { try { - const res = await fetchApi.post('/api/team/invite', { team_id, user_email }); - if (res.status === 204) throw new Error(); + const res = await fetchApi.patch(`/api/teams/${teamId}/users/${userId}`, { role: newRole }); + if (res.status === 403) throw new Error('😣 권한이 없습니다!'); + else if (res.status === 409) throw new Error('😣 권한 수정에 실패했습니다!'); + setLoadTrigger((prev: number) => prev + 1); } catch (err) { - toast.error('😣 해당 유저가 존재하지 않습니다!'); + toast.error((err as Error).message); } }; diff --git a/frontend/src/apis/user.ts b/frontend/src/apis/user.ts index 1c06caf..d145056 100644 --- a/frontend/src/apis/user.ts +++ b/frontend/src/apis/user.ts @@ -3,18 +3,18 @@ import fetchApi from '@utils/fetch'; export const updateName = async ({ newName }: { newName: string }, cb?: any) => { try { - const res = await fetchApi.patch('/api/user/name', { newName }); + const res = await fetchApi.patch('/api/users', { newName }); if (res.status === 204) { cb(); toast.success('😎 닉네임 변경 성공'); - } - if (res.status === 401) { - toast.warn('😣 유저 정보를 찾을 수 없습니다!'); - } - if (res.status === 409) { - toast.warn('😣 이미 존재하는 이름입니다!'); + } else if (res.status === 401) { + throw new Error('😣 유저 정보를 찾을 수 없습니다!'); + } else if (res.status === 409) { + throw new Error('😣 이미 존재하는 이름입니다!'); + } else { + throw new Error('😣 서버와의 연결이 심상치 않습니다!'); } } catch (err) { - toast.error('😣 서버와의 연결이 심상치 않습니다!'); + toast.error((err as Error).message); } }; diff --git a/frontend/src/apis/users.ts b/frontend/src/apis/users.ts deleted file mode 100644 index 38eefbd..0000000 --- a/frontend/src/apis/users.ts +++ /dev/null @@ -1,13 +0,0 @@ -import fetchApi from '@utils/fetch'; - -export const readTeamInfo = async (id: number) => { - const res = await fetchApi.get(`/api/team/${id}`); - const data = await res.json(); - return data[0]; -}; - -export const readTeamUsers = async (id: number) => { - const res = await fetchApi.get(`/api/team/users/${id}`); - const data = await res.json(); - return data; -}; diff --git a/frontend/src/components/Board/Canvas/index.tsx b/frontend/src/components/Board/Canvas/index.tsx index 3ee99f8..f6c01ba 100644 --- a/frontend/src/components/Board/Canvas/index.tsx +++ b/frontend/src/components/Board/Canvas/index.tsx @@ -1,66 +1,54 @@ import React from 'react'; import { Stage, Layer } from 'react-konva'; -import { PostitType } from '@pages/BoardPage'; -import { REM } from '@utils/constants'; +import { KonvaEventObject } from 'konva/lib/Node'; +import { useRecoilValue } from 'recoil'; +import userState from '@src/stores/user'; +import { CANVAS } from '@utils/constants'; +import { IPostit, ISocketApi } from '@src/types/board'; +import { Dispatch, SetStateAction } from 'hoist-non-react-statics/node_modules/@types/react'; import Postit from '../Postit'; interface Props { - postits: any[]; - socketApi: any; - setPostits: (postit: PostitType[]) => void; - setModalType: (type: string) => void; - setClickedPostit: (postit: PostitType) => void; + postits: IPostit[]; + socketApi: ISocketApi; + getUserNameById: (userId: number) => string; + handleDrag: (e: KonvaEventObject) => void; + handleDragStart: (e: KonvaEventObject) => void; + handleDragEnd: (e: KonvaEventObject) => void; + setModalType: Dispatch>; + setClickedPostit: (postit: IPostit) => void; handleModalOpen: () => void; } const Canvas: React.FC = ({ postits, socketApi, - setPostits, + getUserNameById, + handleDrag, + handleDragStart, + handleDragEnd, setModalType, setClickedPostit, handleModalOpen, }) => { - const handleDragStart = (e: any) => { - const id = Number(e.target.id()); - const postitList = [...postits]; - const postitIdx = postitList.findIndex((postit) => postit.id === id); - const postit = { ...postitList.splice(postitIdx, 1)[0], isDragging: true }; - postitList.push(postit); - setPostits(postitList); - }; - - const handleDragEnd = (e: any) => { - const id = Number(e.target.id()); - const postitList = [...postits]; - const postitIdx = postitList.findIndex((postit) => postit.id === id); - postitList[postitIdx] = { - ...postitList[postitIdx], - x: e.target.x(), - y: e.target.y(), - isDragging: false, - }; - setPostits(postitList); - }; - - const handleDrag = (event: any) => { - socketApi.dragPostit(event); - }; - + const userId = useRecoilValue(userState).id; return ( - + {postits && postits.map((postit) => ( ))} @@ -69,40 +57,3 @@ const Canvas: React.FC = ({ }; export default Canvas; - -/* -const Canvas: React.FC = ({ postits, setModalType, setClickedPostit, handleModalOpen }) => { - const socket = useContext(SocketContext); - const dragPostit = (id: number, x: number, y: number) => socket.current.emit('drag postit', { id, x, y }); - - return ( - - - { - setModalType('create'); - handleModalOpen(); - }} - text='새 포스트잇 작성' - /> - {postits.map((postit) => ( - - ))} - - - ); -}; -export default Canvas; -*/ diff --git a/frontend/src/components/Board/CreateButton/index.tsx b/frontend/src/components/Board/CreateButton/index.tsx index 5a647fc..3877918 100644 --- a/frontend/src/components/Board/CreateButton/index.tsx +++ b/frontend/src/components/Board/CreateButton/index.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { BiMessageAltAdd } from 'react-icons/bi'; import { Wrapper, Container } from './style'; interface Props { @@ -13,11 +14,12 @@ const CreateButton: React.FC = ({ setModalType, handleModalOpen }) => { }; return ( - - + + + 새 포스트잇 생성 - - + + ); }; diff --git a/frontend/src/components/Board/CreateButton/style.ts b/frontend/src/components/Board/CreateButton/style.ts index ad9cf99..5e31f25 100644 --- a/frontend/src/components/Board/CreateButton/style.ts +++ b/frontend/src/components/Board/CreateButton/style.ts @@ -1,33 +1,57 @@ import { ColorCode } from '@src/utils/constants'; -import styled from 'styled-components'; +import styled, { keyframes } from 'styled-components'; -export const Wrapper = styled.div` +const upDownAnimation = keyframes` + 0% { + transform: translateY(-0.5rem); + } + 100% { + transform: translateY(0); + } +`; + +export const Container = styled.div` position: fixed; + display: flex; + flex-direction: column; + justify-content: flex-end; + align-items: center; bottom: 0; right: 5rem; width: 17rem; - max-height: calc(4.8rem + 4px); + height: calc(6rem + 8px); overflow: hidden; + cursor: grab; + color: ${ColorCode.PLACEHOLDER}; + &:hover { + div { + bottom: -4rem; + } + } + svg { + position: absolute; + bottom: 2.5rem; + transition: all 0.1s ease-in-out; + animation: ${upDownAnimation} 0.5s ease-in-out infinite alternate; + } + font-size: 1.5rem; `; -export const Container = styled.div` - position: relative; +export const Wrapper = styled.div` display: flex; + position: absolute; + bottom: -8rem; width: 16rem; - height: 8rem; + height: 10rem; justify-content: center; align-items: center; font-size: 1.5rem; color: ${ColorCode.GRAY}; background-color: ${ColorCode.YELLOW}; box-shadow: 4px -3px 8px rgba(0, 0, 0, 0.25); - transform: translateY(40%); span { margin-bottom: 1rem; } - &:hover { - transform: translateY(8px); - } cursor: pointer; transition: all 0.1s ease-in-out; `; diff --git a/frontend/src/components/Board/Modal/index.tsx b/frontend/src/components/Board/Modal/Create/index.tsx similarity index 64% rename from frontend/src/components/Board/Modal/index.tsx rename to frontend/src/components/Board/Modal/Create/index.tsx index fad1839..99a54ba 100644 --- a/frontend/src/components/Board/Modal/index.tsx +++ b/frontend/src/components/Board/Modal/Create/index.tsx @@ -1,44 +1,43 @@ import React, { useRef, useState, useEffect } from 'react'; import userState from '@stores/user'; import { useRecoilValue } from 'recoil'; -import { PostitType } from '@pages/BoardPage'; import Modal from '@components/common/Modal'; import ColorPicker from '@components/common/ColorPicker'; +import { IPostit, ISocketApi } from '@src/types/board'; import { Container, Input, Textarea, TitleContainer } from './style'; interface Props { - socketApi: any; + socketApi: ISocketApi; modalType: string; - clickedPostit: PostitType; + clickedPostit: IPostit | undefined; handleModalClose: () => void; } -const CreatePostItModal: React.FC = ({ socketApi, modalType, clickedPostit, handleModalClose }) => { +const CreatePostitModal: React.FC = ({ socketApi, modalType, clickedPostit, handleModalClose }) => { const inputRef = useRef(null); const textareaRef = useRef(null); const user = useRecoilValue(userState); const [color, setColor] = useState(0); const makePostitObj = (modalType: string, title: string, content: string) => { - if (modalType === 'update') { + if (modalType === 'update' && clickedPostit) { const updatedPostit = clickedPostit; updatedPostit.id = Number(updatedPostit.id); updatedPostit.title = title; updatedPostit.color = color; updatedPostit.content = content; updatedPostit.updatedBy = user.id; + updatedPostit.whoIsUpdating = -1; return updatedPostit; } - if (modalType === 'create') { - return { - title, - color, - content, - createdBy: user.id, - updatedBy: user.id, - }; - } - return undefined; + // if (modalType === 'create') + return { + title, + color, + content, + createdBy: user.id, + updatedBy: user.id, + }; }; const handleSubmit = () => { @@ -48,31 +47,36 @@ const CreatePostItModal: React.FC = ({ socketApi, modalType, clickedPosti const postit = makePostitObj(modalType, title, content); // 포스트잇 객체, 요청 유저 정보, 팀 아이디 if (modalType === 'create') socketApi.createNewPostit(postit); - else if (modalType === 'update') socketApi.updatePostit(postit); + else if (modalType === 'update') socketApi.updateEndPostit(postit); handleModalClose(); } }; + const handleClose = () => { + if (modalType === 'update' && clickedPostit) socketApi.updateEndPostit({ id: clickedPostit.id }); + handleModalClose(); + }; + useEffect(() => { - if (modalType === 'update') { + if (modalType === 'update' && clickedPostit) { setColor(clickedPostit.color); } }, []); return ( - +