+
-
+
+ {missionUrl !== '' && }
- {/* */}
+
+
);
};
diff --git a/frontend/src/components/PairRoom/PairListCard/PairListSection/PairListSection.tsx b/frontend/src/components/PairRoom/PairListCard/PairListSection/PairListSection.tsx
index 699a9ca2f..e38f310c8 100644
--- a/frontend/src/components/PairRoom/PairListCard/PairListSection/PairListSection.tsx
+++ b/frontend/src/components/PairRoom/PairListCard/PairListSection/PairListSection.tsx
@@ -6,17 +6,19 @@ interface PairListSectionProps {
navigator: string;
}
-const PairListSection = ({ isOpen, driver, navigator }: PairListSectionProps) => (
-
-
- {isOpen && 드라이버}
- {driver}
-
-
- {isOpen && 내비게이터}
- {navigator}
-
-
-);
+const PairListSection = ({ isOpen, driver, navigator }: PairListSectionProps) => {
+ return (
+
+
+ {isOpen && 드라이버}
+ {driver}
+
+
+ {isOpen && 내비게이터}
+ {navigator}
+
+
+ );
+};
export default PairListSection;
diff --git a/frontend/src/components/PairRoom/PairListCard/RepositorySection/RepositorySection.styles.ts b/frontend/src/components/PairRoom/PairListCard/RepositorySection/RepositorySection.styles.ts
new file mode 100644
index 000000000..8d1326514
--- /dev/null
+++ b/frontend/src/components/PairRoom/PairListCard/RepositorySection/RepositorySection.styles.ts
@@ -0,0 +1,29 @@
+import styled from 'styled-components';
+
+export const Layout = styled.div<{ $isOpen: boolean }>`
+ display: flex;
+ justify-content: ${({ $isOpen }) => ($isOpen ? 'space-between' : 'center')};
+ align-items: center;
+
+ width: 100%;
+ height: 6rem;
+ padding: 2rem;
+
+ background-color: ${({ theme }) => theme.color.black[80]};
+
+ transition: background-color 0.3s ease-out;
+
+ cursor: pointer;
+`;
+
+export const RepositoryWrapper = styled.div`
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 1.2rem;
+`;
+
+export const RepositoryText = styled.span`
+ color: ${({ theme }) => theme.color.black[10]};
+ font-size: ${({ theme }) => theme.fontSize.md};
+`;
diff --git a/frontend/src/components/PairRoom/PairListCard/RepositorySection/RepositorySection.tsx b/frontend/src/components/PairRoom/PairListCard/RepositorySection/RepositorySection.tsx
new file mode 100644
index 000000000..a9a8319b9
--- /dev/null
+++ b/frontend/src/components/PairRoom/PairListCard/RepositorySection/RepositorySection.tsx
@@ -0,0 +1,30 @@
+import { Link } from 'react-router-dom';
+
+import { IoIosArrowForward } from 'react-icons/io';
+
+import { GithubLogoWhite } from '@/assets';
+
+import * as S from './RepositorySection.styles';
+
+interface RepositorySectionProps {
+ isOpen: boolean;
+ missionUrl: string;
+}
+
+const RepositorySection = ({ isOpen, missionUrl }: RepositorySectionProps) => {
+ return (
+
+
+
+ {isOpen && (
+
+ 미션 리포지토리로 이동
+
+
+ )}
+
+
+ );
+};
+
+export default RepositorySection;
diff --git a/frontend/src/components/PairRoom/PairListCard/RoomCodeSection/RoomCodeSection.stories.tsx b/frontend/src/components/PairRoom/PairListCard/RoomCodeSection/RoomCodeSection.stories.tsx
deleted file mode 100644
index 119e4795d..000000000
--- a/frontend/src/components/PairRoom/PairListCard/RoomCodeSection/RoomCodeSection.stories.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-import type { Meta, StoryObj } from '@storybook/react';
-
-import RoomCodeSection from './RoomCodeSection';
-
-const meta = {
- title: 'component/PairRoom/PairListCard/RoomCodeSection',
- component: RoomCodeSection,
-} satisfies Meta;
-
-export default meta;
-
-type Story = StoryObj;
-
-export const Default: Story = {
- args: {
- isOpen: true,
- roomCode: 'IUUIASDFJK',
- },
-};
-
-export const Closed: Story = {
- args: {
- isOpen: false,
- roomCode: 'IUUIASDFJK',
- },
-};
diff --git a/frontend/src/components/PairRoom/PairListCard/RoomCodeSection/RoomCodeSection.tsx b/frontend/src/components/PairRoom/PairListCard/RoomCodeSection/RoomCodeSection.tsx
deleted file mode 100644
index c202771f2..000000000
--- a/frontend/src/components/PairRoom/PairListCard/RoomCodeSection/RoomCodeSection.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import { FaRegPaste } from 'react-icons/fa6';
-
-import useCopyClipBoard from '@/hooks/common/useCopyClipboard';
-
-import * as S from './RoomCodeSection.styles';
-
-interface RoomCodeSectionProps {
- isOpen: boolean;
- roomCode: string;
-}
-
-const RoomCodeSection = ({ isOpen, roomCode }: RoomCodeSectionProps) => {
- const [, onCopy] = useCopyClipBoard();
-
- const handleCopyClipBoard = (text: string) => {
- onCopy(text);
- };
-
- return (
- handleCopyClipBoard(roomCode)}>
- {isOpen && (
-
- 방 코드
- {roomCode}
-
- )}
-
-
- );
-};
-
-export default RoomCodeSection;
diff --git a/frontend/src/components/PairRoom/PairRoleCard/PairRoleCard.tsx b/frontend/src/components/PairRoom/PairRoleCard/PairRoleCard.tsx
index aa1b96c11..cf3fdf3d4 100644
--- a/frontend/src/components/PairRoom/PairRoleCard/PairRoleCard.tsx
+++ b/frontend/src/components/PairRoom/PairRoleCard/PairRoleCard.tsx
@@ -10,11 +10,11 @@ interface PairRoleCardProps {
const PairRoleCard = ({ driver, navigator }: PairRoleCardProps) => {
return (
-
+
- 💻
+ 💻
{
>
드라이버
-
{driver}
@@ -36,7 +35,7 @@ const PairRoleCard = ({ driver, navigator }: PairRoleCardProps) => {
{navigator}
- 🧭
+ 🧭
diff --git a/frontend/src/components/PairRoom/PairRoomCard/Header/Header.tsx b/frontend/src/components/PairRoom/PairRoomCard/Header/Header.tsx
index dde5bcfa4..a349e116d 100644
--- a/frontend/src/components/PairRoom/PairRoomCard/Header/Header.tsx
+++ b/frontend/src/components/PairRoom/PairRoomCard/Header/Header.tsx
@@ -1,7 +1,7 @@
import * as S from './Header.styles';
interface HeaderProps {
- icon: React.ReactNode;
+ icon?: React.ReactNode;
secondIcon?: React.ReactNode;
title: string;
isOpen?: boolean;
diff --git a/frontend/src/components/PairRoom/ReferenceCard/AddReferenceForm/AddReferenceForm.tsx b/frontend/src/components/PairRoom/ReferenceCard/AddReferenceForm/AddReferenceForm.tsx
index 38152f953..e97076a90 100644
--- a/frontend/src/components/PairRoom/ReferenceCard/AddReferenceForm/AddReferenceForm.tsx
+++ b/frontend/src/components/PairRoom/ReferenceCard/AddReferenceForm/AddReferenceForm.tsx
@@ -2,7 +2,7 @@ import { LuPlus } from 'react-icons/lu';
import Button from '@/components/common/Button/Button';
import Input from '@/components/common/Input/Input';
-import CategoryDropdown from '@/components/PairRoom/ReferenceCard/AddReferenceForm/CategoryDropdown/CategoryDropdown';
+import CategoryDropdown from '@/components/PairRoom/ReferenceCard/CategoryDropdown/CategoryDropdown';
import { Category } from '@/components/PairRoom/ReferenceCard/ReferenceCard.type';
import useInput from '@/hooks/common/useInput';
@@ -11,13 +11,13 @@ import useReference from '@/hooks/PairRoom/useReference';
import * as S from './AddReferenceForm.styles';
interface ReferenceFormProps {
- getCategoryNameById: (categoryId: string) => string;
categories: Category[];
accessCode: string;
isCategoryExist: (categoryName: string) => boolean;
+ getCategoryNameById: (categoryId: string) => string;
}
-const AddReferenceForm = ({ accessCode, categories, getCategoryNameById, isCategoryExist }: ReferenceFormProps) => {
+const AddReferenceForm = ({ accessCode, categories, isCategoryExist, getCategoryNameById }: ReferenceFormProps) => {
const { value, status, message, handleChange, resetValue } = useInput();
const { currentCategoryId, handleCurrentCategory, handleSubmit } = useReference(accessCode, value, () =>
resetValue(),
@@ -26,10 +26,10 @@ const AddReferenceForm = ({ accessCode, categories, getCategoryNameById, isCateg
return (
@@ -43,13 +43,14 @@ const AddReferenceForm = ({ accessCode, categories, getCategoryNameById, isCateg
onChange={handleChange}
/>
diff --git a/frontend/src/components/PairRoom/ReferenceCard/AddReferenceForm/CategoryDropdown/CategoryDropdown.tsx b/frontend/src/components/PairRoom/ReferenceCard/CategoryDropdown/CategoryDropdown.tsx
similarity index 71%
rename from frontend/src/components/PairRoom/ReferenceCard/AddReferenceForm/CategoryDropdown/CategoryDropdown.tsx
rename to frontend/src/components/PairRoom/ReferenceCard/CategoryDropdown/CategoryDropdown.tsx
index dc059affe..b66f7a1d0 100644
--- a/frontend/src/components/PairRoom/ReferenceCard/AddReferenceForm/CategoryDropdown/CategoryDropdown.tsx
+++ b/frontend/src/components/PairRoom/ReferenceCard/CategoryDropdown/CategoryDropdown.tsx
@@ -1,7 +1,4 @@
/* eslint-disable jsx-a11y/no-autofocus */
-
-import { validateCategory } from '@/validations/validateCategory';
-
import Dropdown from '@/components/common/Dropdown/Dropdown/Dropdown';
import Input from '@/components/common/Input/Input';
import { Category } from '@/components/PairRoom/ReferenceCard/ReferenceCard.type';
@@ -13,9 +10,11 @@ import { DEFAULT_CATEGORY_VALUE } from '@/hooks/PairRoom/useCategories';
import { useAddCategory } from '@/queries/PairRoom/category/mutation';
-interface CategoryDropdownProp {
- categories: Category[];
+import { validateCategoryName } from '@/validations/validateCategory';
+
+interface CategoryDropdownProps {
accessCode: string;
+ categories: Category[];
currentCategoryId: string | null;
handleCurrentCategory: (category: string) => void;
getCategoryNameById: (categoryId: string) => string;
@@ -29,14 +28,24 @@ const CategoryDropdown = ({
handleCurrentCategory,
getCategoryNameById,
isCategoryExist,
-}: CategoryDropdownProp) => {
- const { value, status, message, handleChange, resetValue } = useInput();
+}: CategoryDropdownProps) => {
const { addToast } = useToastStore();
- const addCategory = useAddCategory().mutateAsync;
- const handleCategory = (option: string) => {
- handleCurrentCategory(option);
+ const { value, status, message, handleChange, resetValue } = useInput();
+
+ const { mutateAsync } = useAddCategory();
+
+ const handleSubmit = (event: React.FormEvent) => {
+ event.preventDefault();
+
+ if (status === 'ERROR') {
+ addToast({ status, message });
+ return;
+ }
+
+ mutateAsync({ category: value, accessCode }).then(() => resetValue());
};
+
return (
handleCategory(option)}
+ onSelect={(option) => handleCurrentCategory(option)}
>
-
diff --git a/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoriesEditor/CategoriesEditor.styles.ts b/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoriesEditor/CategoriesEditor.styles.ts
deleted file mode 100644
index c8f9d7a64..000000000
--- a/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoriesEditor/CategoriesEditor.styles.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import styled from 'styled-components';
-
-export const CategoryList = styled.ul`
- display: flex;
- flex-direction: column-reverse;
- gap: 1rem;
-
- width: 100%;
-`;
-
-export const CategoryInput = styled.input`
- width: 100%;
- border: 1px solid;
-`;
diff --git a/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoriesEditor/CategoriesEditor.tsx b/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoriesEditor/CategoriesEditor.tsx
deleted file mode 100644
index 97b2369ef..000000000
--- a/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoriesEditor/CategoriesEditor.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import CategoryItem from '@/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoriesEditor/CategoryItem/CategoryItem';
-import { Category } from '@/components/PairRoom/ReferenceCard/ReferenceCard.type';
-
-import * as S from './CategoriesEditor.styles';
-
-interface CategoryFilterProps {
- categories: Category[];
- selectedCategory: string;
- handleSelectCategory: (categoryId: string) => void;
- accessCode: string;
- closeModal: () => void;
-}
-
-const CategoriesEditor = ({
- closeModal,
- accessCode,
- categories,
- selectedCategory,
- handleSelectCategory,
-}: CategoryFilterProps) => {
- return (
-
- {categories.map((category) => (
-
- ))}
-
- );
-};
-
-export default CategoriesEditor;
diff --git a/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoriesEditor/CategoryItem/CategoryItem.styles.ts b/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoriesEditor/CategoryItem/CategoryItem.styles.ts
deleted file mode 100644
index f58636278..000000000
--- a/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoriesEditor/CategoryItem/CategoryItem.styles.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import styled from 'styled-components';
-
-export const Layout = styled.div`
- display: flex;
- justify-content: space-between;
- gap: 1rem;
-
- width: 100%;
-`;
-
-export const Container = styled.div`
- display: flex;
- flex-direction: column;
- gap: 0.3rem;
-
- width: 100%;
- height: 6rem;
-
- cursor: pointer;
-
- img {
- width: 2rem;
- }
-`;
-
-export const CategoryIconsContainer = styled.div`
- display: flex;
- align-items: center;
- gap: 0.2rem;
-
- height: 4.4rem;
-`;
-
-export const EditForm = styled.form`
- display: flex;
- justify-content: space-between;
- gap: 1rem;
-
- width: 100%;
-`;
diff --git a/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoriesEditor/CategoryItem/CategoryItem.tsx b/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoriesEditor/CategoryItem/CategoryItem.tsx
deleted file mode 100644
index 854741897..000000000
--- a/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoriesEditor/CategoryItem/CategoryItem.tsx
+++ /dev/null
@@ -1,85 +0,0 @@
-import Input from '@/components/common/Input/Input';
-import { Message } from '@/components/common/Input/Input.styles';
-import IconButton from '@/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoriesEditor/CategoryItem/IconButton/IconButton';
-import ReadonlyCategoryItem from '@/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoriesEditor/CategoryItem/ReadonlyCategoryItem/ReadonlyCategoryItem';
-
-import { DEFAULT_CATEGORY_ID } from '@/hooks/PairRoom/useCategories';
-import useEditCategory from '@/hooks/PairRoom/useEditCategory';
-
-import * as S from './CategoryItem.styles';
-
-interface CategoryItemProps {
- accessCode: string;
- categoryName: string;
- categoryId: string;
- isChecked: boolean;
- closeModal: () => void;
- handleSelectCategory: (categoryId: string) => void;
-}
-
-const CategoryItem = ({
- closeModal,
- categoryName,
- categoryId,
- isChecked,
- handleSelectCategory,
- accessCode,
-}: CategoryItemProps) => {
- const { isEditing, categoryInputData, actions } = useEditCategory(accessCode, categoryName, categoryId);
-
- const handleUpdateCategory = async (event: React.FormEvent) => {
- event.preventDefault();
- await actions.updateCategory();
- };
-
- const handleDeleteCategory = async () => {
- await actions.deleteCategory();
- if (isChecked) handleSelectCategory(DEFAULT_CATEGORY_ID);
- };
-
- return (
-
- {isEditing ? (
-
-
- actions.editCategory(event, categoryName)}
- status={categoryInputData.status}
- height="4.4rem"
- width="28rem"
- />
- {categoryInputData.message && (
- {categoryInputData.message}
- )}
-
-
-
-
-
-
- ) : (
- <>
-
-
-
- {categoryId !== DEFAULT_CATEGORY_ID && (
-
-
-
-
- )}
- >
- )}
-
- );
-};
-
-export default CategoryItem;
diff --git a/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoriesEditor/CategoryItem/IconButton/IconButton.tsx b/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoriesEditor/CategoryItem/IconButton/IconButton.tsx
deleted file mode 100644
index 1951b4bf6..000000000
--- a/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoriesEditor/CategoryItem/IconButton/IconButton.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import { AiFillDelete } from 'react-icons/ai';
-import { FaPencilAlt, FaCheck } from 'react-icons/fa';
-import { GiCancel } from 'react-icons/gi';
-
-import * as S from './IconButton.styles';
-
-type Icon = 'CHECK' | 'EDIT' | 'DELETE' | 'CANCEL';
-
-interface IconButtonProps {
- onClick?: () => void;
- icon: Icon;
- type?:'button'|'submit'|'reset';
-}
-
-const IconButton = ({ onClick, icon, type="button" }: IconButtonProps) => {
- const GET_ICON = {
- CHECK: ,
- EDIT: ,
- DELETE: ,
- CANCEL: ,
- };
- return (
- {
- event.stopPropagation();
- onClick && onClick();
- }}
- type={type}
- >
- {GET_ICON[icon]}
-
- );
-};
-
-export default IconButton;
diff --git a/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoriesEditor/CategoryItem/ReadonlyCategoryItem/ReadonlyCategoryItem.tsx b/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoriesEditor/CategoryItem/ReadonlyCategoryItem/ReadonlyCategoryItem.tsx
deleted file mode 100644
index c4bfb1c34..000000000
--- a/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoriesEditor/CategoryItem/ReadonlyCategoryItem/ReadonlyCategoryItem.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-import { CheckBoxChecked, CheckBoxUnchecked } from '@/assets';
-
-import * as S from './ReadonlyCategoryItem.styles';
-
-interface ReadonlyCategoryItemProps {
- isChecked: boolean;
- category: string;
- categoryId: string;
- closeModal: () => void;
- handleSelectCategory: (categoryId: string) => void;
-}
-
-const ReadonlyCategoryItem = ({
- closeModal,
- categoryId,
- isChecked,
- category,
- handleSelectCategory,
-}: ReadonlyCategoryItemProps) => {
- return (
- ) => {
- if (event.currentTarget.id === '카테고리') return;
- if (isChecked) return;
- handleSelectCategory(event.currentTarget.id);
- closeModal();
- }}
- >
-
-
-
- {category}
-
-
- );
-};
-export default ReadonlyCategoryItem;
diff --git a/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoryItem/CategoryItem.styles.ts b/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoryItem/CategoryItem.styles.ts
new file mode 100644
index 000000000..ec03bdbe9
--- /dev/null
+++ b/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoryItem/CategoryItem.styles.ts
@@ -0,0 +1,56 @@
+import styled, { css } from 'styled-components';
+
+export const Layout = styled.div`
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 2rem;
+
+ width: 100%;
+`;
+
+export const Container = styled.div`
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+
+ width: 100%;
+
+ img {
+ width: 2rem;
+ height: 2rem;
+ }
+`;
+
+export const IconContainer = styled.div`
+ display: flex;
+ gap: 0.6rem;
+`;
+
+export const Item = styled.li<{ $isChecked: boolean }>`
+ display: flex;
+ align-items: center;
+
+ width: 100%;
+ height: 4.4rem;
+ padding: 0 1.2rem;
+ border: 1px solid ${({ theme }) => theme.color.black[50]};
+ border-radius: 0.5rem;
+
+ background-color: ${({ theme, $isChecked }) => ($isChecked ? theme.color.primary[700] : theme.color.black[10])};
+ color: ${({ theme, $isChecked }) => ($isChecked ? theme.color.black[10] : theme.color.black[70])};
+ font-size: ${({ theme }) => theme.fontSize.md};
+
+ transition: all 0.2s ease-out;
+
+ &:hover {
+ background-color: ${({ theme }) => theme.color.primary[700]};
+ color: ${({ theme }) => theme.color.black[10]};
+ }
+`;
+
+export const CustomInputMessage = css`
+ top: 4rem;
+
+ font-size: 1rem;
+`;
diff --git a/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoryItem/CategoryItem.tsx b/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoryItem/CategoryItem.tsx
new file mode 100644
index 000000000..9d56469ac
--- /dev/null
+++ b/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoryItem/CategoryItem.tsx
@@ -0,0 +1,100 @@
+import { CheckBoxChecked, CheckBoxUnchecked } from '@/assets';
+
+import Input from '@/components/common/Input/Input';
+import IconButton from '@/components/PairRoom/ReferenceCard/CategoryManagementModal/IconButton/IconButton';
+
+import useToastStore from '@/stores/toastStore';
+
+import { DEFAULT_CATEGORY_ID } from '@/hooks/PairRoom/useCategories';
+import useEditCategory from '@/hooks/PairRoom/useEditCategory';
+
+import * as S from './CategoryItem.styles';
+
+interface CategoryItemProps {
+ accessCode: string;
+ categoryId: string;
+ categoryName: string;
+ isChecked: boolean;
+ closeModal: () => void;
+ handleSelectCategory: (categoryId: string) => void;
+}
+
+const CategoryItem = ({
+ accessCode,
+ categoryId,
+ categoryName,
+ isChecked,
+ closeModal,
+ handleSelectCategory,
+}: CategoryItemProps) => {
+ const {
+ newCategoryName,
+ handleCategoryName,
+ isEditing,
+ startEditing,
+ stopEditing,
+ updateCategoryName,
+ deleteCategoryName,
+ } = useEditCategory(accessCode, categoryId, categoryName);
+
+ const { addToast } = useToastStore();
+
+ const handleSubmit = async (event: React.FormEvent) => {
+ event.preventDefault();
+ await updateCategoryName();
+ };
+
+ const handleDeleteCategory = async () => {
+ await deleteCategoryName();
+ if (isChecked) handleSelectCategory(DEFAULT_CATEGORY_ID);
+ };
+
+ const handleCategoryClick = (event: React.MouseEvent) => {
+ if (isChecked) return;
+
+ handleSelectCategory(event.currentTarget.id);
+ addToast({ status: 'SUCCESS', message: `${categoryName}가 선택되었어요.` });
+ closeModal();
+ };
+
+ if (isEditing) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+
+ {categoryName}
+
+
+ {categoryId !== DEFAULT_CATEGORY_ID && (
+
+
+
+
+ )}
+
+ );
+};
+
+export default CategoryItem;
diff --git a/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoryManagementModal.styles.ts b/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoryManagementModal.styles.ts
index 4499303df..7394cec78 100644
--- a/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoryManagementModal.styles.ts
+++ b/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoryManagementModal.styles.ts
@@ -1,42 +1,40 @@
import styled, { css } from 'styled-components';
-export const inputStyles = css`
- width: 100%;
-
- font-size: ${({ theme }) => theme.fontSize.md};
+export const buttonStyles = css`
+ width: 4.8rem;
+ height: 4.4rem;
+ border-radius: 0.6rem;
`;
-export const CategoryBox = styled.div`
+export const Header = styled.div`
display: flex;
+ align-items: center;
gap: 1rem;
+
+ color: ${({ theme }) => theme.color.primary[700]};
+ font-size: ${({ theme }) => theme.fontSize.h5};
+ font-weight: ${({ theme }) => theme.fontWeight.medium};
`;
-export const Header = styled.div`
+export const CategoryList = styled.ul`
display: flex;
- align-items: center;
- gap: 1rem;
+ flex-direction: column-reverse;
+ gap: 2rem;
- color: ${({ theme }) => theme.color.black[80]};
- font-size: ${({ theme }) => theme.fontSize.lg};
+ width: 100%;
`;
-export const Footer = styled.form`
+export const Form = styled.form`
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 1rem;
width: 100%;
- height: 7rem;
`;
-export const AddNewCategoryInput = styled.div`
+export const InputContainer = styled.div`
display: flex;
align-items: center;
gap: 0.5rem;
`;
-export const buttonStyles = css`
- width: 4.4rem;
- height: 4rem;
- border-radius: 0.6rem;
-`;
diff --git a/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoryManagementModal.tsx b/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoryManagementModal.tsx
index e52658f63..3d696fc30 100644
--- a/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoryManagementModal.tsx
+++ b/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoryManagementModal.tsx
@@ -1,19 +1,17 @@
-import { FaFilter } from 'react-icons/fa';
import { LuPlus } from 'react-icons/lu';
-import { validateCategory } from '@/validations/validateCategory';
-
import Button from '@/components/common/Button/Button';
import Input from '@/components/common/Input/Input';
-import { Message } from '@/components/common/Input/Input.styles';
import { Modal } from '@/components/common/Modal';
-import CategoriesEditor from '@/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoriesEditor/CategoriesEditor';
+import CategoryItem from '@/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoryItem/CategoryItem';
import { Category } from '@/components/PairRoom/ReferenceCard/ReferenceCard.type';
import useInput from '@/hooks/common/useInput';
import { useAddCategory } from '@/queries/PairRoom/category/mutation';
+import { validateCategoryName } from '@/validations/validateCategory';
+
import * as S from './CategoryManagementModal.styles';
interface CategoryManagementModalProps {
@@ -36,49 +34,56 @@ const CategoryManagementModal = ({
handleSelectCategory,
}: CategoryManagementModalProps) => {
const { value, handleChange, resetValue, message, status } = useInput('');
- const addCategory = useAddCategory();
- const closeCategoryManagementModal = () => {
- resetValue();
- closeModal();
- };
+ const addCategory = useAddCategory();
const handleAddCategorySubmit = (event: React.FormEvent) => {
event.preventDefault();
+
if (status === 'ERROR') return;
+
addCategory.mutateAsync({ category: value, accessCode }).then(() => resetValue());
};
+ const handleCloseModal = () => {
+ resetValue();
+ closeModal();
+ };
+
return (
-
+
-
- 카테고리 선택
+ 카테고리 선택하기
-
-
+
+ {categories.map((category) => (
+
+ ))}
+
-
-
-
+
+
handleChange(event, validateCategory(event.target.value, isCategoryExist))}
+ placeholder="추가할 카테고리를 입력해 주세요."
+ height="4.4rem"
status={status}
- $css={S.inputStyles}
+ message={message}
+ onChange={(event) => handleChange(event, validateCategoryName(event.target.value, isCategoryExist))}
/>
-
- {message}
-
+
+
+
);
};
diff --git a/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoriesEditor/CategoryItem/IconButton/IconButton.styles.ts b/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/IconButton/IconButton.styles.ts
similarity index 76%
rename from frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoriesEditor/CategoryItem/IconButton/IconButton.styles.ts
rename to frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/IconButton/IconButton.styles.ts
index 5f5d551f7..79acde5ab 100644
--- a/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoriesEditor/CategoryItem/IconButton/IconButton.styles.ts
+++ b/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/IconButton/IconButton.styles.ts
@@ -1,14 +1,17 @@
import styled from 'styled-components';
-export const IconsButton = styled.button`
+export const Layout = styled.button`
display: flex;
justify-content: center;
align-items: center;
+ width: 3rem;
+ height: 3rem;
padding: 0.5rem;
border-radius: 0.3rem;
color: ${({ theme }) => theme.color.primary[700]};
+ font-size: ${({ theme }) => theme.fontSize.base};
transition: all 0.3s;
diff --git a/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/IconButton/IconButton.tsx b/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/IconButton/IconButton.tsx
new file mode 100644
index 000000000..a86a4cbe2
--- /dev/null
+++ b/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/IconButton/IconButton.tsx
@@ -0,0 +1,34 @@
+import { LuPencil, LuTrash2, LuCheck, LuArrowLeft } from 'react-icons/lu';
+
+import * as S from './IconButton.styles';
+
+type Icon = 'CHECK' | 'EDIT' | 'DELETE' | 'CANCEL';
+
+interface IconButtonProps {
+ icon: Icon;
+ type?: 'button' | 'submit';
+ onClick?: () => void;
+}
+
+const ICON_LIST = {
+ CHECK: ,
+ EDIT: ,
+ DELETE: ,
+ CANCEL: ,
+};
+
+const IconButton = ({ icon, type = 'button', onClick }: IconButtonProps) => {
+ const handleClick = (event: React.MouseEvent) => {
+ event.stopPropagation();
+
+ if (onClick) onClick();
+ };
+
+ return (
+
+ {ICON_LIST[icon]}
+
+ );
+};
+
+export default IconButton;
diff --git a/frontend/src/components/PairRoom/ReferenceCard/Header/Header.tsx b/frontend/src/components/PairRoom/ReferenceCard/Header/Header.tsx
index 8a6f3abf0..0e256cfc5 100644
--- a/frontend/src/components/PairRoom/ReferenceCard/Header/Header.tsx
+++ b/frontend/src/components/PairRoom/ReferenceCard/Header/Header.tsx
@@ -21,12 +21,15 @@ const Header = ({
onButtonClick,
}: React.PropsWithChildren) => {
return (
-
+
{isOpen ? (
-
+
) : (
-
+
)}
링크
{isCustom && (
theme.color.secondary[600]};
+
+ color: ${({ theme }) => theme.color.secondary[600]};
+
+ &:hover {
+ border-color: ${({ theme }) => theme.color.secondary[700]};
+
+ color: ${({ theme }) => theme.color.secondary[700]};
+ }
+`;
+
+export const Layout = styled.div`
+ display: flex;
+ justify-content: space-between;
+`;
+
+export const TitleContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+`;
+
+export const Title = styled.h1`
+ color: ${({ theme }) => theme.color.primary[800]};
+ font-size: ${({ theme }) => theme.fontSize.h3};
+ font-weight: ${({ theme }) => theme.fontWeight.semibold};
+`;
+
+export const SubTitle = styled.h2`
+ color: ${({ theme }) => theme.color.primary[700]};
+ font-size: ${({ theme }) => theme.fontSize.lg};
+`;
diff --git a/frontend/src/components/Retrospect/Header/Header.tsx b/frontend/src/components/Retrospect/Header/Header.tsx
new file mode 100644
index 000000000..7e65bbb19
--- /dev/null
+++ b/frontend/src/components/Retrospect/Header/Header.tsx
@@ -0,0 +1,26 @@
+import Button from '@/components/common/Button/Button';
+
+import * as S from './Header.styles';
+
+interface HeaderProps {
+ title: string;
+ subTitle: string;
+ buttonText: string;
+ onButtonClick: () => void;
+}
+
+const Header = ({ title, subTitle, buttonText, onButtonClick }: HeaderProps) => {
+ return (
+
+
+ {title}
+ {subTitle}
+
+
+ {buttonText}
+
+
+ );
+};
+
+export default Header;
diff --git a/frontend/src/components/Retrospect/Question/Question.styles.ts b/frontend/src/components/Retrospect/Question/Question.styles.ts
new file mode 100644
index 000000000..9a80be7f9
--- /dev/null
+++ b/frontend/src/components/Retrospect/Question/Question.styles.ts
@@ -0,0 +1,19 @@
+import styled from 'styled-components';
+
+export const Container = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 2rem;
+`;
+
+export const LabelContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 1.2rem;
+`;
+
+export const Label = styled.label`
+ color: ${({ theme }) => theme.color.primary[700]};
+ font-size: ${({ theme }) => theme.fontSize.lg};
+ font-weight: ${({ theme }) => theme.fontWeight.medium};
+`;
diff --git a/frontend/src/components/Retrospect/Question/Question.tsx b/frontend/src/components/Retrospect/Question/Question.tsx
new file mode 100644
index 000000000..eead71bdf
--- /dev/null
+++ b/frontend/src/components/Retrospect/Question/Question.tsx
@@ -0,0 +1,22 @@
+import InformationBox from '@/components/PairRoomOnboarding/InformationBox/InformationBox';
+
+import * as S from './Question.styles';
+
+interface QuestionProps extends React.PropsWithChildren {
+ readonly?: boolean;
+ id: string;
+ title: string;
+ subtitle: string;
+}
+
+const Question = ({ readonly = false, id, title, subtitle, children }: QuestionProps) => (
+
+
+ {title}
+ {!readonly && }
+
+ {children}
+
+);
+
+export default Question;
diff --git a/frontend/src/components/Retrospect/Textarea/Textarea.styles.ts b/frontend/src/components/Retrospect/Textarea/Textarea.styles.ts
new file mode 100644
index 000000000..829b93516
--- /dev/null
+++ b/frontend/src/components/Retrospect/Textarea/Textarea.styles.ts
@@ -0,0 +1,50 @@
+import styled from 'styled-components';
+
+export const Layout = styled.div`
+ display: flex;
+ flex-direction: column;
+
+ position: relative;
+`;
+
+export const Textarea = styled.textarea`
+ width: 100%;
+ min-height: 20rem;
+ max-height: 40rem;
+ padding: 2rem;
+ border: 1px solid ${({ theme }) => theme.color.black[50]};
+ border-radius: 0.5rem;
+
+ background-color: ${({ theme }) => theme.color.black[20]};
+ color: ${({ theme }) => theme.color.black[80]};
+ font-size: ${({ theme }) => theme.fontSize.md};
+ line-height: 1.6;
+ resize: vertical;
+
+ word-wrap: break-word;
+ overflow-wrap: break-word;
+
+ &:focus {
+ border: 1px solid ${({ theme }) => theme.color.primary[600]};
+
+ background-color: ${({ theme }) => theme.color.black[10]};
+ color: ${({ theme }) => theme.color.black[90]};
+ }
+
+ &::placeholder {
+ color: ${({ theme }) => theme.color.black[50]};
+ }
+`;
+
+export const CharNumberText = styled.p`
+ position: absolute;
+ right: 1rem;
+ bottom: 1rem;
+
+ padding: 0.5rem 1rem;
+ border-radius: 1rem;
+
+ background-color: ${({ theme }) => theme.color.black[20]};
+ color: ${({ theme }) => theme.color.primary[600]};
+ font-size: ${({ theme }) => theme.fontSize.sm};
+`;
diff --git a/frontend/src/components/Retrospect/Textarea/Textarea.tsx b/frontend/src/components/Retrospect/Textarea/Textarea.tsx
new file mode 100644
index 000000000..155ff1f33
--- /dev/null
+++ b/frontend/src/components/Retrospect/Textarea/Textarea.tsx
@@ -0,0 +1,18 @@
+import { TextareaHTMLAttributes } from 'react';
+
+import * as S from './Textarea.styles';
+
+interface TextareaProps extends TextareaHTMLAttributes {
+ id: string;
+ charNumber: string;
+}
+const TextArea = ({ id, charNumber, ...props }: TextareaProps) => {
+ return (
+
+
+ {charNumber}
+
+ );
+};
+
+export default TextArea;
diff --git a/frontend/src/components/common/Background/WaveBackground.styles.ts b/frontend/src/components/common/Background/WaveBackground.styles.ts
index 98b76f411..6194d670d 100644
--- a/frontend/src/components/common/Background/WaveBackground.styles.ts
+++ b/frontend/src/components/common/Background/WaveBackground.styles.ts
@@ -1,5 +1,7 @@
import styled, { keyframes } from 'styled-components';
+import { Z_INDEX } from '@/constants/style';
+
const drift = keyframes`
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
@@ -9,7 +11,7 @@ export const WaveBackground = styled.div`
overflow: hidden;
position: fixed;
- z-index: -1;
+ z-index: ${Z_INDEX.MINUS};
width: 100vw;
height: calc(100vh - 7rem);
diff --git a/frontend/src/components/common/Button/Button.stories.tsx b/frontend/src/components/common/Button/Button.stories.tsx
index 12c54c849..c52425faa 100644
--- a/frontend/src/components/common/Button/Button.stories.tsx
+++ b/frontend/src/components/common/Button/Button.stories.tsx
@@ -35,6 +35,6 @@ export const UsedCss: Story = {
args: {
onClick: () => console.log(),
children: '확인',
- css: CustomButton,
+ $css: CustomButton,
},
};
diff --git a/frontend/src/components/common/Button/Button.tsx b/frontend/src/components/common/Button/Button.tsx
index bfeb21f8b..1d165f94c 100644
--- a/frontend/src/components/common/Button/Button.tsx
+++ b/frontend/src/components/common/Button/Button.tsx
@@ -6,36 +6,35 @@ import * as S from '@/components/common/Button/Button.styles';
import type { ButtonColor, ButtonSize } from '@/components/common/Button/Button.type';
interface ButtonProp extends ButtonHTMLAttributes {
+ $css?: ReturnType;
size?: ButtonSize;
fontSize?: string;
-
color?: ButtonColor;
filled?: boolean;
rounded?: boolean;
animation?: boolean;
-
- css?: ReturnType;
}
const Button = ({
+ $css,
size = 'lg',
filled = true,
rounded = false,
animation = true,
color = 'primary',
children,
- css,
disabled = false,
...props
}: React.PropsWithChildren) => {
return (
diff --git a/frontend/src/components/common/ConfirmModal/ConfirmModal.styles.ts b/frontend/src/components/common/ConfirmModal/ConfirmModal.styles.ts
new file mode 100644
index 000000000..bbfb8e3d7
--- /dev/null
+++ b/frontend/src/components/common/ConfirmModal/ConfirmModal.styles.ts
@@ -0,0 +1,51 @@
+import styled, { css } from 'styled-components';
+
+export const confirmButtonStyles = css`
+ font-size: ${({ theme }) => theme.fontSize.md};
+`;
+
+export const cancelButtonStyles = css`
+ border-color: ${({ theme }) => theme.color.black[40]};
+
+ background-color: ${({ theme }) => theme.color.black[40]};
+ font-size: ${({ theme }) => theme.fontSize.md};
+
+ &:hover {
+ border-color: ${({ theme }) => theme.color.black[50]};
+
+ background-color: ${({ theme }) => theme.color.black[50]};
+ }
+
+ &:active {
+ border-color: ${({ theme }) => theme.color.black[50]};
+
+ background-color: ${({ theme }) => theme.color.black[50]};
+ }
+`;
+
+export const Layout = styled.div`
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 3rem;
+
+ width: 100%;
+`;
+
+export const Container = styled.div<{ $type: 'SUCCESS' | 'DANGER' }>`
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 1rem;
+
+ padding: 2rem 0 3.4rem;
+
+ color: ${({ theme, $type }) => ($type === 'SUCCESS' ? theme.color.success[700] : theme.color.danger[600])};
+ font-size: ${({ theme }) => theme.fontSize.md};
+
+ p {
+ color: ${({ theme }) => theme.color.black[90]};
+ font-size: ${({ theme }) => theme.fontSize.base};
+ font-weight: ${({ theme }) => theme.fontWeight.medium};
+ }
+`;
diff --git a/frontend/src/components/common/ConfirmModal/ConfirmModal.tsx b/frontend/src/components/common/ConfirmModal/ConfirmModal.tsx
new file mode 100644
index 000000000..9f4b1db8d
--- /dev/null
+++ b/frontend/src/components/common/ConfirmModal/ConfirmModal.tsx
@@ -0,0 +1,44 @@
+import Button from '@/components/common/Button/Button';
+import { Modal } from '@/components/common/Modal';
+
+import * as S from './ConfirmModal.styles';
+
+interface ConfirmModalProps {
+ isOpen: boolean;
+ title: string;
+ subTitle: string;
+ confirmText?: string;
+ onConfirm: () => void;
+ close: () => void;
+ type?: 'SUCCESS' | 'DANGER';
+}
+
+const ConfirmModal = ({
+ isOpen,
+ close,
+ type = 'DANGER',
+ title,
+ subTitle,
+ confirmText = '확인',
+ onConfirm,
+}: ConfirmModalProps) => {
+ return (
+
+
+
+ {title}
+ {subTitle}
+
+
+
+ 취소
+
+
+ {confirmText}
+
+
+
+ );
+};
+
+export default ConfirmModal;
diff --git a/frontend/src/components/common/Dropdown/Dropdown/Dropdown.styles.tsx b/frontend/src/components/common/Dropdown/Dropdown/Dropdown.styles.tsx
index ba1fa352e..3ec607f8e 100644
--- a/frontend/src/components/common/Dropdown/Dropdown/Dropdown.styles.tsx
+++ b/frontend/src/components/common/Dropdown/Dropdown/Dropdown.styles.tsx
@@ -4,6 +4,8 @@ import styled from 'styled-components';
import Button from '@/components/common/Button/Button';
import { Direction } from '@/components/common/Dropdown/Dropdown/Dropdown';
+import { Z_INDEX } from '@/constants/style';
+
const getDirection = {
lower: {
open: 180,
@@ -79,7 +81,7 @@ export const ItemList = styled.ul<{ $height: string; $direction: Direction }>`
top: ${({ $direction }) => ($direction === 'lower' ? '5rem' : '')};
bottom: ${({ $direction }) => ($direction === 'lower' ? '' : '5rem')};
left: 0;
- z-index: 1000;
+ z-index: ${Z_INDEX.DROPDOWN};
width: 100%;
max-height: 20rem;
diff --git a/frontend/src/components/common/Dropdown/Dropdown/Dropdown.tsx b/frontend/src/components/common/Dropdown/Dropdown/Dropdown.tsx
index 879252810..666a37bba 100644
--- a/frontend/src/components/common/Dropdown/Dropdown/Dropdown.tsx
+++ b/frontend/src/components/common/Dropdown/Dropdown/Dropdown.tsx
@@ -13,10 +13,11 @@ export interface Option {
id: string;
value: string;
}
+
interface DropdownProps {
placeholder: string;
- valueOptions?: Option[];
options?: string[];
+ valueOptions?: Option[];
selectedOption?: string;
width?: string;
height?: string;
@@ -70,12 +71,12 @@ const Dropdown = ({
$isSelected={!!selectedOption}
$isOpen={isOpen}
onClick={toggleDropdown}
+ aria-label={isOpen ? '드롭다운을 닫습니다' : '드롭다운을 엽니다'}
>
{selectedOption || placeholder}
)}
-
{options && !options.some((option) => option === '') && isOpen && (
{options.map((option, index) => (
@@ -95,7 +96,6 @@ const Dropdown = ({
))}
)}
-
{valueOptions && !valueOptions.some((option) => option.value === '') && isOpen && (
{valueOptions.map((option, index) => (
diff --git a/frontend/src/components/common/Dropdown/HiddenDropdown/HiddenDropdown.tsx b/frontend/src/components/common/Dropdown/HiddenDropdown/HiddenDropdown.tsx
index 1faee3771..2c9a28396 100644
--- a/frontend/src/components/common/Dropdown/HiddenDropdown/HiddenDropdown.tsx
+++ b/frontend/src/components/common/Dropdown/HiddenDropdown/HiddenDropdown.tsx
@@ -22,7 +22,6 @@ const HiddenDropdown = ({ options, selectedOption, handleSelect, valueOptions }:
{option}
))}
-
{valueOptions &&
!valueOptions.every((option) => option.value === '') &&
valueOptions.map((option) => (
diff --git a/frontend/src/components/common/Header/Header.styles.ts b/frontend/src/components/common/Header/Header.styles.ts
index d0779f16c..027ce9c5e 100644
--- a/frontend/src/components/common/Header/Header.styles.ts
+++ b/frontend/src/components/common/Header/Header.styles.ts
@@ -1,25 +1,32 @@
+import { Link } from 'react-router-dom';
+
import styled from 'styled-components';
+import { Z_INDEX } from '@/constants/style';
+
export const Layout = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
+ position: fixed;
+ z-index: ${Z_INDEX.HEADER};
+
+ width: 100%;
height: 7rem;
padding: 0 5rem;
- border-bottom: 0.1rem solid ${({ theme }) => theme.color.black[30]};
-
+ background-color: ${({ theme }) => theme.color.black[10]};
color: ${({ theme }) => theme.color.black[80]};
font-size: ${({ theme }) => theme.fontSize.base};
- a {
- display: flex;
+ border-bottom: 0.1rem solid ${({ theme }) => theme.color.black[30]};
+
+ a,
+ button {
justify-content: center;
align-items: center;
- }
- button {
transition: all 0.1s;
cursor: pointer;
@@ -47,17 +54,27 @@ export const Logo = styled.img`
export const LinkContainer = styled.div`
display: flex;
- justify-content: space-between;
+ justify-content: space-evenly;
align-items: center;
gap: 1.4rem;
`;
-export const HowToPairText = styled.button`
+export const ResponsiveLink = styled(Link)`
+ display: inline;
+
@media (max-width: ${({ theme }) => theme.deviceWidth.mobile}) {
display: none;
}
`;
+export const ResponsiveIcon = styled.div`
+ display: none;
+
+ @media (max-width: ${({ theme }) => theme.deviceWidth.mobile}) {
+ display: inline;
+ }
+`;
+
export const HowToPairIcon = styled.div`
display: none;
diff --git a/frontend/src/components/common/Header/Header.tsx b/frontend/src/components/common/Header/Header.tsx
index c35b4f306..c4a6582e7 100644
--- a/frontend/src/components/common/Header/Header.tsx
+++ b/frontend/src/components/common/Header/Header.tsx
@@ -21,23 +21,23 @@ const Header = () => {
return (
-
-
+
+
-
- 코딩해듀오 시작하기
-
-
-
-
-
-
+
+ 코딩해듀오 시작하기
+
+
+
+
+
+
{userStatus === 'SIGNED_IN' ? (
<>
로그아웃
-
- {username}
+
+ {username}
>
) : (
diff --git a/frontend/src/components/common/HiddenMessage/HiddenMessage.styles.ts b/frontend/src/components/common/HiddenMessage/HiddenMessage.styles.ts
new file mode 100644
index 000000000..6e9eba739
--- /dev/null
+++ b/frontend/src/components/common/HiddenMessage/HiddenMessage.styles.ts
@@ -0,0 +1,16 @@
+import styled from 'styled-components';
+
+export const Layout = styled.p`
+ overflow: hidden;
+
+ position: absolute;
+
+ width: 1px;
+ height: 1px;
+ margin: -1px;
+ padding: 0;
+ border: 0;
+
+ white-space: nowrap;
+ clip: rect(0, 0, 0, 0);
+`;
diff --git a/frontend/src/components/common/HiddenMessage/HiddenMessage.tsx b/frontend/src/components/common/HiddenMessage/HiddenMessage.tsx
new file mode 100644
index 000000000..71096f03f
--- /dev/null
+++ b/frontend/src/components/common/HiddenMessage/HiddenMessage.tsx
@@ -0,0 +1,7 @@
+import * as S from './HiddenMessage.styles';
+
+const HiddenMessage = ({ children }: React.PropsWithChildren) => {
+ return {children};
+};
+
+export default HiddenMessage;
diff --git a/frontend/src/components/common/Input/Input.styles.ts b/frontend/src/components/common/Input/Input.styles.ts
index 14a5c202d..4451872f3 100644
--- a/frontend/src/components/common/Input/Input.styles.ts
+++ b/frontend/src/components/common/Input/Input.styles.ts
@@ -2,17 +2,19 @@ import styled, { css } from 'styled-components';
import type { InputStatus } from '@/components/common/Input/Input.type';
-interface InputProps {
- $status: InputStatus;
- $height: string;
- $css?: ReturnType;
-}
-
-interface LayoutProps {
- $width: string;
-}
+const messageStatusStyles = {
+ DEFAULT: css`
+ color: ${({ theme }) => theme.color.black[80]};
+ `,
+ ERROR: css`
+ color: ${({ theme }) => theme.color.danger[600]};
+ `,
+ SUCCESS: css`
+ color: ${({ theme }) => theme.color.success[700]};
+ `,
+};
-const inputStatusCss = {
+const inputStatusStyles = {
DEFAULT: css`
border: 1px solid ${({ theme }) => theme.color.black[40]};
@@ -30,25 +32,20 @@ const inputStatusCss = {
`,
};
-const inputStatusMessageCss = {
- DEFAULT: css`
- color: ${({ theme }) => theme.color.black[80]};
- `,
- ERROR: css`
- color: ${({ theme }) => theme.color.danger[600]};
- `,
+export const Layout = styled.div<{ $width: string }>`
+ display: flex;
+ flex-direction: column;
+ gap: 0.8rem;
- SUCCESS: css`
- color: ${({ theme }) => theme.color.success[700]};
- `,
-};
+ width: ${({ $width }) => $width};
+`;
-export const Layout = styled.div`
+export const Container = styled.div`
display: flex;
flex-direction: column;
gap: 0.8rem;
- width: ${({ $width }) => $width};
+ position: relative;
`;
export const Label = styled.label`
@@ -57,17 +54,29 @@ export const Label = styled.label`
font-weight: ${({ theme }) => theme.fontWeight.medium};
`;
-export const Message = styled.p<{ $status: InputStatus }>`
+export const Message = styled.p<{ $css?: ReturnType; $height: string; $status: InputStatus }>`
+ ${({ $status }) => messageStatusStyles[$status]};
+ position: absolute;
+ top: ${({ $height }) => $height};
+
+ margin-top: 0.6rem;
+ margin-left: 0.2rem;
+
font-size: ${({ theme }) => theme.fontSize.sm};
- ${({ $status }) => inputStatusMessageCss[$status]};
+
+ ${({ $css }) => $css}
`;
-export const Input = styled.input`
- ${({ $status }) => inputStatusCss[$status]};
- ${({ $status }) => inputStatusCss[$status]};
- width: 100%;
+export const Input = styled.input<{
+ $css?: ReturnType;
+ $width: string;
+ $height: string;
+ $status: InputStatus;
+}>`
+ ${({ $status }) => inputStatusStyles[$status]};
+ width: ${({ $width }) => $width};
height: ${({ $height }) => $height};
- padding: 0 1rem;
+ padding: 0 1.2rem;
border-radius: 0.5rem;
font-size: ${({ theme }) => theme.fontSize.md};
@@ -88,5 +97,5 @@ export const Input = styled.input`
background-color: ${({ theme }) => theme.color.black[30]};
}
- ${(props) => props.$css}
+ ${({ $css }) => $css}
`;
diff --git a/frontend/src/components/common/Input/Input.tsx b/frontend/src/components/common/Input/Input.tsx
index 7f8b33d14..9bd001e39 100644
--- a/frontend/src/components/common/Input/Input.tsx
+++ b/frontend/src/components/common/Input/Input.tsx
@@ -6,25 +6,45 @@ import * as S from '@/components/common/Input/Input.styles';
import type { InputStatus } from '@/components/common/Input/Input.type';
interface InputProps extends InputHTMLAttributes {
- width?: string;
- height?: string;
- status?: InputStatus;
label?: string;
+ status?: InputStatus;
message?: string;
+
$css?: ReturnType;
+ width?: string;
+ height?: string;
+
+ $messageCss?: ReturnType;
}
+
const Input = forwardRef(
- ({ width = '100%', status = 'DEFAULT', message, label, height = '4.8rem', ...props }: InputProps, ref) => {
+ (
+ { width = '100%', status = 'DEFAULT', message, label, height = '4.8rem', $messageCss, ...props }: InputProps,
+ ref,
+ ) => {
return (
{label && {label}}
-
- {message && {message}}
+
+
+ {message && (
+
+ {message}
+
+ )}
+
);
},
);
-Input.displayName = 'InputComponent';
+Input.displayName = 'Input';
export default Input;
diff --git a/frontend/src/components/common/Modal/CloseButton/CloseButton.tsx b/frontend/src/components/common/Modal/CloseButton/CloseButton.tsx
index 01dd6127e..b68bff211 100644
--- a/frontend/src/components/common/Modal/CloseButton/CloseButton.tsx
+++ b/frontend/src/components/common/Modal/CloseButton/CloseButton.tsx
@@ -8,7 +8,7 @@ interface CloseButtonProps {
const CloseButton = ({ close }: CloseButtonProps) => {
return (
-
+
);
diff --git a/frontend/src/components/common/Modal/Header/Header.styles.ts b/frontend/src/components/common/Modal/Header/Header.styles.ts
index 1bc6fa8a4..22d97b77a 100644
--- a/frontend/src/components/common/Modal/Header/Header.styles.ts
+++ b/frontend/src/components/common/Modal/Header/Header.styles.ts
@@ -6,7 +6,7 @@ export const Layout = styled.div`
gap: 0.5rem;
`;
-export const Title = styled.h3`
+export const Title = styled.h2`
color: ${({ theme }) => theme.color.primary[700]};
font-size: ${({ theme }) => theme.fontSize.h3};
font-weight: ${({ theme }) => theme.fontWeight.medium};
diff --git a/frontend/src/components/common/Modal/Header/Header.tsx b/frontend/src/components/common/Modal/Header/Header.tsx
index 0872df1fe..9904fd24d 100644
--- a/frontend/src/components/common/Modal/Header/Header.tsx
+++ b/frontend/src/components/common/Modal/Header/Header.tsx
@@ -8,7 +8,7 @@ interface HeaderProps {
const Header = ({ title, subTitle, children }: HeaderProps) => {
return (
-
+
{title}
{subTitle && {subTitle}}
{children}
diff --git a/frontend/src/components/common/Modal/Modal.styles.ts b/frontend/src/components/common/Modal/Modal.styles.ts
index 25008c92b..90040ca3b 100644
--- a/frontend/src/components/common/Modal/Modal.styles.ts
+++ b/frontend/src/components/common/Modal/Modal.styles.ts
@@ -1,5 +1,7 @@
import styled, { keyframes, css } from 'styled-components';
+import { Z_INDEX } from '@/constants/style';
+
import type { Size, Position, BackdropType } from './Modal.type';
const fadeIn = keyframes`
@@ -40,6 +42,7 @@ export const Layout = styled.div<{ $position: Position }>`
position: fixed;
top: 0;
+ z-index: ${Z_INDEX.MODAL};
width: 100%;
height: 100%;
diff --git a/frontend/src/components/common/Modal/Modal.tsx b/frontend/src/components/common/Modal/Modal.tsx
index 7b13e4b92..9ea363053 100644
--- a/frontend/src/components/common/Modal/Modal.tsx
+++ b/frontend/src/components/common/Modal/Modal.tsx
@@ -1,5 +1,6 @@
import { createPortal } from 'react-dom';
+import useAriaTrap from '@/hooks/common/useAriaTrap';
import useEscapeKey from '@/hooks/common/useEscapeKey';
import useFocusTrap from '@/hooks/common/useFocusTrap';
import usePreventScroll from '@/hooks/common/usePreventScroll';
@@ -31,13 +32,14 @@ const Modal = ({
}: React.PropsWithChildren) => {
const modalRef = useFocusTrap(isOpen);
+ useAriaTrap(isOpen, 'root');
useEscapeKey(isOpen, close);
usePreventScroll(isOpen);
if (!isOpen) return null;
return createPortal(
-
+
{children}
diff --git a/frontend/src/components/common/ScrollIcon/ScrollIcon.styles.ts b/frontend/src/components/common/ScrollIcon/ScrollIcon.styles.ts
index f96305e07..70714dc09 100644
--- a/frontend/src/components/common/ScrollIcon/ScrollIcon.styles.ts
+++ b/frontend/src/components/common/ScrollIcon/ScrollIcon.styles.ts
@@ -1,6 +1,8 @@
import { RiArrowDownDoubleLine } from 'react-icons/ri';
import styled, { css, keyframes } from 'styled-components';
+import { Z_INDEX } from '@/constants/style';
+
const bounce = keyframes`
0%, 20%, 50%, 80%, 100% {
transform: translateY(0);
@@ -37,7 +39,7 @@ export const Layout = styled.div<{ $isBottom: boolean }>`
position: fixed;
bottom: 2rem;
left: calc(50% - 3rem);
- z-index: 10;
+ z-index: ${Z_INDEX.PLUS};
padding: 1.5rem;
border-radius: 3rem;
diff --git a/frontend/src/components/common/ScrollIcon/ScrollIcon.tsx b/frontend/src/components/common/ScrollIcon/ScrollIcon.tsx
index 6f1063855..3bd2b38ec 100644
--- a/frontend/src/components/common/ScrollIcon/ScrollIcon.tsx
+++ b/frontend/src/components/common/ScrollIcon/ScrollIcon.tsx
@@ -19,7 +19,13 @@ const ScrollIcon = ({ targetSections }: ScrollIconProps) => {
return (
-
+
);
};
diff --git a/frontend/src/components/common/Spinner/Spinner.styles.ts b/frontend/src/components/common/Spinner/Spinner.styles.ts
index da4cc50b0..9a5f61f65 100644
--- a/frontend/src/components/common/Spinner/Spinner.styles.ts
+++ b/frontend/src/components/common/Spinner/Spinner.styles.ts
@@ -19,6 +19,15 @@ const spinnerSizes = {
xl: '16rem',
};
+export const Layout = styled.div`
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ width: fit-content;
+ height: fit-content;
+`;
+
export const Spinner = styled.div<{ $size: SpinnerSize }>`
display: flex;
justify-content: center;
diff --git a/frontend/src/components/common/Spinner/Spinner.tsx b/frontend/src/components/common/Spinner/Spinner.tsx
index 1e3e61fe1..df2860616 100644
--- a/frontend/src/components/common/Spinner/Spinner.tsx
+++ b/frontend/src/components/common/Spinner/Spinner.tsx
@@ -9,10 +9,12 @@ interface SpinnerProps {
const Spinner = ({ size = 'md', color = 'primary' }: SpinnerProps) => {
return (
-
-
-
-
+
+
+
+
+
+
);
};
diff --git a/frontend/src/components/common/Toast/Toast.styles.ts b/frontend/src/components/common/Toast/Toast.styles.ts
index 5c3f9bf5a..2c92dcd88 100644
--- a/frontend/src/components/common/Toast/Toast.styles.ts
+++ b/frontend/src/components/common/Toast/Toast.styles.ts
@@ -51,6 +51,7 @@ const backgroundMapper: Record> = {
export const Layout = styled.div<{ $isOpen: boolean; $isPush: boolean; $status: Status }>`
display: flex;
align-items: center;
+ gap: 0.8rem;
width: 30rem;
min-height: 5rem;
diff --git a/frontend/src/components/common/Toast/Toast.tsx b/frontend/src/components/common/Toast/Toast.tsx
index d2c421766..84293cacd 100644
--- a/frontend/src/components/common/Toast/Toast.tsx
+++ b/frontend/src/components/common/Toast/Toast.tsx
@@ -18,8 +18,9 @@ const TOAST_IMOJI: Record = {
const Toast = ({ isOpen, isPush, message, status = 'ERROR' }: ToastProps) => {
return (
-
- {`${TOAST_IMOJI[status]} ${message}`}
+
+ {TOAST_IMOJI[status]}
+ {message}
);
};
diff --git a/frontend/src/components/common/ToastList/ToastList.styles.ts b/frontend/src/components/common/ToastList/ToastList.styles.ts
index a905ed2a2..e0e844b76 100644
--- a/frontend/src/components/common/ToastList/ToastList.styles.ts
+++ b/frontend/src/components/common/ToastList/ToastList.styles.ts
@@ -1,5 +1,7 @@
import styled from 'styled-components';
+import { Z_INDEX } from '@/constants/style';
+
export const Layout = styled.div`
display: flex;
flex-direction: column;
@@ -8,5 +10,5 @@ export const Layout = styled.div`
position: fixed;
top: 9rem;
right: 2rem;
- z-index: 9999;
+ z-index: ${Z_INDEX.TOAST};
`;
diff --git a/frontend/src/components/common/ToolTipQuestionBox/ToolTipQuestionBox.tsx b/frontend/src/components/common/ToolTipQuestionBox/ToolTipQuestionBox.tsx
index b254a1967..32d787ba0 100644
--- a/frontend/src/components/common/ToolTipQuestionBox/ToolTipQuestionBox.tsx
+++ b/frontend/src/components/common/ToolTipQuestionBox/ToolTipQuestionBox.tsx
@@ -20,7 +20,7 @@ const ToolTipQuestionBox = ({
}: ToolTipQuestionBoxProps) => {
return (
-
+
);
};
diff --git a/frontend/src/components/common/Tooltip/Tooltip.styles.ts b/frontend/src/components/common/Tooltip/Tooltip.styles.ts
index b66159404..b238a842a 100644
--- a/frontend/src/components/common/Tooltip/Tooltip.styles.ts
+++ b/frontend/src/components/common/Tooltip/Tooltip.styles.ts
@@ -2,6 +2,8 @@ import styled, { css, keyframes } from 'styled-components';
import { Direction } from '@/components/common/Tooltip/Tooltip.type';
+import { Z_INDEX } from '@/constants/style';
+
const fadeIn = keyframes`
0% {
opacity: 0;
@@ -52,8 +54,9 @@ const directionStyle = (direction: Direction, color: string) => {
top: 100%;
left: 50%;
- transform: translateX(-50%);
border-color: ${color} transparent transparent transparent;
+
+ transform: translateX(-50%);
}
`;
case 'bottom':
@@ -68,8 +71,9 @@ const directionStyle = (direction: Direction, color: string) => {
bottom: 100%;
left: 50%;
- transform: translateX(-50%);
border-color: transparent transparent ${color} transparent;
+
+ transform: translateX(-50%);
}
`;
case 'left':
@@ -84,8 +88,9 @@ const directionStyle = (direction: Direction, color: string) => {
top: 50%;
left: 100%;
- transform: translateY(-50%);
border-color: transparent transparent transparent ${color};
+
+ transform: translateY(-50%);
}
`;
case 'right':
@@ -100,8 +105,9 @@ const directionStyle = (direction: Direction, color: string) => {
top: 50%;
right: 100%;
- transform: translateY(-50%);
border-color: transparent ${color} transparent transparent;
+
+ transform: translateY(-50%);
}
`;
}
@@ -111,7 +117,7 @@ export const Content = styled.div<{ $color: string; $direction: Direction }>`
display: none;
position: absolute;
- z-index: 100;
+ z-index: ${Z_INDEX.TOOLTIP};
width: fit-content;
min-width: 20rem;
diff --git a/frontend/src/components/common/Tooltip/Tooltip.tsx b/frontend/src/components/common/Tooltip/Tooltip.tsx
index 8aaff2e19..e4bba4b12 100644
--- a/frontend/src/components/common/Tooltip/Tooltip.tsx
+++ b/frontend/src/components/common/Tooltip/Tooltip.tsx
@@ -17,7 +17,7 @@ const Tooltip = ({
children,
}: React.PropsWithChildren) => {
return (
-
+
{children}
{message}
diff --git a/frontend/src/constants/button.ts b/frontend/src/constants/button.ts
deleted file mode 100644
index 86f0772be..000000000
--- a/frontend/src/constants/button.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-export const BUTTON_TEXT = {
- CLOSE: '닫기',
- BACK: '이전',
- NEXT: '다음',
- COMPLETE: '완료',
- CANCEL: '취소',
- CONFIRM: '확인',
-};
diff --git a/frontend/src/constants/message.ts b/frontend/src/constants/message.ts
index ab15aedff..d297cc138 100644
--- a/frontend/src/constants/message.ts
+++ b/frontend/src/constants/message.ts
@@ -1,18 +1,27 @@
export const ERROR_MESSAGES = {
- GET_REFERENCE_LINKS: '레퍼런스 링크를 불러오지 못했습니다.',
- ADD_REFERENCE_LINKS: '레퍼런스 링크를 저장하지 못했습니다.',
- DELETE_REFERENCE_LINKS: '레퍼런스 링크 삭제에 실패했습니다.',
- GET_PAIR_ROOM: '페어룸 정보를 불러오지 못했습니다.',
- ADD_PAIR_NAMES: '페어룸 생성에 실패했습니다.',
- GET_TODOS: '투두 리스트를 불러오지 못했습니다.',
- ADD_TODO: '투두 아이템을 저장하지 못했습니다.',
- UPDATE_TODO: '투두 아이템을 수정하지 못했습니다.',
- DELETE_TODO: '투두 아이템을 삭제하지 못했습니다.',
- GET_CATEGORIES: '카테고리 정보를 가져오지 못했어요 🥲',
- ADD_CATEGORY: '카테고리를 추가하지 못했어요 🥲',
- SIGN_IN: '로그인에 실패했습니다.',
- SIGN_UP: '회원가입에 실패했습니다.',
- SIGN_OUT: '로그아웃에 실패했습니다.',
- CHECK_USER_LOGIN: '유저 로그인 여부를 확인하지 못했습니다.',
- GET_MEMBER: '유저 정보를 가져오지 못했습니다.',
+ GET_REFERENCE_LINKS: '레퍼런스 링크를 불러오지 못했습니다. 다시 시도해 주세요.',
+ ADD_REFERENCE_LINKS: '레퍼런스 링크를 저장하지 못했습니다. 다시 시도해 주세요.',
+ DELETE_REFERENCE_LINKS: '레퍼런스 링크 삭제에 실패했습니다. 다시 시도해 주세요.',
+ GET_PAIR_ROOM: '페어룸 정보를 불러오지 못했습니다. 다시 시도해 주세요.',
+ DELETE_PAIR_ROOM: '페어룸 삭제에 실패했습니다. 다시 시도해 주세요.',
+ ADD_PAIR_NAMES: '페어룸 생성에 실패했습니다. 다시 시도해 주세요.',
+ GET_TODOS: '투두 리스트를 불러오지 못했습니다. 다시 시도해 주세요.',
+ ADD_TODO: '투두 아이템을 저장하지 못했습니다. 다시 시도해 주세요.',
+ UPDATE_TODO: '투두 아이템을 수정하지 못했습니다. 다시 시도해 주세요.',
+ DELETE_TODO: '투두 아이템을 삭제하지 못했습니다. 다시 시도해 주세요.',
+ GET_CATEGORIES: '카테고리 정보를 가져오지 못했습니다. 다시 시도해 주세요.',
+ ADD_CATEGORY: '카테고리를 추가하지 못했습니다. 다시 시도해 주세요.',
+ SIGN_IN: '로그인에 실패했습니다. 다시 시도해 주세요.',
+ SIGN_UP: '회원가입에 실패했습니다. 다시 시도해 주세요.',
+ SIGN_OUT: '로그아웃에 실패했습니다. 다시 시도해 주세요.',
+ CHECK_USER_LOGIN: '로그인 여부를 확인하지 못했습니다. 다시 시도해 주세요.',
+ GET_MEMBER: '회원 정보를 가져오지 못했습니다. 다시 시도해 주세요.',
+ DELETE_MEMBER: '회원 탈퇴에 실패했습니다. 다시 시도해 주세요.',
+ ADD_RETROSPECT: '회고를 작성하지 못했어요. 다시 시도해 주세요.',
+ GET_RETROSPECT: '회고 내용을 불러오지 못했어요. 새로고침 해 주세요.',
+ UPDATE_PAIR_ROOM_STATUS: '페어 프로그래밍을 완료하는 데 실패했습니다. 다시 시도해 주세요.',
+ GET_USER_IS_IN_PAIR_ROOM: '페어룸 참여 여부를 확인하지 못했습니다. 다시 시도해 주세요.',
+ GET_USER_RETROSPECTS: '회고 정보를 가져오지 못했습니다. 다시 시도해 주세요.',
+ GET_USER_RETROSPECT_EXISTS: '회고 작성 여부를 확인하지 못했습니다. 다시 시도해 주세요.',
+ DELETE_RETROSPECT: '회고를 삭제하지 못했어요',
};
diff --git a/frontend/src/constants/mypage.ts b/frontend/src/constants/mypage.ts
new file mode 100644
index 000000000..ba09169c6
--- /dev/null
+++ b/frontend/src/constants/mypage.ts
@@ -0,0 +1,12 @@
+import { TabConfig } from '@/pages/MyPage/MyPage.type';
+
+export const TAB_CONFIG: TabConfig[] = [
+ {
+ key: 'pairRoom',
+ title: '나의 페어룸',
+ },
+ {
+ key: 'retrospect',
+ title: '나의 회고',
+ },
+];
diff --git a/frontend/src/constants/queryKeys.ts b/frontend/src/constants/queryKeys.ts
index 8f48ef906..13e0cf1c0 100644
--- a/frontend/src/constants/queryKeys.ts
+++ b/frontend/src/constants/queryKeys.ts
@@ -4,10 +4,15 @@ export const QUERY_KEYS = {
GET_PAIR_ROOM_TIMER: 'getPairRoomTimer',
GET_PAIR_ROOM_HISTORY: 'getPairRoomHistory',
GET_MY_PAIR_ROOMS: 'getMyPairRooms',
+ GET_MEMBER_NAME: 'getMemberName',
GET_REPOSITORIES: 'getRepositories',
GET_BRANCHES: 'getBranches',
GET_CATEGORIES: 'getCategories',
GET_SIGN_IN: 'getSignIn',
GET_SIGN_OUT: 'getSignOut',
GET_TODOS: 'getTodos',
+ GET_RETROSPECT_ANSWER: 'getRetrospectAnswer',
+ GET_USER_IS_IN_PAIR_ROOM: 'getUserIsInPairRoom',
+ GET_USER_RETROSPECT_EXISTS: 'getUserRetrospectExists',
+ GET_MY_RETROSPECTS: 'getUserRetrospects',
};
diff --git a/frontend/src/constants/retrospect.ts b/frontend/src/constants/retrospect.ts
new file mode 100644
index 000000000..008f3078f
--- /dev/null
+++ b/frontend/src/constants/retrospect.ts
@@ -0,0 +1,33 @@
+export const RETROSPECT_QUESTIONS = [
+ {
+ title: '자신의 의견 대신 페어의 의견을 수용한 적이 있나요?',
+ subtitle: '그 과정에서 무엇을 배웠고, 어떤 것을 느꼈나요?',
+ id: '1',
+ },
+ {
+ title: '내 의견을 페어에게 효과적으로 설명하고 이해시켰나요?',
+ subtitle: '의견 전달 과정에서 어려웠던 점은 무엇인가요?',
+ id: '2',
+ },
+ {
+ title: '상대방의 코드를 이해하지 못한 때가 있었나요?',
+ subtitle: '질문을 통해 모르는 부분에 대해 소통했나요? 새롭게 배운 내용을 적어 주셔도 좋아요.',
+ id: '3',
+ },
+ {
+ title: '다음 페어 프로그래밍을 위해 내가 개선해야 할 점은 무엇인가요?',
+ subtitle:
+ '이번 페어 프로그래밍을 통해 아쉬웠던 점은 무엇이었나요? 혹은 다음 페어 프로그래밍에서 시도해 보고 싶은 것이 있나요?',
+ id: '4',
+ },
+ {
+ title: '페어 프로그래밍을 하면서 만족한 점이 있었나요?',
+ subtitle: '다음 페어 프로그래밍 때도 이어나가고 싶은 점을 적어 주세요.',
+ id: '5',
+ },
+ {
+ title: '추가로 작성하고 싶은 내용이 있다면 자유롭게 작성해 주세요!',
+ subtitle: '느낀 점, 배운 것들... 어떤 내용이든 괜찮아요.',
+ id: '6',
+ },
+];
diff --git a/frontend/src/constants/style.ts b/frontend/src/constants/style.ts
new file mode 100644
index 000000000..1b7eb1635
--- /dev/null
+++ b/frontend/src/constants/style.ts
@@ -0,0 +1,9 @@
+export const Z_INDEX = {
+ MINUS: -1,
+ PLUS: 1,
+ DROPDOWN: 99,
+ HEADER: 100,
+ TOOLTIP: 999,
+ MODAL: 9998,
+ TOAST: 9999,
+};
diff --git a/frontend/src/hooks/PairRoom/useEditCategory.ts b/frontend/src/hooks/PairRoom/useEditCategory.ts
index e948214a1..3209788ce 100644
--- a/frontend/src/hooks/PairRoom/useEditCategory.ts
+++ b/frontend/src/hooks/PairRoom/useEditCategory.ts
@@ -1,7 +1,5 @@
import { useState } from 'react';
-import { validateCategory } from '@/validations/validateCategory';
-
import useToastStore from '@/stores/toastStore';
import useInput from '@/hooks/common/useInput';
@@ -9,53 +7,54 @@ import useCategories from '@/hooks/PairRoom/useCategories';
import { useDeleteCategory, useUpdateCategory } from '@/queries/PairRoom/category/mutation';
-const useEditCategory = (accessCode: string, categoryName: string, categoryId: string) => {
+import { validateCategoryName } from '@/validations/validateCategory';
+
+const useEditCategory = (accessCode: string, categoryId: string, categoryName: string) => {
const { addToast } = useToastStore();
const [isEditing, setIsEditing] = useState(false);
+
const { value, handleChange, resetValue, message, status } = useInput(categoryName);
- const { isCategoryExist } = useCategories(accessCode);
+ const { isCategoryExist } = useCategories(accessCode);
const updateCategoryMutation = useUpdateCategory();
const deleteCategoryMutation = useDeleteCategory();
const startEditing = () => setIsEditing(true);
- const cancelEditing = () => {
- setIsEditing(false);
+
+ const stopEditing = () => {
resetValue();
+ setIsEditing(false);
};
- const editCategory = (event: React.ChangeEvent, prevCategoryName: string) => {
- handleChange(event, validateCategory(event.target.value, isCategoryExist, prevCategoryName));
+ const handleCategoryName = (event: React.ChangeEvent, prevCategoryName: string) => {
+ handleChange(event, validateCategoryName(event.target.value, isCategoryExist, prevCategoryName));
};
- const updateCategory = async () => {
- if (value === categoryName) return;
- if (status === 'ERROR') return;
- await updateCategoryMutation.mutateAsync({
- categoryId,
- updatedCategoryName: value,
- accessCode,
- });
- setIsEditing(false);
- addToast({ status: 'SUCCESS', message: '카테고리 이름이 수정되었어요.' });
+ const updateCategoryName = async () => {
+ if (value === categoryName) {
+ stopEditing();
+ return;
+ }
+
+ await updateCategoryMutation.mutateAsync({ categoryId, updatedCategoryName: value, accessCode });
+ addToast({ status: 'SUCCESS', message: '카테고리가 수정되었습니다.' });
+ stopEditing();
};
- const deleteCategory = async () => {
+ const deleteCategoryName = async () => {
await deleteCategoryMutation.mutateAsync({ categoryId, accessCode });
- addToast({ status: 'SUCCESS', message: '카테고리가 삭제되었어요.' });
+ addToast({ status: 'SUCCESS', message: '카테고리가 삭제되었습니다.' });
};
return {
+ newCategoryName: { value, message, status },
+ handleCategoryName,
isEditing,
- categoryInputData: { value, message, status },
- actions: {
- startEditing,
- cancelEditing,
- editCategory,
- updateCategory,
- deleteCategory,
- },
+ startEditing,
+ stopEditing,
+ updateCategoryName,
+ deleteCategoryName,
};
};
diff --git a/frontend/src/hooks/PairRoom/usePairRoomValid.ts b/frontend/src/hooks/PairRoom/usePairRoomValid.ts
new file mode 100644
index 000000000..5ab4ea3f6
--- /dev/null
+++ b/frontend/src/hooks/PairRoom/usePairRoomValid.ts
@@ -0,0 +1,41 @@
+import { useEffect } from 'react';
+import { useNavigate, useLocation } from 'react-router-dom';
+
+import useToastStore from '@/stores/toastStore';
+
+import { getPairRoomExists } from '@/apis/pairRoom';
+
+const usePairRoomValid = (accessCode: string) => {
+ const location = useLocation();
+ const navigate = useNavigate();
+
+ const { addToast } = useToastStore();
+
+ useEffect(() => {
+ const checkAccessValid = async () => {
+ if (!location.state?.valid) {
+ navigate('/main');
+ addToast({ status: 'ERROR', message: '유효하지 않은 접근입니다. 올바른 경로로 접근해 주세요.' });
+ return;
+ }
+
+ if (!accessCode) {
+ navigate('/main');
+ addToast({ status: 'ERROR', message: '존재하지 않는 페어룸 코드입니다. 다시 확인해 주세요.' });
+ return;
+ }
+
+ const { exists } = await getPairRoomExists(accessCode);
+
+ if (!exists) {
+ navigate('/main');
+ addToast({ status: 'ERROR', message: '존재하지 않는 페어룸 코드입니다. 다시 확인해 주세요.' });
+ return;
+ }
+ };
+
+ checkAccessValid();
+ }, [accessCode]);
+};
+
+export default usePairRoomValid;
diff --git a/frontend/src/hooks/PairRoom/useTimer.ts b/frontend/src/hooks/PairRoom/useTimer.ts
index 58a6a55c0..a252da688 100644
--- a/frontend/src/hooks/PairRoom/useTimer.ts
+++ b/frontend/src/hooks/PairRoom/useTimer.ts
@@ -53,6 +53,12 @@ const useTimer = (accessCode: string, defaultTime: number, defaultTimeleft: numb
const sse = getSSEConnection(accessCode);
const handleStatus = (event: MessageEvent) => {
+ if (event.data === 'complete') {
+ navigate(`/room/${accessCode}/retrospectForm`, { state: { valid: true } });
+ addToast({ status: 'WARNING', message: '페어룸이 종료되었습니다.' });
+ return;
+ }
+
if (event.data === 'start') {
setIsActive(true);
addToast({ status: 'SUCCESS', message: '타이머가 시작되었습니다.' });
diff --git a/frontend/src/hooks/PairRoomOnboarding/usePairRoomMission.ts b/frontend/src/hooks/PairRoomOnboarding/useMissionBranch.ts
similarity index 59%
rename from frontend/src/hooks/PairRoomOnboarding/usePairRoomMission.ts
rename to frontend/src/hooks/PairRoomOnboarding/useMissionBranch.ts
index 8e774f9af..2ad499416 100644
--- a/frontend/src/hooks/PairRoomOnboarding/usePairRoomMission.ts
+++ b/frontend/src/hooks/PairRoomOnboarding/useMissionBranch.ts
@@ -1,33 +1,21 @@
-import { useState } from 'react';
-
-import { validateBranchName } from '@/validations/validateBranchName';
-
import useInput from '@/hooks/common/useInput';
-const usePairRoomMission = () => {
- const [repositoryName, setRepositoryName] = useState('');
+import { validateBranchName } from '@/validations/validateBranchName';
+const useMissionBranch = () => {
const { value, status, message, handleChange, resetValue } = useInput();
- const isRepositorySelected = repositoryName !== '';
const isValidBranchName = status === 'DEFAULT' && value !== '' && value.length <= 30;
- const handleRepositoryName = (name: string) => {
- setRepositoryName(name);
- resetValue();
- };
-
const handleBranchName = (event: React.ChangeEvent, existingBranches: string[]) =>
handleChange(event, validateBranchName(event.target.value, existingBranches));
return {
- repositoryName,
branchName: { value, status, message },
- isRepositorySelected,
isValidBranchName,
- handleRepositoryName,
+ resetBranchName: resetValue,
handleBranchName,
};
};
-export default usePairRoomMission;
+export default useMissionBranch;
diff --git a/frontend/src/hooks/PairRoomOnboarding/usePairRoomInformation.ts b/frontend/src/hooks/PairRoomOnboarding/usePairRoomInformation.ts
index c41fe3bd9..63d6e2c0f 100644
--- a/frontend/src/hooks/PairRoomOnboarding/usePairRoomInformation.ts
+++ b/frontend/src/hooks/PairRoomOnboarding/usePairRoomInformation.ts
@@ -1,100 +1,112 @@
import { useState } from 'react';
+import { InputType, InputStatus } from '@/components/common/Input/Input.type';
+
+import useUserStore from '@/stores/userStore';
+
import { validateName, validateDuplicateName } from '@/validations/validatePairName';
import { validateTimerDuration } from '@/validations/validateTimerDuration';
-import { InputType, InputStatus } from '@/components/common/Input/Input.type';
-
export type Role = 'DRIVER' | 'NAVIGATOR';
const usePairRoomInformation = () => {
- const [firstPairName, setFirstPairName] = useState({
- value: '',
+ const { username, userStatus } = useUserStore();
+
+ const [userPairName, setUserPairName] = useState({
+ value: userStatus === 'SIGNED_IN' ? username : '',
status: 'DEFAULT' as InputStatus,
message: '',
});
- const [secondPairName, setSecondPairName] = useState({
+
+ const [pairName, setPairName] = useState({
value: '',
status: 'DEFAULT' as InputStatus,
message: '',
});
+ const [pairId, setPairId] = useState('');
const [driver, setDriver] = useState('');
const [navigator, setNavigator] = useState('');
const [timerDuration, setTimerDuration] = useState('');
- const isPairNameValid =
- firstPairName.value !== '' &&
- secondPairName.value !== '' &&
- firstPairName.status !== 'ERROR' &&
- secondPairName.status !== 'ERROR';
+ const isPairRoomNameValid =
+ userPairName.value !== '' &&
+ pairName.value !== '' &&
+ userPairName.status !== 'ERROR' &&
+ pairName.status !== 'ERROR';
const isPairRoleValid = driver !== '' && navigator !== '';
-
const isTimerDurationValid = timerDuration !== '' && validateTimerDuration(timerDuration);
- const handlePairName = (firstPairName: string, secondPairName: string) => {
- const isValidFirstPairName = validateName(firstPairName);
- const isValidSecondPairName = validateName(secondPairName);
+ const updatePairNames = (userPairName: string, pairName: string) => {
+ const isValidUserPairName = validateName(userPairName);
+ const isValidPairName = validateName(pairName);
- const isDuplicateName = validateDuplicateName(firstPairName, secondPairName);
+ const isDuplicateName = validateDuplicateName(userPairName, pairName);
- setFirstPairName({
- value: firstPairName,
- status: isValidFirstPairName.status !== 'ERROR' ? isDuplicateName.status : isValidFirstPairName.status,
- message: isValidFirstPairName.status !== 'ERROR' ? isDuplicateName.message : isValidFirstPairName.message,
+ setUserPairName({
+ value: userPairName,
+ status: isValidUserPairName.status !== 'ERROR' ? isDuplicateName.status : isValidUserPairName.status,
+ message: isValidUserPairName.status !== 'ERROR' ? isDuplicateName.message : isValidUserPairName.message,
});
- setSecondPairName({
- value: secondPairName,
- status: isValidSecondPairName.status !== 'ERROR' ? isDuplicateName.status : isValidSecondPairName.status,
- message: isValidSecondPairName.status !== 'ERROR' ? isDuplicateName.message : isValidSecondPairName.message,
+ setPairName({
+ value: pairName,
+ status: isValidPairName.status !== 'ERROR' ? isDuplicateName.status : isValidPairName.status,
+ message: isValidPairName.status !== 'ERROR' ? isDuplicateName.message : isValidPairName.message,
});
};
- const handleFirstPairName = (event: React.ChangeEvent) => {
- if (firstPairName.value === driver || firstPairName.value === navigator) {
+ const handleUserPairName = (event: React.ChangeEvent) => {
+ if (userPairName.value === driver || userPairName.value === navigator) {
setDriver('');
setNavigator('');
}
- handlePairName(event.target.value, secondPairName.value);
+ updatePairNames(event.target.value, pairName.value);
};
- const handleSecondPairName = (event: React.ChangeEvent) => {
- if (secondPairName.value === driver || secondPairName.value === navigator) {
+ const handlePairName = (event: React.ChangeEvent) => {
+ if (pairName.value === driver || pairName.value === navigator) {
setDriver('');
setNavigator('');
}
- handlePairName(firstPairName.value, event.target.value);
+ updatePairNames(userPairName.value, event.target.value);
+ };
+
+ const handlePairData = (pairId: string, pairName: string) => {
+ setPairId(pairId);
+ updatePairNames(userPairName.value, pairName);
};
- const handlePairRole = (pairName: string, role: Role) => {
- const otherPair = firstPairName.value === pairName ? secondPairName.value : firstPairName.value;
+ const handlePairRole = (name: string, role: Role) => {
+ const otherPair = userPairName.value === name ? pairName.value : userPairName.value;
if (role === 'DRIVER') {
- setDriver(pairName);
+ setDriver(name);
setNavigator(otherPair);
} else {
setDriver(otherPair);
- setNavigator(pairName);
+ setNavigator(name);
}
};
const handleTimerDuration = (timerDuration: string) => setTimerDuration(timerDuration);
return {
- firstPairName,
- secondPairName,
+ userPairName,
+ pairId,
+ pairName,
driver,
navigator,
timerDuration,
- isPairNameValid,
+ isPairRoomNameValid,
isPairRoleValid,
isTimerDurationValid,
- handleFirstPairName,
- handleSecondPairName,
+ handleUserPairName,
+ handlePairName,
+ handlePairData,
handlePairRole,
handleTimerDuration,
};
diff --git a/frontend/src/hooks/Retrospect/useInputAnswer.ts b/frontend/src/hooks/Retrospect/useInputAnswer.ts
new file mode 100644
index 000000000..cddc20de5
--- /dev/null
+++ b/frontend/src/hooks/Retrospect/useInputAnswer.ts
@@ -0,0 +1,42 @@
+import { useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+
+import useToastStore from '@/stores/toastStore';
+
+import { useAddRetrospect } from '@/queries/Retrospect/useAddRetrospect';
+
+import { RETROSPECT_QUESTIONS } from '@/constants/retrospect';
+
+const useInputAnswer = (accessCode: string) => {
+ const { addToast } = useToastStore();
+
+ const [answers, setAnswers] = useState(Array(RETROSPECT_QUESTIONS.length).fill(''));
+
+ const { mutateAsync, isPending } = useAddRetrospect();
+
+ const navigate = useNavigate();
+
+ const handleChange = (index: number, value: string) => {
+ if (value.length > 1000) return;
+
+ const newAnswer = [...answers];
+ newAnswer[index] = value;
+
+ setAnswers(newAnswer);
+ };
+
+ const handleSubmit = async (event: React.FormEvent) => {
+ event.preventDefault();
+
+ if (isPending) return;
+
+ await mutateAsync({ accessCode, answers }).then(() => {
+ addToast({ status: 'SUCCESS', message: '회고 작성이 완료되었습니다.' });
+ navigate(`/room/${accessCode}/completed`, { state: { valid: true }, replace: true });
+ });
+ };
+
+ return { answers, handleChange, handleSubmit };
+};
+
+export default useInputAnswer;
diff --git a/frontend/src/hooks/common/useAriaTrap.ts b/frontend/src/hooks/common/useAriaTrap.ts
new file mode 100644
index 000000000..52e463687
--- /dev/null
+++ b/frontend/src/hooks/common/useAriaTrap.ts
@@ -0,0 +1,19 @@
+import { useEffect } from 'react';
+
+const useAriaTrap = (isOpen: boolean, elementId: string) => {
+ useEffect(() => {
+ const element = document.getElementById(elementId);
+
+ if (!element) return;
+
+ if (isOpen) {
+ element.setAttribute('aria-hidden', 'true');
+ } else {
+ element.removeAttribute('aria-hidden');
+ }
+
+ return () => element.removeAttribute('aria-hidden');
+ }, [isOpen]);
+};
+
+export default useAriaTrap;
diff --git a/frontend/src/hooks/common/useHashScroll.ts b/frontend/src/hooks/common/useHashScroll.ts
index d7c3d6ea9..1a58c441b 100644
--- a/frontend/src/hooks/common/useHashScroll.ts
+++ b/frontend/src/hooks/common/useHashScroll.ts
@@ -52,7 +52,15 @@ const useHashScroll = () => {
if (location.hash) {
const element = document.getElementById(currentHash);
if (element) {
- element.scrollIntoView({ behavior: 'smooth' });
+ const headerHeight = 70;
+ const marginTop = 50;
+ const elementPosition = element.getBoundingClientRect().top + window.scrollY;
+ const offsetPosition = elementPosition - headerHeight - marginTop;
+
+ window.scrollTo({
+ top: offsetPosition,
+ behavior: 'smooth',
+ });
}
}
}, [location, currentHash]);
diff --git a/frontend/src/hooks/common/usePreventPageRefresh.ts b/frontend/src/hooks/common/usePreventPageRefresh.ts
new file mode 100644
index 000000000..174489062
--- /dev/null
+++ b/frontend/src/hooks/common/usePreventPageRefresh.ts
@@ -0,0 +1,18 @@
+import { useEffect } from 'react';
+
+const usePreventPageRefresh = () => {
+ useEffect(() => {
+ const handleBeforeUnload = (event: BeforeUnloadEvent) => {
+ event.preventDefault();
+ event.returnValue = '';
+ };
+
+ window.addEventListener('beforeunload', handleBeforeUnload);
+
+ return () => {
+ window.removeEventListener('beforeunload', handleBeforeUnload);
+ };
+ }, []);
+};
+
+export default usePreventPageRefresh;
diff --git a/frontend/src/pages/CompletedPairRoom/CompletedPairRoom.styles.ts b/frontend/src/pages/CompletedPairRoom/CompletedPairRoom.styles.ts
new file mode 100644
index 000000000..47a691fb0
--- /dev/null
+++ b/frontend/src/pages/CompletedPairRoom/CompletedPairRoom.styles.ts
@@ -0,0 +1,97 @@
+import styled from 'styled-components';
+
+export const Layout = styled.div`
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 4rem;
+
+ width: 100%;
+ height: calc(100vh - 7rem);
+ min-height: 60rem;
+ padding: 2rem;
+
+ background: ${({ theme }) => theme.color.black[20]};
+`;
+
+export const InfoContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ justify-content: space-evenly;
+ align-items: center;
+
+ width: 30%;
+ min-width: 40rem;
+ height: calc(100vh - 7rem);
+ padding: 2rem;
+`;
+
+export const CardContainer = styled.div`
+ display: flex;
+ gap: 2rem;
+
+ width: 70%;
+`;
+
+export const TitleContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 3rem;
+`;
+
+export const Title = styled.div`
+ color: ${({ theme }) => theme.color.black[80]};
+ font-size: ${({ theme }) => theme.fontSize.h2};
+ font-weight: ${({ theme }) => theme.fontWeight.bold};
+`;
+
+export const PairInfoWrapper = styled.div`
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ padding: 1.2rem 1.5rem;
+ border-radius: 1rem;
+
+ background-color: ${({ theme }) => theme.color.black[30]};
+ color: ${({ theme }) => theme.color.black[70]};
+ font-size: ${({ theme }) => theme.fontSize.base};
+ font-weight: ${({ theme }) => theme.fontWeight.medium};
+`;
+
+export const FirstPair = styled.span`
+ color: ${({ theme }) => theme.color.primary[600]};
+`;
+
+export const SecondPair = styled.span`
+ color: ${({ theme }) => theme.color.secondary[600]};
+`;
+
+export const RepositoryButton = styled.button`
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 1.2rem;
+
+ width: fit-content;
+ height: 4rem;
+ padding: 1rem 2rem;
+ border-radius: 1rem;
+
+ background-color: ${({ theme }) => theme.color.black[80]};
+ color: ${({ theme }) => theme.color.black[10]};
+ font-size: ${({ theme }) => theme.fontSize.base};
+
+ transition: all 0.2s;
+
+ &:hover {
+ background-color: ${({ theme }) => theme.color.black[90]};
+
+ transform: scale(1.01);
+ }
+`;
+
+export const GithubLogo = styled.img`
+ width: 2rem;
+ height: 2rem;
+`;
diff --git a/frontend/src/pages/CompletedPairRoom/CompletedPairRoom.tsx b/frontend/src/pages/CompletedPairRoom/CompletedPairRoom.tsx
new file mode 100644
index 000000000..9391f8d7c
--- /dev/null
+++ b/frontend/src/pages/CompletedPairRoom/CompletedPairRoom.tsx
@@ -0,0 +1,52 @@
+import { Link, useParams } from 'react-router-dom';
+
+import { GithubLogoWhite } from '@/assets';
+
+import Loading from '@/pages/Loading/Loading';
+
+import ReferenceCard from '@/components/CompletedPairRoom/ReferenceCard/ReferenceCard';
+import RetrospectButton from '@/components/CompletedPairRoom/RetrospectButton/RetrospectButton';
+import TodoListCard from '@/components/CompletedPairRoom/TodoListCard/TodoListCard';
+
+import useGetPairRoom from '@/queries/PairRoom/useGetPairRoom';
+
+import * as S from './CompletedPairRoom.styles';
+
+const CompletedPairRoom = () => {
+ const { accessCode } = useParams();
+
+ const { driver, navigator, missionUrl, isFetching } = useGetPairRoom(accessCode || '');
+
+ if (isFetching) {
+ return ;
+ }
+
+ return (
+
+
+
+ {accessCode}
+
+ {driver}와(과)
+ {navigator}의 기록이에요
+
+
+ {missionUrl && (
+
+
+
+ 미션 리포지토리로 이동
+
+
+ )}
+
+
+
+
+
+
+
+ );
+};
+
+export default CompletedPairRoom;
diff --git a/frontend/src/pages/Error/Error.tsx b/frontend/src/pages/Error/Error.tsx
index 534e9f073..0b60eefee 100644
--- a/frontend/src/pages/Error/Error.tsx
+++ b/frontend/src/pages/Error/Error.tsx
@@ -12,7 +12,7 @@ const Error = () => {
페이지를 불러오는 중 문제가 발생했습니다.
-
+
홈으로 이동
diff --git a/frontend/src/pages/Landing/Landing.tsx b/frontend/src/pages/Landing/Landing.tsx
index a793eb273..dc8c875bf 100644
--- a/frontend/src/pages/Landing/Landing.tsx
+++ b/frontend/src/pages/Landing/Landing.tsx
@@ -18,45 +18,47 @@ import useSignInHandler from '@/hooks/member/useSignInHandler';
const Landing = () => {
const navigate = useNavigate();
- const targetSections: TargetSection[] = [
- { id: 'landing', position: 'top' },
- { id: 'how-to-pair', position: 'bottom' },
- ];
- useTitleTime();
- usePreventBackNavigation();
-
const { userStatus } = useUserStore();
- const { handleSignInGithub } = useSignInHandler();
useEffect(() => {
if (userStatus === 'SIGNED_IN') navigate('/main');
}, [userStatus]);
+ const { handleSignInGithub } = useSignInHandler();
+
+ useTitleTime();
+ usePreventBackNavigation();
+
+ const targetSections: TargetSection[] = [
+ { id: 'landing', position: 'top' },
+ { id: 'how-to-pair', position: 'bottom' },
+ ];
+
return (
<>
-
당신의 첫 번째 페어 프로그래밍,
-
+
-
+
Github로 로그인
- navigate('/main')}>
+ navigate('/main')}>
회원가입 없이 사용하기
+
>
);
};
diff --git a/frontend/src/pages/Layout.styles.ts b/frontend/src/pages/Layout.styles.ts
index fdbdd727d..3d7cc4b2c 100644
--- a/frontend/src/pages/Layout.styles.ts
+++ b/frontend/src/pages/Layout.styles.ts
@@ -3,6 +3,11 @@ import styled from 'styled-components';
export const Layout = styled.div`
display: flex;
flex-direction: column;
+ overflow: hidden;
min-width: fit-content;
`;
+
+export const Main = styled.main`
+ margin-top: 7rem;
+`;
diff --git a/frontend/src/pages/Layout.tsx b/frontend/src/pages/Layout.tsx
index 7515fd333..30189a69a 100644
--- a/frontend/src/pages/Layout.tsx
+++ b/frontend/src/pages/Layout.tsx
@@ -9,9 +9,9 @@ const Layout = () => {
return (
-
+
-
+
);
diff --git a/frontend/src/pages/Main/Main.tsx b/frontend/src/pages/Main/Main.tsx
index fc9db0726..2810d6ae0 100644
--- a/frontend/src/pages/Main/Main.tsx
+++ b/frontend/src/pages/Main/Main.tsx
@@ -30,18 +30,22 @@ const Main = () => {
-
-
- 협업과 성장을 위한
-
- 페어프로그래밍-
+
+
+
+ 협업과 성장을 위한
+
+ 페어 프로그래밍-
+
-
- 코딩해듀오
+
+
+ 코딩해듀오
+
-
- 코딩해듀오는 페어프로그래밍을 통해 더 나은 결과를 만들어내는 것을 목표로 합니다.
+
+ 코딩해듀오는 페어 프로그래밍을 통해 더 나은 결과를 만들어내는 것을 목표로 합니다.
직관적인 인터페이스와 실시간 협업 도구로, 누구나 쉽게 사용할 수 있습니다.
@@ -49,13 +53,13 @@ const Main = () => {
-
- 방 만들기
+
+ 페어룸 만들기
-
- 방 들어가기
+
+ 페어룸 들어가기
diff --git a/frontend/src/pages/MyPage/MyPage.styles.ts b/frontend/src/pages/MyPage/MyPage.styles.ts
index 6da414d33..9391ee832 100644
--- a/frontend/src/pages/MyPage/MyPage.styles.ts
+++ b/frontend/src/pages/MyPage/MyPage.styles.ts
@@ -44,12 +44,8 @@ export const SubTitle = styled.p`
export const ListWrapper = styled.div`
display: flex;
flex-direction: column;
+ align-items: center;
gap: 2.4rem;
-
- h2 {
- font-size: ${({ theme }) => theme.fontSize.h4};
- font-weight: ${({ theme }) => theme.fontWeight.medium};
- }
`;
export const List = styled.div`
@@ -65,11 +61,6 @@ export const AllText = styled.p`
font-size: ${({ theme }) => theme.fontSize.md};
`;
-export const EmptyText = styled.p`
- color: ${({ theme }) => theme.color.black[70]};
- font-size: ${({ theme }) => theme.fontSize.base};
-`;
-
export const LeaveButton = styled.button`
display: flex;
justify-content: flex-end;
@@ -88,3 +79,8 @@ export const LeaveButton = styled.button`
font-size: ${({ theme }) => theme.fontSize.md};
}
`;
+
+export const BottomLine = styled.div`
+ margin: 1rem 0;
+ border: 1px solid ${({ theme }) => theme.color.black[30]};
+`;
diff --git a/frontend/src/pages/MyPage/MyPage.tsx b/frontend/src/pages/MyPage/MyPage.tsx
index 58926a616..fe16cf251 100644
--- a/frontend/src/pages/MyPage/MyPage.tsx
+++ b/frontend/src/pages/MyPage/MyPage.tsx
@@ -1,18 +1,23 @@
import { IoIosArrowForward } from 'react-icons/io';
-import Spinner from '@/components/common/Spinner/Spinner';
-import PairRoomButton from '@/components/MyPage/PairRoomButton/PairRoomButton';
+import MyPageContent from '@/pages/MyPage/MyPageContent/MyPageContent';
+
+import ConfirmModal from '@/components/common/ConfirmModal/ConfirmModal';
import useUserStore from '@/stores/userStore';
-import useMyPairRooms from '@/queries/MyPage/useMyPairRooms';
+import useModal from '@/hooks/common/useModal';
+
+import useDeleteMember from '@/queries/MyPage/useDeleteMember';
import * as S from './MyPage.styles';
const MyPage = () => {
const { username } = useUserStore();
- const { data: pairRooms, isFetching } = useMyPairRooms();
+ const { isModalOpen, openModal, closeModal } = useModal();
+
+ const { handleDeleteMember } = useDeleteMember();
return (
@@ -22,35 +27,24 @@ const MyPage = () => {
{username} 님의 마이 페이지에 오신 걸 환영합니다!
+
- 나의 페어룸 목록
-
-
총 {pairRooms && pairRooms.length}개
-
- {isFetching && }
- {!isFetching && pairRooms?.length === 0 ? (
- 생성한 페어룸이 없습니다.
- ) : (
- pairRooms &&
- pairRooms.map((pairRoom) => (
-
- ))
- )}
-
-
+
-
+
회원 탈퇴하기
+
);
};
diff --git a/frontend/src/pages/MyPage/MyPage.type.ts b/frontend/src/pages/MyPage/MyPage.type.ts
new file mode 100644
index 000000000..8502381a6
--- /dev/null
+++ b/frontend/src/pages/MyPage/MyPage.type.ts
@@ -0,0 +1,5 @@
+export type CurrentTabType = 'pairRoom' | 'retrospect';
+export interface TabConfig {
+ key: CurrentTabType;
+ title: string;
+}
diff --git a/frontend/src/pages/MyPage/MyPageContent/ListLayout/ListLayout.styles.ts b/frontend/src/pages/MyPage/MyPageContent/ListLayout/ListLayout.styles.ts
new file mode 100644
index 000000000..901bc960d
--- /dev/null
+++ b/frontend/src/pages/MyPage/MyPageContent/ListLayout/ListLayout.styles.ts
@@ -0,0 +1,8 @@
+import styled from 'styled-components';
+
+export const EmptyText = styled.p`
+ margin-top: 5rem;
+
+ color: ${({ theme }) => theme.color.black[70]};
+ font-size: ${({ theme }) => theme.fontSize.base};
+`;
diff --git a/frontend/src/pages/MyPage/MyPageContent/ListLayout/ListLayout.tsx b/frontend/src/pages/MyPage/MyPageContent/ListLayout/ListLayout.tsx
new file mode 100644
index 000000000..35817c18c
--- /dev/null
+++ b/frontend/src/pages/MyPage/MyPageContent/ListLayout/ListLayout.tsx
@@ -0,0 +1,19 @@
+import Spinner from '@/components/common/Spinner/Spinner';
+
+import * as S from './ListLayout.styles';
+
+interface ListLayoutProps extends React.PropsWithChildren {
+ isFetching: boolean;
+ emptyMessage: string;
+ length: number;
+}
+
+const ListLayout = ({ isFetching, length, emptyMessage, children }: ListLayoutProps) => {
+ if (isFetching) {
+ return ;
+ }
+
+ return length < 1 ? {emptyMessage} : <>{children}>;
+};
+
+export default ListLayout;
diff --git a/frontend/src/pages/MyPage/MyPageContent/MyPageContent.tsx b/frontend/src/pages/MyPage/MyPageContent/MyPageContent.tsx
new file mode 100644
index 000000000..8753a2d83
--- /dev/null
+++ b/frontend/src/pages/MyPage/MyPageContent/MyPageContent.tsx
@@ -0,0 +1,68 @@
+import { useState } from 'react';
+
+import { CurrentTabType } from '@/pages/MyPage/MyPage.type';
+import ListLayout from '@/pages/MyPage/MyPageContent/ListLayout/ListLayout';
+import MyPageTab from '@/pages/MyPage/MyPageTab/MyPageTab';
+
+import PairRoomButton from '@/components/MyPage/PairRoomButton/PairRoomButton';
+import RetrospectButton from '@/components/MyPage/PairRoomButton/RetrospectButton';
+
+import { useMyPairRooms } from '@/queries/MyPage/useMyPairRooms';
+import { useGetRetrospects } from '@/queries/Retrospect/useGetRetrospects';
+
+import { TAB_CONFIG } from '@/constants/mypage';
+
+const MyPageContent = () => {
+ const [currentTab, setCurrentTab] = useState('pairRoom');
+
+ const handleTabClick = (tabKey: CurrentTabType) => {
+ setCurrentTab(tabKey);
+ };
+
+ const { myPairRoomList, myPairRoomLoading } = useMyPairRooms();
+ const { myRetrospects, myRetrospectLoading } = useGetRetrospects();
+
+ const myPairRoomLength = myPairRoomList?.length || 0;
+ const myRetrospectsLength = myRetrospects?.length || 0;
+
+ return (
+ <>
+
+
+ {currentTab === TAB_CONFIG[0].key && (
+
+ {myPairRoomList?.map((pairRoom) => (
+
+ ))}
+
+ )}
+ {currentTab === TAB_CONFIG[1].key && (
+
+ {myRetrospects?.map((retrospect) => (
+
+ ))}
+
+ )}
+ >
+ );
+};
+
+export default MyPageContent;
diff --git a/frontend/src/pages/MyPage/MyPageTab/MyPageTab.styles.ts b/frontend/src/pages/MyPage/MyPageTab/MyPageTab.styles.ts
new file mode 100644
index 000000000..1d455b624
--- /dev/null
+++ b/frontend/src/pages/MyPage/MyPageTab/MyPageTab.styles.ts
@@ -0,0 +1,36 @@
+import styled from 'styled-components';
+
+export const Tab = styled.button<{ $isActive: boolean }>`
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+
+ width: 20rem;
+ padding-bottom: 1rem;
+
+ cursor: pointer;
+
+ p {
+ color: ${({ theme }) => theme.color.black[80]};
+ font-size: ${({ theme }) => theme.fontSize.lg};
+ font-weight: ${({ theme }) => theme.fontWeight.medium};
+ }
+
+ ${({ theme, $isActive }) =>
+ $isActive &&
+ `
+ border-bottom: 2px solid ${theme.color.primary[700]};
+
+ p {
+ color: ${theme.color.primary[700]};
+ }
+ `}
+`;
+
+export const Layout = styled.div`
+ display: flex;
+ justify-content: center;
+ gap: 12rem;
+
+ width: 100%;
+`;
diff --git a/frontend/src/pages/MyPage/MyPageTab/MyPageTab.tsx b/frontend/src/pages/MyPage/MyPageTab/MyPageTab.tsx
new file mode 100644
index 000000000..933bb6041
--- /dev/null
+++ b/frontend/src/pages/MyPage/MyPageTab/MyPageTab.tsx
@@ -0,0 +1,24 @@
+import { CurrentTabType } from '@/pages/MyPage/MyPage.type';
+
+import { TAB_CONFIG } from '@/constants/mypage';
+
+import * as S from './MyPageTab.styles';
+interface MyPageTabProps {
+ length: number[];
+ currentTab: CurrentTabType;
+ handleTabClick: (tabKey: CurrentTabType) => void;
+}
+const MyPageTab = ({ length, currentTab, handleTabClick }: MyPageTabProps) => {
+ return (
+
+ {TAB_CONFIG.map((tab, index) => (
+ handleTabClick(tab.key)} $isActive={currentTab === tab.key}>
+
+ {tab.title} ({length[index]})
+
+
+ ))}
+
+ );
+};
+export default MyPageTab;
diff --git a/frontend/src/pages/PairRoom/PairRoom.tsx b/frontend/src/pages/PairRoom/PairRoom.tsx
index 07a376c0e..e9a5471c9 100644
--- a/frontend/src/pages/PairRoom/PairRoom.tsx
+++ b/frontend/src/pages/PairRoom/PairRoom.tsx
@@ -3,13 +3,14 @@ import { useNavigate, useParams } from 'react-router-dom';
import Loading from '@/pages/Loading/Loading';
+import GuideModal from '@/components/PairRoom/GuideModal/GuideModal';
import PairListCard from '@/components/PairRoom/PairListCard/PairListCard';
import PairRoleCard from '@/components/PairRoom/PairRoleCard/PairRoleCard';
import ReferenceCard from '@/components/PairRoom/ReferenceCard/ReferenceCard';
import TimerCard from '@/components/PairRoom/TimerCard/TimerCard';
import TodoListCard from '@/components/PairRoom/TodoListCard/TodoListCard';
-import { getPairRoomExists } from '@/apis/pairRoom';
+import useModal from '@/hooks/common/useModal';
import useGetPairRoom from '@/queries/PairRoom/useGetPairRoom';
import useUpdatePairRoom from '@/queries/PairRoom/useUpdatePairRoom';
@@ -20,44 +21,40 @@ const PairRoom = () => {
const navigate = useNavigate();
const { accessCode } = useParams();
- useEffect(() => {
- const checkPairRoomExists = async () => {
- if (!accessCode) navigate('/error');
-
- const { exists } = await getPairRoomExists(accessCode || '');
-
- if (!exists) navigate('/error');
- };
-
- checkPairRoomExists();
- }, [accessCode]);
-
const [driver, setDriver] = useState('');
const [navigator, setNavigator] = useState('');
+ const [isCardOpen, setIsCardOpen] = useState(false);
+
+ const { isModalOpen, closeModal } = useModal(true);
const {
driver: latestDriver,
navigator: latestNavigator,
+ status,
+ missionUrl,
duration,
remainingTime,
isFetching,
} = useGetPairRoom(accessCode || '');
+
const { handleUpdatePairRole } = useUpdatePairRoom(accessCode || '');
+ useEffect(() => {
+ if (status === 'COMPLETED') navigate(`/room/${accessCode}/completed`, { state: { valid: true }, replace: true });
+ }, [status]);
+
useEffect(() => {
setDriver(latestDriver);
setNavigator(latestNavigator);
}, [latestDriver, latestNavigator]);
- const [isCardOpen, setIsCardOpen] = useState(false);
-
if (isFetching) {
return ;
}
return (
- {}} />
+
{
setIsCardOpen(false)} />
setIsCardOpen(true)} />
+
);
};
diff --git a/frontend/src/pages/PairRoomOnboarding/PairRoomOnboarding.styles.ts b/frontend/src/pages/PairRoomOnboarding/PairRoomOnboarding.styles.ts
index 9a04487a6..eefd3e068 100644
--- a/frontend/src/pages/PairRoomOnboarding/PairRoomOnboarding.styles.ts
+++ b/frontend/src/pages/PairRoomOnboarding/PairRoomOnboarding.styles.ts
@@ -10,7 +10,7 @@ export const Layout = styled.div`
background-color: ${({ theme }) => theme.color.primary[100]};
`;
-export const Title = styled.h2`
+export const Title = styled.h1`
color: ${({ theme }) => theme.color.primary[800]};
font-size: ${({ theme }) => theme.fontSize.h3};
font-weight: ${({ theme }) => theme.fontWeight.semibold};
diff --git a/frontend/src/pages/PairRoomOnboarding/PairRoomOnboarding.tsx b/frontend/src/pages/PairRoomOnboarding/PairRoomOnboarding.tsx
index 7ebf3654a..a1d05ecef 100644
--- a/frontend/src/pages/PairRoomOnboarding/PairRoomOnboarding.tsx
+++ b/frontend/src/pages/PairRoomOnboarding/PairRoomOnboarding.tsx
@@ -1,3 +1,4 @@
+import { useState } from 'react';
import { useLocation } from 'react-router-dom';
import MissionSettingSection from '@/components/PairRoomOnboarding/MissionSettingSection/MissionSettingSection';
@@ -12,14 +13,26 @@ const PairRoomOnboarding = () => {
const searchParams = new URLSearchParams(location.search);
const mission = searchParams.get('mission');
+ const [repositoryName, setRepositoryName] = useState('');
+
const { handleCreateBranch, isSuccess } = useCreateBranch();
+ const handleRepositoryName = (repositoryName: string) => setRepositoryName(repositoryName);
+
return (
{mission === 'true' ? '미션과 함께 시작하기' : '그냥 시작하기'}
- {mission === 'true' && !isSuccess && }
- {((mission === 'true' && isSuccess) || mission === 'false') && }
+ {mission === 'true' && !isSuccess && (
+
+ )}
+ {((mission === 'true' && isSuccess) || mission === 'false') && (
+
+ )}
);
diff --git a/frontend/src/pages/PrivateRoutes.tsx b/frontend/src/pages/PrivateRoutes.tsx
new file mode 100644
index 000000000..fa6ded29d
--- /dev/null
+++ b/frontend/src/pages/PrivateRoutes.tsx
@@ -0,0 +1,47 @@
+import { useEffect, useState } from 'react';
+import { Navigate, Outlet, useLocation, useParams } from 'react-router-dom';
+
+import Loading from '@/pages/Loading/Loading';
+
+import useToastStore from '@/stores/toastStore';
+
+import { getPairRoomExists } from '@/apis/pairRoom';
+
+const PrivateRoutes = () => {
+ const location = useLocation();
+ const { accessCode } = useParams();
+
+ const [isValid, setIsValid] = useState(null);
+
+ const { addToast } = useToastStore();
+
+ const validateAccess = async () => {
+ if (!accessCode || !location.state?.valid) {
+ setIsValid(false);
+ addToast({ status: 'ERROR', message: '유효하지 않은 접근입니다. 올바른 경로로 접근해 주세요.' });
+ return;
+ }
+
+ const { exists } = await getPairRoomExists(accessCode || '');
+
+ if (!exists) {
+ setIsValid(false);
+ addToast({ status: 'ERROR', message: '유효하지 않은 접근입니다. 올바른 경로로 접근해 주세요.' });
+ return;
+ }
+
+ setIsValid(true);
+ };
+
+ useEffect(() => {
+ validateAccess();
+ }, []);
+
+ if (isValid === null) return ;
+
+ if (!isValid) return ;
+
+ return ;
+};
+
+export default PrivateRoutes;
diff --git a/frontend/src/pages/RetrospectForm/RetrospectForm.styles.ts b/frontend/src/pages/RetrospectForm/RetrospectForm.styles.ts
new file mode 100644
index 000000000..5a80bea03
--- /dev/null
+++ b/frontend/src/pages/RetrospectForm/RetrospectForm.styles.ts
@@ -0,0 +1,71 @@
+import styled, { css } from 'styled-components';
+
+const positionFixed = css`
+ position: fixed;
+ bottom: 0;
+ left: 50%;
+
+ transform: translate(-50%);
+`;
+
+export const buttonStyles = css`
+ ${positionFixed}
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ width: 60%;
+ min-width: 76.8rem;
+ height: 6rem;
+ border-radius: 0;
+
+ &:hover {
+ ${positionFixed}
+ }
+
+ &:active {
+ ${positionFixed}
+ }
+
+ @media (max-width: ${({ theme }) => theme.deviceWidth.mobile}) {
+ width: 100%;
+ }
+`;
+
+export const Layout = styled.div`
+ display: flex;
+ justify-content: center;
+
+ width: 100%;
+ min-height: calc(100vh - 7rem);
+
+ background-color: ${({ theme }) => theme.color.primary[100]};
+`;
+
+export const Container = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 5rem;
+
+ position: relative;
+
+ width: 60%;
+ min-width: 76.8rem;
+ padding: 4rem 4rem 12rem;
+
+ background-color: ${({ theme }) => theme.color.black[10]};
+
+ @media (max-width: ${({ theme }) => theme.deviceWidth.mobile}) {
+ width: 100%;
+ min-width: 0;
+ padding: 4rem 4rem 12rem;
+ }
+`;
+
+export const Form = styled.form`
+ display: flex;
+ flex-direction: column;
+ gap: 4rem;
+
+ width: 100%;
+`;
diff --git a/frontend/src/pages/RetrospectForm/RetrospectForm.tsx b/frontend/src/pages/RetrospectForm/RetrospectForm.tsx
new file mode 100644
index 000000000..29eee418e
--- /dev/null
+++ b/frontend/src/pages/RetrospectForm/RetrospectForm.tsx
@@ -0,0 +1,67 @@
+import { useParams, useNavigate } from 'react-router-dom';
+
+import Button from '@/components/common/Button/Button';
+import ConfirmModal from '@/components/common/ConfirmModal/ConfirmModal';
+import Header from '@/components/Retrospect/Header/Header';
+import Question from '@/components/Retrospect/Question/Question';
+import TextArea from '@/components/Retrospect/Textarea/Textarea';
+
+import useModal from '@/hooks/common/useModal';
+import usePreventPageRefresh from '@/hooks/common/usePreventPageRefresh';
+import useInputAnswer from '@/hooks/Retrospect/useInputAnswer';
+
+import { RETROSPECT_QUESTIONS } from '@/constants/retrospect';
+
+import * as S from './RetrospectForm.styles';
+
+const RetrospectForm = () => {
+ const navigate = useNavigate();
+ const { accessCode } = useParams();
+
+ usePreventPageRefresh();
+
+ const { isModalOpen, openModal, closeModal } = useModal();
+
+ const { answers, handleChange, handleSubmit } = useInputAnswer(accessCode || '');
+
+ const isAnswersEmpty = answers.every((answer) => !answer.trim());
+
+ return (
+
+
+
+
+ {RETROSPECT_QUESTIONS.map((question, index) => (
+
+
+ ))}
+
+ 작성 완료
+
+
+ navigate(`/room/${accessCode}/completed`, { state: { valid: true }, replace: true })}
+ />
+
+
+ );
+};
+
+export default RetrospectForm;
diff --git a/frontend/src/pages/RetrospectView/RetrospectView.styles.ts b/frontend/src/pages/RetrospectView/RetrospectView.styles.ts
new file mode 100644
index 000000000..e8f9ddbad
--- /dev/null
+++ b/frontend/src/pages/RetrospectView/RetrospectView.styles.ts
@@ -0,0 +1,50 @@
+import styled from 'styled-components';
+
+export const Layout = styled.div`
+ display: flex;
+ justify-content: center;
+
+ width: 100%;
+ min-height: calc(100vh - 7rem);
+
+ background-color: ${({ theme }) => theme.color.primary[100]};
+`;
+
+export const Container = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 5rem;
+
+ position: relative;
+
+ width: 60%;
+ min-width: 76.8rem;
+ padding: 4rem 4rem 12rem;
+
+ background-color: ${({ theme }) => theme.color.black[10]};
+
+ @media (max-width: ${({ theme }) => theme.deviceWidth.mobile}) {
+ width: 100%;
+ min-width: 0;
+ padding: 4rem 4rem 12rem;
+ }
+`;
+
+export const TextWrapper = styled.pre`
+ overflow-y: auto;
+
+ width: 100%;
+ margin: 0;
+ padding: 2rem;
+ border: 1px solid ${({ theme }) => theme.color.black[50]};
+ border-radius: 0.5rem;
+
+ background-color: ${({ theme }) => theme.color.black[20]};
+ color: ${({ theme }) => theme.color.black[80]};
+ font-size: ${({ theme }) => theme.fontSize.md};
+ font-weight: ${({ theme }) => theme.fontWeight.light};
+ line-height: 1.6;
+ white-space: pre-wrap;
+ word-break: break-all;
+ font-family: 'Pretendard Variable', sans-serif !important;
+`;
diff --git a/frontend/src/pages/RetrospectView/RetrospectView.tsx b/frontend/src/pages/RetrospectView/RetrospectView.tsx
new file mode 100644
index 000000000..26bdb8f96
--- /dev/null
+++ b/frontend/src/pages/RetrospectView/RetrospectView.tsx
@@ -0,0 +1,46 @@
+import { useNavigate, useParams } from 'react-router-dom';
+
+import Spinner from '@/components/common/Spinner/Spinner';
+import Header from '@/components/Retrospect/Header/Header';
+import Question from '@/components/Retrospect/Question/Question';
+
+import { useGetRetrospectAnswer } from '@/queries/Retrospect/useGetRetrospectAnswer';
+
+import { RETROSPECT_QUESTIONS } from '@/constants/retrospect';
+
+import * as S from './RetrospectView.styles';
+
+const RetrospectView = () => {
+ const { accessCode } = useParams();
+
+ const navigate = useNavigate();
+
+ const { answers, isFetching } = useGetRetrospectAnswer(accessCode || '');
+
+ if (isFetching) return ;
+ return (
+
+
+ navigate(`/room/${accessCode}/completed`, { state: { valid: true }, replace: true })}
+ />
+ {RETROSPECT_QUESTIONS.map((question, index) => (
+
+ {answers[index]}
+
+ ))}
+
+
+ );
+};
+
+export default RetrospectView;
diff --git a/frontend/src/pages/SignUp/SignUp.tsx b/frontend/src/pages/SignUp/SignUp.tsx
index c829ed459..b86274ce3 100644
--- a/frontend/src/pages/SignUp/SignUp.tsx
+++ b/frontend/src/pages/SignUp/SignUp.tsx
@@ -3,8 +3,6 @@ import { useNavigate } from 'react-router-dom';
import { LogoIconWithTitle } from '@/assets';
-import { validateName } from '@/validations/validatePairName';
-
import Button from '@/components/common/Button/Button';
import Input from '@/components/common/Input/Input';
@@ -13,6 +11,8 @@ import useUserStore from '@/stores/userStore';
import useInput from '@/hooks/common/useInput';
import useSignUpHandler from '@/hooks/member/useSignUpHandler';
+import { validateName } from '@/validations/validatePairName';
+
import * as S from './SignUp.styles';
const SignUp = () => {
@@ -56,7 +56,7 @@ const SignUp = () => {
placeholder="이름(또는 닉네임)을 입력해주세요."
onChange={handleChange}
/>
-
+
계정 만들기 🥳
diff --git a/frontend/src/queries/CompletedPairRoom/useGetUserIsInPairRoom.ts b/frontend/src/queries/CompletedPairRoom/useGetUserIsInPairRoom.ts
new file mode 100644
index 000000000..012188612
--- /dev/null
+++ b/frontend/src/queries/CompletedPairRoom/useGetUserIsInPairRoom.ts
@@ -0,0 +1,15 @@
+import { useQuery } from '@tanstack/react-query';
+
+import { getUserIsInPairRoom } from '@/apis/member';
+
+import { QUERY_KEYS } from '@/constants/queryKeys';
+
+export const useGetUserIsInPairRoom = (accessCode: string) => {
+ const { data, isFetching } = useQuery({
+ queryKey: [QUERY_KEYS.GET_USER_IS_IN_PAIR_ROOM],
+ queryFn: () => getUserIsInPairRoom({ accessCode }),
+ enabled: !!accessCode,
+ });
+
+ return { isUserInPairRoom: data?.exists, isUserInPairRoomFetching: isFetching };
+};
diff --git a/frontend/src/queries/CompletedPairRoom/useGetUserRetrospectExists.ts b/frontend/src/queries/CompletedPairRoom/useGetUserRetrospectExists.ts
new file mode 100644
index 000000000..9a6aaabb9
--- /dev/null
+++ b/frontend/src/queries/CompletedPairRoom/useGetUserRetrospectExists.ts
@@ -0,0 +1,15 @@
+import { useQuery } from '@tanstack/react-query';
+
+import { getUserRetrospectExists } from '@/apis/member';
+
+import { QUERY_KEYS } from '@/constants/queryKeys';
+
+export const useGetUserRetrospectExists = (accessCode: string) => {
+ const { data, isFetching } = useQuery({
+ queryKey: [QUERY_KEYS.GET_USER_RETROSPECT_EXISTS],
+ queryFn: () => getUserRetrospectExists({ accessCode }),
+ enabled: !!accessCode,
+ });
+
+ return { isUserRetrospectExist: data?.existRetrospect, isUserRetrospectExistsFetching: isFetching };
+};
diff --git a/frontend/src/queries/Main/useAddPairRoom.ts b/frontend/src/queries/Main/useAddPairRoom.ts
index 5fa7f1a0a..891b4666b 100644
--- a/frontend/src/queries/Main/useAddPairRoom.ts
+++ b/frontend/src/queries/Main/useAddPairRoom.ts
@@ -14,13 +14,21 @@ const useAddPairRoom = () => {
const { mutate, isPending } = useMutation({
mutationFn: addPairRoom,
onError: (error) => addToast({ status: 'ERROR', message: error.message }),
- onSuccess: (accessCode) => navigate(`/room/${accessCode}`),
+ onSuccess: (accessCode) => navigate(`/room/${accessCode}`, { state: { valid: true }, replace: true }),
});
- const handleAddPairRoom = async (driver: string, navigator: string, timerDuration: string) => {
+ const handleAddPairRoom = async (
+ pairId: string,
+ driver: string,
+ navigator: string,
+ missionUrl: string,
+ timerDuration: string,
+ ) => {
return mutate({
+ pairId,
driver,
navigator,
+ missionUrl,
timerDuration: Number(timerDuration) * 60 * 1000,
timerRemainingTime: Number(timerDuration) * 60 * 1000,
});
diff --git a/frontend/src/queries/MyPage/useDeleteMember.ts b/frontend/src/queries/MyPage/useDeleteMember.ts
new file mode 100644
index 000000000..02c6b87bb
--- /dev/null
+++ b/frontend/src/queries/MyPage/useDeleteMember.ts
@@ -0,0 +1,29 @@
+import { useNavigate } from 'react-router-dom';
+
+import { useMutation } from '@tanstack/react-query';
+
+import useToastStore from '@/stores/toastStore';
+import useUserStore from '@/stores/userStore';
+
+import { deleteMember } from '@/apis/member';
+
+const useDeleteMember = () => {
+ const navigate = useNavigate();
+
+ const { resetUser } = useUserStore();
+ const { addToast } = useToastStore();
+
+ const { mutate: handleDeleteMember, isSuccess } = useMutation({
+ mutationFn: deleteMember,
+ onSuccess: () => {
+ resetUser();
+ addToast({ status: 'SUCCESS', message: '지금까지 코딩해듀오와 함께 해 주셔서 감사해요. 다음에 또 만나요 👋🏻' });
+ navigate('/', { replace: true });
+ },
+ onError: (error) => addToast({ status: 'ERROR', message: error.message }),
+ });
+
+ return { handleDeleteMember, isSuccess };
+};
+
+export default useDeleteMember;
diff --git a/frontend/src/queries/MyPage/useDeleteRoom.ts b/frontend/src/queries/MyPage/useDeleteRoom.ts
new file mode 100644
index 000000000..c2ae09d49
--- /dev/null
+++ b/frontend/src/queries/MyPage/useDeleteRoom.ts
@@ -0,0 +1,28 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+
+import useToastStore from '@/stores/toastStore';
+
+import { deletePairRoom } from '@/apis/pairRoom';
+
+import { QUERY_KEYS } from '@/constants/queryKeys';
+
+const useDeletePairRoom = () => {
+ const queryClient = useQueryClient();
+ const { addToast } = useToastStore();
+
+ const { mutate, isPending } = useMutation({
+ mutationFn: deletePairRoom,
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.GET_MY_PAIR_ROOMS] });
+ addToast({ status: 'SUCCESS', message: '페어룸이 삭제되었습니다.' });
+ },
+
+ onError: () => {
+ addToast({ status: 'ERROR', message: '페어룸 삭제에 실패했습니다.' });
+ },
+ });
+
+ return { mutate, isPending };
+};
+
+export default useDeletePairRoom;
diff --git a/frontend/src/queries/MyPage/useMyPairRooms.ts b/frontend/src/queries/MyPage/useMyPairRooms.ts
index 4a522e8b0..50ebab6c4 100644
--- a/frontend/src/queries/MyPage/useMyPairRooms.ts
+++ b/frontend/src/queries/MyPage/useMyPairRooms.ts
@@ -4,14 +4,13 @@ import { getMyPairRooms } from '@/apis/member';
import { QUERY_KEYS } from '@/constants/queryKeys';
-const useMyPairRooms = () => {
+export const useMyPairRooms = () => {
const { data, isFetching } = useQuery({
queryKey: [QUERY_KEYS.GET_MY_PAIR_ROOMS],
queryFn: getMyPairRooms,
+ retry: 0,
refetchOnWindowFocus: false,
});
- return { data, isFetching };
+ return { myPairRoomList: data, myPairRoomLoading: isFetching };
};
-
-export default useMyPairRooms;
diff --git a/frontend/src/queries/PairRoom/category/mutation.ts b/frontend/src/queries/PairRoom/category/mutation.ts
index c451b32c8..1b7cf7807 100644
--- a/frontend/src/queries/PairRoom/category/mutation.ts
+++ b/frontend/src/queries/PairRoom/category/mutation.ts
@@ -8,6 +8,7 @@ import { QUERY_KEYS } from '@/constants/queryKeys';
export const useAddCategory = (onSuccess?: () => void) => {
const queryClient = useQueryClient();
+
const { addToast } = useToastStore();
return useMutation({
diff --git a/frontend/src/queries/PairRoom/reference/mutation.ts b/frontend/src/queries/PairRoom/reference/mutation.ts
index 2d958419f..10f88a70d 100644
--- a/frontend/src/queries/PairRoom/reference/mutation.ts
+++ b/frontend/src/queries/PairRoom/reference/mutation.ts
@@ -8,6 +8,7 @@ import { QUERY_KEYS } from '@/constants/queryKeys';
export const useAddReferenceLink = () => {
const queryClient = useQueryClient();
+
const { addToast } = useToastStore();
return useMutation({
@@ -19,6 +20,7 @@ export const useAddReferenceLink = () => {
export const useDeleteReferenceLink = () => {
const queryClient = useQueryClient();
+
const { addToast } = useToastStore();
return useMutation({
diff --git a/frontend/src/queries/PairRoom/useCompletePairRoom.ts b/frontend/src/queries/PairRoom/useCompletePairRoom.ts
new file mode 100644
index 000000000..e390c6f4e
--- /dev/null
+++ b/frontend/src/queries/PairRoom/useCompletePairRoom.ts
@@ -0,0 +1,28 @@
+import { useNavigate } from 'react-router-dom';
+
+import { useMutation } from '@tanstack/react-query';
+
+import useToastStore from '@/stores/toastStore';
+
+import { updatePairRoomStatus } from '@/apis/pairRoom';
+
+const useCompletePairRoom = (accessCode: string) => {
+ const { addToast } = useToastStore();
+
+ const navigate = useNavigate();
+
+ const { mutate } = useMutation({
+ mutationFn: updatePairRoomStatus,
+ onSuccess: () => {
+ addToast({ status: 'SUCCESS', message: '페어 프로그래밍이 완료되었습니다.' });
+ navigate(`/room/${accessCode}/retrospectForm`, { state: { valid: true } });
+ },
+ onError: (error) => addToast({ status: 'ERROR', message: error.message }),
+ });
+
+ const handleCompletePairRoom = () => mutate({ accessCode });
+
+ return { handleCompletePairRoom };
+};
+
+export default useCompletePairRoom;
diff --git a/frontend/src/queries/PairRoom/useGetPairRoom.ts b/frontend/src/queries/PairRoom/useGetPairRoom.ts
index 166076638..0a7658f53 100644
--- a/frontend/src/queries/PairRoom/useGetPairRoom.ts
+++ b/frontend/src/queries/PairRoom/useGetPairRoom.ts
@@ -1,4 +1,6 @@
-import { useQuery } from '@tanstack/react-query';
+import { useEffect } from 'react';
+
+import { useQuery, useQueryClient } from '@tanstack/react-query';
import { getPairRoom } from '@/apis/pairRoom';
import { getTimer } from '@/apis/timer';
@@ -6,32 +8,36 @@ import { getTimer } from '@/apis/timer';
import { QUERY_KEYS } from '@/constants/queryKeys';
const useGetPairRoom = (accessCode: string) => {
+ const queryClient = useQueryClient();
+
const {
data: pairRoom,
isFetching: isPairRoomFetching,
isRefetching: isPairRoomReFetching,
- refetch,
} = useQuery({
- queryKey: [QUERY_KEYS.GET_PAIR_ROOM],
+ queryKey: [QUERY_KEYS.GET_PAIR_ROOM, accessCode],
queryFn: () => getPairRoom(accessCode),
- enabled: !!accessCode,
refetchOnWindowFocus: false,
});
const { data: timer, isFetching: isTimerFetching } = useQuery({
- queryKey: [QUERY_KEYS.GET_PAIR_ROOM_TIMER],
+ queryKey: [QUERY_KEYS.GET_PAIR_ROOM_TIMER, accessCode],
queryFn: () => getTimer(accessCode),
- enabled: !!accessCode,
refetchOnWindowFocus: false,
});
+ useEffect(() => {
+ queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.GET_PAIR_ROOM, QUERY_KEYS.GET_PAIR_ROOM_TIMER] });
+ }, [accessCode]);
+
return {
driver: pairRoom?.driver || '',
navigator: pairRoom?.navigator || '',
+ status: pairRoom?.status || '',
+ missionUrl: pairRoom?.missionUrl || '',
duration: timer?.duration || 0,
remainingTime: timer?.remainingTime || 0,
isFetching: (isPairRoomFetching && !isPairRoomReFetching) || isTimerFetching,
- refetch,
};
};
diff --git a/frontend/src/queries/PairRoomOnboarding/useCreateBranch.ts b/frontend/src/queries/PairRoomOnboarding/useCreateBranch.ts
index 0861c1c31..ea8335115 100644
--- a/frontend/src/queries/PairRoomOnboarding/useCreateBranch.ts
+++ b/frontend/src/queries/PairRoomOnboarding/useCreateBranch.ts
@@ -9,13 +9,8 @@ const useCreateBranch = () => {
const { mutate, isSuccess } = useMutation({
mutationFn: createBranch,
- onSuccess: () => {
- addToast({ status: 'SUCCESS', message: '브랜치 생성에 성공했습니다.' });
- },
- onError: () => {
- addToast({ status: 'ERROR', message: '브랜치 생성에 실패했습니다.' });
- //TODO: 추후에 status 분기처리하기
- },
+ onSuccess: () => addToast({ status: 'SUCCESS', message: '브랜치 생성에 성공했습니다.' }),
+ onError: () => addToast({ status: 'ERROR', message: '브랜치 생성에 실패했습니다.' }),
});
const handleCreateBranch = async (currentRepository: string, branchName: string) => {
diff --git a/frontend/src/queries/Retrospect/useAddRetrospect.ts b/frontend/src/queries/Retrospect/useAddRetrospect.ts
new file mode 100644
index 000000000..627049f24
--- /dev/null
+++ b/frontend/src/queries/Retrospect/useAddRetrospect.ts
@@ -0,0 +1,17 @@
+import { useMutation } from '@tanstack/react-query';
+
+import useToastStore from '@/stores/toastStore';
+
+import { addRetrospect } from '@/apis/retrospect';
+
+export const useAddRetrospect = (onSuccess?: () => void) => {
+ const { addToast } = useToastStore();
+
+ return useMutation({
+ mutationFn: addRetrospect,
+ onSuccess: () => {
+ onSuccess && onSuccess();
+ },
+ onError: (error) => addToast({ status: 'ERROR', message: error.message }),
+ });
+};
diff --git a/frontend/src/queries/Retrospect/useDeleteRetrospect.ts b/frontend/src/queries/Retrospect/useDeleteRetrospect.ts
new file mode 100644
index 000000000..6b8e3047b
--- /dev/null
+++ b/frontend/src/queries/Retrospect/useDeleteRetrospect.ts
@@ -0,0 +1,22 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+
+import useToastStore from '@/stores/toastStore';
+
+import { deleteRetrospectAnswer } from '@/apis/retrospect';
+
+import { QUERY_KEYS } from '@/constants/queryKeys';
+
+export const useDeleteRetrospect = (onSuccess?: () => void) => {
+ const queryClient = useQueryClient();
+
+ const { addToast } = useToastStore();
+
+ return useMutation({
+ mutationFn: deleteRetrospectAnswer,
+ onSuccess: () => {
+ onSuccess && onSuccess();
+ queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.GET_MY_RETROSPECTS] });
+ },
+ onError: (error) => addToast({ status: 'ERROR', message: error.message }),
+ });
+};
diff --git a/frontend/src/queries/Retrospect/useGetRetrospectAnswer.ts b/frontend/src/queries/Retrospect/useGetRetrospectAnswer.ts
new file mode 100644
index 000000000..13cd1c588
--- /dev/null
+++ b/frontend/src/queries/Retrospect/useGetRetrospectAnswer.ts
@@ -0,0 +1,17 @@
+import { useSuspenseQuery } from '@tanstack/react-query';
+
+import { getRetrospectAnswer } from '@/apis/retrospect';
+
+import { QUERY_KEYS } from '@/constants/queryKeys';
+
+export const useGetRetrospectAnswer = (accessCode: string) => {
+ const { data, isFetching } = useSuspenseQuery({
+ queryKey: [QUERY_KEYS.GET_RETROSPECT_ANSWER],
+ queryFn: () => getRetrospectAnswer({ accessCode }),
+ retry: false,
+ });
+
+ const answers = data.answers;
+
+ return { answers, isFetching };
+};
diff --git a/frontend/src/queries/Retrospect/useGetRetrospects.ts b/frontend/src/queries/Retrospect/useGetRetrospects.ts
new file mode 100644
index 000000000..cd8a909c2
--- /dev/null
+++ b/frontend/src/queries/Retrospect/useGetRetrospects.ts
@@ -0,0 +1,17 @@
+import { useQuery } from '@tanstack/react-query';
+
+import { getUserRetrospects } from '@/apis/retrospect';
+
+import { QUERY_KEYS } from '@/constants/queryKeys';
+
+export const useGetRetrospects = () => {
+ const { data, isFetching } = useQuery({
+ queryKey: [QUERY_KEYS.GET_MY_RETROSPECTS],
+ queryFn: getUserRetrospects,
+ retry: false,
+ });
+
+ const myRetrospects = data?.retrospects;
+
+ return { myRetrospects, myRetrospectLoading: isFetching };
+};
diff --git a/frontend/src/stores/userStore.ts b/frontend/src/stores/userStore.ts
index e682ab257..8ad096bb8 100644
--- a/frontend/src/stores/userStore.ts
+++ b/frontend/src/stores/userStore.ts
@@ -6,14 +6,14 @@ interface UserStore {
username: string;
userStatus: UserStatus;
setUser: (username: string, userStatus: UserStatus) => void;
+ resetUser: () => void;
}
const useUserStore = create((set) => ({
username: '',
userStatus: 'SIGNED_OUT',
- setUser: (username, userStatus) => {
- set(() => ({ username, userStatus }));
- },
+ setUser: (username, userStatus) => set(() => ({ username, userStatus })),
+ resetUser: () => set(() => ({ username: '', userStatus: 'SIGNED_OUT' })),
}));
export default useUserStore;
diff --git a/frontend/src/validations/validateCategory.ts b/frontend/src/validations/validateCategory.ts
index 7dfdfd76c..24ce55110 100644
--- a/frontend/src/validations/validateCategory.ts
+++ b/frontend/src/validations/validateCategory.ts
@@ -4,18 +4,22 @@ import { DEFAULT_CATEGORY_VALUE } from '@/hooks/PairRoom/useCategories';
const MAX_CATEGORY_NAME_LENGTH = 10;
-export const validateCategory = (
- category: string,
- isCategoryExist: (categoryName: string) => boolean,
+export const validateCategoryName = (
+ categoryName: string,
+ isCategoryNameExists: (categoryName: string) => boolean,
prevCategoryName?: string,
) => {
- if (category.length > MAX_CATEGORY_NAME_LENGTH)
+ if (categoryName.length > MAX_CATEGORY_NAME_LENGTH) {
return { status: 'ERROR' as InputStatus, message: `${MAX_CATEGORY_NAME_LENGTH}자 이하로 입력해주세요` };
- if (prevCategoryName === category)
+ }
+
+ if (prevCategoryName === categoryName) {
return { status: 'ERROR' as InputStatus, message: '이전과 동일한 카테고리 이름입니다. 다른 이름을 입력해주세요.' };
+ }
- if (isCategoryExist(category) || category === DEFAULT_CATEGORY_VALUE)
+ if (isCategoryNameExists(categoryName) || categoryName === DEFAULT_CATEGORY_VALUE) {
return { status: 'ERROR' as InputStatus, message: '중복된 카테고리 입니다.' };
+ }
return { status: 'DEFAULT' as InputStatus, message: '' };
};
diff --git a/frontend/src/validations/validatePairName.ts b/frontend/src/validations/validatePairName.ts
index 92cdd76cc..c69c88f4b 100644
--- a/frontend/src/validations/validatePairName.ts
+++ b/frontend/src/validations/validatePairName.ts
@@ -14,3 +14,8 @@ export const validateDuplicateName = (firstPairName: string, secondPairName: str
return { status: 'DEFAULT' as InputStatus, message: '' };
};
+
+export const validatePairInfo = (info: string) => {
+ if (info.trim() === '') return { status: 'ERROR' as InputStatus, message: '값을 입력해 주세요.' };
+ return { status: 'DEFAULT' as InputStatus, message: '' };
+};