Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 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]};
`;
44 changes: 44 additions & 0 deletions src/frontend/src/components/guild/CategoriesList/index.tsx
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>
Copy link
Collaborator

Choose a reason for hiding this comment

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

채널 타입에 따라 channel.name 앞에 맞는 아이콘을 넣으면 좋겠어요! 당장 반영하지 않아도 괜찮습니다.

Copy link
Member Author

Choose a reason for hiding this comment

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

image
다음과 같이 반영해놨어요! 👍🏻

))}
</S.Category>
))}
</S.CategoriesList>
);
};

export default CategoriesList;
31 changes: 31 additions & 0 deletions src/frontend/src/components/guild/CategoriesList/styles.ts
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
Copy link
Collaborator

Choose a reason for hiding this comment

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

image
align-items: center 속성이 필요할 것 같습니다 :)

Copy link
Member Author

Choose a reason for hiding this comment

The 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
Copy link
Collaborator

Choose a reason for hiding this comment

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

사소하지만, channel의 경우 padding-left로 indent를 주면 가독성이 더 좋아질 것 같아요!

Copy link
Member Author

Choose a reason for hiding this comment

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

사소함이 굉장히 큰 변화네요 ✨ 덕분에 가독성이 훨씬 나아진 것 같아요!

Comment on lines 29 to 47
Copy link
Collaborator

Choose a reason for hiding this comment

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

cursor: pointer도 주면 좋을 것 같습니다 :)

Copy link
Member Author

Choose a reason for hiding this comment

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

:완완:

109 changes: 109 additions & 0 deletions src/frontend/src/components/guild/CreateChannelModal/index.tsx
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
Copy link
Collaborator

Choose a reason for hiding this comment

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

image
length가 0이 아니어야 한다는 조건 때문에 채널 이름을 완전히 지울 수 없는 버그를 발견했습니다!

Copy link
Member Author

Choose a reason for hiding this comment

The 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;
103 changes: 103 additions & 0 deletions src/frontend/src/components/guild/CreateChannelModal/styles.ts
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
Copy link
Collaborator

Choose a reason for hiding this comment

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

사소하지만, 선택되거나 호버했을 때 배경 색상을 조금 더 어둡게 하는 것은 어떨까요? 폰트 색상과 큰 차이가 없어서 읽는 데 영향을 줄 수 있다고 생각해요!

혹은 폰트 색상을 더 밝게 하는 것도 방법이 될 것 같아요 :)

Copy link
Member Author

Choose a reason for hiding this comment

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

좋은 것 같아요!
이전에는 밝은 감이 있는 것 같아요 ㅎㅎ
image
와 같은 색상을 아래처럼 수정해놨어요!
image

Copy link
Collaborator

Choose a reason for hiding this comment

The 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
Copy link
Collaborator

Choose a reason for hiding this comment

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

image

disabled가 버튼의 기본 속성을 활용하는 게 아니라서, $disabled가 true인 경우에는 click 동작을 막아야 할 것 같아요. 지금은 이름 없는 카테고리나 채널이 만들어질 수 있네요...!

(pointer-events?)

Copy link
Member Author

Choose a reason for hiding this comment

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

뜨헉 버튼 속성을 위한 disabled 추가할게요! 😅 추가로 cursor도 not-allowed 처리해둘게요 ㅎㅎ

Copy link
Collaborator

Choose a reason for hiding this comment

The 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};
}
`;
Loading