Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
fac42e1
[FE] setting: react-icons 추가 (#31)
zelkovaria Feb 7, 2025
5e08399
[FE] feat: GuildList 기본 버튼 UI 구현 (#31)
zelkovaria Feb 7, 2025
7dfe4e6
[FE] setting: immer 추가 (#31)
zelkovaria Feb 8, 2025
4664d18
[FE] feat: router 설정 (#31)
zelkovaria Feb 10, 2025
c7f3683
[FE] feat: modal 관리 및 렌더링 컴포넌트 구현 (#31)
zelkovaria Feb 10, 2025
65ac73e
[FE] feat: 공개/비공개 서버 선택 모달 구현 (#31)
zelkovaria Feb 10, 2025
ea7eaf8
[FE] feat: 길드 생성 버튼 클릭시 모달 동작 추가 (#31)
zelkovaria Feb 10, 2025
90ce689
[FE] feat: 모달 상태관리 기능 추가 (#31)
zelkovaria Feb 10, 2025
bedc52b
[FE] feat: 메인 Friends 페이지 추가 (#31)
zelkovaria Feb 10, 2025
b625246
[FE] feat: Modal 공통 컴포넌트 구현 (#31)
zelkovaria Feb 10, 2025
95e5903
[FE] feat: 이미지 업로드 버튼 UI 구현 (#31)
zelkovaria Feb 10, 2025
f44e0bb
[FE] feat: 길드 이름 input ui 구현 (#31)
zelkovaria Feb 10, 2025
fd28ac7
[FE] fix: 모달 padding 영역 및 정렬 수정 (#31)
zelkovaria Feb 10, 2025
7056150
[FE] feat: customize 모달 버튼 ui 구현 (#31)
zelkovaria Feb 10, 2025
7e78254
[FE] fix: 모달 정렬 수정 (#31)
zelkovaria Feb 10, 2025
e7cf5c5
[FE] feat: 모달 전환 구현 (#31)
zelkovaria Feb 10, 2025
5407061
[FE] fix: 모달 닫힘 오류 수정 (#31)
zelkovaria Feb 10, 2025
bf22f6b
[FE] refactor: type 분리 (#31)
zelkovaria Feb 10, 2025
e0540df
[FE] fix: modal에서 key를 제거한 구조로 수정 (#31)
zelkovaria Feb 10, 2025
39d39af
[FE] refactor: 길드 생성 버튼 스타일 통합 (#31)
zelkovaria Feb 10, 2025
cdd15b9
[FE] refactor: 페이지 화면 비율 수정 (#31)
zelkovaria Feb 11, 2025
3ab65ee
[FE] refactor: CreateGuildModal 폰스 사이즈 수정 (#31)
zelkovaria Feb 11, 2025
883aa54
[FE] refactor: AddServerButton 컴포넌트명 변경 (#31)
zelkovaria Feb 11, 2025
0ea8acd
[FE] refactor: router 구조 및 App.tsx 삭제 (#31)
zelkovaria Feb 12, 2025
48c8092
[FE] fix: modal의 footer margin 조정 (#31)
zelkovaria Feb 12, 2025
24f1b57
[FE] fix: 첫번째 step에서 모달이 닫히지 않는 오류 수정 (#31)
zelkovaria Feb 13, 2025
6e60bab
[FE] refactor: eslint rule 추가 (#31)
zelkovaria Feb 14, 2025
208c6a8
[FE] refactor: Popup의 타입을 modal에서 bottomSheet까지 확장 및 type명 변경 (#31)
zelkovaria Feb 14, 2025
9bc338a
[FE] feat: useFunnel hook 구현 (#31)
zelkovaria Feb 14, 2025
87681ef
[FE] refactor: funnel 구조에 맞춰 guild 생성 모달들의 props를 변경 (#31)
zelkovaria Feb 14, 2025
6bc1edb
[FE] refactor: 길드 생성 모달에 퍼널 구조 적용 (#31)
zelkovaria Feb 14, 2025
012d22f
[FE] refactor: AddGuildButton 컴포넌트 분리 (#31)
zelkovaria Feb 14, 2025
5c90803
[FE] refactor: AddGuildButton 컴포넌트 병합 (#31)
zelkovaria Feb 14, 2025
8f01a77
[FE] refactor: type 및 변수명 수정 (#31)
zelkovaria Feb 14, 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
1 change: 1 addition & 0 deletions src/frontend/.eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ module.exports = {
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {
'@typescript-eslint/no-unused-vars': 'warn',
'react/react-in-jsx-scope': 'off',
'react/no-unknown-property': ['error', { ignore: ['css'] }],
'import/order': [
Expand Down
1 change: 1 addition & 0 deletions src/frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
</head>
<body>
<div id="root"></div>
<div id="portal-root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
2 changes: 2 additions & 0 deletions src/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@
"@tanstack/react-query": "^5.66.0",
"@tanstack/react-query-devtools": "^5.66.0",
"motion": "^12.4.2",
"immer": "^10.1.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-icons": "^5.4.0",
"react-router-dom": "^7.1.5",
"styled-components": "^6.1.14",
"zustand": "^5.0.3"
Expand Down
45 changes: 45 additions & 0 deletions src/frontend/src/components/common/Modal/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import React from 'react';

import useModalStore from '../../../stores/modalStore';
import { ModalType } from '../../../types';

import * as S from './styles';

interface ModalProps {
children: React.ReactNode;
name: ModalType;
}

const Modal = ({ children, name }: ModalProps) => {
const { closeModal, closeAllModal } = useModalStore();

return (
<>
<S.Overlay onClick={() => closeModal(name, 'close-modal')} />
<S.ModalContainer>
<S.CloseButton>
<S.CloseIcon size={28} onClick={() => closeAllModal()} />
</S.CloseButton>
{children}
</S.ModalContainer>
</>
);
};

const Header = ({ children }: { children: React.ReactNode }) => {
return <S.HeaderWrapper>{children}</S.HeaderWrapper>;
};

const Content = ({ children }: { children: React.ReactNode }) => {
return <S.ContentWrapper>{children}</S.ContentWrapper>;
};

const Footer = ({ children }: { children: React.ReactNode }) => {
return <S.FooterWrapper>{children}</S.FooterWrapper>;
};

Modal.Header = Header;
Modal.Content = Content;
Modal.Footer = Footer;

export default Modal;
66 changes: 66 additions & 0 deletions src/frontend/src/components/common/Modal/styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { TbX } from 'react-icons/tb';
import styled from 'styled-components';

export const Overlay = styled.div`
position: fixed;
z-index: 1001;
top: 0;
left: 0;

width: 100%;
height: 100%;

background-color: rgb(0 0 0 / 50%);
`;

export const ModalContainer = styled.div`
position: fixed;
z-index: 1002;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);

display: flex;
flex-direction: column;

min-width: 49rem;
min-height: 36rem;
border-radius: 0.4rem;

color: ${({ theme }) => theme.colors.white};

background-color: ${({ theme }) => theme.colors.dark[500]};

svg {
color: ${({ theme }) => theme.colors.white};
}
`;

export const HeaderWrapper = styled.div`
padding: 0 2.4rem;
text-align: center;
`;

export const ContentWrapper = styled.div`
margin-top: 0.8rem;
padding: 0 2.4rem;
`;

export const FooterWrapper = styled.div`
display: flex;
gap: 1.2rem;
align-items: center;
justify-content: center;

margin-top: 1.6rem;
`;

export const CloseButton = styled.div`
display: flex;
justify-content: end;
padding: 2.4rem 2.4rem 0;
`;

export const CloseIcon = styled(TbX)`
cursor: pointer;
`;
28 changes: 28 additions & 0 deletions src/frontend/src/components/common/ModalRender/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Fragment, useEffect } from 'react';
import { createPortal } from 'react-dom';
import { useLocation } from 'react-router-dom';

import { useModalStore } from '../../../stores/modalStore';

const ModalRenderer = () => {
const location = useLocation();
const { modal, closeAllModal } = useModalStore();

useEffect(() => {
closeAllModal();
}, [location.pathname]);

const portalRoot = document.getElementById('portal-root');
if (!portalRoot) return null;

const renderModals = () => {
return Object.entries(modal).flatMap(([type, typeModals]) => {
if (!typeModals) return null;
return <Fragment key={type}>{typeModals.content}</Fragment>;
});
};

return createPortal(<>{renderModals()}</>, portalRoot);
};

export default ModalRenderer;
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import useFunnel from '@/hooks/useFunnel';
import CreateGuildModal from '@/pages/FriendsPage/components/CreateGuildModal';
import CustomizeGuildModal from '@/pages/FriendsPage/components/CustomizeGuildModal';

type CreateGuildSteps = '서버공개여부' | '서버커스텀';
const STEP_SEQUENCE: CreateGuildSteps[] = ['서버공개여부', '서버커스텀'];

const CreateGuildModalContent = () => {
const { Funnel, moveToNextStep, moveToPrevStep, currentStep } = useFunnel({
defaultStep: '서버공개여부',
stepList: STEP_SEQUENCE,
});

return (
<Funnel currentStep={currentStep}>
<Funnel.Step name="서버공개여부">
<CreateGuildModal onNext={moveToNextStep} />
</Funnel.Step>

<Funnel.Step name="서버커스텀">
<CustomizeGuildModal onPrev={moveToPrevStep} />
</Funnel.Step>
</Funnel>
);
};

export default CreateGuildModalContent;
30 changes: 30 additions & 0 deletions src/frontend/src/components/guild/GuildList/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import useModalStore from '@/stores/modalStore';

import CreateGuildModalContent from '../CreateGuildModalContent';

import * as S from './styles';

const GuildList = () => {
const { openModal } = useModalStore();

const handleChangeModal = () => {
openModal('basic', <CreateGuildModalContent />);
};

return (
<S.GuildList>
<S.DMButton>
<S.DiscordIcon size={32} />
</S.DMButton>
{/* 서버 리스트 추가 예정 */}
<S.AddGuildButton onClick={handleChangeModal}>
<S.PlusIcon size={24} />
</S.AddGuildButton>
<S.SearchCommunityButton>
<S.CompassIcon size={36} />
</S.SearchCommunityButton>
</S.GuildList>
);
};

export default GuildList;
59 changes: 59 additions & 0 deletions src/frontend/src/components/guild/GuildList/styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { BiCompass } from 'react-icons/bi';
import { BsDiscord } from 'react-icons/bs';
import { TbPlus } from 'react-icons/tb';
import styled from 'styled-components';

export const GuildList = styled.nav`
display: flex;
flex-direction: column;
gap: 2rem;

width: 8.4rem;
height: 100%;
padding: 2.2rem 1rem;

background-color: ${({ theme }) => theme.colors.dark[800]};
`;

export const DMButton = styled.button`
display: flex;
align-items: center;
justify-content: center;

width: 6.2rem;
height: 6.2rem;
border-radius: 1.4rem;

background-color: ${({ theme }) => theme.colors.blue};
`;

export const DiscordIcon = styled(BsDiscord)`
color: ${({ theme }) => theme.colors.white};
`;

export const CircleButton = styled.button`
display: flex;
align-items: center;
justify-content: center;

width: 6.25rem;
height: 6.2rem;
border-radius: 100%;

background-color: ${({ theme }) => theme.colors.dark[600]};
`;

export const AddGuildButton = styled(CircleButton)`
/* 버튼별 역할 구별을 위해 분리 */
`;
export const SearchCommunityButton = styled(CircleButton)`
/* 버튼별 역할 구별을 위해 분리 */
`;

export const PlusIcon = styled(TbPlus)`
color: ${({ theme }) => theme.colors.dark[400]};
`;

export const CompassIcon = styled(BiCompass)`
color: ${({ theme }) => theme.colors.dark[400]};
`;
1 change: 1 addition & 0 deletions src/frontend/src/components/guild/types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type CreateGuildStep = 'initial' | 'customize';
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { Outlet } from 'react-router-dom';

import * as S from './styles';

const AuthFullLayout = () => {
return (
<S.LayoutContainer>

<Outlet />
</S.LayoutContainer>
);
};
Expand Down
11 changes: 11 additions & 0 deletions src/frontend/src/components/layout/AuthLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Outlet } from 'react-router-dom';

const AuthLayout = () => {
return (
<div>
<Outlet />
</div>
);
};

export default AuthLayout;
61 changes: 61 additions & 0 deletions src/frontend/src/hooks/useFunnel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import React, { useState } from 'react';

type UseFunnelProps<T extends string> = {
defaultStep: T;
stepList: T[];
};

type StepProps<T extends string> = {
children: React.ReactNode;
name: T;
};

type FunnelProps<T extends string> = {
currentStep: T;
children: React.ReactElement<StepProps<T>>[];
};

const Step = <T extends string>(stepProps: StepProps<T>) => {
return <>{stepProps.children}</>;
};

const Funnel = <T extends string>({ children, currentStep }: FunnelProps<T>) => {
const targetStep = children.find((curStep) => curStep.props.name === currentStep);
if (!targetStep) {
throw new Error(`${currentStep} 단계에 해당하는 컴포넌트가 존재하지 않습니다.`);
}
return <>{targetStep}</>;
};

Funnel.Step = Step;

const useFunnel = <T extends string>({ defaultStep, stepList }: UseFunnelProps<T>) => {
const [currentStep, setCurrentStep] = useState(defaultStep);
const currentIndex = stepList.indexOf(currentStep);

if (!stepList.includes(defaultStep)) {
throw new Error('defaultStep은 반드시 stepList에 포함되어 있어야 합니다.');
}

const moveToNextStep = () => {
const hasNext = currentIndex < stepList.length - 1;
if (!hasNext) return;
setCurrentStep(stepList[currentIndex + 1]);
};

const moveToPrevStep = () => {
const hasPrev = currentIndex > 0;
if (!hasPrev) return;
setCurrentStep(stepList[currentIndex - 1]);
};

return {
Funnel,
Step,
currentStep,
moveToNextStep,
moveToPrevStep,
};
};

export default useFunnel;
Loading