Skip to content

Commit 63d2f8c

Browse files
fix 유저 정보 불러오기 add 감정 분석 api
fix 유저 정보 불러오기 add 감정 분석 api
1 parent e327eb1 commit 63d2f8c

10 files changed

Lines changed: 612 additions & 40 deletions

File tree

dist/index.js

Lines changed: 9 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/controllers/chat.controller.js

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { createRoom,
2020
updateTopics,
2121
changeParticipantRole as serviceChangeRole,
2222
joinBattleRoom,
23-
createChatMessage
23+
getMessageSentiment
2424
} from "../services/chat.service.js"
2525
import { toJoinRoomDto } from "../dtos/chat.dto.js"
2626

@@ -1937,6 +1937,72 @@ export const handlePostChatMessage = async (req, res) => {
19371937
}
19381938
};
19391939

1940+
// 배틀방 단일 채팅 감정 분석 api
1941+
export const handleGetMessageSentiment = async (req, res) => {
1942+
/**
1943+
#swagger.summary = '특정 채팅 메시지 감정 분석 조회 API'
1944+
#swagger.security = [{ "BearerAuth": [] }]
1945+
#swagger.tags = ['Chat']
1946+
#swagger.parameters['roomId'] = { in:'path', description:'배틀방 ID', required:true, type:'integer', example:1 }
1947+
#swagger.parameters['messageId'] = { in:'path', description:'메시지 ID', required:true, type:'integer', example:42 }
1948+
#swagger.responses[200] = {
1949+
description: "감정 분석 조회 성공",
1950+
schema: {
1951+
isSuccess: true, code:"200", message:"success!",
1952+
result: {
1953+
messageId: "42",
1954+
roomId: "1",
1955+
userId: "9",
1956+
side: "A",
1957+
createdAt: "2025-06-15T10:00:00.000Z",
1958+
sentiment: {
1959+
emotion: "긍정",
1960+
probabilities: { 긍정:0.85, 부정:0.05, 중립:0.10 },
1961+
warning: false
1962+
}
1963+
}
1964+
}
1965+
}
1966+
#swagger.responses[400] = { description:"잘못된 요청", schema:{ isSuccess:false, code:"COMMON001", message:"잘못된 요청입니다.", result:null } }
1967+
#swagger.responses[401] = { description:"토큰 형식 오류", schema:{ isSuccess:false, code:"MEMBER4006", message:"토큰 오류", result:null } }
1968+
#swagger.responses[403] = { description:"권한 없음", schema:{ isSuccess:false, code:"COMMON004", message:"금지된 요청입니다.", result:null } }
1969+
#swagger.responses[404] = { description:"메시지 없음", schema:{ isSuccess:false, code:"CHAT4041", message:"메시지를 찾을 수 없습니다.", result:null } }
1970+
*/
1971+
try {
1972+
// 1) 토큰 검증
1973+
const raw = req.get("Authorization");
1974+
const token = raw && checkFormat(raw);
1975+
if (!token) return res.send(response(status.TOKEN_FORMAT_INCORRECT, null));
1976+
1977+
// 2) 파라미터 파싱
1978+
const roomId = req.params.roomId;
1979+
const messageId = req.params.messageId;
1980+
if (isNaN(Number(roomId)) || isNaN(Number(messageId))) {
1981+
return res.send(response(status.BAD_REQUEST, null));
1982+
}
1983+
1984+
// 3) 서비스 호출
1985+
const result = await getMessageSentiment({
1986+
roomId,
1987+
userId: req.userId,
1988+
messageId
1989+
});
1990+
1991+
// 4) 성공 응답
1992+
return res.send(response(status.SUCCESS, result));
1993+
1994+
} catch (err) {
1995+
console.error("🔴 handleGetMessageSentiment 오류:", err);
1996+
if (err.code === "MESSAGE_NOT_FOUND") {
1997+
return res.send(response(status.NOT_FOUND, null));
1998+
}
1999+
if (err.code === "FORBIDDEN") {
2000+
return res.send(response(status.FORBIDDEN, null));
2001+
}
2002+
return res.send(response(status.INTERNAL_SERVER_ERROR, null));
2003+
}
2004+
};
2005+
19402006
// 배틀방 투표하기
19412007
export const handlePostVote = async (req, res) => {
19422008
/*

src/controllers/user.controller.js

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -235,33 +235,37 @@ export const handleLogin = async (req, res, next) => {
235235

236236
// 유저 정보 불러오기
237237
export const handleUserInfo = async (req, res) => {
238-
/*
238+
/**
239239
#swagger.summary = '유저 정보 불러오기 API'
240240
#swagger.tags = ['User']
241-
#swagger.security = [{
242-
"BearerAuth": []
243-
}]
241+
#swagger.security = [{ "BearerAuth": [] }]
242+
244243
#swagger.responses[200] = {
245-
description: "유저 정보 성공 응답",
244+
description: "유저 정보 조회 성공",
246245
content: {
247246
"application/json": {
248247
schema: {
249248
type: "object",
250249
properties: {
251-
isSuccess: { type: "boolean", example: true },
252-
code: { type: "number", example: 200 },
253-
message: { type: "string", example: "유저 정보 조회 성공" },
250+
isSuccess: { type: "boolean", example: true },
251+
code: { type: "number", example: 200 },
252+
message: { type: "string", example: "유저 정보 조회 성공" },
254253
result: {
255254
type: "object",
256255
properties: {
257-
id: { type: "string", example: "1" },
258-
email: { type: "string", example: "user@example.com" },
259-
nickname: { type: "string", example: "nickname" },
260-
name: { type: "string", example: "John Doe" },
261-
status: { type: "string", example: "active" },
256+
id: { type: "string", example: "1" },
257+
email: { type: "string", example: "user@example.com" },
258+
nickname: { type: "string", example: "nickname" },
259+
name: { type: "string", example: "John Doe" },
262260
profileImageUrl: { type: "string", nullable: true, example: null },
263-
createdAt: { type: "string", format: "date", example: "2021-05-12" },
264-
updatedAt: { type: "string", format: "date", example: "2021-06-15" }
261+
gender: { type: "string", example: "M" },
262+
birth: { type: "string", format: "date-time", example: "1992-07-15T00:00:00.000Z" },
263+
phoneNumber: { type: "string", example: "010-1234-5678" },
264+
point: { type: "number", example: 3300 },
265+
tier: { type: "string", example: "Gold" },
266+
rank: { type: "number", example: 123 },
267+
createdAt: { type: "string", format: "date-time", example: "2025-05-25T19:42:11.304Z" },
268+
updatedAt: { type: "string", format: "date-time", example: "2025-06-14T20:03:16.935Z" }
265269
}
266270
}
267271
}
@@ -316,8 +320,8 @@ export const handleUserInfo = async (req, res) => {
316320
res.send(response(status.TOKEN_FORMAT_INCORRECT, null));
317321
}
318322
} catch (err) {
319-
console.log(err);
320-
res.send(response(BaseError));
323+
console.error("🔴 handleUserInfo 오류:", err);
324+
return res.send(response(status.INTERNAL_SERVER_ERROR, null));
321325
}
322326
};
323327

src/index.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import {
4444
handleLeaveRoom,
4545
handleChangeToARole,
4646
handleChangeToBRole,
47+
handleGetMessageSentiment
4748
} from "./controllers/chat.controller.js";
4849
import { registerChatHandlers
4950
} from "./socket/chat.socket.js";
@@ -130,6 +131,10 @@ app.post("/battle/rooms/:roomId/start", handleStartBattle);
130131
app.post("/battle/rooms/:roomId/leave", handleLeaveRoom);
131132
app.get("/battle/rooms/:roomId/chat/messages", handleGetChatHistory);
132133
app.post("/battle/rooms/:roomId/chat/messages", handlePostChatMessage);
134+
app.get(
135+
"/battle/rooms/:roomId/chat/messages/:messageId/sentiment",
136+
handleGetMessageSentiment
137+
);
133138
app.post("/battle/rooms/:roomId/end", handleEndBattle);
134139

135140
app.post("/battle/rooms/:roomId/votes", handlePostVote);

src/repositories/ai.repository.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export const callFilterProfanity = async (text) => {
1212

1313
export const callAnalyzeSentiment = async (text) => {
1414
// FastAPI 쪽 /analyze 엔드포인트 호출
15-
const response = await axios.post(`${AI_SERVER_URL}/analyze`, { text });
15+
const response = await axios.post(`${AI_SERVER_URL}/analyze`, { text }, { timeout: 2000 });
1616
return response.data;
1717
// 반환 예시: { emotion: "긍정", probabilities: { 긍정: 0.8, 부정: 0.2, ... } }
1818
};

src/repositories/chat.repository.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,4 +340,19 @@ export const listRoomParticipantsWithUser = (roomId) => {
340340
},
341341
orderBy: { joinedAt: "asc" }
342342
});
343+
};
344+
345+
// (4) 단일 채팅 메시지 조회용
346+
export const findChatMessageById = (messageId) => {
347+
return prisma.chatMessage.findUnique({
348+
where: { id: BigInt(messageId) },
349+
select: {
350+
id: true,
351+
roomId: true,
352+
userId: true,
353+
side: true,
354+
message: true,
355+
createdAt: true
356+
}
357+
});
343358
};

src/repositories/user.repository.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,17 @@ export const findEmail = async (req) => {
126126

127127
// 유저 정보 불러오는 기능
128128
export const userInfoRep = async (user_id) => {
129-
const user = await prisma.user.findFirstOrThrow({ where: { id: user_id } });
129+
const user = await prisma.user.findUnique({
130+
where: { id: BigInt(user_id) },
131+
include: {
132+
ranking: {
133+
select: {
134+
tier: true,
135+
rank: true
136+
}
137+
}
138+
}
139+
});
130140
return user;
131141
};
132142

src/services/chat.service.js

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,18 @@ import { createBattleRoom,
2020
updateBattleRoomTopics as repoUpdateBattleRoomTopics,
2121
findActiveParticipant,
2222
updateParticipantEndAt,
23-
getRoomsPaginated,
2423
deleteExistingParticipationRecords,
2524
listRoomParticipantsWithUser,
25+
findChatMessageById
2626
} from '../repositories/chat.repository.js';
2727
import { toCreateRoomDto,
2828
responseFromRoom
2929
} from '../dtos/chat.dto.js';
3030

3131
import { callFilterProfanity,
3232
callAnalyzeDebate,
33-
callGenerateTopic
33+
callGenerateTopic,
34+
callAnalyzeSentiment
3435
} from "../repositories/ai.repository.js";
3536

3637
// 방 생성 service
@@ -728,6 +729,39 @@ export const createChatMessage = async ({ roomId, userId, side, message }) => {
728729
return record;
729730
};
730731

732+
// 단일 채팅 감정 분석
733+
export const getMessageSentiment = async ({ roomId, userId, messageId }) => {
734+
// 1) 메시지가 있는지
735+
const msg = await findChatMessageById(messageId);
736+
if (!msg || msg.roomId.toString() !== roomId.toString()) {
737+
const e = new Error("MESSAGE_NOT_FOUND"); e.code = "MESSAGE_NOT_FOUND";
738+
throw e;
739+
}
740+
// 2) 조회 권한 확인 (방 참가자/관전자)
741+
const cnt = await prisma.roomParticipant.count({
742+
where: {
743+
roomId: BigInt(roomId),
744+
userId: BigInt(userId),
745+
endAt: null
746+
}
747+
});
748+
if (cnt === 0) {
749+
const e = new Error("FORBIDDEN"); e.code = "FORBIDDEN";
750+
throw e;
751+
}
752+
// 3) AI 감정 분석 호출
753+
const sentiment = await callAnalyzeSentiment(msg.message);
754+
// 4) 결과 조합
755+
return {
756+
messageId: msg.id.toString(),
757+
roomId: msg.roomId.toString(),
758+
userId: msg.userId.toString(),
759+
side: msg.side,
760+
createdAt: msg.createdAt,
761+
sentiment // { emotion, probabilities, warning }
762+
};
763+
};
764+
731765
// 채팅 정보 조회
732766
export const getChatHistory = async (roomId) => {
733767
const history = await findChatHistoryByRoomId(roomId);

src/services/user.service.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,20 @@ export const loginService = async (req) => {
200200
// 유저 정보 불러오는 service
201201
export const userInfoService = async (user_id) => {
202202
const userInfo = await userInfoRep(user_id);
203+
204+
// 비밀번호 숨기기
203205
userInfo.password = "hidden";
206+
207+
// ranking 데이터가 있으면 풀어서 top-level로 올리고, 원본 프로퍼티는 삭제
208+
if (userInfo.ranking) {
209+
userInfo.tier = userInfo.ranking.tier;
210+
userInfo.rank = userInfo.ranking.rank;
211+
} else {
212+
userInfo.tier = null;
213+
userInfo.rank = null;
214+
}
215+
delete userInfo.ranking;
216+
204217
return userInfo;
205218
};
206219

0 commit comments

Comments
 (0)