Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
50c5701
[FE] feat: Toggle 컴포넌트 구현 (#63)
zelkovaria Feb 23, 2025
de0140c
[FE] feat: 카테고리 생성 모달 UI 구현 (#63)
zelkovaria Feb 23, 2025
92ae760
[FE] feat: 길드 설정 dropdown에 카테고리 생성 모달 연결 (#63)
zelkovaria Feb 23, 2025
0aa54a5
[FE] feat: 카테고리 생성 api 함수 및 type 구현 (#63)
zelkovaria Feb 23, 2025
3164671
[FE] feat: 카테고리 생성 api 연결 (#63)
zelkovaria Feb 23, 2025
e6afd25
[FE] refactor: CategoryDataResult type 개선 (#63)
zelkovaria Feb 23, 2025
bfdeb53
[FE] feat: 단일 길드의 category list 조회 구현 (#63)
zelkovaria Feb 23, 2025
482a126
[FE] feat: 카테고리별 채널 리스트 조회 구현 (#63)
zelkovaria Feb 23, 2025
0465167
[FE] feat: theme dark 계열 색상 추가 (#63)
zelkovaria Feb 23, 2025
96a33a6
[FE] feat: channel 생성 api 함수 구현 (#63)
zelkovaria Feb 23, 2025
888cb33
[FE] feat: 채널 생성 클릭시 모달 열림 (#63)
zelkovaria Feb 23, 2025
7679dfc
[FE] feat: 채널 생성 모달 UI 구현 (#63)
zelkovaria Feb 23, 2025
4b00e81
[FE] feat: 채널 생성 api 연결 (#63)
zelkovaria Feb 23, 2025
0dd4407
[FE] fix: 채널 생성 활성화 색상 변수 수정 (#63)
zelkovaria Feb 23, 2025
865b70d
[FE] fix: 카테고리, 채널 공개값이 false로만 가던 오류 수정 (#63)
zelkovaria Feb 23, 2025
e3aafb7
[FE] fix: 채널, 카테고리 이름이 지워지지 않는 오류 수정 (#63)
zelkovaria Feb 23, 2025
15d27a1
[FE] refactor: dark 색상 추가 및 채널 생성 모달 css 변경 (#63)
zelkovaria Feb 23, 2025
8d56b89
[FE] refactor: CategoryName css 수정 (#63)
zelkovaria Feb 23, 2025
b48986b
[FE] refactor: Channels에 cursor:pointer 추가 (#63)
zelkovaria Feb 23, 2025
1d2824a
[FE] refactor: Channels에 indent 추가 (#63)
zelkovaria Feb 23, 2025
6208ae0
[FE] 채널 타입에 따른 icon 추가 (#63)
zelkovaria Feb 23, 2025
c61fd77
[FE] fix: 생성버튼 비활성화를 위한 disabled 속성 추가 (#63)
zelkovaria Feb 23, 2025
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
18 changes: 17 additions & 1 deletion src/frontend/src/api/guild.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import { endPoint } from '@/constants/endPoint';
import { CreateGuildRequest, CreateGuildResponse, GetGuildResponse, GetGuildsResponse } from '@/types/guilds';
import {
CreateCategoryRequest,
CreateCategoryResponse,
CreateChannelRequest,
CreateGuildRequest,
CreateGuildResponse,
GetGuildResponse,
GetGuildsResponse,
} from '@/types/guilds';
import { tokenAxios } from '@/utils/axios';
import { convertFormData } from '@/utils/convertFormData';

Expand All @@ -22,3 +30,11 @@ export const getGuild = async (guildId: string) => {
const { data } = await tokenAxios.get<GetGuildResponse>(endPoint.guilds.GET_GUILD(guildId));
return data.result;
};

export const createGuildCategory = async (data: CreateCategoryRequest) => {
return await tokenAxios.post<CreateCategoryResponse>(endPoint.guilds.CREATE_CATEGORY, data);
};

export const createGuildChannel = async (data: CreateChannelRequest) => {
return await tokenAxios.post<CreateChannelRequest>(endPoint.guilds.CREATE_CHANNEL, data);
};
38 changes: 38 additions & 0 deletions src/frontend/src/components/common/Toggle/index.tsx
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) => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

애니메이션 동작까지 좋아요 👍
컴포넌트 이름을 ToggleButton으로 두는 것은 어떤가요?

Copy link
Member Author

Choose a reason for hiding this comment

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

Toggle, ToggleSwitch, ToggleButton 등 다양하게 불리는 것 같은데 전 Toggle이 깔끔해보여서 요건 그대로 유지해둘게요! 의견 감사합니당 :)

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;
34 changes: 34 additions & 0 deletions src/frontend/src/components/common/Toggle/styles.ts
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]};
`;
49 changes: 49 additions & 0 deletions src/frontend/src/components/guild/CategoriesList/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { BiHash } from 'react-icons/bi';
import { BsFillMicFill } from 'react-icons/bs';
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>
<S.Channels>
{channels
?.filter((channel) => category.categoryId === channel.categoryId)
.map((channel) => (
<S.ChannelName key={channel.channelId}>
{channel.channelType === 'TEXT' ? <BiHash size={18} /> : <BsFillMicFill size={18} />}
<BodyRegularText>{channel.name}</BodyRegularText>
</S.ChannelName>
))}
</S.Channels>
</S.Category>
))}
</S.CategoriesList>
);
};

export default CategoriesList;
47 changes: 47 additions & 0 deletions src/frontend/src/components/guild/CategoriesList/styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
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;
align-items: center;
justify-content: space-between;

width: 100%;

svg {
color: ${({ theme }) => theme.colors.dark[300]};
}
`;

export const Channels = styled.div`
display: flex;
flex-direction: column;
gap: 0.6rem;
margin-top: 0.6rem;
`;

export const ChannelName = styled.div`
cursor: pointer;

display: flex;
gap: 0.5rem;
align-items: center;

padding-left: 1rem;
`;
111 changes: 111 additions & 0 deletions src/frontend/src/components/guild/CreateChannelModal/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
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>) => {
setChannelName(event.target.value);
};

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((prev) => !prev)} />
</S.PrivateSetting>
<S.ChannelInfoText>선택한 멤버들과 역할만 이 채널을 볼 수 있어요</S.ChannelInfoText>
</Modal.Content>
<S.FooterContainer>
<S.CancelButton onClick={closeAllModal}>취소</S.CancelButton>
<S.CreateButton
disabled={!channelName.trim()}
$disabled={!isSelectedType || !channelName.trim()}
onClick={handleSubmit}
>
채널 만들기
</S.CreateButton>
</S.FooterContainer>
</Modal>
);
};

export default CreateChannelModal;
Loading