diff --git a/frontend/.eslintrc.cjs b/frontend/.eslintrc.cjs index 0c1061647..a5df1c80c 100644 --- a/frontend/.eslintrc.cjs +++ b/frontend/.eslintrc.cjs @@ -77,17 +77,11 @@ module.exports = { group: 'internal', position: 'after', }, - { - pattern: '@/validations/**', - group: 'internal', - position: 'after', - }, { pattern: '@/components/**', group: 'internal', position: 'after', }, - { pattern: '@/common/**', group: 'internal', @@ -118,6 +112,11 @@ module.exports = { group: 'internal', position: 'after', }, + { + pattern: '@/validations/**', + group: 'internal', + position: 'after', + }, { pattern: '@/types/**', group: 'internal', diff --git a/frontend/.stylelintrc b/frontend/.stylelintrc index c6c1810ee..28c0981fe 100644 --- a/frontend/.stylelintrc +++ b/frontend/.stylelintrc @@ -59,6 +59,7 @@ "padding-bottom", "padding-left", "border", + "border-color", "border-radius" ] }, diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 6b2bed03d..02b923456 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -8,6 +8,7 @@ const PairRoom = lazy(() => import('@/pages/PairRoom/PairRoom')); import Callback from '@/pages/Callback/Callback'; import CoduoDocs from '@/pages/CoduoDocs/CoduoDocs'; +import CompletedPairRoom from '@/pages/CompletedPairRoom/CompletedPairRoom'; import Error from '@/pages/Error/Error'; import Landing from '@/pages/Landing/Landing'; import Layout from '@/pages/Layout'; @@ -15,6 +16,9 @@ import Loading from '@/pages/Loading/Loading'; import Main from '@/pages/Main/Main'; import MyPage from '@/pages/MyPage/MyPage'; import PairRoomOnboarding from '@/pages/PairRoomOnboarding/PairRoomOnboarding'; +import PrivateRoutes from '@/pages/PrivateRoutes'; +import RetrospectForm from '@/pages/RetrospectForm/RetrospectForm'; +import RetrospectView from '@/pages/RetrospectView/RetrospectView'; import SignUp from '@/pages/SignUp/SignUp'; import HowToPair from '@/components/Landing/HowToPair/HowToPair'; @@ -71,17 +75,39 @@ const App = () => { path: 'onboarding', element: ( }> - {' '} + ), }, { - path: 'room/:accessCode', - element: ( - }> - - - ), + path: 'room', + element: , + children: [ + { + path: ':accessCode', + element: ( + }> + + + ), + }, + { + path: ':accessCode/completed', + element: ( + }> + + + ), + }, + { + path: ':accessCode/retrospect', + element: , + }, + { + path: ':accessCode/retrospectForm', + element: , + }, + ], }, { path: 'sign-up', @@ -99,6 +125,7 @@ const App = () => { path: 'my-page', element: , }, + { path: 'error', element: , diff --git a/frontend/src/apis/member.ts b/frontend/src/apis/member.ts index 475b84dc9..dd76c9f96 100644 --- a/frontend/src/apis/member.ts +++ b/frontend/src/apis/member.ts @@ -21,6 +21,22 @@ export const getMember = async (): Promise<{ username: string }> => { return response.json(); }; +export const getMemberName = async (userId: string): Promise<{ memberName: string }> => { + const response = await fetcher.get({ + url: `${API_URL}/member/exists?user_id=${userId}`, + errorMessage: ERROR_MESSAGES.GET_MEMBER, + }); + + return response.json(); +}; + +export const deleteMember = async () => { + await fetcher.delete({ + url: `${API_URL}/member`, + errorMessage: ERROR_MESSAGES.DELETE_MEMBER, + }); +}; + interface GetMyPairRoomsResponse { id: number; status: PairRoomStatus; @@ -37,3 +53,39 @@ export const getMyPairRooms = async (): Promise => { return response.json(); }; + +interface GetUserIsInPairRoomRequest { + accessCode: string; +} + +interface GetUserIsInPairRoomResponse { + exists: boolean; +} + +export const getUserIsInPairRoom = async ({ + accessCode, +}: GetUserIsInPairRoomRequest): Promise => { + const response = await fetcher.get({ + url: `${API_URL}/member/${accessCode}/exists`, + errorMessage: ERROR_MESSAGES.GET_USER_IS_IN_PAIR_ROOM, + }); + + return await response.json(); +}; + +interface GetUserRetrospectExistsRequest { + accessCode: string; +} +interface GetUserRetrospectExistsResponse { + existRetrospect: boolean; +} +export const getUserRetrospectExists = async ({ + accessCode, +}: GetUserRetrospectExistsRequest): Promise => { + const response = await fetcher.get({ + url: `${API_URL}/member/retrospect/${accessCode}/exists`, + errorMessage: ERROR_MESSAGES.GET_USER_RETROSPECT_EXISTS, + }); + + return await response.json(); +}; diff --git a/frontend/src/apis/pairRoom.ts b/frontend/src/apis/pairRoom.ts index 2a917c660..2b84b36c2 100644 --- a/frontend/src/apis/pairRoom.ts +++ b/frontend/src/apis/pairRoom.ts @@ -8,8 +8,9 @@ export type PairRoomStatus = 'IN_PROGRESS' | 'COMPLETED'; export interface GetPairRoomResponse { id: number; - navigator: string; driver: string; + navigator: string; + missionUrl: string; status: PairRoomStatus; } @@ -32,16 +33,32 @@ export const getPairRoomExists = async (accessCode: string): Promise<{ exists: b }; interface AddPairRoomRequest { + pairId: string; driver: string; navigator: string; + missionUrl: string; timerDuration: number; timerRemainingTime: number; } -export const addPairRoom = async ({ driver, navigator, timerDuration, timerRemainingTime }: AddPairRoomRequest) => { +export const addPairRoom = async ({ + pairId, + driver, + navigator, + missionUrl, + timerDuration, + timerRemainingTime, +}: AddPairRoomRequest) => { const response = await fetcher.post({ url: `${API_URL}/pair-room`, - body: JSON.stringify({ driver, navigator, timerDuration, timerRemainingTime, status: 'IN_PROGRESS' }), + body: JSON.stringify({ + pairId: pairId || null, + driver, + navigator, + missionUrl, + timerDuration, + timerRemainingTime, + }), errorMessage: '', }); @@ -60,3 +77,22 @@ export const updatePairRole = async ({ accessCode }: UpdatePairRoleRequest) => { errorMessage: '', }); }; + +interface UpdatePairRoomStatusRequest { + accessCode: string; +} + +export const updatePairRoomStatus = async ({ accessCode }: UpdatePairRoomStatusRequest) => { + await fetcher.patch({ + url: `${API_URL}/pair-room/${accessCode}/status`, + errorMessage: ERROR_MESSAGES.UPDATE_PAIR_ROOM_STATUS, + body: JSON.stringify({ status: 'COMPLETED' }), + }); +}; + +export const deletePairRoom = async (accessCode: string) => { + await fetcher.delete({ + url: `${API_URL}/pair-room/${accessCode}`, + errorMessage: ERROR_MESSAGES.DELETE_PAIR_ROOM, + }); +}; diff --git a/frontend/src/apis/referenceLink.ts b/frontend/src/apis/referenceLink.ts index 1477bc733..7600d8e26 100644 --- a/frontend/src/apis/referenceLink.ts +++ b/frontend/src/apis/referenceLink.ts @@ -4,7 +4,7 @@ import { ERROR_MESSAGES } from '@/constants/message'; const API_URL = process.env.REACT_APP_API_URL; -export interface Link { +export interface Reference { id: number; url: string; headTitle: string; @@ -19,7 +19,7 @@ interface GetReferenceLinksRequest { categoryId: string; } -export const getReferenceLinks = async ({ accessCode, categoryId }: GetReferenceLinksRequest): Promise => { +export const getReferenceLinks = async ({ accessCode, categoryId }: GetReferenceLinksRequest): Promise => { const categoryParamsUrl = categoryId === '0' ? `` : `?categoryId=${categoryId}`; const response = await fetcher.get({ @@ -29,7 +29,7 @@ export const getReferenceLinks = async ({ accessCode, categoryId }: GetReference return await response.json(); }; - + interface AddReferenceLinkRequest { url: string; accessCode: string; diff --git a/frontend/src/apis/retrospect.ts b/frontend/src/apis/retrospect.ts new file mode 100644 index 000000000..f489ac966 --- /dev/null +++ b/frontend/src/apis/retrospect.ts @@ -0,0 +1,56 @@ +import fetcher from '@/apis/fetcher'; + +import { ERROR_MESSAGES } from '@/constants/message'; + +const API_URL = process.env.REACT_APP_API_URL; + +interface AddRetrospectRequest { + accessCode: string; + answers: string[]; +} + +export const addRetrospect = async ({ accessCode, answers }: AddRetrospectRequest) => { + await fetcher.post({ + url: `${API_URL}/retrospects`, + body: JSON.stringify({ accessCode, answers }), + errorMessage: ERROR_MESSAGES.ADD_RETROSPECT, + }); +}; + +interface GetRetrospectRequest { + accessCode: string; +} + +interface GetRetrospectResponse { + answers: string[]; +} + +export const getRetrospectAnswer = async ({ accessCode }: GetRetrospectRequest): Promise => { + const response = await fetcher.get({ + url: `${API_URL}/retrospects/${accessCode}`, + errorMessage: ERROR_MESSAGES.GET_RETROSPECT, + }); + + return await response.json(); +}; + +export const deleteRetrospectAnswer = async ({ accessCode }: GetRetrospectRequest) => { + await fetcher.delete({ + url: `${API_URL}/retrospects/${accessCode}`, + errorMessage: ERROR_MESSAGES.DELETE_RETROSPECT, + }); +}; + +export interface Retrospect { + accessCode: string; + answer: string; +} + +export const getUserRetrospects = async (): Promise<{ retrospects: Retrospect[] }> => { + const response = await fetcher.get({ + url: `${API_URL}/retrospects`, + errorMessage: ERROR_MESSAGES.GET_USER_RETROSPECTS, + }); + + return await response.json(); +}; diff --git a/frontend/src/components/CompletedPairRoom/ReferenceCard/.DS_Store b/frontend/src/components/CompletedPairRoom/ReferenceCard/.DS_Store new file mode 100644 index 000000000..1187dd7e1 Binary files /dev/null and b/frontend/src/components/CompletedPairRoom/ReferenceCard/.DS_Store differ diff --git a/frontend/src/components/CompletedPairRoom/ReferenceCard/CategoryManagementModal/.DS_Store b/frontend/src/components/CompletedPairRoom/ReferenceCard/CategoryManagementModal/.DS_Store new file mode 100644 index 000000000..fd335169f Binary files /dev/null and b/frontend/src/components/CompletedPairRoom/ReferenceCard/CategoryManagementModal/.DS_Store differ diff --git a/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoriesEditor/CategoryItem/ReadonlyCategoryItem/ReadonlyCategoryItem.styles.ts b/frontend/src/components/CompletedPairRoom/ReferenceCard/CategoryManagementModal/CategoryItem/CategoryItem.styles.ts similarity index 75% rename from frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoriesEditor/CategoryItem/ReadonlyCategoryItem/ReadonlyCategoryItem.styles.ts rename to frontend/src/components/CompletedPairRoom/ReferenceCard/CategoryManagementModal/CategoryItem/CategoryItem.styles.ts index 4b5b2d040..de3a3ea42 100644 --- a/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoriesEditor/CategoryItem/ReadonlyCategoryItem/ReadonlyCategoryItem.styles.ts +++ b/frontend/src/components/CompletedPairRoom/ReferenceCard/CategoryManagementModal/CategoryItem/CategoryItem.styles.ts @@ -1,21 +1,34 @@ import styled 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: 28rem; + width: 100%; + + img { + width: 2rem; + height: 2rem; + } `; -export const ReadonlyCategoryItem = styled.li<{ $isChecked: boolean }>` +export const Item = styled.li<{ $isChecked: boolean }>` display: flex; - justify-content: space-between; align-items: center; width: 100%; height: 4.4rem; - padding: 0 1rem; + padding: 0 1.2rem; border: 1px solid ${({ theme }) => theme.color.black[50]}; border-radius: 0.5rem; @@ -29,9 +42,4 @@ export const ReadonlyCategoryItem = styled.li<{ $isChecked: boolean }>` background-color: ${({ theme }) => theme.color.primary[700]}; color: ${({ theme }) => theme.color.black[10]}; } - - &:active { - background-color: ${({ theme }) => theme.color.primary[800]}; - color: ${({ theme }) => theme.color.black[10]}; - } `; diff --git a/frontend/src/components/CompletedPairRoom/ReferenceCard/CategoryManagementModal/CategoryItem/CategoryItem.tsx b/frontend/src/components/CompletedPairRoom/ReferenceCard/CategoryManagementModal/CategoryItem/CategoryItem.tsx new file mode 100644 index 000000000..4724a97bf --- /dev/null +++ b/frontend/src/components/CompletedPairRoom/ReferenceCard/CategoryManagementModal/CategoryItem/CategoryItem.tsx @@ -0,0 +1,38 @@ +import { CheckBoxChecked, CheckBoxUnchecked } from '@/assets'; + +import useToastStore from '@/stores/toastStore'; + +import * as S from './CategoryItem.styles'; + +interface CategoryItemProps { + categoryName: string; + categoryId: string; + isChecked: boolean; + closeModal: () => void; + handleSelectCategory: (categoryId: string) => void; +} + +const CategoryItem = ({ closeModal, categoryName, categoryId, isChecked, handleSelectCategory }: CategoryItemProps) => { + const { addToast } = useToastStore(); + + const handleCategoryClick = (event: React.MouseEvent) => { + if (isChecked) return; + + handleSelectCategory(event.currentTarget.id); + addToast({ status: 'SUCCESS', message: `${categoryName}가 선택되었어요.` }); + closeModal(); + }; + + return ( + + + {isChecked + +

{categoryName}

+
+
+
+ ); +}; + +export default CategoryItem; diff --git a/frontend/src/components/CompletedPairRoom/ReferenceCard/CategoryManagementModal/CategoryManagementModal.styles.ts b/frontend/src/components/CompletedPairRoom/ReferenceCard/CategoryManagementModal/CategoryManagementModal.styles.ts new file mode 100644 index 000000000..a5aacd188 --- /dev/null +++ b/frontend/src/components/CompletedPairRoom/ReferenceCard/CategoryManagementModal/CategoryManagementModal.styles.ts @@ -0,0 +1,24 @@ +import styled, { css } from 'styled-components'; + +export const inputStyles = css` + width: 100%; + + font-size: ${({ theme }) => theme.fontSize.md}; +`; + +export const Header = styled.div` + display: flex; + align-items: center; + gap: 1rem; + + color: ${({ theme }) => theme.color.black[80]}; + font-size: ${({ theme }) => theme.fontSize.lg}; +`; + +export const CategoryList = styled.ul` + display: flex; + flex-direction: column-reverse; + gap: 2rem; + + width: 100%; +`; diff --git a/frontend/src/components/CompletedPairRoom/ReferenceCard/CategoryManagementModal/CategoryManagementModal.tsx b/frontend/src/components/CompletedPairRoom/ReferenceCard/CategoryManagementModal/CategoryManagementModal.tsx new file mode 100644 index 000000000..971603ff8 --- /dev/null +++ b/frontend/src/components/CompletedPairRoom/ReferenceCard/CategoryManagementModal/CategoryManagementModal.tsx @@ -0,0 +1,49 @@ +import { Modal } from '@/components/common/Modal'; +import CategoryItem from '@/components/CompletedPairRoom/ReferenceCard/CategoryManagementModal/CategoryItem/CategoryItem'; +import { Category } from '@/components/PairRoom/ReferenceCard/ReferenceCard.type'; + +import * as S from './CategoryManagementModal.styles'; + +interface CategoryManagementModalProps { + isOpen: boolean; + closeModal: () => void; + categories: Category[]; + isCategoryExist: (categoryName: string) => boolean; + selectedCategory: string; + handleSelectCategory: (categoryId: string) => void; +} + +const CategoryManagementModal = ({ + isOpen, + closeModal, + categories, + selectedCategory, + handleSelectCategory, +}: CategoryManagementModalProps) => { + return ( + + + +

카테고리 선택

+
+
+ + + + {categories.map((category) => ( + + ))} + + +
+ ); +}; + +export default CategoryManagementModal; diff --git a/frontend/src/components/CompletedPairRoom/ReferenceCard/Header/Header.styles.ts b/frontend/src/components/CompletedPairRoom/ReferenceCard/Header/Header.styles.ts new file mode 100644 index 000000000..91157791d --- /dev/null +++ b/frontend/src/components/CompletedPairRoom/ReferenceCard/Header/Header.styles.ts @@ -0,0 +1,25 @@ +import styled, { css } from 'styled-components'; + +export const buttonStyles = css` + width: fit-content; + min-width: 6rem; + padding: 0 1rem; +`; + +export const Layout = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + + width: 100%; + height: 6rem; + padding: 2rem; + + font-size: ${({ theme }) => theme.fontSize.lg}; +`; + +export const Container = styled.div` + display: flex; + align-items: center; + gap: 0.8rem; +`; diff --git a/frontend/src/components/CompletedPairRoom/ReferenceCard/Header/Header.tsx b/frontend/src/components/CompletedPairRoom/ReferenceCard/Header/Header.tsx new file mode 100644 index 000000000..0d7f0c964 --- /dev/null +++ b/frontend/src/components/CompletedPairRoom/ReferenceCard/Header/Header.tsx @@ -0,0 +1,36 @@ +import { IoIosLink } from 'react-icons/io'; + +import Button from '@/components/common/Button/Button'; + +import { theme } from '@/styles/theme'; + +import * as S from './Header.styles'; + +interface HeaderProps { + selectedFilteringCategoryName: string; + onButtonClick: () => void; +} + +const Header = ({ selectedFilteringCategoryName, onButtonClick }: React.PropsWithChildren) => { + return ( + + + +

링크

+
+ +
+ ); +}; + +export default Header; diff --git a/frontend/src/components/CompletedPairRoom/ReferenceCard/ReferenceCard.styles.ts b/frontend/src/components/CompletedPairRoom/ReferenceCard/ReferenceCard.styles.ts new file mode 100644 index 000000000..e113c9c73 --- /dev/null +++ b/frontend/src/components/CompletedPairRoom/ReferenceCard/ReferenceCard.styles.ts @@ -0,0 +1,16 @@ +import styled from 'styled-components'; + +export const Layout = styled.div` + width: 50%; + min-width: 45rem; +`; + +export const Body = styled.div` + display: flex; + flex-direction: column; + overflow: hidden; + + height: calc(100vh - 20rem); + + border-top: 1px solid ${({ theme }) => theme.color.black[30]}; +`; diff --git a/frontend/src/components/CompletedPairRoom/ReferenceCard/ReferenceCard.tsx b/frontend/src/components/CompletedPairRoom/ReferenceCard/ReferenceCard.tsx new file mode 100644 index 000000000..f86f6fb17 --- /dev/null +++ b/frontend/src/components/CompletedPairRoom/ReferenceCard/ReferenceCard.tsx @@ -0,0 +1,48 @@ +import { useState } from 'react'; + +import CategoryManagementModal from '@/components/CompletedPairRoom/ReferenceCard/CategoryManagementModal/CategoryManagementModal'; +import Header from '@/components/CompletedPairRoom/ReferenceCard/Header/Header'; +import ReferenceList from '@/components/CompletedPairRoom/ReferenceCard/ReferenceList/ReferenceList'; +import { PairRoomCard } from '@/components/PairRoom/PairRoomCard'; + +import useModal from '@/hooks/common/useModal'; +import useCategories, { DEFAULT_CATEGORY_ID, DEFAULT_CATEGORY_VALUE } from '@/hooks/PairRoom/useCategories'; + +import { useGetReference } from '@/queries/PairRoom/reference/query'; + +import * as S from './ReferenceCard.styles'; + +interface ReferenceCardProps { + accessCode: string; +} + +const ReferenceCard = ({ accessCode }: ReferenceCardProps) => { + const [selectedFilteringCategoryId, setSelectedFilteringCategoryId] = useState(DEFAULT_CATEGORY_ID); + const { isModalOpen, openModal, closeModal } = useModal(); + + const { categories, isCategoryExist, getCategoryNameById } = useCategories(accessCode); + + const { data: references } = useGetReference(selectedFilteringCategoryId, accessCode); + const selectedFilteringCategoryName = getCategoryNameById(selectedFilteringCategoryId) || DEFAULT_CATEGORY_VALUE; + + return ( + + +
+ + + + + setSelectedFilteringCategoryId(categoryId)} + /> + + ); +}; + +export default ReferenceCard; diff --git a/frontend/src/components/PairRoom/ReferenceCard/ReferenceList/Reference/Reference.styles.ts b/frontend/src/components/CompletedPairRoom/ReferenceCard/ReferenceList/ReferenceList.styles.ts similarity index 62% rename from frontend/src/components/PairRoom/ReferenceCard/ReferenceList/Reference/Reference.styles.ts rename to frontend/src/components/CompletedPairRoom/ReferenceCard/ReferenceList/ReferenceList.styles.ts index 348d1dde1..598a63842 100644 --- a/frontend/src/components/PairRoom/ReferenceCard/ReferenceList/Reference/Reference.styles.ts +++ b/frontend/src/components/CompletedPairRoom/ReferenceCard/ReferenceList/ReferenceList.styles.ts @@ -1,7 +1,51 @@ -import { MdClose } from 'react-icons/md'; -import styled from 'styled-components'; +import styled, { css } from 'styled-components'; -export const Layout = styled.div` +export const Layout = styled.div<{ $columns: number }>` + display: flex; + flex-grow: 1; + flex-direction: column; + align-items: ${({ $columns }) => ($columns > 2 ? 'center' : '')}; + gap: 1rem; + overflow-y: auto; + + padding: 3rem; +`; + +export const List = styled.ul<{ $columns: number }>` + gap: 3rem 0; + + width: 100%; + padding: 0; + + ${({ $columns }) => + $columns <= 2 + ? css` + display: flex; + flex-wrap: wrap; + gap: 3rem; + ` + : css` + display: grid; + grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr)); + place-items: center; + `} + + li { + list-style-type: none; + } +`; + +export const EmptyLayout = styled.div` + flex-grow: 1; + + height: 0; + padding: 2rem; + + color: ${({ theme }) => theme.color.black[60]}; + font-size: ${({ theme }) => theme.fontSize.md}; +`; + +export const Item = styled.div` display: flex; flex-direction: column; @@ -22,7 +66,7 @@ export const Image = styled.img` border-top-right-radius: 1.5rem; `; -export const NoneImage = styled.div` +export const EmptyImage = styled.div` display: flex; justify-content: center; align-items: center; @@ -79,40 +123,3 @@ export const Content = styled.p` -webkit-box-orient: vertical; -webkit-line-clamp: 3; `; - -export const DeleteButton = styled(MdClose)` - position: absolute; - top: 1rem; - right: 1rem; - - width: 2rem; - height: 2rem; - padding: 0.3rem; - border-radius: 100%; - - background-color: ${({ theme }) => theme.color.black[90]}; - opacity: 0.6; - color: ${({ theme }) => theme.color.black[20]}; - - cursor: pointer; - - &:hover { - opacity: 1; - } -`; - -export const Header = styled.div` - display: flex; - justify-content: space-between; - - position: absolute; - top: 1.2rem; - - width: 100%; - padding: 0 1rem; - - button { - width: fit-content; - padding: 0 1rem; - } -`; diff --git a/frontend/src/components/CompletedPairRoom/ReferenceCard/ReferenceList/ReferenceList.tsx b/frontend/src/components/CompletedPairRoom/ReferenceCard/ReferenceList/ReferenceList.tsx new file mode 100644 index 000000000..114524120 --- /dev/null +++ b/frontend/src/components/CompletedPairRoom/ReferenceCard/ReferenceList/ReferenceList.tsx @@ -0,0 +1,43 @@ +import { Link } from 'react-router-dom'; + +import type { Reference } from '@/apis/referenceLink'; + +import * as S from './ReferenceList.styles'; + +interface ReferenceListProps { + references?: Reference[]; +} + +const ReferenceList = ({ references }: ReferenceListProps) => { + if (!references || references.length < 1) return 저장된 링크가 없습니다.; + + return ( + + + {references.map((reference) => { + return ( + + + {reference.image ? ( + + ) : ( + + 이미지가 +
+ 없습니다 +
+ )} + + {reference.openGraphTitle || reference.headTitle} + {reference.description} + + +
+ ); + })} +
+
+ ); +}; + +export default ReferenceList; diff --git a/frontend/src/components/CompletedPairRoom/RetrospectButton/RetrospectButton.styles.ts b/frontend/src/components/CompletedPairRoom/RetrospectButton/RetrospectButton.styles.ts new file mode 100644 index 000000000..51adeec94 --- /dev/null +++ b/frontend/src/components/CompletedPairRoom/RetrospectButton/RetrospectButton.styles.ts @@ -0,0 +1,12 @@ +import styled from 'styled-components'; + +export const Layout = styled.div` + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; +`; +export const ButtonPrompt = styled.div` + color: ${({ theme }) => theme.color.black[70]}; + font-size: ${({ theme }) => theme.fontSize.sm}; +`; diff --git a/frontend/src/components/CompletedPairRoom/RetrospectButton/RetrospectButton.tsx b/frontend/src/components/CompletedPairRoom/RetrospectButton/RetrospectButton.tsx new file mode 100644 index 000000000..25a90eeba --- /dev/null +++ b/frontend/src/components/CompletedPairRoom/RetrospectButton/RetrospectButton.tsx @@ -0,0 +1,67 @@ +import { useNavigate } from 'react-router-dom'; + +import Button from '@/components/common/Button/Button'; + +import useUserStore from '@/stores/userStore'; + +import { useGetUserIsInPairRoom } from '@/queries/CompletedPairRoom/useGetUserIsInPairRoom'; +import { useGetUserRetrospectExists } from '@/queries/CompletedPairRoom/useGetUserRetrospectExists'; + +import * as S from './RetrospectButton.styles'; + +interface RetrospectButtonProps { + accessCode: string; +} + +const RetrospectButton = ({ accessCode }: RetrospectButtonProps) => { + const navigate = useNavigate(); + + const { userStatus } = useUserStore(); + + const { isUserInPairRoom, isUserInPairRoomFetching } = useGetUserIsInPairRoom(accessCode); + const { isUserRetrospectExist, isUserRetrospectExistsFetching } = useGetUserRetrospectExists(accessCode); + + if (isUserInPairRoomFetching || isUserRetrospectExistsFetching) { + return ( + + + 잠시만 기다려주세요... + + ); + } + + if (userStatus !== 'SIGNED_IN' || !isUserInPairRoom) + return ( + + + 로그인 후 현재 페어룸에 등록된 사람만 회고를 작성할 수 있어요. + + ); + + const handleRetrospectButtonClick = async () => { + if (isUserRetrospectExist) { + navigate(`/room/${accessCode}/retrospect`, { state: { valid: true } }); + } else { + navigate(`/room/${accessCode}/retrospectForm`, { state: { valid: true } }); + } + }; + + return ( + + + + {isUserRetrospectExist + ? '작성된 회고를 확인하러 가볼까요?' + : '이번 페어 프로그래밍은 어떠셨나요? 회고를 작성해 주세요.'} + + + ); +}; + +export default RetrospectButton; diff --git a/frontend/src/components/CompletedPairRoom/TodoListCard/Header/Header.styles.ts b/frontend/src/components/CompletedPairRoom/TodoListCard/Header/Header.styles.ts new file mode 100644 index 000000000..c00c2c5f3 --- /dev/null +++ b/frontend/src/components/CompletedPairRoom/TodoListCard/Header/Header.styles.ts @@ -0,0 +1,13 @@ +import styled from 'styled-components'; + +export const Layout = styled.div` + display: flex; + align-items: center; + gap: 0.8rem; + + width: 100%; + height: 6rem; + padding: 2rem; + + font-size: ${({ theme }) => theme.fontSize.lg}; +`; diff --git a/frontend/src/components/CompletedPairRoom/TodoListCard/Header/Header.tsx b/frontend/src/components/CompletedPairRoom/TodoListCard/Header/Header.tsx new file mode 100644 index 000000000..bac3f056e --- /dev/null +++ b/frontend/src/components/CompletedPairRoom/TodoListCard/Header/Header.tsx @@ -0,0 +1,16 @@ +import { IoIosCheckbox } from 'react-icons/io'; + +import { theme } from '@/styles/theme'; + +import * as S from './Header.styles'; + +const Header = () => { + return ( + + +

투두 리스트

+
+ ); +}; + +export default Header; diff --git a/frontend/src/components/CompletedPairRoom/TodoListCard/TodoItem/TodoItem.styles.ts b/frontend/src/components/CompletedPairRoom/TodoListCard/TodoItem/TodoItem.styles.ts new file mode 100644 index 000000000..f89a5eb72 --- /dev/null +++ b/frontend/src/components/CompletedPairRoom/TodoListCard/TodoItem/TodoItem.styles.ts @@ -0,0 +1,51 @@ +import { AiFillCopy } from 'react-icons/ai'; +import styled from 'styled-components'; + +export const Layout = styled.div<{ $isChecked: boolean; $isIconHovered: boolean }>` + display: flex; + justify-content: space-between; + align-items: center; + gap: 1.2rem; + + padding: 1.6rem; + border-radius: 1rem; + + background: ${({ $isChecked, theme }) => ($isChecked ? theme.color.black[30] : theme.color.secondary[200])}; + font-size: ${({ theme }) => theme.fontSize.md}; + + transition: background 0.1s ease; +`; + +export const TodoContainer = styled.div<{ $isChecked: boolean }>` + display: flex; + align-items: center; + gap: 1.2rem; + + p { + text-decoration: ${({ $isChecked }) => $isChecked && 'line-through'}; + word-break: break-all; + + transition: text-decoration 0.1s ease; + } +`; + +export const IconContainer = styled.div` + display: flex; + align-items: center; + gap: 0.3rem; +`; + +export const CopyIcon = styled(AiFillCopy)<{ $isChecked: boolean }>` + width: 1.7rem; + height: 1.7rem; + + color: ${({ $isChecked, theme }) => ($isChecked ? theme.color.black[50] : theme.color.secondary[500])}; + + transition: color 0.1s ease; + + cursor: pointer; + + &:hover { + color: ${({ $isChecked, theme }) => ($isChecked ? theme.color.black[60] : theme.color.secondary[600])}; + } +`; diff --git a/frontend/src/components/CompletedPairRoom/TodoListCard/TodoItem/TodoItem.tsx b/frontend/src/components/CompletedPairRoom/TodoListCard/TodoItem/TodoItem.tsx new file mode 100644 index 000000000..fc06ab709 --- /dev/null +++ b/frontend/src/components/CompletedPairRoom/TodoListCard/TodoItem/TodoItem.tsx @@ -0,0 +1,36 @@ +import { useState } from 'react'; + +import { Todo } from '@/apis/todo'; + +import useCopyClipBoard from '@/hooks/common/useCopyClipboard'; + +import * as S from './TodoItem.styles'; + +interface TodoItemProps { + todo: Todo; +} + +const TodoItem = ({ todo }: TodoItemProps) => { + const [isIconHovered, setIsIconHovered] = useState(false); + const [, onCopy] = useCopyClipBoard(); + + const { isChecked, content } = todo; + + return ( + + +

{content}

+
+ + setIsIconHovered(true)} + onMouseLeave={() => setIsIconHovered(false)} + onClick={() => onCopy(content)} + /> + +
+ ); +}; + +export default TodoItem; diff --git a/frontend/src/components/CompletedPairRoom/TodoListCard/TodoList/TodoList.styles.ts b/frontend/src/components/CompletedPairRoom/TodoListCard/TodoList/TodoList.styles.ts new file mode 100644 index 000000000..8b3e073bd --- /dev/null +++ b/frontend/src/components/CompletedPairRoom/TodoListCard/TodoList/TodoList.styles.ts @@ -0,0 +1,29 @@ +import styled from 'styled-components'; + +export const Layout = styled.div` + display: flex; + flex-grow: 1; + flex-direction: column; + gap: 1.5rem; + overflow-y: auto; + + padding: 2rem; +`; + +export const TodoListContainer = styled.div` + display: flex; + flex-grow: 1; + flex-direction: column; + gap: 1rem; + overflow-y: auto; +`; + +export const CountText = styled.p` + color: ${({ theme }) => theme.color.black[70]}; + font-size: ${({ theme }) => theme.fontSize.sm}; +`; + +export const EmptyText = styled.p` + color: ${({ theme }) => theme.color.black[60]}; + font-size: ${({ theme }) => theme.fontSize.md}; +`; diff --git a/frontend/src/components/CompletedPairRoom/TodoListCard/TodoList/TodoList.tsx b/frontend/src/components/CompletedPairRoom/TodoListCard/TodoList/TodoList.tsx new file mode 100644 index 000000000..a3033eace --- /dev/null +++ b/frontend/src/components/CompletedPairRoom/TodoListCard/TodoList/TodoList.tsx @@ -0,0 +1,32 @@ +import { useParams } from 'react-router-dom'; + +import TodoItem from '@/components/CompletedPairRoom/TodoListCard/TodoItem/TodoItem'; + +import useTodos from '@/queries/PairRoom/useTodos'; + +import * as S from './TodoList.styles'; + +const TodoList = () => { + const { accessCode } = useParams(); + + const { todos } = useTodos(accessCode || ''); + + return ( + + {todos.length > 0 ? ( + <> + 총 {todos.length}개 + + {todos.map((todo) => ( + + ))} + + + ) : ( + 저장된 투두 리스트가 없습니다. + )} + + ); +}; + +export default TodoList; diff --git a/frontend/src/components/CompletedPairRoom/TodoListCard/TodoListCard.styles.ts b/frontend/src/components/CompletedPairRoom/TodoListCard/TodoListCard.styles.ts new file mode 100644 index 000000000..e113c9c73 --- /dev/null +++ b/frontend/src/components/CompletedPairRoom/TodoListCard/TodoListCard.styles.ts @@ -0,0 +1,16 @@ +import styled from 'styled-components'; + +export const Layout = styled.div` + width: 50%; + min-width: 45rem; +`; + +export const Body = styled.div` + display: flex; + flex-direction: column; + overflow: hidden; + + height: calc(100vh - 20rem); + + border-top: 1px solid ${({ theme }) => theme.color.black[30]}; +`; diff --git a/frontend/src/components/CompletedPairRoom/TodoListCard/TodoListCard.tsx b/frontend/src/components/CompletedPairRoom/TodoListCard/TodoListCard.tsx new file mode 100644 index 000000000..888021b4f --- /dev/null +++ b/frontend/src/components/CompletedPairRoom/TodoListCard/TodoListCard.tsx @@ -0,0 +1,20 @@ +import Header from '@/components/CompletedPairRoom/TodoListCard/Header/Header'; +import TodoList from '@/components/CompletedPairRoom/TodoListCard/TodoList/TodoList'; +import { PairRoomCard } from '@/components/PairRoom/PairRoomCard'; + +import * as S from './TodoListCard.styles'; + +const TodoListCard = () => { + return ( + + +
+ + + + + + ); +}; + +export default TodoListCard; diff --git a/frontend/src/components/Landing/HowToPair/HowToPair.styles.ts b/frontend/src/components/Landing/HowToPair/HowToPair.styles.ts index f6c626787..ea5c42e97 100644 --- a/frontend/src/components/Landing/HowToPair/HowToPair.styles.ts +++ b/frontend/src/components/Landing/HowToPair/HowToPair.styles.ts @@ -77,7 +77,7 @@ export const SectionText = styled.div` width: 100%; `; -export const SectionTitle = styled.h2` +export const SectionTitle = styled.h1` margin: 2rem 0; color: ${({ theme }) => theme.color.primary[800]}; diff --git a/frontend/src/components/Landing/HowToPair/HowToPair.tsx b/frontend/src/components/Landing/HowToPair/HowToPair.tsx index 42a0f261a..56997a8b0 100644 --- a/frontend/src/components/Landing/HowToPair/HowToPair.tsx +++ b/frontend/src/components/Landing/HowToPair/HowToPair.tsx @@ -64,7 +64,7 @@ const HowToPair = () => { }} > - + 왜 페어 프로그래밍을 해야 할까요? @@ -94,7 +94,7 @@ const HowToPair = () => { 더 나은 협업을 할 수 있도록 합니다. - + diff --git a/frontend/src/components/Main/PairRoomCreateModal/PairRoomCreateComplete/PairRoomCreateComplete.tsx b/frontend/src/components/Main/PairRoomCreateModal/PairRoomCreateComplete/PairRoomCreateComplete.tsx index 5202550db..a76ebf235 100644 --- a/frontend/src/components/Main/PairRoomCreateModal/PairRoomCreateComplete/PairRoomCreateComplete.tsx +++ b/frontend/src/components/Main/PairRoomCreateModal/PairRoomCreateComplete/PairRoomCreateComplete.tsx @@ -7,8 +7,6 @@ import { Modal } from '@/components/common/Modal'; import useCopyClipBoard from '@/hooks/common/useCopyClipboard'; -import { BUTTON_TEXT } from '@/constants/button'; - import * as S from '../PairRoomCreateModal.styles'; interface PairRoomCreateCompleteProps { @@ -36,7 +34,7 @@ const PairRoomCreateComplete = ({ accessCode, closeModal }: PairRoomCreateComple diff --git a/frontend/src/components/Main/PairRoomCreateModal/PairRoomCreateModal.tsx b/frontend/src/components/Main/PairRoomCreateModal/PairRoomCreateModal.tsx index 614808a19..405d5927c 100644 --- a/frontend/src/components/Main/PairRoomCreateModal/PairRoomCreateModal.tsx +++ b/frontend/src/components/Main/PairRoomCreateModal/PairRoomCreateModal.tsx @@ -11,39 +11,25 @@ interface PairRoomCreateModalProps { } const PairRoomCreateModal = ({ isOpen, closeModal }: PairRoomCreateModalProps) => { - // const handleSuccess = () => setStatus('COMPLETE'); - - // const { addPairRoom, accessCode, isPending } = useAddPairRoom(handleSuccess); - - // const createPairRoom = (firstPair: string, secondPair: string) => addPairRoom({ firstPair, secondPair }); - - // if (isPending) - // return ( - // - // - // - // - // - // - // - // ); - return ( - - - - - + ); }; diff --git a/frontend/src/components/Main/PairRoomEntryModal/PairRoomEntryModal.tsx b/frontend/src/components/Main/PairRoomEntryModal/PairRoomEntryModal.tsx index 6da864096..ed0cce49f 100644 --- a/frontend/src/components/Main/PairRoomEntryModal/PairRoomEntryModal.tsx +++ b/frontend/src/components/Main/PairRoomEntryModal/PairRoomEntryModal.tsx @@ -10,8 +10,6 @@ import { getPairRoomExists } from '@/apis/pairRoom'; import useInput from '@/hooks/common/useInput'; -import { BUTTON_TEXT } from '@/constants/button'; - interface PairRoomEntryModal { isOpen: boolean; closeModal: () => void; @@ -31,13 +29,12 @@ const PairRoomEntryModal = ({ isOpen, closeModal }: PairRoomEntryModal) => { return; } - navigate(`/room/${value}`); + navigate(`/room/${value}`, { state: { valid: true }, replace: true }); }; return ( - { + ); }; diff --git a/frontend/src/components/MyPage/PairRoomButton/PairRoomButton.styles.ts b/frontend/src/components/MyPage/PairRoomButton/PairRoomButton.styles.ts index d00b9d6af..bf36c0643 100644 --- a/frontend/src/components/MyPage/PairRoomButton/PairRoomButton.styles.ts +++ b/frontend/src/components/MyPage/PairRoomButton/PairRoomButton.styles.ts @@ -1,7 +1,12 @@ +import { Link } from 'react-router-dom'; + +import { FaTrashAlt } from 'react-icons/fa'; import styled, { keyframes, css } from 'styled-components'; import type { PairRoomStatus } from '@/apis/pairRoom'; +import { Z_INDEX } from '@/constants/style'; + const flow = keyframes` 0% { background-position: 200% 0; @@ -11,7 +16,94 @@ const flow = keyframes` } `; -export const Layout = styled.button<{ $status: PairRoomStatus }>` +const commonTextStyles = css` + font-size: ${({ theme }) => theme.fontSize.base}; + + transition: color 0.7s ease; +`; + +const inProgressText = css` + background: linear-gradient( + 90deg, + ${({ theme }) => theme.color.black[60]}, + ${({ theme }) => theme.color.black[70]}, + ${({ theme }) => theme.color.black[60]} + ); + + animation: ${flow} 4s linear infinite; + background-size: 200% 100%; + background-clip: text; +`; + +export const Layout = styled.div` + display: flex; + flex-direction: row; + align-items: center; + gap: 3rem; + + width: 100%; +`; + +export const LinkWrapper = styled(Link)` + width: 100%; +`; + +export const StatusText = styled.p<{ $status: PairRoomStatus }>` + width: 15%; + + ${commonTextStyles} + color: ${({ $status, theme }) => ($status === 'IN_PROGRESS' ? 'transparent' : theme.color.black[70])}; + letter-spacing: 0.15rem; + text-align: left; + + ${({ $status }) => $status === 'IN_PROGRESS' && inProgressText} + + &:hover { + color: white; + } +`; + +export const RoleTextContainer = styled.div` + display: flex; + flex-direction: column; + gap: 1rem; + + width: 50%; +`; + +export const RoleText = styled.p<{ $status: PairRoomStatus; $color: 'secondary' | 'primary' }>` + display: flex; + align-items: center; + gap: 1rem; + + font-size: ${({ theme }) => theme.fontSize.md}; + + transition: color 0.7s ease; + + span { + color: ${({ $status, theme, $color }) => + $status === 'IN_PROGRESS' ? theme.color[$color][600] : theme.color.black[70]}; + font-size: ${({ theme }) => theme.fontSize.lg}; + font-weight: ${({ theme }) => theme.fontWeight.medium}; + + transition: color 0.7s ease; + } +`; + +export const ConnectText = styled.div` + display: flex; + justify-content: right; + align-items: center; + gap: 0.4rem; + + width: 11%; + + color: ${({ theme }) => theme.color.black[10]}; + + transition: color 0.7s ease; +`; + +export const PairRoomButton = styled.button<{ $status: PairRoomStatus; $color: 'secondary' | 'primary' }>` display: flex; justify-content: space-between; align-items: center; @@ -25,85 +117,85 @@ export const Layout = styled.button<{ $status: PairRoomStatus }>` font-size: ${({ theme }) => theme.fontSize.base}; + cursor: pointer; + &::before { content: ''; position: absolute; top: 0; left: 0; - z-index: -1; + z-index: ${Z_INDEX.MINUS}; width: 100%; height: 100%; border-radius: 1rem; - background: ${({ $status, theme }) => + background-color: ${({ $status, $color, theme }) => + $status === 'IN_PROGRESS' ? theme.color[$color][100] : theme.color.black[30]}; + background-image: ${({ $status, theme, $color }) => $status === 'IN_PROGRESS' ? `linear-gradient( - 120deg, - ${theme.color.secondary[100]} 0 75%, - ${theme.color.secondary[600]} 75% 100% + 90deg, + ${theme.color[$color][100]} 0 75%, + ${theme.color[$color][600]} 75% 100% )` : `linear-gradient( - 120deg, + 90deg, ${theme.color.black[30]} 0 75%, ${theme.color.black[60]} 75% 100% )`}; + background-size: 400% 100%; + + background-position: 72.5% 0; opacity: 0.7; - transition: opacity 0.2s ease-in-out; + transition: + background-position 0.5s ease, + opacity 0.2s ease; } &:hover::before { + background-position: 105.8% 0; opacity: 1; } -`; - -export const RoleTextContainer = styled.div` - display: flex; - flex-direction: column; - gap: 1rem; -`; -export const RoleText = styled.p<{ $status: PairRoomStatus }>` - display: flex; - align-items: center; - gap: 1rem; + &:hover ${StatusText} { + color: white; + ${({ $status }) => + $status === 'IN_PROGRESS' && + css` + animation: ${flow} 4s linear infinite; + `} + } + &:hover ${RoleText} { + color: ${({ theme }) => theme.color.black[20]}; - font-size: ${({ theme }) => theme.fontSize.md}; + span { + color: ${({ theme }) => theme.color.black[10]}; + } + } - span { - color: ${({ $status, theme }) => ($status === 'IN_PROGRESS' ? theme.color.secondary[600] : theme.color.black[70])}; - font-size: ${({ theme }) => theme.fontSize.lg}; - font-weight: ${({ theme }) => theme.fontWeight.medium}; + &:hover ${ConnectText} { + color: ${({ theme }) => theme.color.black[70]}; } `; -const inProgressText = css` - background: linear-gradient( - 90deg, - ${({ theme }) => theme.color.black[60]}, - ${({ theme }) => theme.color.black[70]}, - ${({ theme }) => theme.color.black[60]} - ); +export const DeleteButton = styled(FaTrashAlt)` + color: ${({ theme }) => theme.color.black[60]}; + font-size: 1.6rem; - animation: ${flow} 4s linear infinite; - background-size: 200% 100%; - background-clip: text; -`; + transition: color 0.3s ease; -export const StatusText = styled.p<{ $status: PairRoomStatus }>` - color: ${({ $status, theme }) => ($status === 'IN_PROGRESS' ? 'transparent' : theme.color.black[70])}; - font-size: ${({ theme }) => theme.fontSize.base}; - letter-spacing: 0.15rem; + cursor: pointer; - ${({ $status }) => $status === 'IN_PROGRESS' && inProgressText} + &:hover { + color: ${({ theme }) => theme.color.danger[600]}; + } `; -export const ConnectText = styled.div` - display: flex; - align-items: center; - gap: 0.4rem; - - color: ${({ theme }) => theme.color.black[10]}; +export const DangerText = styled.span` + color: ${({ theme }) => theme.color.danger[600]}; + font-size: ${({ theme }) => theme.fontSize.base}; + line-height: 1.5; `; diff --git a/frontend/src/components/MyPage/PairRoomButton/PairRoomButton.tsx b/frontend/src/components/MyPage/PairRoomButton/PairRoomButton.tsx index 2f1a7c2ef..e40a560b5 100644 --- a/frontend/src/components/MyPage/PairRoomButton/PairRoomButton.tsx +++ b/frontend/src/components/MyPage/PairRoomButton/PairRoomButton.tsx @@ -1,9 +1,14 @@ -import { Link } from 'react-router-dom'; - import { IoIosArrowForward } from 'react-icons/io'; +import ConfirmModal from '@/components/common/ConfirmModal/ConfirmModal'; +import Spinner from '@/components/common/Spinner/Spinner'; + import type { PairRoomStatus } from '@/apis/pairRoom'; +import useModal from '@/hooks/common/useModal'; + +import useDeletePairRoom from '@/queries/MyPage/useDeleteRoom'; + import * as S from './PairRoomButton.styles'; interface PairRoomButtonProps { @@ -14,26 +19,60 @@ interface PairRoomButtonProps { } const PairRoomButton = ({ driver, navigator, status, accessCode }: PairRoomButtonProps) => { - return ( - - - - - 드라이버 - {driver} - - - 내비게이터 - {navigator} - - - {status === 'IN_PROGRESS' ? '진행 중' : '진행 완료'} - - 접속 - - + const { openModal, closeModal, isModalOpen } = useModal(); + + const { mutate, isPending } = useDeletePairRoom(); + + const handleOpenDeleteModal = (event: React.MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + openModal(); + }; + + const handleDeletePairRoom = async () => { + mutate(accessCode); + closeModal(); + }; + + if (isPending) { + return ( + + - + ); + } + + return ( + + + + + + 드라이버 + {driver} + + + 내비게이터 + {navigator} + + + {status === 'IN_PROGRESS' ? '진행 중' : '진행 완료'} + + 입장 + + + + + + + ); }; diff --git a/frontend/src/components/MyPage/PairRoomButton/RetrospectButton.tsx b/frontend/src/components/MyPage/PairRoomButton/RetrospectButton.tsx new file mode 100644 index 000000000..a04ba591c --- /dev/null +++ b/frontend/src/components/MyPage/PairRoomButton/RetrospectButton.tsx @@ -0,0 +1,73 @@ +import { IoIosArrowForward } from 'react-icons/io'; + +import ConfirmModal from '@/components/common/ConfirmModal/ConfirmModal'; +import Spinner from '@/components/common/Spinner/Spinner'; + +import useModal from '@/hooks/common/useModal'; + +import { useDeleteRetrospect } from '@/queries/Retrospect/useDeleteRetrospect'; + +import * as S from './PairRoomButton.styles'; + +interface RetrospectButtonProps { + answer: string; + accessCode: string; +} + +const RetrospectButton = ({ accessCode, answer }: RetrospectButtonProps) => { + const { openModal, closeModal, isModalOpen } = useModal(); + const { mutate, isPending } = useDeleteRetrospect(); + + const handleOpenDeleteModal = (event: React.MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + openModal(); + }; + + const handleDeletePairRoom = async () => { + mutate({ accessCode }); + closeModal(); + }; + + const splitAnswer = (answer: string): string => { + return answer.length > 10 ? answer.slice(0, 10) + '...' : answer; + }; + + if (isPending) { + return ( + + + + ); + } + + return ( + + + + + + {accessCode} + {splitAnswer(answer)} + + + + 보기 + + + + + + + + ); +}; + +export default RetrospectButton; diff --git a/frontend/src/components/PairRoom/GuideModal/GuideModal.styles.ts b/frontend/src/components/PairRoom/GuideModal/GuideModal.styles.ts new file mode 100644 index 000000000..cdb006e95 --- /dev/null +++ b/frontend/src/components/PairRoom/GuideModal/GuideModal.styles.ts @@ -0,0 +1,87 @@ +import styled, { css } from 'styled-components'; + +export const buttonStyles = css` + width: 9rem; + height: 3.2rem; + border-color: ${({ theme }) => theme.color.danger[400]}; + border-radius: 0.8rem; + + background-color: ${({ theme }) => theme.color.danger[400]}; + font-size: ${({ theme }) => theme.fontSize.md}; + + &:hover { + border-color: ${({ theme }) => theme.color.danger[500]}; + + background-color: ${({ theme }) => theme.color.danger[500]}; + } + + &:active { + border-color: ${({ theme }) => theme.color.danger[500]}; + + background-color: ${({ theme }) => theme.color.danger[500]}; + } +`; + +export const startButtonStyles = css` + width: 18rem; +`; + +export const Layout = styled.div` + display: flex; + flex-direction: column; + gap: 3rem; + + padding: 1rem 0; +`; + +export const Title = styled.h1` + font-size: ${({ theme }) => theme.fontSize.h5}; + font-weight: ${({ theme }) => theme.fontWeight.medium}; +`; + +export const List = styled.ul` + display: flex; + flex-direction: column; + gap: 2rem; +`; + +export const Item = styled.li` + display: flex; + flex-direction: column; + gap: 1rem; +`; + +export const Question = styled.p` + display: flex; + align-items: center; + gap: 0.6rem; + + font-size: ${({ theme }) => theme.fontSize.base}; +`; + +export const Description = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + gap: 1rem; + + height: 5.2rem; + padding: 1rem 1rem 1rem 1.6rem; + border-radius: 1rem; + + background-color: ${({ theme }) => theme.color.danger[100]}; + font-size: ${({ theme }) => theme.fontSize.md}; + font-weight: ${({ theme }) => theme.fontWeight.light}; +`; + +export const ButtonContainer = styled.div` + display: flex; + flex-direction: column; + align-items: center; + gap: 0.8rem; + + p { + color: ${({ theme }) => theme.color.black[60]}; + font-size: ${({ theme }) => theme.fontSize.sm}; + } +`; diff --git a/frontend/src/components/PairRoom/GuideModal/GuideModal.tsx b/frontend/src/components/PairRoom/GuideModal/GuideModal.tsx new file mode 100644 index 000000000..2a70c9fef --- /dev/null +++ b/frontend/src/components/PairRoom/GuideModal/GuideModal.tsx @@ -0,0 +1,94 @@ +import { useRef } from 'react'; + +import { FaCheck } from 'react-icons/fa6'; + +import { AlarmSound } from '@/assets'; + +import Button from '@/components/common/Button/Button'; +import { Modal } from '@/components/common/Modal'; + +import useToastStore from '@/stores/toastStore'; + +import useCopyClipBoard from '@/hooks/common/useCopyClipboard'; + +import * as S from './GuideModal.styles'; + +interface GuideModalProps { + isOpen: boolean; + close: () => void; + accessCode: string; +} + +const GuideModal = ({ isOpen, close, accessCode }: GuideModalProps) => { + const alarmAudio = useRef(new Audio(AlarmSound)); + + const { addToast } = useToastStore(); + + const [, onCopy] = useCopyClipBoard(); + + const checkPermission = () => { + if (Notification.permission !== 'granted') { + addToast({ status: 'ERROR', message: '알림 권한이 허용되지 않았습니다. 설정에서 권한을 허용해 주세요.' }); + return; + } + + addToast({ status: 'SUCCESS', message: '알림 권한이 허용된 상태입니다.' }); + }; + + return ( + + + + 페어 프로그래밍을 시작하기 전에... + + + + + 브라우저 알림을 허용하셨나요? + + + 브라우저 알림을 허용하지 않으면 타이머 종료 시 올바르게 알림을 제공할 수 없어요. + + + + + + + 사용 중인 기기의 소리가 켜져 있나요? + + + 사용 중인 기기의 소리가 꺼져 있다면 타이머 종료 시 알람 소리를 들으실 수 없어요. + + + + + + + 페어룸 코드를 복사하셨나요? + + + 페어에게 페어룸 코드를 전달하여 페어룸에 들어올 수 있도록 해주세요. + + + + + + +

모두 확인하셨나요?

+ +
+
+
+
+ ); +}; + +export default GuideModal; diff --git a/frontend/src/components/PairRoom/PairListCard/AccessCodeSection/AccessCodeSection.stories.tsx b/frontend/src/components/PairRoom/PairListCard/AccessCodeSection/AccessCodeSection.stories.tsx new file mode 100644 index 000000000..34b931a16 --- /dev/null +++ b/frontend/src/components/PairRoom/PairListCard/AccessCodeSection/AccessCodeSection.stories.tsx @@ -0,0 +1,26 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import AccessCodeSection from './AccessCodeSection'; + +const meta = { + title: 'component/PairRoom/PairListCard/AccessCodeSection', + component: AccessCodeSection, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + isOpen: true, + accessCode: 'IUUIASDFJK', + }, +}; + +export const Closed: Story = { + args: { + isOpen: false, + accessCode: 'IUUIASDFJK', + }, +}; diff --git a/frontend/src/components/PairRoom/PairListCard/RoomCodeSection/RoomCodeSection.styles.ts b/frontend/src/components/PairRoom/PairListCard/AccessCodeSection/AccessCodeSection.styles.ts similarity index 84% rename from frontend/src/components/PairRoom/PairListCard/RoomCodeSection/RoomCodeSection.styles.ts rename to frontend/src/components/PairRoom/PairListCard/AccessCodeSection/AccessCodeSection.styles.ts index 80885e59f..05a939d61 100644 --- a/frontend/src/components/PairRoom/PairListCard/RoomCodeSection/RoomCodeSection.styles.ts +++ b/frontend/src/components/PairRoom/PairListCard/AccessCodeSection/AccessCodeSection.styles.ts @@ -16,14 +16,14 @@ export const Layout = styled.div<{ $isOpen: boolean }>` cursor: pointer; `; -export const RoomCodeWrapper = styled.div` +export const AccessCodeWrapper = styled.div` display: flex; justify-content: space-between; align-items: center; gap: 1.2rem; `; -export const RoomCodeTitle = styled.span` +export const AccessCodeTitle = styled.span` height: 2rem; color: ${({ theme }) => theme.color.black[70]}; @@ -31,6 +31,6 @@ export const RoomCodeTitle = styled.span` font-weight: ${({ theme }) => theme.fontWeight.bold}; `; -export const RoomCode = styled.span` +export const AccessCode = styled.span` font-size: ${({ theme }) => theme.fontSize.md}; `; diff --git a/frontend/src/components/PairRoom/PairListCard/AccessCodeSection/AccessCodeSection.tsx b/frontend/src/components/PairRoom/PairListCard/AccessCodeSection/AccessCodeSection.tsx new file mode 100644 index 000000000..cb2a79e20 --- /dev/null +++ b/frontend/src/components/PairRoom/PairListCard/AccessCodeSection/AccessCodeSection.tsx @@ -0,0 +1,37 @@ +import { FaRegPaste } from 'react-icons/fa6'; + +import useCopyClipBoard from '@/hooks/common/useCopyClipboard'; + +import * as S from './AccessCodeSection.styles'; + +interface AccessCodeSectionProps { + isOpen: boolean; + accessCode: string; +} + +const AccessCodeSection = ({ isOpen, accessCode }: AccessCodeSectionProps) => { + const [, onCopy] = useCopyClipBoard(); + + const handleCopyClipBoard = (text: string) => { + onCopy(text); + }; + + return ( + handleCopyClipBoard(accessCode)} + aria-label={`페어룸 코드는 ${accessCode}입니다. 클릭하시면 페어룸 코드가 클립보드에 복사됩니다.`} + > + {isOpen && ( + + 방 코드 + {accessCode} + + )} + + + ); +}; + +export default AccessCodeSection; diff --git a/frontend/src/components/PairRoom/PairListCard/CompleteRoomButton/CompleteRoomButton.styles.ts b/frontend/src/components/PairRoom/PairListCard/CompleteRoomButton/CompleteRoomButton.styles.ts new file mode 100644 index 000000000..e2a0139ce --- /dev/null +++ b/frontend/src/components/PairRoom/PairListCard/CompleteRoomButton/CompleteRoomButton.styles.ts @@ -0,0 +1,36 @@ +import styled from 'styled-components'; + +import Tooltip from '@/components/common/Tooltip/Tooltip'; + +export const Layout = styled.button<{ disabled: boolean }>` + display: flex; + justify-content: center; + align-items: center; + gap: 0.8rem; + + position: absolute; + bottom: 0; + + width: 100%; + height: 6rem; + margin-top: auto; + border-radius: 0 0 2rem 2rem; + + background-color: ${({ theme, disabled }) => (disabled ? theme.color.black[20] : theme.color.danger[200])}; + color: ${({ theme, disabled }) => (disabled ? theme.color.black[60] : theme.color.danger[600])}; + font-size: ${({ theme }) => theme.fontSize.base}; + + cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')}; +`; + +export const StyledTooltip = styled(Tooltip)` + display: flex; + align-items: center; + gap: 1.2rem; +`; + +export const TextWrapper = styled.p` + display: flex; + align-items: center; + gap: 0.8rem; +`; diff --git a/frontend/src/components/PairRoom/PairListCard/CompleteRoomButton/CompleteRoomButton.tsx b/frontend/src/components/PairRoom/PairListCard/CompleteRoomButton/CompleteRoomButton.tsx new file mode 100644 index 000000000..4e5f08b5c --- /dev/null +++ b/frontend/src/components/PairRoom/PairListCard/CompleteRoomButton/CompleteRoomButton.tsx @@ -0,0 +1,40 @@ +import { ImExit } from 'react-icons/im'; + +import useUserStore from '@/stores/userStore'; + +import * as S from './CompleteRoomButton.styles'; + +interface CompleteRoomButtonProps { + isOpen: boolean; + openModal: () => void; +} + +const CompleteRoomButton = ({ isOpen, openModal }: CompleteRoomButtonProps) => { + const { userStatus } = useUserStore(); + + const isDisabled = userStatus !== 'SIGNED_IN'; + + return ( + + {isDisabled ? ( + + + + {isOpen && 페어룸 종료하기} + + + ) : ( + <> + + {isOpen && 페어룸 종료하기} + + )} + + ); +}; + +export default CompleteRoomButton; diff --git a/frontend/src/components/PairRoom/PairListCard/DeleteButton/DeleteButton.stories.tsx b/frontend/src/components/PairRoom/PairListCard/DeleteButton/DeleteButton.stories.tsx deleted file mode 100644 index efd170476..000000000 --- a/frontend/src/components/PairRoom/PairListCard/DeleteButton/DeleteButton.stories.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react'; - -import DeleteButton from './DeleteButton'; - -const meta = { - title: 'component/PairRoom/PairListCard/DeleteButton', - component: DeleteButton, -} satisfies Meta; - -export default meta; - -type Story = StoryObj; - -export const Default: Story = { - args: { - isOpen: true, - onRoomDelete: () => alert('Room deleted'), - }, -}; - -export const Closed: Story = { - args: { - isOpen: false, - onRoomDelete: () => alert('Room deleted'), - }, -}; diff --git a/frontend/src/components/PairRoom/PairListCard/DeleteButton/DeleteButton.styles.ts b/frontend/src/components/PairRoom/PairListCard/DeleteButton/DeleteButton.styles.ts deleted file mode 100644 index 379a87b57..000000000 --- a/frontend/src/components/PairRoom/PairListCard/DeleteButton/DeleteButton.styles.ts +++ /dev/null @@ -1,22 +0,0 @@ -import styled from 'styled-components'; - -export const Layout = styled.button` - display: flex; - justify-content: center; - align-items: center; - gap: 0.8rem; - - position: absolute; - bottom: 0; - - width: 100%; - height: 6rem; - margin-top: auto; - border-radius: 0 0 2rem 2rem; - - background-color: ${({ theme }) => theme.color.danger[200]}; - color: ${({ theme }) => theme.color.danger[600]}; - font-size: ${({ theme }) => theme.fontSize.base}; - - cursor: pointer; -`; diff --git a/frontend/src/components/PairRoom/PairListCard/DeleteButton/DeleteButton.tsx b/frontend/src/components/PairRoom/PairListCard/DeleteButton/DeleteButton.tsx deleted file mode 100644 index 162847db8..000000000 --- a/frontend/src/components/PairRoom/PairListCard/DeleteButton/DeleteButton.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { FaTrashAlt } from 'react-icons/fa'; - -import * as S from './DeleteButton.styles'; - -interface DeleteButtonProps { - isOpen: boolean; - onRoomDelete: () => void; -} - -const DeleteButton = ({ isOpen, onRoomDelete }: DeleteButtonProps) => ( - - - {isOpen && 방 삭제하기} - -); - -export default DeleteButton; diff --git a/frontend/src/components/PairRoom/PairListCard/Header/Header.tsx b/frontend/src/components/PairRoom/PairListCard/Header/Header.tsx index 58da2f4a7..104fb180c 100644 --- a/frontend/src/components/PairRoom/PairListCard/Header/Header.tsx +++ b/frontend/src/components/PairRoom/PairListCard/Header/Header.tsx @@ -12,8 +12,11 @@ interface HeaderProps { } const Header = ({ isOpen, toggleOpen }: HeaderProps) => ( - : <>} title={isOpen ? '페어' : ''}> - diff --git a/frontend/src/components/PairRoom/PairListCard/PairListCard.stories.tsx b/frontend/src/components/PairRoom/PairListCard/PairListCard.stories.tsx index e45dc2c4c..03c3b9f09 100644 --- a/frontend/src/components/PairRoom/PairListCard/PairListCard.stories.tsx +++ b/frontend/src/components/PairRoom/PairListCard/PairListCard.stories.tsx @@ -13,9 +13,8 @@ type Story = StoryObj; export const Default: Story = { args: { - roomCode: 'IUUIASDFJK', + accessCode: 'IUUIASDFJK', driver: '퍼렁', navigator: '포롱', - onRoomDelete: () => alert('방이 삭제되었습니다.'), }, }; diff --git a/frontend/src/components/PairRoom/PairListCard/PairListCard.tsx b/frontend/src/components/PairRoom/PairListCard/PairListCard.tsx index 68f02478e..83f9e8217 100644 --- a/frontend/src/components/PairRoom/PairListCard/PairListCard.tsx +++ b/frontend/src/components/PairRoom/PairListCard/PairListCard.tsx @@ -1,35 +1,55 @@ import { useState } from 'react'; -// import DeleteButton from '@/components/PairRoom/PairListCard/DeleteButton/DeleteButton'; +import ConfirmModal from '@/components/common/ConfirmModal/ConfirmModal'; +import AccessCodeSection from '@/components/PairRoom/PairListCard/AccessCodeSection/AccessCodeSection'; +import CompleteRoomButton from '@/components/PairRoom/PairListCard/CompleteRoomButton/CompleteRoomButton'; import Header from '@/components/PairRoom/PairListCard/Header/Header'; import PairListSection from '@/components/PairRoom/PairListCard/PairListSection/PairListSection'; -import RoomCodeSection from '@/components/PairRoom/PairListCard/RoomCodeSection/RoomCodeSection'; +import RepositorySection from '@/components/PairRoom/PairListCard/RepositorySection/RepositorySection'; import { PairRoomCard } from '@/components/PairRoom/PairRoomCard'; +import useModal from '@/hooks/common/useModal'; + +import useCompletePairRoom from '@/queries/PairRoom/useCompletePairRoom'; + import * as S from './PairListCard.styles'; interface PairListCardProps { driver: string; navigator: string; - roomCode: string; - onRoomDelete?: () => void; + missionUrl: string; + accessCode: string; } -const PairListCard = ({ driver, navigator, roomCode }: PairListCardProps) => { +const PairListCard = ({ driver, navigator, missionUrl, accessCode }: PairListCardProps) => { const [isOpen, setIsOpen] = useState(true); + const { isModalOpen, openModal, closeModal } = useModal(); + + const { handleCompletePairRoom } = useCompletePairRoom(accessCode); + const toggleOpen = () => setIsOpen(!isOpen); return ( - +
- + + {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)} > -
{ - event.preventDefault(); - if (status === 'ERROR') { - addToast({ status, message }); - return; - } - addCategory({ category: value, accessCode }).then(() => resetValue()); - }} - > + handleChange(event, validateCategory(event.target.value, isCategoryExist))} - maxLength={15} + autoFocus + placeholder="+ 새 카테고리 추가" height="4rem" + maxLength={15} + value={value} status={status} - placeholder="+ 새 카테고리 추가" - autoFocus + onChange={(event) => handleChange(event, validateCategoryName(event.target.value, isCategoryExist))} />
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(); - }} - > - {isChecked - - -

{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 ( +
+ + handleCategoryName(event, categoryName)} + $messageCss={S.CustomInputMessage} + /> + + + + + +
+ ); + } + + return ( + + + {isChecked + +

{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 ? ( - + ) : ( - + )}

링크

diff --git a/frontend/src/components/PairRoom/TodoListCard/Header/Header.tsx b/frontend/src/components/PairRoom/TodoListCard/Header/Header.tsx index 85411246e..de044c8dd 100644 --- a/frontend/src/components/PairRoom/TodoListCard/Header/Header.tsx +++ b/frontend/src/components/PairRoom/TodoListCard/Header/Header.tsx @@ -13,11 +13,14 @@ interface HeaderProps { const Header = ({ isOpen, toggleIsOpen }: React.PropsWithChildren) => { return ( - + {isOpen ? ( - + ) : ( - + )}

투두 리스트

{ maxLength={100} placeholder="할 일의 내용을 입력해 주세요." /> - diff --git a/frontend/src/components/PairRoomOnboarding/AddPairModal/AddPairModal.styles.ts b/frontend/src/components/PairRoomOnboarding/AddPairModal/AddPairModal.styles.ts new file mode 100644 index 000000000..6a980376e --- /dev/null +++ b/frontend/src/components/PairRoomOnboarding/AddPairModal/AddPairModal.styles.ts @@ -0,0 +1,5 @@ +import styled from 'styled-components'; + +export const Body = styled.div` + margin: 4rem 0; +`; diff --git a/frontend/src/components/PairRoomOnboarding/AddPairModal/AddPairModal.tsx b/frontend/src/components/PairRoomOnboarding/AddPairModal/AddPairModal.tsx new file mode 100644 index 000000000..ee804b7bc --- /dev/null +++ b/frontend/src/components/PairRoomOnboarding/AddPairModal/AddPairModal.tsx @@ -0,0 +1,69 @@ +import Button from '@/components/common/Button/Button'; +import Input from '@/components/common/Input/Input'; +import { Modal } from '@/components/common/Modal'; + +import useToastStore from '@/stores/toastStore'; + +import { getMemberName } from '@/apis/member'; + +import useInput from '@/hooks/common/useInput'; + +import { validatePairInfo } from '@/validations/validatePairName'; + +import * as S from './AddPairModal.styles'; + +interface AddPairModalProps { + isOpen: boolean; + closeModal: () => void; + onPairData: (pairId: string, pairName: string) => void; +} + +const AddPairModal = ({ isOpen, closeModal, onPairData }: AddPairModalProps) => { + const { value, status, message, handleChange, resetValue } = useInput(); + const { addToast } = useToastStore(); + + const handleCloseModal = () => { + resetValue(); + closeModal(); + }; + + const connectPairData = async (pairId: string) => { + try { + const { memberName } = await getMemberName(pairId); + onPairData(pairId, memberName); + handleCloseModal(); + addToast({ status: 'SUCCESS', message: '페어 정보 연동에 성공했습니다.' }); + } catch (error) { + if (error instanceof Error) { + addToast({ status: 'ERROR', message: error.message }); + } + } + }; + + return ( + + + + handleChange(event, validatePairInfo(event.target.value))} + /> + + + + + + + + ); +}; + +export default AddPairModal; diff --git a/frontend/src/components/PairRoomOnboarding/CreateBranchInput/CreateBranchInput.tsx b/frontend/src/components/PairRoomOnboarding/CreateBranchInput/CreateBranchInput.tsx index 9474e9539..fb2f2ed92 100644 --- a/frontend/src/components/PairRoomOnboarding/CreateBranchInput/CreateBranchInput.tsx +++ b/frontend/src/components/PairRoomOnboarding/CreateBranchInput/CreateBranchInput.tsx @@ -1,3 +1,4 @@ +/* eslint-disable jsx-a11y/no-autofocus */ import { GithubLogoWhite } from '@/assets'; import Input from '@/components/common/Input/Input'; @@ -17,20 +18,21 @@ const CreateBranchInput = ({ repositoryName, branchName, onBranchName }: CreateB const { branches } = useGetBranches(repositoryName); return ( - + {repositoryName} 미션을 시작할 브랜치 이름을 입력해 주세요. - + - +