-
Notifications
You must be signed in to change notification settings - Fork 5
[FE] feat: 채널 생성 구현 #73
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 14 commits
50c5701
de0140c
92ae760
0aa54a5
3164671
e6afd25
bfdeb53
482a126
0465167
96a33a6
888cb33
7679dfc
4b00e81
0dd4407
865b70d
e3aafb7
15d27a1
8d56b89
b48986b
1d2824a
6208ae0
c61fd77
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| import * as S from './styles'; | ||
|
|
||
| /** | ||
| * | ||
| * @example | ||
| * ```tsx | ||
| * <Toggle isOn={공개여부 상태값} | ||
| * onToggle={() => 해당 상태값 setter함수} | ||
| * /> | ||
| * ``` | ||
| * | ||
| * @param isOn - 토글 스위치 ON/OFF 상태입니다(공개/비공개) | ||
| * @param onToggle - 실행 시 실행될 콜백 함수 | ||
| */ | ||
| interface ToggleProps { | ||
| isOn: boolean; | ||
| onToggle: () => void; | ||
| } | ||
| const Toggle = ({ isOn, onToggle }: ToggleProps) => { | ||
| return ( | ||
| <S.Toggle $isOn={isOn} onClick={onToggle}> | ||
| <S.ToggleCircle | ||
| animate={{ | ||
| x: isOn ? 25 : 0, | ||
| }} | ||
| transition={{ | ||
| type: 'spring', | ||
| stiffness: 500, | ||
| damping: 30, | ||
| }} | ||
| > | ||
| <S.IconWrapper>{isOn ? '✓' : '✕'}</S.IconWrapper> | ||
| </S.ToggleCircle> | ||
| </S.Toggle> | ||
| ); | ||
| }; | ||
|
|
||
| export default Toggle; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| import { motion } from 'framer-motion'; | ||
| import styled from 'styled-components'; | ||
|
|
||
| export const Toggle = styled.div<{ $isOn: boolean }>` | ||
| cursor: pointer; | ||
|
|
||
| display: flex; | ||
| align-items: center; | ||
|
|
||
| width: 5rem; | ||
| height: 2.5rem; | ||
| padding: 0.2rem; | ||
| border-radius: 1.2rem; | ||
|
|
||
| background-color: ${({ theme, $isOn }) => ($isOn ? theme.colors.green : theme.colors.dark[300])}; | ||
| `; | ||
|
|
||
| export const ToggleCircle = styled(motion.div)` | ||
| width: 2rem; | ||
| height: 2rem; | ||
| border-radius: 50%; | ||
| background-color: ${({ theme }) => theme.colors.white}; | ||
| `; | ||
|
|
||
| export const IconWrapper = styled.div` | ||
| display: flex; | ||
| align-items: center; | ||
| justify-content: center; | ||
|
|
||
| width: 100%; | ||
| height: 100%; | ||
|
|
||
| color: ${({ theme }) => theme.colors.dark[500]}; | ||
| `; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| import { TbPlus } from 'react-icons/tb'; | ||
|
|
||
| import { useGuildInfoStore } from '@/stores/guildInfo'; | ||
| import useModalStore from '@/stores/modalStore'; | ||
| import { BodyMediumText, BodyRegularText } from '@/styles/Typography'; | ||
| import { CategoryDataResult, ChannelResult } from '@/types/guilds'; | ||
|
|
||
| import CreateChannelModal from '../CreateChannelModal'; | ||
|
|
||
| import * as S from './styles'; | ||
|
|
||
| export interface CategoriesListProps { | ||
| categories?: CategoryDataResult[]; | ||
| channels?: ChannelResult[]; | ||
| } | ||
| const CategoriesList = ({ categories, channels }: CategoriesListProps) => { | ||
| const { openModal } = useModalStore(); | ||
| const { guildId } = useGuildInfoStore(); | ||
| const handleOpenModal = (categoryId: string, guildId: string) => { | ||
| openModal('withFooter', <CreateChannelModal categoryId={categoryId} guildId={guildId} />); | ||
| }; | ||
|
|
||
| return ( | ||
| <S.CategoriesList> | ||
| {categories?.map((category) => ( | ||
| <S.Category key={category.categoryId}> | ||
| <S.CategoryName> | ||
| <BodyMediumText>{category.name}</BodyMediumText> | ||
| <TbPlus size={18} onClick={() => handleOpenModal(category.categoryId, guildId)} /> | ||
| </S.CategoryName> | ||
| {channels | ||
| ?.filter((channel) => category.categoryId === channel.categoryId) | ||
| .map((channel) => ( | ||
| <S.Channels key={channel.channelId}> | ||
| <BodyRegularText>{channel.name}</BodyRegularText> | ||
| </S.Channels> | ||
|
||
| ))} | ||
| </S.Category> | ||
| ))} | ||
| </S.CategoriesList> | ||
| ); | ||
| }; | ||
|
|
||
| export default CategoriesList; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| import styled from 'styled-components'; | ||
|
|
||
| export const CategoriesList = styled.div` | ||
| display: flex; | ||
| flex-direction: column; | ||
| gap: 2rem; | ||
|
|
||
| margin-top: 1.5rem; | ||
|
|
||
| color: ${({ theme }) => theme.colors.dark[300]}; | ||
| `; | ||
|
|
||
| export const Category = styled.div` | ||
| display: flex; | ||
| flex-direction: column; | ||
| `; | ||
|
|
||
| export const CategoryName = styled.div` | ||
| cursor: pointer; | ||
| display: flex; | ||
| justify-content: space-between; | ||
| width: 100%; | ||
|
|
||
| svg { | ||
| color: ${({ theme }) => theme.colors.dark[300]}; | ||
| } | ||
| `; | ||
|
Comment on lines
18
to
30
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 매의 눈 👍🏻 반영해둘게용! |
||
|
|
||
| export const Channels = styled.div` | ||
| display: flex; | ||
| `; | ||
|
Comment on lines
29
to
47
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 사소하지만, channel의 경우 padding-left로 indent를 주면 가독성이 더 좋아질 것 같아요!
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 사소함이 굉장히 큰 변화네요 ✨ 덕분에 가독성이 훨씬 나아진 것 같아요!
Comment on lines
29
to
47
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. cursor: pointer도 주면 좋을 것 같습니다 :)
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. :완완: |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| import { useQueryClient } from '@tanstack/react-query'; | ||
| import { useState } from 'react'; | ||
| import { BiHash, BiSolidLock } from 'react-icons/bi'; | ||
| import { BsFillMicFill } from 'react-icons/bs'; | ||
|
|
||
| import { createGuildChannel } from '@/api/guild'; | ||
| import Modal from '@/components/common/Modal'; | ||
| import Toggle from '@/components/common/Toggle'; | ||
| import useModalStore from '@/stores/modalStore'; | ||
| import { BodyMediumText, ChipText, SmallText, TitleText2 } from '@/styles/Typography'; | ||
| import { ChannelType, CreateChannelRequest } from '@/types/guilds'; | ||
|
|
||
| import * as S from './styles'; | ||
|
|
||
| interface CreateChannelModalProps { | ||
| guildId: string; | ||
| categoryId: string; | ||
| } | ||
|
|
||
| const CreateChannelModal = ({ categoryId, guildId }: CreateChannelModalProps) => { | ||
| const { closeAllModal } = useModalStore(); | ||
| const [isPublicChannel, setIsPublicChannel] = useState(false); | ||
| const [channelName, setChannelName] = useState(''); | ||
| const [isSelectedType, setIsSelectedType] = useState<ChannelType>(null); | ||
| const queryClient = useQueryClient(); | ||
|
|
||
| const handleChannelNameChange = (event: React.ChangeEvent<HTMLInputElement>) => { | ||
| const newName = event.target.value; | ||
|
|
||
| if (newName.length !== 0) setChannelName(newName); | ||
| }; | ||
|
Comment on lines
27
to
29
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 버그 제보 감사합니다!! 생각치 못했던 부분이었네요 🫠 버튼 활성화 여부도 함께 포함해서 수정해둘게요! |
||
|
|
||
| const handleChangeTextType = () => { | ||
| setIsSelectedType('TEXT'); | ||
| }; | ||
|
|
||
| const handleChangeVoiceType = () => { | ||
| setIsSelectedType('VOICE'); | ||
| }; | ||
|
|
||
| const handleSubmit = async () => { | ||
| try { | ||
| const requestData: CreateChannelRequest = { | ||
| name: channelName, | ||
| guildId, | ||
| categoryId, | ||
| channelType: isSelectedType, | ||
| private: !isPublicChannel, | ||
| }; | ||
|
|
||
| await createGuildChannel(requestData); | ||
|
|
||
| await queryClient.invalidateQueries({ queryKey: ['guildInfo', guildId] }); | ||
|
|
||
| closeAllModal(); | ||
| } catch (error) { | ||
| console.error('카테고리 생성 중 오류가 발생했어요', error); | ||
| } | ||
| }; | ||
|
|
||
| return ( | ||
| <Modal name="withFooter"> | ||
| <Modal.Header> | ||
| <TitleText2>채널 만들기</TitleText2> | ||
| </Modal.Header> | ||
| <Modal.Content> | ||
| <S.ChannelType> | ||
| <ChipText>채널 유형</ChipText> | ||
| <S.ChannelTypeContent $isSelectedType={isSelectedType === 'TEXT'} onClick={handleChangeTextType}> | ||
| <BiHash size={24} /> | ||
| <S.TypeInfo> | ||
| <BodyMediumText>텍스트</BodyMediumText> | ||
| <SmallText>메시지, 이미지, GIF, 의견, 농담을 전송하세요</SmallText> | ||
| </S.TypeInfo> | ||
| </S.ChannelTypeContent> | ||
| <S.ChannelTypeContent $isSelectedType={isSelectedType === 'VOICE'} onClick={handleChangeVoiceType}> | ||
| <BsFillMicFill size={24} /> | ||
| <S.TypeInfo> | ||
| <BodyMediumText>음성</BodyMediumText> | ||
| <SmallText>음성, 영상, 화면 공유로 함께 어울리세요</SmallText> | ||
| </S.TypeInfo> | ||
| </S.ChannelTypeContent> | ||
| </S.ChannelType> | ||
| <ChipText>채널 이름</ChipText> | ||
| <S.ChannelNameInput | ||
| onChange={handleChannelNameChange} | ||
| value={channelName} | ||
| placeholder="채널 이름을 입력해주세요" | ||
| /> | ||
| <S.PrivateSetting> | ||
| <S.SettingText> | ||
| <BiSolidLock size={24} /> | ||
| <BodyMediumText>비공개 채널</BodyMediumText> | ||
| </S.SettingText> | ||
| <Toggle isOn={isPublicChannel} onToggle={() => setIsPublicChannel(!isPublicChannel)} /> | ||
| </S.PrivateSetting> | ||
| <S.ChannelInfoText>선택한 멤버들과 역할만 이 채널을 볼 수 있어요</S.ChannelInfoText> | ||
| </Modal.Content> | ||
| <S.FooterContainer> | ||
| <S.CancelButton onClick={closeAllModal}>취소</S.CancelButton> | ||
| <S.CreateButton $disabled={!isSelectedType} onClick={handleSubmit}> | ||
| 채널 만들기 | ||
| </S.CreateButton> | ||
| </S.FooterContainer> | ||
| </Modal> | ||
| ); | ||
| }; | ||
|
|
||
| export default CreateChannelModal; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| import styled from 'styled-components'; | ||
|
|
||
| import { CaptionText } from '@/styles/Typography'; | ||
|
|
||
| export const ChannelType = styled.div` | ||
| display: flex; | ||
| flex-direction: column; | ||
| gap: 1rem; | ||
| margin-bottom: 2rem; | ||
| `; | ||
|
|
||
| export const ChannelTypeContent = styled.div<{ $isSelectedType: boolean }>` | ||
| display: flex; | ||
| gap: 1rem; | ||
| align-items: center; | ||
|
|
||
| min-height: 5rem; | ||
| padding: 0.6rem; | ||
| border-radius: 0.8rem; | ||
|
|
||
| background-color: ${({ theme, $isSelectedType }) => | ||
| $isSelectedType ? theme.colors.dark[350] : theme.colors.dark[600]}; | ||
|
|
||
| svg { | ||
| color: ${({ theme }) => theme.colors.dark[400]}; | ||
| } | ||
|
|
||
| span { | ||
| color: ${({ theme }) => theme.colors.dark[400]}; | ||
| } | ||
|
|
||
| &:hover { | ||
| cursor: pointer; | ||
| background-color: ${({ theme }) => theme.colors.dark[350]}; | ||
| } | ||
|
Comment on lines
21
to
35
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 사소하지만, 선택되거나 호버했을 때 배경 색상을 조금 더 어둡게 하는 것은 어떨까요? 폰트 색상과 큰 차이가 없어서 읽는 데 영향을 줄 수 있다고 생각해요! 혹은 폰트 색상을 더 밝게 하는 것도 방법이 될 것 같아요 :)
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. UI가 더 예뻐졌네요!! 👍 |
||
| `; | ||
|
|
||
| export const TypeInfo = styled.div` | ||
| display: flex; | ||
| flex-direction: column; | ||
| `; | ||
|
|
||
| export const ChannelInfoText = styled(CaptionText)` | ||
| color: ${({ theme }) => theme.colors.dark[300]}; | ||
| `; | ||
|
|
||
| export const FooterContainer = styled.div` | ||
| display: flex; | ||
| gap: 4rem; | ||
| justify-content: flex-end; | ||
|
|
||
| padding: 1.5rem 2rem; | ||
|
|
||
| background-color: ${({ theme }) => theme.colors.dark[600]}; | ||
| `; | ||
|
|
||
| export const ChannelNameInput = styled.input` | ||
| display: flex; | ||
| align-items: center; | ||
|
|
||
| width: 100%; | ||
| height: 4rem; | ||
| margin: 0.4rem 0; | ||
| padding: 0.4rem 0 0.4rem 0.8rem; | ||
| border: none; | ||
| border-radius: 0.4rem; | ||
|
|
||
| color: ${({ theme }) => theme.colors.white}; | ||
|
|
||
| background-color: ${({ theme }) => theme.colors.dark[800]}; | ||
| outline: none; | ||
| `; | ||
|
|
||
| export const CancelButton = styled.button` | ||
| color: ${({ theme }) => theme.colors.white}; | ||
| background-color: transparent; | ||
| `; | ||
|
|
||
| export const CreateButton = styled.button<{ $disabled: boolean }>` | ||
| width: 11rem; | ||
| height: 4rem; | ||
| border-radius: 0.8rem; | ||
|
|
||
| color: ${({ theme }) => theme.colors.white}; | ||
|
|
||
| background-color: ${({ theme, $disabled }) => ($disabled ? theme.colors.dark[400] : theme.colors.blue)}; | ||
| `; | ||
|
Comment on lines
79
to
89
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 뜨헉 버튼 속성을 위한 disabled 추가할게요! 😅 추가로 cursor도
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. disabled, $disabled를 모두 사용해도 스타일 prop으로 구분돼서 크게 어색하진 않네요! |
||
|
|
||
| export const PrivateSetting = styled.div` | ||
| display: flex; | ||
| justify-content: space-between; | ||
| margin: 1rem 0; | ||
| `; | ||
|
|
||
| export const SettingText = styled.div` | ||
| display: flex; | ||
| gap: 0.5rem; | ||
| align-items: center; | ||
|
|
||
| svg { | ||
| color: ${({ theme }) => theme.colors.white}; | ||
| } | ||
| `; | ||






There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
애니메이션 동작까지 좋아요 👍
컴포넌트 이름을 ToggleButton으로 두는 것은 어떤가요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Toggle,ToggleSwitch,ToggleButton등 다양하게 불리는 것 같은데 전Toggle이 깔끔해보여서 요건 그대로 유지해둘게요! 의견 감사합니당 :)