diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 545365b..520d62d 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -42,6 +42,12 @@ jobs: ssh ec2 "echo '$ENV_FILE' > /opt/app/.env" env: ENV_FILE: ${{ secrets.ENV_FILE }} + + - name: Restore Firebase service account key + run: | + ssh ec2 "mkdir -p /opt/app/config && echo $FIREBASE_SERVICE_ACCOUNT_BASE64 | base64 -d > /opt/app/config/service-account-key.json" + env: + FIREBASE_SERVICE_ACCOUNT_BASE64: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_BASE64 }} - name: Install dependencies run: ssh ec2 "cd /opt/app && npm install" diff --git a/src/chat/chat.routes.js b/src/chat/chat.routes.js index 749a165..908623f 100644 --- a/src/chat/chat.routes.js +++ b/src/chat/chat.routes.js @@ -1,25 +1,26 @@ import express from "express"; import { createChatroom } from "./controller/chatroom.controller.js"; -import { showChatroom } from "./controller/chatroom.controller.js"; +import { getChatroom } from "./controller/chatroom.controller.js"; import { deleteChatrooms } from "./controller/chatroom.controller.js"; -import { showMessages } from "./controller/chat.controller.js"; +import { getMessages } from "./controller/chat.controller.js"; import { getMessageByKeyword } from "./controller/chat.controller.js"; +import { authenticate } from "../middlewares/auth.middleware.js"; const router = express.Router(); // 채팅방 생성 API -router.post("", createChatroom); +router.post("", authenticate, createChatroom); // 채팅방 삭제 API -router.delete("/delete", deleteChatrooms); +router.delete("/delete", authenticate, deleteChatrooms); -// 채팅 메시지 검색 API -router.get("/search/messages", getMessageByKeyword); +// 채팅 메시지 검색 API (다중) +router.get("/search/messages", authenticate, getMessageByKeyword); // 채팅방 조회 API -router.get("/:consumerId", showChatroom); +router.get("/list", authenticate, getChatroom); // 채팅 메시지 조회 API -router.get("/:chatroomId/messages", showMessages); +router.get("/:chatroomId/messages", authenticate, getMessages); export default router; \ No newline at end of file diff --git a/src/chat/controller/chat.controller.js b/src/chat/controller/chat.controller.js index 156bb4c..f667ec6 100644 --- a/src/chat/controller/chat.controller.js +++ b/src/chat/controller/chat.controller.js @@ -1,15 +1,18 @@ import { StatusCodes } from "http-status-codes"; import { parseWithBigInt, stringifyWithBigInt } from "../../bigintJson.js"; -import { ShowMessagesDto } from "../dto/chat.dto.js"; +import { GetMessagesDto } from "../dto/chat.dto.js"; import { ChatService } from "../service/chat.service.js"; import { FindChatroomByMessageDto } from "../dto/chat.dto.js"; -export const showMessages = async (req, res, next) => { +export const getMessages = async (req, res, next) => { try { - const dto = new ShowMessagesDto ({ + const userId = BigInt(req.user.userId); + + const dto = new GetMessagesDto ({ chatroomId: BigInt(req.params.chatroomId), limit: req.query.limit, cursor: req.query.cursor, + userId: userId, }); const messages = await ChatService.getMessagesByChatroomId(dto); @@ -23,9 +26,14 @@ export const showMessages = async (req, res, next) => { export const getMessageByKeyword = async (req, res, next) => { try { - const dto = new FindChatroomByMessageDto(req.query); + const userId = BigInt(req.user.userId); + + const dto = new FindChatroomByMessageDto({ + keyword: req.query.keyword, + userId: userId, + }); - const messages = await ChatService.searchMessagesByKeyword(dto.keyword); + const messages = await ChatService.searchMessagesByKeyword(dto); const responseData = parseWithBigInt(stringifyWithBigInt(messages)); res.status(StatusCodes.OK).success(responseData); diff --git a/src/chat/controller/chatroom.controller.js b/src/chat/controller/chatroom.controller.js index a276a0a..240e895 100644 --- a/src/chat/controller/chatroom.controller.js +++ b/src/chat/controller/chatroom.controller.js @@ -1,14 +1,16 @@ import { StatusCodes } from "http-status-codes"; import { ChatroomService } from "../service/chatroom.service.js"; import { CreateChatroomDto } from "../dto/chatroom.dto.js"; -import { ShowChatroomDto } from "../dto/chatroom.dto.js"; +import { GetChatroomDto } from "../dto/chatroom.dto.js"; import { DeleteChatroomDto } from "../dto/chatroom.dto.js"; import { parseWithBigInt, stringifyWithBigInt } from "../../bigintJson.js"; export const createChatroom = async (req, res, next) => { try { + const consumerId = BigInt(req.user.userId); + const dto = new CreateChatroomDto({ - consumerId: BigInt(req.body.consumerId), + consumerId: consumerId, artistId: BigInt(req.body.artistId), requestId: BigInt(req.body.requestId), }); @@ -22,10 +24,10 @@ export const createChatroom = async (req, res, next) => { } }; -export const showChatroom = async (req, res, next) => { +export const getChatroom = async (req, res, next) => { try { - const dto = new ShowChatroomDto({ - consumerId: BigInt(req.params.consumerId) + const dto = new GetChatroomDto({ + consumerId: BigInt(req.user.userId) }); const chatrooms = await ChatroomService.getChatroomsByUserId(dto); @@ -39,9 +41,12 @@ export const showChatroom = async (req, res, next) => { export const deleteChatrooms = async (req, res, next) => { try { + const userId = BigInt(req.user.userId); + const dto = new DeleteChatroomDto({ chatroomIds: req.body.chatroomIds, userType: req.body.userType, + userId: userId, }); const chatrooms = await ChatroomService.softDeleteChatroomsByUser(dto); diff --git a/src/chat/dto/chat.dto.js b/src/chat/dto/chat.dto.js index 4799f3f..4fe7adc 100644 --- a/src/chat/dto/chat.dto.js +++ b/src/chat/dto/chat.dto.js @@ -1,13 +1,15 @@ -export class ShowMessagesDto { - constructor({ chatroomId, limit, cursor }) { +export class GetMessagesDto { + constructor({ chatroomId, limit, cursor, userId }) { this.chatroomId = chatroomId; this.limit = limit ? Number(limit) : 20; this.cursor = cursor ? BigInt(cursor) : null; + this.userId = BigInt(userId); } } export class FindChatroomByMessageDto { - constructor(query) { - this.keyword = query.keyword; + constructor({ keyword, userId }) { + this.keyword = keyword; + this.userId = BigInt(userId); } } \ No newline at end of file diff --git a/src/chat/dto/chatroom.dto.js b/src/chat/dto/chatroom.dto.js index bc75415..61f6c84 100644 --- a/src/chat/dto/chatroom.dto.js +++ b/src/chat/dto/chatroom.dto.js @@ -6,15 +6,16 @@ export class CreateChatroomDto { } } -export class ShowChatroomDto { +export class GetChatroomDto { constructor({ consumerId }) { - this.consumerId = consumerId; + this.consumerId = BigInt(consumerId); } } export class DeleteChatroomDto { - constructor({ chatroomIds, userType }) { - this.chatroomIds = chatroomIds.map(id => BigInt(id)); - this.userType = userType; - } + constructor({ chatroomIds, userType, userId }) { + this.chatroomIds = chatroomIds.map(id => BigInt(id)); + this.userType = userType; + this.userId = BigInt(userId); + } } \ No newline at end of file diff --git a/src/chat/repository/chatroom.repository.js b/src/chat/repository/chatroom.repository.js index 91ff292..d344949 100644 --- a/src/chat/repository/chatroom.repository.js +++ b/src/chat/repository/chatroom.repository.js @@ -26,16 +26,19 @@ export const ChatroomRepository = { return await prisma.chatroom.findMany({ where: { consumerId: consumerId, - }, + hiddenConsumer: false, + } }); }, - async softDeleteChatrooms(chatroomIds, userType) { + async softDeleteChatrooms(chatroomIds, userType, userId) { const hiddenField = userType === "consumer" ? "hiddenConsumer" : "hiddenArtist"; + const userField = userType === "consumer" ? "consumerId" : "artistId"; await prisma.chatroom.updateMany({ where: { - id: { in: chatroomIds } + id: { in: chatroomIds }, + [userField]: userId }, data: { [hiddenField]: true diff --git a/src/chat/service/chat.service.js b/src/chat/service/chat.service.js index 3d89767..bf98dc0 100644 --- a/src/chat/service/chat.service.js +++ b/src/chat/service/chat.service.js @@ -1,6 +1,7 @@ import { ChatroomRepository } from "../repository/chatroom.repository.js"; import { ChatRepository } from "../repository/chat.repository.js"; import { ChatroomNotFoundError } from "../../common/errors/chat.errors.js"; +import { ForbiddenError } from "../../common/errors/chat.errors.js"; export const ChatService = { async getMessagesByChatroomId(dto) { @@ -8,13 +9,20 @@ export const ChatService = { if (!chatroom) { throw new ChatroomNotFoundError({ chatroomId: dto.chatroomId }); } + + if (dto.userId !== chatroom.consumerId && dto.userId !== chatroom.artistId) { + throw new ForbiddenError({ consumerId: dto.userId }); + } const messages = await ChatRepository.findMessagesWithImages(dto); return messages; }, - async searchMessagesByKeyword(keyword) { - const messages = await ChatRepository.searchByKeyword(keyword); + async searchMessagesByKeyword(dto) { + const userChatrooms = await ChatroomRepository.findChatroomsByUser(dto.userId); + const chatroomIds = userChatrooms.map(cr => cr.id); + + const messages = await ChatRepository.searchByKeyword(dto.keyword, chatroomIds); return messages; }, }; \ No newline at end of file diff --git a/src/chat/service/chatroom.service.js b/src/chat/service/chatroom.service.js index 56bb9ad..5bf20d3 100644 --- a/src/chat/service/chatroom.service.js +++ b/src/chat/service/chatroom.service.js @@ -62,7 +62,7 @@ export const ChatroomService = { throw new ChatroomNotFoundError({ chatroomIds: dto.chatroomIds }); } - await ChatroomRepository.softDeleteChatrooms(dto.chatroomIds, dto.userType); + await ChatroomRepository.softDeleteChatrooms(dto.chatroomIds, dto.userType, dto.userId); const chatrooms = await ChatroomRepository.findChatroomsByIds(dto.chatroomIds); diff --git a/src/common/errors/chat.errors.js b/src/common/errors/chat.errors.js index fe016fd..b63f269 100644 --- a/src/common/errors/chat.errors.js +++ b/src/common/errors/chat.errors.js @@ -9,4 +9,15 @@ export class ChatroomNotFoundError extends BaseError { data, }); } +} + +export class ForbiddenError extends BaseError { + constructor(data = null) { + super({ + errorCode: "M002", + reason: "권한이 없습니다.", + statusCode: 403, + data, + }); + } } \ No newline at end of file diff --git a/src/common/swagger/chat.json b/src/common/swagger/chat.json index 4100f76..e483125 100644 --- a/src/common/swagger/chat.json +++ b/src/common/swagger/chat.json @@ -4,6 +4,11 @@ "post": { "summary": "채팅방 생성", "description": "consumerId, artistId, requestId로 채팅방을 생성합니다.", + "security": [ + { + "bearerAuth": [] + } + ], "tags": ["Chat"], "requestBody": { "required": true, @@ -45,28 +50,41 @@ } }, "404": { - "description": "사용자, 작가, 요청 중 하나를 찾을 수 없음" + "description": "사용자를 찾을 수 없음", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "resultType": { "type": "string", "example": "FAIL" }, + "error": { + "type": "object", + "properties": { + "errorCode": { "type": "string", "example": "U001" }, + "reason": { "type": "string", "example": "사용자 id를 찾을 수 없습니다." }, + "data": { "type": "object" } + } + }, + "success": { "type": "object", "nullable": true, "example": null } + } + } + } + } } } } }, - "/api/chatrooms/{consumerId}": { + "/api/chatrooms/list": { "get": { - "summary": "사용자 ID로 채팅방 목록 조회", - "description": "사용자 ID를 바탕으로 해당 사용자가 참여한 채팅방 목록을 조회합니다.", - "tags": ["Chat"], - "parameters": [ + "summary": "채팅방 목록 조회", + "description": "사용자가 참여한 채팅방 목록을 조회합니다.", + "security": [ { - "name": "consumerId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "example": "1" - }, - "description": "조회할 사용자의 ID" + "bearerAuth": [] } ], + "tags": ["Chat"], + "parameters": [], "responses": { "200": { "description": "채팅방 목록 조회 성공", @@ -132,6 +150,11 @@ "delete": { "summary": "채팅방 소프트 삭제 (hidden 처리) 및 조건부 하드 삭제", "description": "사용자가 선택한 채팅방들을 hidden 처리 후, hiddenUser와 hiddenArtist가 모두 true인 경우 DB에서 삭제합니다.", + "security": [ + { + "bearerAuth": [] + } + ], "tags": ["Chat"], "requestBody": { "required": true, @@ -163,10 +186,54 @@ "description": "삭제 처리 완료 (내용 없음)" }, "400": { - "description": "잘못된 요청 파라미터" + "description": "잘못된 요청 파라미터", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "resultType": { "type": "string", "example": "FAIL" }, + "error": { + "type": "object", + "properties": { + "errorCode": { "type": "string", "example": "C001" }, + "reason": { "type": "string", "example": "userType은 'consumer' 또는 'artist'여야 합니다." }, + "data": { "type": "object", "example": {} } + } + }, + "success": { + "type": ["object", "null"], + "example": null + } + } + } + } + } }, "404": { - "description": "채팅방을 찾을 수 없음" + "description": "채팅방을 찾을 수 없음", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "resultType": { "type": "string", "example": "FAIL" }, + "error": { + "type": "object", + "properties": { + "errorCode": { "type": "string", "example": "C002" }, + "reason": { "type": "string", "example": "요청한 채팅방을 찾을 수 없습니다." }, + "data": { "type": "object", "example": { "chatroomIds": [999] } } + } + }, + "success": { + "type": ["object", "null"], + "example": null + } + } + } + } + } } } } @@ -174,7 +241,12 @@ "/api/chatrooms/{chatroomId}/messages": { "get": { "summary": "채팅방 메시지 조회", - "description": "채팅방 ID를 기반으로 해당 채팅방의 메시지를 조회합니다. 무한 스크롤을 위해 cursor(이전 페이지 마지막 메시지 id)와 limit을 query로 받습니다.", + "description": "해당 채팅방의 메시지를 조회합니다. 무한 스크롤을 위해 cursor(이전 페이지 마지막 메시지 id)와 limit을 query로 받습니다.", + "security": [ + { + "bearerAuth": [] + } + ], "tags": ["Chat"], "parameters": [ { @@ -277,10 +349,57 @@ } }, "404": { - "description": "채팅방을 찾을 수 없음" + "description": "채팅방을 찾을 수 없음", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "resultType": { "type": "string", "example": "FAIL" }, + "error": { + "type": "object", + "properties": { + "errorCode": { "type": "string", "example": "C002" }, + "reason": { "type": "string", "example": "요청한 채팅방을 찾을 수 없습니다." }, + "data": { + "type": "object", + "example": { "chatroomIds": [999] } + } + } + }, + "success": { + "type": ["object", "null"], + "example": null + } + } + } + } + } }, "500": { - "description": "서버 오류" + "description": "서버 오류", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "resultType": { "type": "string", "example": "FAIL" }, + "error": { + "type": "object", + "properties": { + "errorCode": { "type": "string", "example": "S001" }, + "reason": { "type": "string", "example": "알 수 없는 서버 오류가 발생했습니다." }, + "data": { "type": "object", "example": {} } + } + }, + "success": { + "type": ["object", "null"], + "example": null + } + } + } + } + } } } } @@ -289,6 +408,11 @@ "get": { "summary": "메시지 키워드 검색", "description": "키워드가 포함된 메시지를 검색하여 반환합니다.", + "security": [ + { + "bearerAuth": [] + } + ], "tags": ["Chat"], "parameters": [ { @@ -361,10 +485,54 @@ } }, "400": { - "description": "잘못된 요청" + "description": "잘못된 요청", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "resultType": { "type": "string", "example": "FAIL" }, + "error": { + "type": "object", + "properties": { + "errorCode": { "type": "string", "example": "R001" }, + "reason": { "type": "string", "example": "keyword 쿼리 파라미터는 필수입니다." }, + "data": { "type": "object", "example": {} } + } + }, + "success": { + "type": ["object", "null"], + "example": null + } + } + } + } + } }, "500": { - "description": "서버 오류" + "description": "서버 오류", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "resultType": { "type": "string", "example": "FAIL" }, + "error": { + "type": "object", + "properties": { + "errorCode": { "type": "string", "example": "S001" }, + "reason": { "type": "string", "example": "알 수 없는 서버 오류가 발생했습니다." }, + "data": { "type": "object", "example": {} } + } + }, + "success": { + "type": ["object", "null"], + "example": null + } + } + } + } + } } } }