Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/assets/images/SendButton.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
235 changes: 147 additions & 88 deletions src/components/chat/ChatMessage.tsx
Original file line number Diff line number Diff line change
@@ -1,91 +1,57 @@
import { useChatInfo } from "@/store/chat.store";
import axios from "axios";
import React, { useEffect, useState, useRef } from "react";
import React, { useEffect, useState } from "react";
import { useLocation } from "react-router-dom";
import { styled } from "styled-components";
import { CompatClient, Stomp } from "@stomp/stompjs";
import SvgIcon from "../Icon/SvgIcon";

type Message = {
content: string;
sender: string;
sender: string | null; // null 허용
};

const ChatMessage = () => {
const location = useLocation();
const queryParams = new URLSearchParams(location.search);
const nickname = queryParams.get("nickname");
const chatRoomId = useChatInfo((state) => state.chatRoomId);
const activeHours = useChatInfo((state) => state.activeHours);
const isMatching = useChatInfo((state) => state.isMatching);
//웹소켓 연결 객체
const stompClient = useRef<CompatClient | null>(null);
// 메시지 리스트
const [messages, setMessages] = useState<Message[]>([]);
// 사용자 입력을 저장할 변수
const [inputValue, setInputValue] = useState("");

const [messages, setMessages] = useState<Message[]>([]); // 메시지 상태
const [inputValue, setInputValue] = useState(""); // 입력 필드 상태

const initialMessages = [
{ content: "안녕하세요.", sender: "나" },
{ content: "제가 지금 재활치료를 받고 있는데요", sender: "나" },
{ content: "병행해서 트레이닝 가능할까요?", sender: "나" },
{ content: "반갑습니다, 정회원님😄", sender: nickname },
{ content: "물론입니다!", sender: nickname },
{
content:
"저는 헬스트레이너 지도사 1급과 함께 운동 처방사 1급 자격증도 보유하고 있어서 안전하고 효율적인 트레이닝이 가능합니다.",
sender: nickname
}
];
// 입력 필드 변경 핸들러
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(event.target.value);
};

//메세지 전송
// 메시지 전송
const sendMessage = () => {
if (stompClient.current && inputValue) {
//현재로서는 임의의 테스트 값을 삽입
const body = {
id: 1,
name: "테스트1",
message: inputValue
if (inputValue) {
const newMessage: Message = {
content: inputValue,
sender: "나" // 닉네임이 없으면 '익명'으로 설정
};
stompClient.current.send(
`/pub/chat-rooms/${chatRoomId}`,
{},
JSON.stringify(body)
);
setInputValue("");
setMessages((prevMessages) => [...prevMessages, newMessage]); // 새 메시지 추가
setInputValue(""); // 입력 필드 초기화
}
};

useEffect(() => {
connect();
fetchMessages();
// 컴포넌트 언마운트 시 웹소켓 연결 해제
return () => disconnect();
}, []);

const connect = () => {
//웹소켓 연결
const socket = new WebSocket(
process.env.NODE_ENV === "production"
? "wss://coach-coach.site/ws"
: "ws://localhost:8080/ws"
);
stompClient.current = Stomp.over(socket);
stompClient.current.connect({}, () => {
//메시지 수신(1은 roomId를 임시로 표현)
stompClient.current?.subscribe(
`/sub/chat-rooms/${chatRoomId}`,
(message) => {
//누군가 발송했던 메시지를 리스트에 추가
const newMessage: Message = JSON.parse(message.body);
setMessages((prevMessages) => [...prevMessages, newMessage]);
}
);
});
};

const fetchMessages = () => {
return axios
.get("/v1/chat-rooms/{chatRoomId}/messages")
.then((response) => {
setMessages(response.data);
});
};

const disconnect = () => {
if (stompClient.current) {
stompClient.current.disconnect();
}
};
setMessages(initialMessages); // 기존 메시지 가져오기
}, [chatRoomId]);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [eslint] <react-hooks/exhaustive-deps> reported by reviewdog 🐶
React Hook useEffect has a missing dependency: 'initialMessages'. Either include it or remove the dependency array.


const title = (() => {
if (activeHours.length > 0 && isMatching) return "코치님과 매칭되었습니다";
Expand All @@ -103,42 +69,71 @@ const ChatMessage = () => {
<ChatTitleStyle>
<div className="title">{title}</div>
{activeHours.length > 0 && (
<>
<AbleTimeStyle>
{`${nickname}님의 채팅 가능 시간은 `}
<HighlightedText>{activeHours}</HighlightedText>
{` 에요`}
</AbleTimeStyle>
</>
<AbleTimeStyle>
{`${nickname}님의 채팅 가능 시간은 `}
<HighlightedText>{activeHours}</HighlightedText>
{` 에요`}
</AbleTimeStyle>
)}
</ChatTitleStyle>

<ul>
<div>
{/* 입력 필드 */}
<input type="text" value={inputValue} onChange={handleInputChange} />
{/* 메시지 전송, 메시지 리스트에 추가 */}
<button onClick={sendMessage}>입력</button>
</div>
{/* 메시지 리스트 출력 */}
{messages.map((item, index) => (
<div key={index} className="list-item">
{item.content}
</div>
))}
</ul>
<ChatContainer>
{Array.isArray(messages) &&
messages.map((item, index) =>
item.sender === "나" ? (
<MyChatStyle key={index}>{item.content}</MyChatStyle>
) : (
<PartnerChatStyle key={index}>{item.content}</PartnerChatStyle>
)
)}
</ChatContainer>
<ChatInputAndButtonStyle>
<ChatInputStyle
type="text"
value={inputValue}
onChange={handleInputChange}
placeholder="메시지를 입력하세요"
/>
<SvgIcon
name="send"
width="60px"
height="60px"
fill="#0075FF"
onClick={sendMessage}
/>
</ChatInputAndButtonStyle>
</ChatMessageStyle>
);
};

const ChatMessageStyle = styled.div``;
const ChatMessageStyle = styled.div`
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 10px;
padding: 10px;
width: 100%;
max-width: 600px;
margin: 0 auto;
position: relative; /* 하단 입력 필드 위치 조정을 위해 relative 추가 */
height: 100vh; /* 전체 화면 높이 사용 */
`;

const ChatContainer = styled.div`
overflow-y: auto;
width: 100%;
display: flex;
flex-direction: column;
gap: 10px;
flex-grow: 1; /* 화면의 남은 공간을 채움 */
`;

const ChatTitleStyle = styled.div`
position: relative;
display: flex;
flex-direction: column;
align-items: center;
padding: 20px 0;
width: 100%;

.title {
font-size: 20px;
Expand All @@ -162,13 +157,77 @@ const ChatTitleStyle = styled.div`
`;

const AbleTimeStyle = styled.div`
word-wrap: break-word; /* 긴 단어 자동 줄바꿈 */
white-space: pre-wrap; /* 공백 유지 및 줄바꿈 가능 */
word-wrap: break-word;
white-space: pre-wrap;
font-size: 12px;
`;

const HighlightedText = styled.span`
color: #0075ff;
`;

const PartnerChatStyle = styled.div`
align-self: flex-start; /* 부모 컨테이너 기준 왼쪽 정렬 */
background-color: #0075ff;
color: #ffffff;
border-radius: 20px;
max-width: 350px;
min-height: 40px;
padding: 10px 15px;
margin: 5px 0;
word-wrap: break-word;
white-space: pre-wrap;
display: inline-block; /* 글자 크기에 따라 넓이 조정 */
`;

const MyChatStyle = styled.div`
align-self: flex-end; /* 부모 컨테이너 기준 오른쪽 정렬 */
background-color: #3a3a3a;
color: #ffffff;
border-radius: 20px;
max-width: 350px;
min-height: 40px;
padding: 10px 15px;
margin: 5px 0;
word-wrap: break-word;
white-space: pre-wrap;
display: inline-block;
margin-left: auto; /* 오른쪽 끝으로 이동 */
`;

const ChatInputAndButtonStyle = styled.div`
display: flex;
align-items: center; /* 입력 필드와 버튼을 세로 중앙 정렬 */
position: absolute;
bottom: 150px; /* 화면 하단에서 10px 위로 */
left: 50%; /* 화면 중앙 정렬을 위해 왼쪽 50% 이동 */
transform: translateX(-50%); /* 중앙 정렬 보정 */
width: 100%; /* 부모 컨테이너 너비에 맞춤 */
max-width: 600px; /* 최대 너비 제한 */
padding: 10px;
background-color: #1e1e1e; /* 배경색 추가 (선택 사항) */
border-radius: 10px; /* 테두리 둥글게 */

svg {
margin-left: 10px;
}
`;

const ChatInputStyle = styled.input`
flex-grow: 1; /* 남은 공간을 채움 */
min-height: 60px;
border-radius: 10px;
background-color: #252932;
padding-left: 20px;
font-size: 18px;
color: #ffffff;
border: none; /* 테두리 제거 */
outline: none; /* 포커스 시 테두리 제거 */

&::placeholder {
color: #777c89;
font-size: 18px;
}
`;

export default ChatMessage;
6 changes: 3 additions & 3 deletions src/components/mypage/CoachProfileSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,6 @@ const CoachProfileSection = () => {
if (userMeData?.isCoach !== undefined) {
if (userMeData?.isCoach) {
setShouldFetchCoachProfile(true);
} else {
openModal();
}
}
}, [userMeData, openModal]);
Expand Down Expand Up @@ -123,7 +121,9 @@ const CoachProfileSection = () => {
<CustomButton
size="mini"
variant="contained"
onClick={closeModal}
onClick={() => {
closeModal();
}}
>
확인
</CustomButton>
Expand Down
2 changes: 1 addition & 1 deletion src/constants/apiPath.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,5 @@ export const API_PATH = {
recordV2: "/v2/records",
review: "/v1/coaches/reviews",
memberChat: "/v1/users/chat-rooms",
coachChat: "v1/coaches/chat-rooms"
coachChat: "/v1/coaches/chat-rooms"
};
4 changes: 3 additions & 1 deletion src/constants/assets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import Star from "@/assets/images/star.svg?react";
import Warning from "@/assets/images/warning.svg?react";
import XCircle from "@/assets/images/x-circle.svg?react";
import X from "@/assets/images/x.svg?react";
import SendButton from "@/assets/images/SendButton.svg?react";
import { CgDetailsMore } from "react-icons/cg";
import { FaCheck, FaRegStar } from "react-icons/fa";
import { IoIosArrowBack, IoMdMore } from "react-icons/io";
Expand Down Expand Up @@ -70,5 +71,6 @@ export const ICONS = {
arrow: ArrowDown,
noLocation: NoLocation,
warning: Warning,
notification: Notification
notification: Notification,
send: SendButton
};
13 changes: 0 additions & 13 deletions src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,8 @@
import React from "react";
import App from "./App.tsx";
import { HelmetProvider } from "react-helmet-async";
import * as Sentry from "@sentry/react";
import ReactDOM from "react-dom/client";

Sentry.init({
dsn: import.meta.env.VITE_SENTRY_DSN,
integrations: [
Sentry.browserTracingIntegration(),
Sentry.replayIntegration()
],
tracesSampleRate: 1.0,
tracePropagationTargets: [import.meta.env.VITE_SENTRY_URL],
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0
});

async function enableMocking() {
if (import.meta.env.MODE !== "development") {
return;
Expand Down