• 카테고리

    질문 & 답변
  • 세부 분야

    프론트엔드

  • 해결 여부

    미해결

Chats안에 Route된 페이지가 화면에 출력 되지 않는 현상

22.11.05 22:13 작성 조회수 224

0

import {
    AddButton, Channels, Chats, Header, LogOutButton, MenuScroll, ProfileImg,
    ProfileModal, RightMenu, WorkspaceButton, WorkspaceModal, WorkspaceName, Workspaces, WorkspaceWrapper
} from '@layouts/Workspace/styles'
import fetcher from '@utils/fetcher';
import React, { useCallback, useState } from 'react';
import useSWR from 'swr';
import axios from 'axios'
import gravatar from 'gravatar'
import loadable from '@loadable/component';
import { Navigate, Route, Routes } from 'react-router';
import { useParams } from 'react-router';
import { toast } from 'react-toastify';

import Menu from '@components/Menu';
import useInput from '@hooks/useInput';
import Modal from '@components/Modal';
import CreateChannelModal from '@components/CreateChannelModal';
import { IChannel, IUser } from '@typings/db';
import { Link } from 'react-router-dom';
import { Button, Input, Label } from '@pages/SignUp/styles';
import InviteWorkspaceModal from '@components/InviteWorkspaceModal';
import InviteChannelModal from '@components/InviteChannelModal';
import ChannelList from '@components/ChannelList';
import DMList from '@components/DMList';

const Channel = loadable(() => import('@pages/Ch'));
const DirectMessage = loadable(() => import('@pages/DirectMessage'));

const Workspace = () => {
    // const Workspace: React.FC<Props> = ({ children }) => {
    // const { data: userData, mutate: revalidateUser } = useSWR<IUser | false>('http://localhost:3095/api/users', fetcher);
    const [showUserMenu, setShowUserMenu] = useState(false);
    const [showCreateWorkspaceModal, setShowCreateWorkspaceModal] = useState(false);
    const [showInviteWorkspaceModal, setShowInviteWorkspaceModal] = useState(false);
    const [showInviteChannelModal, setShowInviteChannelModal] = useState(false);
    const [showWorkspaceModal, setShowWorkspaceModal] = useState(false);
    const [showCreateChannelModal, setShowCreateChannelModal] = useState(false);
    const [newWorkspace, onChangeNewWorkspace, setNewWorkpsace] = useInput('');
    const [newUrl, onChangeNewUrl, setNewUrl] = useInput('');

    const { workspace } = useParams<{ workspace: string }>();
    const { data: userData, error, mutate: revalidateUser } = useSWR<IUser | false>('/api/users', fetcher, {
        dedupingInterval: 2000, 
    });

    const { data: channelData } = useSWR<IChannel[]>(
        userData ? `/api/workspaces/${workspace}/channels` : null, fetcher);

    const onLogout = useCallback(() => {
        console.log(userData)
        axios.post(
            '/api/users/logout', null, { withCredentials: true, })
            .then(() => {
              
                revalidateUser(false, false);
            })
            .catch((err) => {
                console.log(err)
            })
    }, [])

   
    const onClickUserProfile = useCallback(() => {
        setShowUserMenu((prev) => !prev);
    }, [])
  
    const onCloseUserProfile = useCallback((e: React.MouseEvent<HTMLInputElement>) => {
        // console.trace('click')
        e.stopPropagation();
        setShowUserMenu((prev) => !prev);
    }, [])

    const onClickCreateWorkspace = useCallback(() => {
        setShowCreateWorkspaceModal(true);
    }, [])

    // 메뉴창에 채널생성
    const onCreateWorkspace = useCallback((e: React.FormEvent<HTMLFormElement>) => {
        e.preventDefault();
       
        if (!newWorkspace || !newWorkspace.trim()) return;
        if (!newUrl || !newUrl.trim()) return;
        axios.post('/api/workspaces', {
            workspace: newWorkspace,
            url: newUrl,
        }, {
            // 내가 로그인 된 상태라는걸 쿠키를 전달해서 안다
            withCredentials: true,
        }).then(() => {
            revalidateUser();
            // 초기화
            setShowCreateWorkspaceModal(false);
            setNewWorkpsace('');
            setNewUrl('');
        }).catch((error) => {
            console.dir(error);
            // toastify npm으로 에러 메세지
            toast.error(error.response?.data, { position: 'bottom-center' })
        })
    }, [newWorkspace, newUrl])

    // 모달닫기
    const onCloseModal = useCallback(() => {
        setShowCreateWorkspaceModal(false);
        setShowCreateChannelModal(false);
        setShowInviteWorkspaceModal(false);
        setShowInviteChannelModal(false);
    }, [])

    const toggleWorkspaceModal = useCallback(() => {
        setShowWorkspaceModal((prev) => !prev);
    }, [])

    const onClickAddChannel = useCallback(() => {
        setShowCreateChannelModal(true);
    }, [])

    const onClickInviteWorkspace = useCallback(() => {
        setShowInviteWorkspaceModal(true);
    }, []);
    // return 아래에 hooks가 있으면 Invalid hook call 에러가 뜬다
  

    if (userData === undefined) return null;

    if (userData === false) {
        return <Routes><Route path="/*" element={<Navigate replace to="/login" />} /></Routes>
    }

    return (
        <div>
            <Header>
                <RightMenu>

                    <span onClick={onClickUserProfile}>
                        
                        <ProfileImg src={gravatar.url(userData.email, { s: '28px', d: 'retro' })} alt={userData.nickname} />
                       
                        {showUserMenu && (<Menu style={{ right: 0, top: 38 }} show={showUserMenu} onCloseModal={onCloseUserProfile}>
                            <ProfileModal>
                                <img src={gravatar.url(userData.email, { s: '28px', d: 'retro' })} alt={userData.nickname} />
                                <div>
                                    <span id="profile-name">{userData.nickname}</span>
                                    <span id="profile-active">Active</span>
                                </div>
                            </ProfileModal>
                            <LogOutButton onClick={onLogout}>로그아웃</LogOutButton>
                        </Menu>
                        )}
                    </span>
                </RightMenu>
            </Header>
            <WorkspaceWrapper>
                <Workspaces>
                    {userData?.Workspaces && userData?.Workspaces.map((ws: any) => {
                        return (
                            <Link key={ws.id} to={`/workspace/${ws.url}/channel/일반`}>
                                <WorkspaceButton>{ws.name.slice(0, 1).toUpperCase()}</WorkspaceButton>
                            </Link>
                        );
                    })}
                    <AddButton onClick={onClickCreateWorkspace}>+</AddButton>
                </Workspaces>
                <Channels>
                    <WorkspaceName onClick={toggleWorkspaceModal}>Sleact</WorkspaceName>
                    <MenuScroll>
                        {/* Menu에서 div옆에 style을 받았기 때문에 style사용가능 */}
                        <Menu show={showWorkspaceModal} onCloseModal={toggleWorkspaceModal} style={{ top: 95, left: 80 }}>
                            <WorkspaceModal>
                                <h2>Sleact</h2>
                                <button onClick={onClickInviteWorkspace}>워크스페이스에 사용자 초대</button>
                                <button onClick={onClickAddChannel}>채널 만들기</button>
                                <button onClick={onLogout}>로그아웃</button>
                            </WorkspaceModal>
                        </Menu>
                        <ChannelList />
                        <DMList />
                    </MenuScroll>
                </Channels>
                <Chats>
                    <Routes>
                        <Route path='/workspace/:workspace/channel/:channel/*' element={<><Channel /></>} />
                        <Route path='/workspace/:workspace/dm/:id/*' element={<><DirectMessage /></>} />
                    </Routes>
                </Chats>
            </WorkspaceWrapper>
            <Modal show={showCreateWorkspaceModal} onCloseModal={onCloseModal}>
                <form onSubmit={onCreateWorkspace}>
                    <Label id="workspace-label">
                        <span>워크스페이스 이름</span>
                        <Input id="workspace" value={newWorkspace} onChange={onChangeNewWorkspace} />
                    </Label>
                    <Label id="workspace-url-label">
                        <span>워크스페이스 url</span>
                        <Input id="workspace" value={newUrl} onChange={onChangeNewUrl} />
                    </Label>
                    <Button type="submit">생성하기</Button>
                </form>
            </Modal>
            <CreateChannelModal show={showCreateChannelModal} onCloseModal={onCloseModal}
                setShowCreateChannelModal={setShowCreateChannelModal} />
            <InviteWorkspaceModal show={showInviteWorkspaceModal} onCloseModal={onCloseModal} setShowInviteWorkspaceModal={setShowInviteWorkspaceModal} />
            <InviteChannelModal show={showInviteChannelModal} onCloseModal={onCloseModal} setShowInviteChannelModal={setShowInviteChannelModal} />
        </div>
    )
}

export default Workspace;

Workspace

 

import React from 'react';
import gravatar from 'gravatar';
import { Container, Header } from './styles';
import { useParams } from 'react-router';
import useSWR from 'swr';
import fetcher from '@utils/fetcher';
import ChatBox from '@components/ChatBox';
import ChatList from '@components/ChatList';

const DirectMessage = () => {
    const { workspace, id } = useParams<{ workspace: string; id: string }>();
    const { data: userData } = useSWR(`/api/workspaces/${workspace}/users/${id}`, fetcher);
    const { data: myData } = useSWR('/api/users', fetcher);

    if (!userData || !myData) {
        return null
    }

    return (
        <Container>
            <Header>
                <img src={gravatar.url(userData.email, { s: '24px', d: 'retro' })} alt={userData.nickname} />
                <span>{userData.nickname}</span>
            </Header>
            <ChatList />
            <ChatBox chat="" />
        </Container>
    )
}

export default DirectMessage;

DirectionMessage

 

import React, { useCallback } from 'react'
import { Form } from 'react-router-dom';
import { ChatArea, MentionsTextarea, SendButton, Toolbox } from './styles';

interface Props {
    chat: string;
}


const ChatBox: React.FC<Props> = ({ chat }) => {
    const onSubmitForm = useCallback(() => {

    }, []);
    return (
        <ChatArea>
            <Form onSubmit={onSubmitForm}>
                <MentionsTextarea>
                    <textarea />
                </MentionsTextarea>
                <Toolbox>
                    <SendButton
                        className={
                            'c-button-unstyled c-icon_button c-icon_button--light c-icon_button--size_medium c-texty_input__button c-texty_input__button--send' +
                            (chat?.trim() ? '' : ' c-texty_input__button--disabled')
                        }
                        data-qa="texty_send_button"
                        aria-label="Send message"
                        data-sk="tooltip_parent"
                        type="submit"
                        disabled={!chat?.trim()}
                    >
                        <i className="c-icon c-icon--paperplane-filled" aria-hidden="true" />
                    </SendButton>
                </Toolbox>
            </Form>
        </ChatArea>
    )
}

export default ChatBox;

ChatBox

import styled from '@emotion/styled';

export const Container = styled.div`
  display: flex;
  flex-wrap: wrap;
  height: calc(100vh - 38px);
  flex-flow: column;
  position: relative;
`;

export const Header = styled.header`
  height: 64px;
  display: flex;
  width: 100%;
  --saf-0: rgba(var(--sk_foreground_low, 29, 28, 29), 0.13);
  box-shadow: 0 1px 0 var(--saf-0);
  padding: 20px 16px 20px 20px;
  font-weight: bold;
  align-items: center;
  & img {
    margin-right: 5px;
  }
`;

export const DragOver = styled.div`
  position: absolute;
  top: 64px;
  left: 0;
  width: 100%;
  height: calc(100% - 64px);
  background: white;
  opacity: 0.7;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 40px;
`;

DirectMessage styles

import styled from '@emotion/styled';
import { MentionsInput } from 'react-mentions';

export const ChatArea = styled.div`
  display: flex;
  width: 100%;
  padding: 20px;
  padding-top: 0;
`;

export const Form = styled.form`
  color: rgb(29, 28, 29);
  font-size: 15px;
  width: 100%;
  border-radius: 4px;
  border: 1px solid rgb(29, 28, 29);
`;

export const MentionsTextarea = styled(MentionsInput)`
  font-family: Slack-Lato, appleLogo, sans-serif;
  font-size: 15px;
  padding: 8px 9px;
  width: 100%;
  & strong {
    background: skyblue;
  }
  & textarea {
    height: 44px;
    padding: 9px 10px !important;
    outline: none !important;
    border-radius: 4px !important;
    resize: none !important;
    line-height: 22px;
    border: none;
  }
  & ul {
    border: 1px solid lightgray;
    max-height: 200px;
    overflow-y: auto;
    padding: 9px 10px;
    background: white;
    border-radius: 4px;
    width: 150px;
  }
`;

export const Toolbox = styled.div`
  position: relative;
  background: rgb(248, 248, 248);
  height: 41px;
  display: flex;
  border-top: 1px solid rgb(221, 221, 221);
  align-items: center;
  border-bottom-left-radius: 4px;
  border-bottom-right-radius: 4px;
`;

export const SendButton = styled.button`
  position: absolute;
  right: 5px;
  top: 5px;
`;

export const EachMention = styled.button<{ focus: boolean }>`
  padding: 4px 20px;
  background: transparent;
  border: none;
  display: flex;
  align-items: center;
  color: rgb(28, 29, 28);
  width: 100%;
  & img {
    margin-right: 5px;
  }
  ${({ focus }) =>
    focus &&
    `
    background: #1264a3;
    color: white;
  `};
`;

ChatBox styles

import styled from '@emotion/styled';

export const RightMenu = styled.div`
  float: right;
`;

export const Header = styled.header`
  height: 38px;
  background: #350d36;
  color: #ffffff;
  box-shadow: 0 1px 0 0 rgba(255, 255, 255, 0.1);
  padding: 5px;
  text-align: center;
`;

export const ProfileImg = styled.img`
  width: 28px;
  height: 28px;
  position: absolute;
  top: 5px;
  right: 16px;
`;

export const ProfileModal = styled.div`
  display: flex;
  padding: 20px;
  & img {
    display: flex;
  }
  & > div {
    display: flex;
    flex-direction: column;
    margin-left: 10px;
  }
  & #profile-name {
    font-weight: bold;
    display: inline-flex;
  }
  & #profile-active {
    font-size: 13px;
    display: inline-flex;
  }
`;

export const LogOutButton = styled.button`
  border: none;
  width: 100%;
  border-top: 1px solid rgb(29, 28, 29);
  background: transparent;
  display: block;
  height: 33px;
  padding: 5px 20px 5px;
  outline: none;
  cursor: pointer;
`;

export const WorkspaceWrapper = styled.div`
  display: flex;
  flex: 1;
`;

export const Workspaces = styled.div`
  width: 65px;
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  background: #3f0e40;
  border-top: 1px solid rgb(82, 38, 83);
  border-right: 1px solid rgb(82, 38, 83);
  vertical-align: top;
  text-align: center;
  padding: 15px 0 0;
`;

export const Channels = styled.nav`
  width: 260px;
  display: inline-flex;
  flex-direction: column;
  background: #3f0e40;
  color: rgb(188, 171, 188);
  vertical-align: top;
  & a {
    padding-left: 36px;
    color: inherit;
    text-decoration: none;
    height: 28px;
    line-height: 28px;
    display: flex;
    align-items: center;
    &.selected {
      color: white;
    }
  }
  & .bold {
    color: white;
    font-weight: bold;
  }
  & .count {
    margin-left: auto;
    background: #cd2553;
    border-radius: 16px;
    display: inline-block;
    font-size: 12px;
    font-weight: 700;
    height: 18px;
    line-height: 18px;
    padding: 0 9px;
    color: white;
    margin-right: 16px;
  }
  & h2 {
    height: 36px;
    line-height: 36px;
    margin: 0;
    text-overflow: ellipsis;
    overflow: hidden;
    white-space: nowrap;
    font-size: 15px;
  }
`;

export const WorkspaceName = styled.button`
  height: 64px;
  line-height: 64px;
  border: none;
  width: 100%;
  text-align: left;
  border-top: 1px solid rgb(82, 38, 83);
  border-bottom: 1px solid rgb(82, 38, 83);
  font-weight: 900;
  font-size: 24px;
  background: transparent;
  text-overflow: ellipsis;
  overflow: hidden;
  white-space: nowrap;
  padding: 0;
  padding-left: 16px;
  margin: 0;
  color: white;
  cursor: pointer;
`;

export const MenuScroll = styled.div`
  height: calc(100vh - 102px);
  overflow-y: auto;
`;

export const WorkspaceModal = styled.div`
  padding: 10px 0 0;
  & h2 {
    padding-left: 20px;
  }
  & > button {
    width: 100%;
    height: 28px;
    padding: 4px;
    border: none;
    background: transparent;
    border-top: 1px solid rgb(28, 29, 28);
    cursor: pointer;
    &:last-of-type {
      border-bottom: 1px solid rgb(28, 29, 28);
    }
  }
`;

export const Chats = styled.div`
  flex: 1;
`;

export const AddButton = styled.button`
  color: white;
  font-size: 24px;
  display: inline-block;
  width: 40px;
  height: 40px;
  background: transparent;
  border: none;
  cursor: pointer;
`;

export const WorkspaceButton = styled.button`
  display: inline-block;
  width: 40px;
  height: 40px;
  border-radius: 10px;
  background: white;
  border: 3px solid #3f0e40;
  margin-bottom: 15px;
  font-size: 18px;
  font-weight: 700;
  color: black;
  cursor: pointer;
`;

Workspace Styles (혹시나 해서 스타일까지 올렸습니다)

저기 흰색 부분 (Chats) 부분에 아무것도 출력이 되지 않습니다. 아예 DirectMessage 페이지 자체를 못 받아오는 것 같은데 route를 App으로 옮겨서 <div>TEST</div>만 출력 시킬 땐 작동이 됩니다..

그래서 <Channel />이랑 <DirectMessage />이렇게 Route바깥으로 빼서 출력해보면

const { data: userData } = useSWR(`/api/workspaces/${workspace}/users/${id}`, fetcher);

저 ${id} 부분이 undefined가 나옵니다..

 

메세지 클릭시 url 주소는 제대로 다 바뀌고 있습니다

답변 1

답변을 작성해보세요.

0

react-router 쪽 코드가 잘못돼서 useParams가 제대로 안 도는 겁니다. Route 컴포넌트 쪽을 보셔야 합니다.

TaeIl Lee님의 프로필

TaeIl Lee

질문자

2022.11.06

<Chats>
   <Routes>
     <Route path='/workspace/:workspace/channel/:channel' element={<Channel />} />
     <Route path='/workspace/:workspace/dm/:id' element={<DirectMessage />} />
   </Routes>
</Chats>

어제부터 해봤는데 해결이 안되네요..

윗글에 올린 workspace안에 저 Routes부분이 아예 안 불러 와지는 것 같습니다.

 



이 스크린샷처럼 아예 Chats 안의 컨텐츠가 로딩이 안됩니다.

 

import ChatBox from '@components/ChatBox';
import ChatList from '@components/ChatList';
import useInput from '@hooks/useInput';
import React, { useCallback } from 'react';
import { Container, Header } from './styles';

const Channel = () => {
    const [chat, onChangeChat, setChat] = useInput('')
    const onSubmitForm = useCallback((e: React.FormEvent<HTMLFormElement>) => {
        e.preventDefault
        setChat('');
    }, [])
    return (
        <Container>
            <Header>채널</Header>
            <ChatList />
            <ChatBox chat={chat} onChangeChat={onChangeChat} onSubmitForm={onSubmitForm} />
        </Container>
    )
}

export default Channel;

Channel

 

const LogIn = loadable(() => import('@pages/LogIn'))
const SignUp = loadable(() => import('@pages/SignUp'));
const Workspace = loadable(() => import('@layouts/Workspace'));

const App = () => {

    return (
        <Routes >
            <Route
                path="/*"
                element={<Navigate to="/login" replace />}
            />
            <Route path='/login/*' element={<LogIn />} />
            <Route path='/signup' element={<SignUp />} />
            <Route path='/workspace/:workspace/*' element={<><Workspace /></>} />
        </Routes>
    )
}

export default App;

App

 

추가로 Channel이랑 App도 올리겠습니다.

Route랑 useParams도 한번 쭉 봤는데 안고쳐지네요..

<Channel />만 Routes밖으로 뺐을 때 나오는 undefined도 저 부분이 출력이 돼야 콘솔을 찍던 해서 고쳐 볼 수 있을텐데

지금까지 에러 메세지도 없어서 왜 안되는지 모르겠습니다..

네 저 Routes 주소가 잘못됐습니다. 공지사항 따라서 버전6에 맞게 주소 수정하세요

TaeIl Lee님의 프로필

TaeIl Lee

질문자

2022.11.06

감사합니다 고쳤습니다 !