diff --git a/frontend/src/apis/room.ts b/frontend/src/apis/room.ts new file mode 100644 index 000000000..c1fea4851 --- /dev/null +++ b/frontend/src/apis/room.ts @@ -0,0 +1,14 @@ +import fetcher from '@/apis/fetcher'; +import { BASE_URL, ENDPOINT } from '@/apis/url'; + +export const getRoomCompare = async (roomId1: number, roomId2: number) => { + const response = await fetcher.get({ url: BASE_URL + ENDPOINT.ROOM_COMPARE(roomId1, roomId2) }); + const data = await response.json(); + return data.checklists; +}; + +export const getRoomCategoryDetail = async ({ roomId, categoryId }: { roomId: number; categoryId: number }) => { + const response = await fetcher.get({ url: BASE_URL + ENDPOINT.ROOM_CATEGORY_DETAIL(roomId, categoryId) }); + const data = await response.json(); + return data; +}; diff --git a/frontend/src/apis/url.ts b/frontend/src/apis/url.ts index 71c936628..5bff1a849 100644 --- a/frontend/src/apis/url.ts +++ b/frontend/src/apis/url.ts @@ -15,7 +15,9 @@ export const ENDPOINT = { CHECKLIST_ID: (id: number) => `/checklists/${id}`, CHECKLIST_ID_V1: (id: number) => `/v1/checklists/${id}`, //compare - CHECKLIST_COMPARE: (roomId1: number, roomId2: number) => `/v1/checklists/compare?id=${roomId1}&id=${roomId2}`, + ROOM_COMPARE: (roomId1: number, roomId2: number) => `/v1/checklists/comparison?id=${roomId1}&id=${roomId2}`, + ROOM_CATEGORY_DETAIL: (roomId: number, categoryId: number) => + `/v1/comparison/checklists/${roomId}/categories/${categoryId}/questions`, // like LIKE: (id: number | ':id') => `/checklists/${id}/like`, diff --git a/frontend/src/components/ChecklistDetail/CategoryAccordion.tsx b/frontend/src/components/ChecklistDetail/CategoryAccordion.tsx deleted file mode 100644 index 7270f2aa9..000000000 --- a/frontend/src/components/ChecklistDetail/CategoryAccordion.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import Divider from '@/components/_common/Divider/Divider'; -import ChecklistAnswer from '@/components/ChecklistDetail/CheckListAnswer'; -import { ChecklistCategoryWithAnswer } from '@/types/checklist'; - -interface Props { - category: ChecklistCategoryWithAnswer; -} - -const CategoryAccordion = ({ category }: Props) => { - return ( - <> - {category.questions.map((question, index) => ( - <> - - {index !== category.questions.length - 1 && } - - ))} - - ); -}; - -export default CategoryAccordion; diff --git a/frontend/src/components/ChecklistDetail/ChecklistAnswerSection.tsx b/frontend/src/components/ChecklistDetail/ChecklistAnswerSection.tsx index 31c5bf2a9..562a43939 100644 --- a/frontend/src/components/ChecklistDetail/ChecklistAnswerSection.tsx +++ b/frontend/src/components/ChecklistDetail/ChecklistAnswerSection.tsx @@ -1,5 +1,6 @@ import Accordion from '@/components/_common/Accordion/Accordion'; -import CategoryAccordion from '@/components/ChecklistDetail/CategoryAccordion'; +import Divider from '@/components/_common/Divider/Divider'; +import ChecklistAnswer from '@/components/ChecklistDetail/CheckListAnswer'; import { CATEGORY_COUNT } from '@/constants/category'; import { ChecklistCategoryWithAnswer } from '@/types/checklist'; @@ -17,7 +18,12 @@ const ChecklistAnswerSection = ({ categories }: Props) => {
- + {category.questions.map((question, index) => ( + <> + + {index !== category.questions.length - 1 && } + + ))}
); diff --git a/frontend/src/components/ChecklistList/CustomBanner.tsx b/frontend/src/components/ChecklistList/CustomBanner.tsx index ef092a01e..203bd5e5d 100644 --- a/frontend/src/components/ChecklistList/CustomBanner.tsx +++ b/frontend/src/components/ChecklistList/CustomBanner.tsx @@ -1,21 +1,25 @@ import styled from '@emotion/styled'; -import { PencilIcon } from '@/assets/assets'; import Button from '@/components/_common/Button/Button'; import { boxShadow, flexCenter, flexRow } from '@/styles/common'; interface Props { onClick?: () => void; + title: string; + buttonColor: string; + buttonText: string; + Icon: React.ReactElement; + buttonDetailText: string; } -const CustomBanner = ({ onClick }: Props) => { +const CustomBanner = ({ onClick, Icon, title, buttonColor, buttonText, buttonDetailText }: Props) => { return ( - - + ); }; @@ -47,10 +51,10 @@ const S = { Title: styled.span` ${flexCenter} `, - Button: styled(Button)` + Button: styled(Button)<{ buttonColor: string }>` padding: 0.6rem 1rem; - background-color: ${({ theme }) => theme.palette.green500}; + background-color: ${({ buttonColor }) => buttonColor}; color: ${({ theme }) => theme.palette.white}; border-radius: 0.8rem; diff --git a/frontend/src/components/EditChecklist/ChecklistContent/EditChecklistQuestionTemplate.tsx b/frontend/src/components/EditChecklist/ChecklistContent/EditChecklistQuestionTemplate.tsx index 2e147161f..cd325b9b2 100644 --- a/frontend/src/components/EditChecklist/ChecklistContent/EditChecklistQuestionTemplate.tsx +++ b/frontend/src/components/EditChecklist/ChecklistContent/EditChecklistQuestionTemplate.tsx @@ -4,9 +4,10 @@ import Divider from '@/components/_common/Divider/Divider'; import Layout from '@/components/_common/layout/Layout'; import { useTabContext } from '@/components/_common/Tabs/TabContext'; import ChecklistQuestionItem from '@/components/NewChecklist/ChecklistQuestion/ChecklistQuestion'; +import ChecklistQuestionAnswers from '@/components/NewChecklist/ChecklistQuestion/ChecklistQuestionAnswers'; import MoveNextButton from '@/components/NewChecklist/MoveNextButton'; import useChecklistStore from '@/store/useChecklistStore'; -import { flexColumn } from '@/styles/common'; +import { flexColumn, flexRow, flexSpaceBetween } from '@/styles/common'; import theme from '@/styles/theme'; import { ChecklistQuestion } from '@/types/checklist'; @@ -28,11 +29,15 @@ const EditChecklistQuestionTemplate = () => { const isLastQuestion = questions?.questions.length - 1 === index; return ( <> - + + + + + {!isLastQuestion && } ); @@ -56,6 +61,15 @@ const S = { gap: 0.2rem; `, QuestionBox: styled.div` + position: relative; + width: 100%; + ${flexRow} + ${flexSpaceBetween} + padding: 1.6rem; + border-radius: 0.8rem; + + box-sizing: border-box; + background-color: ${({ theme }) => theme.palette.white}; `, }; diff --git a/frontend/src/components/Main/ChecklistPreviewCard.tsx b/frontend/src/components/Main/ChecklistPreviewCard.tsx index 6ff35fe4c..37ef7cc32 100644 --- a/frontend/src/components/Main/ChecklistPreviewCard.tsx +++ b/frontend/src/components/Main/ChecklistPreviewCard.tsx @@ -15,7 +15,7 @@ interface Props { const ChecklistPreviewCard = ({ index, checklist }: Props) => { const navigate = useNavigate(); - const colorList = ['green', 'blue', 'red']; + const colorList = ['green', 'blue', 'yellow']; const { color200, color500 } = getSeqColor(index, colorList); const { checklistId, station, roomName, deposit, rent, address } = checklist; diff --git a/frontend/src/components/MyPage/UserFeature.tsx b/frontend/src/components/MyPage/UserFeature.tsx index 3dc9e67bd..75882560e 100644 --- a/frontend/src/components/MyPage/UserFeature.tsx +++ b/frontend/src/components/MyPage/UserFeature.tsx @@ -14,6 +14,7 @@ const UserFeature = () => { useUserQuery(); const queryClient = useQueryClient(); + const checklist = queryClient.getQueryData([QUERY_KEYS.CHECKLIST_LIST]); const { isModalOpen: isLogoutModalOpen, openModal: openLogoutModal, closeModal: closeLogoutModal } = useModal(); diff --git a/frontend/src/components/NewChecklist/ChecklistQuestion/ChecklistQuestion.tsx b/frontend/src/components/NewChecklist/ChecklistQuestion/ChecklistQuestion.tsx index ed0ff21e4..5e6e63628 100644 --- a/frontend/src/components/NewChecklist/ChecklistQuestion/ChecklistQuestion.tsx +++ b/frontend/src/components/NewChecklist/ChecklistQuestion/ChecklistQuestion.tsx @@ -2,79 +2,31 @@ import styled from '@emotion/styled'; import React from 'react'; import HighlightText from '@/components/_common/Highlight/HighlightText'; -import ChecklistQuestionAnswers from '@/components/NewChecklist/ChecklistQuestion/ChecklistQuestionAnswers'; -import { flexCenter, flexRow, flexSpaceBetween } from '@/styles/common'; -import { AnswerType } from '@/types/answer'; import { ChecklistQuestion } from '@/types/checklist'; +type FontSize = 'medium' | 'small'; interface Props { question: ChecklistQuestion; - answer: AnswerType; + width?: string; + fontSize?: FontSize; } -const ChecklistQuestionItem = ({ question, answer }: Props) => { - const { questionId, title, highlights } = question; +const ChecklistQuestionItem = ({ question, width = '100%', fontSize = 'medium' }: Props) => { + const { title, highlights } = question; return ( - - - - - - - - + + + ); }; export default React.memo(ChecklistQuestionItem); const S = { - Container: styled.div` - position: relative; - width: 100%; - ${flexRow} - ${flexSpaceBetween} - padding: 1.6rem; - border-radius: 0.8rem; - - box-sizing: border-box; - - background-color: ${({ theme }) => theme.palette.white}; - `, - Question: styled.div` + Question: styled.div<{ width: string }>` display: flex; - width: 80%; + width: ${({ width }) => width}; flex-flow: column wrap; `, - Subtitle: styled.div` - width: 100%; - - color: ${({ theme }) => theme.palette.grey500}; - font-size: ${({ theme }) => theme.text.size.small}; - word-break: keep-all; - `, - Options: styled.div` - width: 8rem; - - ${flexRow} - gap: 1.5rem; - - ${flexSpaceBetween} - align-items: center; - cursor: pointer; - `, - ButtonBox: styled.div` - position: absolute; - top: 1rem; - right: 1rem; - border-radius: 50%; - width: 4rem; - height: 4rem; - ${flexCenter} - - :hover { - background-color: ${({ theme }) => theme.palette.background}; - } - `, }; diff --git a/frontend/src/components/NewChecklist/ChecklistQuestion/ChecklistQuestionAnswers.tsx b/frontend/src/components/NewChecklist/ChecklistQuestion/ChecklistQuestionAnswers.tsx index 69193b1ae..a16d79988 100644 --- a/frontend/src/components/NewChecklist/ChecklistQuestion/ChecklistQuestionAnswers.tsx +++ b/frontend/src/components/NewChecklist/ChecklistQuestion/ChecklistQuestionAnswers.tsx @@ -1,3 +1,4 @@ +import styled from '@emotion/styled'; import React, { useCallback } from 'react'; import { useTabContext } from '@/components/_common/Tabs/TabContext'; @@ -5,6 +6,7 @@ import AnswerIcon from '@/components/Answer/AnswerIcon'; import { ANSWER_OPTIONS } from '@/constants/answer'; import useChecklistQuestionAnswer from '@/hooks/useChecklistQuestionAnswer'; import { trackChecklistQuestion } from '@/service/amplitude/trackEvent'; +import { flexRow, flexSpaceBetween } from '@/styles/common'; import { Answer, AnswerType } from '@/types/answer'; interface Props { @@ -26,7 +28,7 @@ const ChecklistQuestionAnswers = ({ questionId, answer, title }: Props) => { ); return ( - <> + {ANSWER_OPTIONS.map((option: Answer) => { const isSelected = answer === option.name; @@ -46,8 +48,21 @@ const ChecklistQuestionAnswers = ({ questionId, answer, title }: Props) => { {title + statusMessage} )} - + ); }; +const S = { + Options: styled.div` + width: 8rem; + + ${flexRow} + gap: 1.5rem; + + ${flexSpaceBetween} + align-items: center; + cursor: pointer; + `, +}; + export default React.memo(ChecklistQuestionAnswers); diff --git a/frontend/src/components/NewChecklist/ChecklistQuestion/ChecklistQuestionTemplate.tsx b/frontend/src/components/NewChecklist/ChecklistQuestion/ChecklistQuestionTemplate.tsx index 3483e1f2a..d254aaf2f 100644 --- a/frontend/src/components/NewChecklist/ChecklistQuestion/ChecklistQuestionTemplate.tsx +++ b/frontend/src/components/NewChecklist/ChecklistQuestion/ChecklistQuestionTemplate.tsx @@ -4,10 +4,11 @@ import Divider from '@/components/_common/Divider/Divider'; import Layout from '@/components/_common/layout/Layout'; import { useTabContext } from '@/components/_common/Tabs/TabContext'; import ChecklistQuestionItem from '@/components/NewChecklist/ChecklistQuestion/ChecklistQuestion'; +import ChecklistQuestionAnswers from '@/components/NewChecklist/ChecklistQuestion/ChecklistQuestionAnswers'; import MoveNextButton from '@/components/NewChecklist/MoveNextButton'; import useInitialChecklist from '@/hooks/useInitialChecklist'; import useChecklistStore from '@/store/useChecklistStore'; -import { flexCenter, flexColumn } from '@/styles/common'; +import { flexCenter, flexColumn, flexRow, flexSpaceBetween } from '@/styles/common'; import theme from '@/styles/theme'; import { ChecklistQuestion } from '@/types/checklist'; @@ -29,17 +30,20 @@ const ChecklistQuestionTemplate = () => { const isLastQuestion = questions?.questions.length - 1 === index; return ( <> - + + + + + {!isLastQuestion && } ); })} - ); @@ -57,6 +61,15 @@ const S = { gap: 0.2rem; `, QuestionBox: styled.div` + position: relative; + width: 100%; + ${flexRow} + ${flexSpaceBetween} + padding: 1.6rem; + border-radius: 0.8rem; + + box-sizing: border-box; + background-color: ${({ theme }) => theme.palette.white}; `, }; diff --git a/frontend/src/components/NewChecklist/MemoModal/MemoButton.tsx b/frontend/src/components/NewChecklist/MemoModal/MemoButton.tsx index b85507f84..2fea1db3c 100644 --- a/frontend/src/components/NewChecklist/MemoModal/MemoButton.tsx +++ b/frontend/src/components/NewChecklist/MemoModal/MemoButton.tsx @@ -3,7 +3,7 @@ import styled from '@emotion/styled'; import { Memo } from '@/assets/assets'; import { boxShadow, title3 } from '@/styles/common'; -interface Props extends React.HTMLAttributes { +interface Props extends React.ButtonHTMLAttributes { onClick?: () => void; } diff --git a/frontend/src/components/RoomCompare/CategoryDetailModal.tsx b/frontend/src/components/RoomCompare/CategoryDetailModal.tsx index 576d630a1..62004ce7c 100644 --- a/frontend/src/components/RoomCompare/CategoryDetailModal.tsx +++ b/frontend/src/components/RoomCompare/CategoryDetailModal.tsx @@ -1,19 +1,110 @@ +import styled from '@emotion/styled'; +import { useSearchParams } from 'react-router-dom'; + +import Accordion from '@/components/_common/Accordion/Accordion'; +import Divider from '@/components/_common/Divider/Divider'; import Modal from '@/components/_common/Modal/Modal'; +import ChecklistQuestionItem from '@/components/NewChecklist/ChecklistQuestion/ChecklistQuestion'; +import useGetRoomCategoryDetailQuery from '@/hooks/query/useGetRoomCategoryDetail'; +import { flexSpaceBetween } from '@/styles/common'; +import theme from '@/styles/theme'; +import { SmallAnswerType } from '@/types/RoomCompare'; interface Props { isOpen: boolean; closeModal: () => void; } +interface CategorySectionType { + id: SmallAnswerType; + text: string; + color: string; +} + +const CateogorySection: CategorySectionType[] = [ + { id: 'good', text: '긍정적', color: theme.palette.green400 }, + { id: 'bad', text: '부정적', color: theme.palette.red500 }, + { id: 'none', text: '무응답', color: theme.palette.grey300 }, +]; + const CategoryDetailModal = ({ isOpen, closeModal }: Props) => { + const [searchParams] = useSearchParams(); + const roomId = Number(searchParams.get('targetRoomId')); + const categoryId = Number(searchParams.get('categoryId')); + + if (!roomId || !categoryId) throw new Error('잘못된 접근입니다.'); + + const { data: category, isLoading } = useGetRoomCategoryDetailQuery({ roomId, categoryId }); + + if (isLoading) + return ( + + 카테고리 질문 상세보기 + +
loading
+
+
+ ); + return ( - - 카테고리 비교 + + 카테고리 질문 상세보기 -
카테고리 비교 내용이 들어갑니다.
+ + {CateogorySection.map((section, index) => { + return ( + + + + {category?.[section.id].map((question, index, questions) => { + const isLastQuestion = questions.length - 1 === index; + return ( + <> + + + + + + {!isLastQuestion && } + + ); + })} + + + ); + })} +
); }; +const S = { + QuestionBox: styled.div` + ${flexSpaceBetween} + width: 100%; + padding: 1rem; + + background-color: ${({ theme }) => theme.palette.white}; + flex-direction: row; + align-items: center; + box-sizing: border-box; + `, + Title: styled.div` + display: flex; + margin: 0.5rem 0; + + font-size: 1.4rem; + align-items: baseline; + `, + FlexBox: styled.div` + gap: 0.4rem; + `, +}; + export default CategoryDetailModal; diff --git a/frontend/src/components/RoomCompare/CategoryScore.tsx b/frontend/src/components/RoomCompare/CategoryScore.tsx index f47b63fc5..fa3cd1f8c 100644 --- a/frontend/src/components/RoomCompare/CategoryScore.tsx +++ b/frontend/src/components/RoomCompare/CategoryScore.tsx @@ -41,8 +41,8 @@ const S = { ${flexCenter} text-align: center; `, - Score: styled.span` - width: 4rem; + Score: styled.button` + width: 6.5rem; padding: 6px 8px; border: 1px solid ${({ theme }) => theme.palette.grey300}; diff --git a/frontend/src/components/RoomCompare/CompareCard.tsx b/frontend/src/components/RoomCompare/CompareCard.tsx index b93f53d90..07c7e0539 100644 --- a/frontend/src/components/RoomCompare/CompareCard.tsx +++ b/frontend/src/components/RoomCompare/CompareCard.tsx @@ -5,10 +5,10 @@ import CategoryScore from '@/components/RoomCompare/CategoryScore'; import CompareCardItem from '@/components/RoomCompare/CompareCardItem'; import { EMPTY_INDICATOR } from '@/constants/system'; import { boxShadow, flexColumn, title1, title4 } from '@/styles/common'; -import { ChecklistCompare } from '@/types/checklistCompare'; +import { RoomCompare } from '@/types/RoomCompare'; interface Props { - room: ChecklistCompare; + room: RoomCompare; index: number; openOptionModal: () => void; openCategoryModal: (roomId: number, categoryId: number) => void; @@ -35,15 +35,16 @@ const CompareCard = ({ room, openOptionModal, openCategoryModal }: Props) => { /> {room.contractTerm}개월} /> } + item={} /> {room.options.length}개} /> {/*카테고리별 질문 평점 섹션*/} - {room.categories.map(category => ( + {room?.categories?.categories.map(category => ( void; + room: ChecklistPreview; +} + +const CompareSelectCard = ({ isSelected, toggleSelectChecklist, room }: Props) => { + const { roomName, address, deposit, rent, checklistId } = room; + + return ( + toggleSelectChecklist(checklistId)} isSelected={isSelected}> + + + + + + + + {roomName} + + {formattedUndefined(deposit)} / {formattedUndefined(rent)} + + + {`"${formattedUndefined(room.summary, 'string')}"`} + + + + + ); +}; + +const S = { + Card: styled.div<{ isSelected: boolean }>` + ${flexColumn} + padding: 14px 18px; + border: 2px solid ${({ isSelected, theme }) => (isSelected ? theme.palette.green300 : theme.palette.grey200)}; + border-radius: 10px; + + background-color: ${({ isSelected, theme }) => (isSelected ? theme.palette.green50 : theme.palette.white)}; + row-gap: 5px; + cursor: pointer; + `, + CheckboxContainer: styled.div` + width: 4rem; + `, + Row: styled.div` + ${flexSpaceBetween} + align-items: center; + `, + Column: styled.div` + ${flexColumn} + `, + SummaryWrapper: styled.div` + align-items: center; + padding: 0.8rem; + + background-color: ${({ theme }) => theme.palette.grey50}; + border-radius: 0.6rem; + box-sizing: content-box; + `, + SummaryBox: styled.div` + box-sizing: content-box; + ${omitText}; + border-radius: 0.4rem; + + font-size: ${({ theme }) => theme.text.size.small}; + `, + Title: styled.p` + ${title3} + `, + Deposit: styled.p` + font-size: ${({ theme }) => theme.text.size.medium}; + `, + LocationWrapper: styled.p` + display: flex; + align-items: center; + gap: 0.5rem; + + font-size: ${({ theme }) => theme.text.size.xSmall}; + `, + HeaderContainer: styled.div` + ${flexRow} + ${flexSpaceBetween} + `, + RentPrice: styled.p` + font-weight: ${({ theme }) => theme.text.weight.bold}; + font-size: ${({ theme }) => theme.text.size.medium}; + `, + FlexRow: styled.div<{ gap?: string; width?: string }>` + ${flexRow} + align-items: center; + column-gap: ${({ gap }) => (gap ? gap : '4%')}; + ${({ width }) => width && `width:${width}`}; + height: 100%; + `, + FlexColumn: styled.div` + ${flexColumn} + gap:0.5rem; + width: 100%; + height: 100%; + `, +}; + +export default CompareSelectCard; diff --git a/frontend/src/components/RoomCompare/CompareSelectCardList.tsx b/frontend/src/components/RoomCompare/CompareSelectCardList.tsx new file mode 100644 index 000000000..b7be1cd6d --- /dev/null +++ b/frontend/src/components/RoomCompare/CompareSelectCardList.tsx @@ -0,0 +1,47 @@ +import styled from '@emotion/styled'; + +import CompareSelectCard from '@/components/RoomCompare/CompareSelectCard'; +import SkChecklistList from '@/components/skeleton/ChecklistList/SkChecklistLst'; +import { flexColumn } from '@/styles/common'; +import { ChecklistPreview } from '@/types/checklist'; + +interface Props { + userChecklists: ChecklistPreview[]; + selectedRoomIds: Set; + toggleSelectChecklist: (roomId: number) => void; + isLoading: boolean; +} + +const CompareSelectCardList = ({ isLoading, userChecklists, selectedRoomIds, toggleSelectChecklist }: Props) => { + if (isLoading) { + return ( + + + + ); + } + + const isSelectedRoom = (roomId: number) => !!selectedRoomIds.has(roomId); + + return ( + + {userChecklists?.map(checklist => ( + + ))} + + ); +}; + +export default CompareSelectCardList; + +const S = { + CardContainer: styled.div` + ${flexColumn} + gap: 1rem; + `, +}; diff --git a/frontend/src/components/RoomCompare/OptionDetailModal.tsx b/frontend/src/components/RoomCompare/OptionDetailModal.tsx index 7d8dddb10..3735b9b5c 100644 --- a/frontend/src/components/RoomCompare/OptionDetailModal.tsx +++ b/frontend/src/components/RoomCompare/OptionDetailModal.tsx @@ -3,6 +3,7 @@ import styled from '@emotion/styled'; import Bad from '@/assets/icons/answer/bad'; import Good from '@/assets/icons/answer/good'; import Modal from '@/components/_common/Modal/Modal'; +import { OptionDetail } from '@/pages/RoomComparePage'; import { flexCenter, omitText } from '@/styles/common'; import theme from '@/styles/theme'; @@ -11,16 +12,11 @@ interface Props { roomTitle2: string; isOpen: boolean; closeModal: () => void; - hasOptions: hasOption[]; + hasOptions: OptionDetail[]; + optionCounts: [number, number]; } -interface hasOption { - optionName: string; - hasRoom1: boolean; - hasRoom2: boolean; -} - -const OptionDetailModal = ({ roomTitle1, roomTitle2, isOpen, closeModal, hasOptions }: Props) => { +const OptionDetailModal = ({ roomTitle1, roomTitle2, isOpen, closeModal, hasOptions, optionCounts }: Props) => { return ( 옵션 비교 @@ -30,19 +26,19 @@ const OptionDetailModal = ({ roomTitle1, roomTitle2, isOpen, closeModal, hasOpti {roomTitle1} {roomTitle2} {hasOptions.map(option => { - const { optionName, hasRoom1, hasRoom2 } = option; + const { optionName, hasOption } = option; return ( <> {optionName} - {hasRoom1 ? ( + {hasOption[0] ? ( ) : ( )} - {hasRoom2 ? ( + {hasOption[1] ? ( ) : ( @@ -52,8 +48,8 @@ const OptionDetailModal = ({ roomTitle1, roomTitle2, isOpen, closeModal, hasOpti ); })} 총 개수 - 3개 - 5개 + {optionCounts[0]}개 + {optionCounts[1]}개 @@ -65,7 +61,7 @@ export default OptionDetailModal; const S = { Container: styled.div` display: grid; - grid-template-columns: 0.8fr 1fr 1fr; + grid-template-columns: 1fr 1fr 1fr; `, ItemText: styled.div<{ isBold?: boolean; hasBorder?: boolean }>` padding: 0.6rem 1rem; diff --git a/frontend/src/components/RoomCompare/RoomMarker.tsx b/frontend/src/components/RoomCompare/RoomMarker.tsx deleted file mode 100644 index ea3eccfc0..000000000 --- a/frontend/src/components/RoomCompare/RoomMarker.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import styled from '@emotion/styled'; - -import { flexCenter } from '@/styles/common'; - -type Size = 'medium' | 'small'; -const RoomMarker = ({ type, size = 'medium', onClick }: { type: 'A' | 'B'; size?: Size; onClick?: () => void }) => { - return ( - - {type} - - ); -}; - -const S = { - RoomMarker: styled.span<{ type: string; size: Size }>` - display: inline; - width: ${({ size }) => (size === 'medium' ? '2.6rem' : '2rem')}; - height: ${({ size }) => (size === 'medium' ? '2.6rem' : '2rem')}; - - ${flexCenter}; - flex-shrink: 0; - - background-color: ${({ type, theme }) => (type === 'A' ? theme.palette.yellow600 : theme.palette.green600)}; - - color: white; - border-radius: 50%; - - font-weight: ${({ theme }) => theme.text.weight.bold}; - font-size: ${({ theme, size }) => (size === 'medium' ? theme.text.size.medium : theme.text.size.xSmall)}; - `, -}; - -export default RoomMarker; diff --git a/frontend/src/components/_common/Accordion/AccordionBody.tsx b/frontend/src/components/_common/Accordion/AccordionBody.tsx index df14b0dc3..8f798bee8 100644 --- a/frontend/src/components/_common/Accordion/AccordionBody.tsx +++ b/frontend/src/components/_common/Accordion/AccordionBody.tsx @@ -34,7 +34,7 @@ const AccordionBody = ({ children, id }: Props) => { const S = { Container: styled.div<{ isOpen: boolean; maxHeight: number }>` overflow: hidden; - margin: 1rem 0 0.5rem; + margin: 0.8rem 0; max-height: ${({ maxHeight }) => maxHeight}px; transition: max-height 0.4s cubic-bezier(0.15, 0.1, 0.25, 1); border-radius: 0.8rem; diff --git a/frontend/src/components/_common/Accordion/AccordionHeader.tsx b/frontend/src/components/_common/Accordion/AccordionHeader.tsx index 7778c4268..dedb14d48 100644 --- a/frontend/src/components/_common/Accordion/AccordionHeader.tsx +++ b/frontend/src/components/_common/Accordion/AccordionHeader.tsx @@ -12,6 +12,7 @@ interface Props { text?: string; isMarked?: boolean; markColor?: string; + isShowMarkerIfOpen?: boolean; } const AccordionHeader = ({ id, @@ -20,18 +21,16 @@ const AccordionHeader = ({ text, isMarked = true, markColor = theme.palette.yellow500, + isShowMarkerIfOpen = true, }: Props) => { const { isAccordionOpen, handleAccordionOpenChange } = useAccordionContext(); return ( handleAccordionOpenChange(id)}> - {isAccordionOpen(id) ? ( - - ) : ( - - )} - + {!isShowMarkerIfOpen && } + {isAccordionOpen(id) && isShowMarkerIfOpen && } + {!isAccordionOpen(id) && isShowMarkerIfOpen && } {text} handleAccordionOpenChange}> {isAccordionOpen(id) ? openButton : closeButton} @@ -44,9 +43,10 @@ const AccordionHeader = ({ export default AccordionHeader; const S = { - HeaderContainer: styled.div` + HeaderContainer: styled.button` display: flex; position: relative; + width: 100%; height: 4.5rem; background-color: ${({ theme }) => theme.palette.white}; diff --git a/frontend/src/components/_common/Button/Button.tsx b/frontend/src/components/_common/Button/Button.tsx index 23ceb722b..de93f2e47 100644 --- a/frontend/src/components/_common/Button/Button.tsx +++ b/frontend/src/components/_common/Button/Button.tsx @@ -1,6 +1,6 @@ import { css } from '@emotion/react'; import styled from '@emotion/styled'; -import { FunctionComponent, SVGProps } from 'react'; +import { ComponentProps, FunctionComponent, SVGProps } from 'react'; import FlexBox from '@/components/_common/FlexBox/FlexBox'; import { flexCenter, title3, title4 } from '@/styles/common'; @@ -10,7 +10,7 @@ type ButtonSize = 'xSmall' | 'small' | 'medium' | 'full'; type ColorOption = 'light' | 'dark' | 'primary' | 'disabled'; type ButtonType = 'button' | 'submit' | 'reset'; -interface Props extends React.HTMLAttributes { +interface Props extends ComponentProps<'button'> { size?: ButtonSize; color?: ColorOption; label: string; diff --git a/frontend/src/components/_common/FloatingButton/FloatingButton.tsx b/frontend/src/components/_common/FloatingButton/FloatingButton.tsx index d0bda27e2..795766961 100644 --- a/frontend/src/components/_common/FloatingButton/FloatingButton.tsx +++ b/frontend/src/components/_common/FloatingButton/FloatingButton.tsx @@ -8,7 +8,7 @@ import theme from '@/styles/theme'; type Size = 'small' | 'medium' | 'extends'; type Color = 'yellow' | 'green' | 'subGreen'; -interface Props extends React.HTMLAttributes { +interface Props extends React.ButtonHTMLAttributes { children: React.ReactNode; onClick: () => void; size?: Size; diff --git a/frontend/src/components/_common/Highlight/HighlightText.tsx b/frontend/src/components/_common/Highlight/HighlightText.tsx index fd6fa6a3f..b6f15e79f 100644 --- a/frontend/src/components/_common/Highlight/HighlightText.tsx +++ b/frontend/src/components/_common/Highlight/HighlightText.tsx @@ -4,29 +4,33 @@ import React from 'react'; import { title3 } from '@/styles/common'; import theme from '@/styles/theme'; +type FontSize = 'medium' | 'small'; interface Props { title: string; highlights: string[]; + fontSize?: FontSize; } -const highlightText = ({ title, highlights }: Props) => { +const highlightText = ({ title, highlights, fontSize = 'medium' }: Props) => { if (!highlights || highlights.length === 0) return title; const regex = new RegExp(`(${highlights.join('|')})`, 'gi'); - return title - .split(regex) - .map((part, index) => - highlights.some(highlight => highlight.toLowerCase() === part.toLowerCase()) ? ( - {part} - ) : ( - {part} - ), - ); + return title.split(regex).map((part, index) => + highlights.some(highlight => highlight.toLowerCase() === part.toLowerCase()) ? ( + + {part} + + ) : ( + + {part} + + ), + ); }; -const HighlightText = ({ title, highlights }: Props) => { - return {highlightText({ title, highlights })}; +const HighlightText = ({ title, highlights, fontSize }: Props) => { + return {highlightText({ title, highlights, fontSize })}; }; export default React.memo(HighlightText); @@ -42,18 +46,21 @@ const S = { white-space: normal; word-break: break-word; `, - Highlight: styled.span` + Highlight: styled.span<{ fontSize: FontSize }>` display: inline; background: linear-gradient(to top, ${theme.palette.yellow400} 50%, transparent 50%); ${title3}; margin: 0 0.2rem; + font-size: ${({ fontSize, theme }) => (fontSize === 'medium' ? theme.text.size.medium : theme.text.size.small)}; word-break: break-word; white-space: normal; `, - NormalText: styled.span` + NormalText: styled.span<{ fontSize: FontSize }>` display: inline; + + font-size: ${({ fontSize, theme }) => (fontSize === 'medium' ? theme.text.size.medium : theme.text.size.small)}; word-break: break-word; white-space: normal; diff --git a/frontend/src/components/_common/Map/AddressMap.tsx b/frontend/src/components/_common/Map/AddressMap.tsx index 2e33c86d6..414d258a3 100644 --- a/frontend/src/components/_common/Map/AddressMap.tsx +++ b/frontend/src/components/_common/Map/AddressMap.tsx @@ -46,7 +46,7 @@ const AddressMap = ({ location }: { location: string }) => { if (location) { loadExternalScriptWithCallback('kakaoMap', initializeMap); } - }, [location]); + }, [createMarker, location]); const handleOpenKakaoMap = () => { window.location.href = `https://map.kakao.com/?q=${location}`; diff --git a/frontend/src/components/_common/Map/RoomCompareMap.tsx b/frontend/src/components/_common/Map/RoomCompareMap.tsx index eba7603ca..18da24320 100644 --- a/frontend/src/components/_common/Map/RoomCompareMap.tsx +++ b/frontend/src/components/_common/Map/RoomCompareMap.tsx @@ -1,7 +1,8 @@ import styled from '@emotion/styled'; import { useEffect, useRef } from 'react'; -import RoomMarker from '@/components/RoomCompare/RoomMarker'; +import Marker from '@/components/_common/Marker/Marker'; +import theme from '@/styles/theme'; import { Position } from '@/types/address'; import createKakaoMapElements from '@/utils/createKakaoMapElements'; import { getDistanceFromLatLonInKm, getMapLevel } from '@/utils/mapHelper'; @@ -90,8 +91,20 @@ const RoomCompareMap = ({ positions }: { positions: Position[] }) => { - handleRoomMarkerClick(0)} /> - handleRoomMarkerClick(1)} /> + handleRoomMarkerClick(0)} + /> + handleRoomMarkerClick(1)} + /> diff --git a/frontend/src/components/_common/Marker/Marker.stories.tsx b/frontend/src/components/_common/Marker/Marker.stories.tsx new file mode 100644 index 000000000..d5c025aa8 --- /dev/null +++ b/frontend/src/components/_common/Marker/Marker.stories.tsx @@ -0,0 +1,41 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import Marker from '@/components/_common/Marker/Marker'; +import theme from '@/styles/theme'; + +const meta = { + title: 'components/Marker', + component: Marker, + tags: ['autodocs'], + parameters: { + docs: { + description: { + component: 'Marker는 작은 텍스트가 들어간 버튼 아이템입니다.', + }, + }, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + isCircle: false, + text: '수인분당선', + backgroundColor: theme.palette.yellow500, + size: 'small', + onClick: () => alert('click'), + }, +}; + +export const medium: Story = { + args: { + isCircle: false, + text: '2', + backgroundColor: theme.palette.green500, + size: 'medium', + onClick: () => alert('click'), + }, +}; diff --git a/frontend/src/components/_common/Marker/Marker.tsx b/frontend/src/components/_common/Marker/Marker.tsx new file mode 100644 index 000000000..ec3a822ed --- /dev/null +++ b/frontend/src/components/_common/Marker/Marker.tsx @@ -0,0 +1,71 @@ +import styled from '@emotion/styled'; + +import { flexCenter } from '@/styles/common'; + +type Size = 'medium' | 'small'; + +interface Props { + text: string; + backgroundColor: string; + size?: Size; + isCircle: boolean; + onClick?: () => void; +} + +const Marker = ({ isCircle, text, backgroundColor, size = 'medium', onClick }: Props) => { + const sharedProps = { + isCircle, + size, + backgroundColor, + }; + + return onClick ? ( + + {text} + + ) : ( + + {text} + + ); +}; + +const sizeMap = { small: '1.4rem', medium: '2rem' }; + +const S = { + Box: styled.span<{ isCircle: boolean; size: Size; backgroundColor: string }>` + ${flexCenter} + display: inline-block; + width: ${({ isCircle, size }) => isCircle && sizeMap[size]}; + height: ${({ isCircle, size }) => isCircle && sizeMap[size]}; + padding: ${({ isCircle }) => (isCircle ? '0.3rem' : '0.3rem 0.6rem')}; + border-radius: 2rem; + + background-color: ${({ backgroundColor }) => backgroundColor}; + + text-align: center; + `, + Button: styled.button<{ isCircle: boolean; size: Size; backgroundColor: string }>` + ${flexCenter} + display: inline-block; + width: ${({ isCircle, size }) => isCircle && sizeMap[size]}; + height: ${({ isCircle, size }) => isCircle && sizeMap[size]}; + padding: ${({ isCircle }) => (isCircle ? '0.3rem' : '0.3rem 0.6rem')}; + border-radius: 2rem; + + background-color: ${({ backgroundColor }) => backgroundColor}; + + text-align: center; + `, + Text: styled.span<{ size: Size }>` + width: 100%; + height: 100%; + + ${flexCenter}; + color: ${({ theme }) => theme.palette.white}; + font-weight: ${({ theme }) => theme.text.weight.semiBold}; + font-size: ${({ theme, size }) => (size === 'small' ? theme.text.size.xSmall : theme.text.size.small)}; + `, +}; + +export default Marker; diff --git a/frontend/src/components/_common/Modal/Modal.tsx b/frontend/src/components/_common/Modal/Modal.tsx index 683bb3f29..bb643dcf3 100644 --- a/frontend/src/components/_common/Modal/Modal.tsx +++ b/frontend/src/components/_common/Modal/Modal.tsx @@ -22,6 +22,7 @@ export interface ModalProps extends ComponentPropsWithRef<'dialog'> { hasCloseButton?: boolean; hasDim?: boolean; color?: string; + backgroundColor?: string; } const modalRoot = document.getElementById('modal'); @@ -33,6 +34,7 @@ const Modal = ({ size = 'large', position = 'center', hasCloseButton = true, + backgroundColor = 'white', hasDim = true, color, }: ModalProps) => { @@ -45,7 +47,7 @@ const Modal = ({ {hasDim && {}} hasDim={hasDim} />} - + {children} {hasCloseButton && ( @@ -92,10 +94,11 @@ const S = { position: fixed; ${({ $position, $size }) => positionStyles[$position]($size)} `, - ModalInner: styled.div<{ isOpen: boolean }>` + ModalInner: styled.div<{ isOpen: boolean; backgroundColor: string }>` + overflow: scroll; width: 100%; - background-color: ${({ color, theme }) => color ?? theme.palette.white}; + background-color: ${({ backgroundColor }) => backgroundColor}; color: ${({ theme }) => theme.palette.black}; @@ -103,6 +106,7 @@ const S = { border-radius: 1rem; box-shadow: 0 0.2rem 1rem rgb(0 0 0 / 40%); min-height: 15rem; + max-height: 80vh; `, CloseButton: styled.button` display: flex; diff --git a/frontend/src/components/_common/Subway/SubwayLineIcon/SubwayLineIcon.stories.tsx b/frontend/src/components/_common/Subway/SubwayLineIcon/SubwayLineIcon.stories.tsx deleted file mode 100644 index dc382dc17..000000000 --- a/frontend/src/components/_common/Subway/SubwayLineIcon/SubwayLineIcon.stories.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react'; - -import SubwayLineIcon from '@/components/_common/Subway/SubwayLineIcon/SubwayLineIcon'; -import { SUBWAY_LINE_NAMES } from '@/styles/subway'; - -const meta: Meta = { - title: 'components/SubwayLineIcon', - component: SubwayLineIcon, - parameters: { - backgrounds: { - default: 'white', - }, - }, - argTypes: { - lineName: { - control: { - type: 'select', - option: SUBWAY_LINE_NAMES, - }, - }, - }, -}; - -export default meta; - -type Story = StoryObj; - -export const SmallSize: Story = { - args: { lineName: '수인분당선' }, -}; diff --git a/frontend/src/components/_common/Subway/SubwayLineIcon/SubwayLineIcon.tsx b/frontend/src/components/_common/Subway/SubwayLineIcon/SubwayLineIcon.tsx deleted file mode 100644 index 71c2f53bf..000000000 --- a/frontend/src/components/_common/Subway/SubwayLineIcon/SubwayLineIcon.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import styled from '@emotion/styled'; - -import { flexCenter } from '@/styles/common'; -import SUBWAY_LINE_PALLETE, { SubwayLineName } from '@/styles/subway'; - -type Size = 'small' | 'medium'; -interface Props { - lineName: SubwayLineName; - size?: Size; -} - -const sizeMap = { small: '1.4rem', medium: '2rem' }; - -const SubwayLineIcon = ({ lineName, size = 'medium' }: Props) => { - const lineColor = SUBWAY_LINE_PALLETE[lineName]; - - const isNumberTypeSubwayName = lineName.slice(-2) === '호선' && lineName.length === 3; - - return ( - - {isNumberTypeSubwayName ? lineName.slice(0, lineName.length - 2) : lineName} - - ); -}; - -const S = { - Box: styled.span<{ color: string; isCircle: boolean; size: Size }>` - display: inline-block; - width: ${({ isCircle, size }) => isCircle && sizeMap[size]}; - height: ${({ isCircle, size }) => isCircle && sizeMap[size]}; - padding: ${({ isCircle }) => (isCircle ? '0.3rem' : '0.3rem 0.6rem')}; - border-radius: 2rem; - - background-color: ${({ color, theme }) => (color ? color : theme.palette.grey400)}; - - text-align: center; - `, - Text: styled.span<{ size: Size }>` - width: 100%; - height: 100%; - - ${flexCenter}; - color: ${({ theme }) => theme.palette.white}; - font-weight: ${({ theme }) => theme.text.weight.semiBold}; - font-size: ${({ theme, size }) => (size === 'small' ? theme.text.size.xSmall : theme.text.size.small)}; - `, -}; - -export default SubwayLineIcon; diff --git a/frontend/src/components/_common/Subway/SubwayStationItem.tsx b/frontend/src/components/_common/Subway/SubwayStationItem.tsx index 36d4c0bba..eaf9f06bd 100644 --- a/frontend/src/components/_common/Subway/SubwayStationItem.tsx +++ b/frontend/src/components/_common/Subway/SubwayStationItem.tsx @@ -1,24 +1,42 @@ import styled from '@emotion/styled'; import FlexBox from '@/components/_common/FlexBox/FlexBox'; -import SubwayLineIcon from '@/components/_common/Subway/SubwayLineIcon/SubwayLineIcon'; +import Marker from '@/components/_common/Marker/Marker'; import { flexCenter } from '@/styles/common'; +import SUBWAY_LINE_PALLETE from '@/styles/subway'; import { SubwayStation } from '@/types/subway'; interface Props { station: SubwayStation; size?: 'medium' | 'small'; + textType?: 'omit' | 'full'; } -const SubwayStationItem = ({ station, size }: Props) => { +const SubwayStationItem = ({ station, size, textType = 'full' }: Props) => { const { stationName, stationLine, walkingTime } = station; return ( - {stationLine?.map(oneLine => )} + {stationLine?.map(oneLine => { + const lineColor = SUBWAY_LINE_PALLETE[oneLine]; + const isNumberTypeSubwayName = oneLine.slice(-2) === '호선' && oneLine.length === 3; + const name = isNumberTypeSubwayName ? oneLine.slice(0, oneLine.length - 2) : oneLine; + + return ( + + ); + })} - {`${stationName}까지 도보 ${walkingTime}분`} + + {textType === 'full' ? `${stationName}까지 도보 ${walkingTime}분` : `${stationName} ${walkingTime}분`} + ); }; diff --git a/frontend/src/components/_common/Subway/SubwayStations.tsx b/frontend/src/components/_common/Subway/SubwayStations.tsx index 69cb276fc..c6ed021d2 100644 --- a/frontend/src/components/_common/Subway/SubwayStations.tsx +++ b/frontend/src/components/_common/Subway/SubwayStations.tsx @@ -9,14 +9,17 @@ interface Props { checklist?: ChecklistInfo; stations: SubwayStation[]; size?: 'small' | 'medium'; + textType?: 'omit' | 'full'; } -const SubwayStations = ({ stations, size }: Props) => { +const SubwayStations = ({ stations, size, textType = 'full' }: Props) => { return ( <> {stations?.length ? ( - {stations?.map(station => )} + {stations?.map(station => ( + + ))} ) : ( {'보신 방과 가까운 지하철역을 찾아드릴게요.'} @@ -30,6 +33,7 @@ export default SubwayStations; const S = { Box: styled.div` ${flexColumn}; + line-height: 1.5; gap: 0.5rem; `, }; diff --git a/frontend/src/constants/queryKeys.ts b/frontend/src/constants/queryKeys.ts index 109e9d441..2cc03bf8f 100644 --- a/frontend/src/constants/queryKeys.ts +++ b/frontend/src/constants/queryKeys.ts @@ -8,4 +8,6 @@ export const QUERY_KEYS = { CHECKLIST_LIST: 'checklist-list', CHECKLIST_QUESTIONS: 'checklists/questions', CHECKLIST_ALL_QUESTIONS: 'checklists/questions/all', + ROOM_COMPARE: 'room/compare', + ROOM_CATEGORY_DETAIL: '/room/category/detail', }; diff --git a/frontend/src/constants/routePath.ts b/frontend/src/constants/routePath.ts index aad2bc391..d18ee3afb 100644 --- a/frontend/src/constants/routePath.ts +++ b/frontend/src/constants/routePath.ts @@ -14,6 +14,7 @@ export const ROUTE_PATH = { checklistOne: (id: number) => `/checklist/${id}`, /*compare*/ roomCompare: '/room/compare', + roomCompareSelect: '/room/compare/select', /* article */ articleList: '/article', articleId: '/article/:articleId', diff --git a/frontend/src/hooks/query/useGetCompareRoomsQuery.ts b/frontend/src/hooks/query/useGetCompareRoomsQuery.ts new file mode 100644 index 000000000..8206b692a --- /dev/null +++ b/frontend/src/hooks/query/useGetCompareRoomsQuery.ts @@ -0,0 +1,16 @@ +import { useQuery } from '@tanstack/react-query'; + +import { getRoomCompare } from '@/apis/room'; +import { QUERY_KEYS } from '@/constants/queryKeys'; +import { STALE_TIME } from '@/constants/system'; +import { RoomCompare } from '@/types/RoomCompare'; + +const useGetCompareRoomsQuery = (roomId1: number, roomId2: number) => { + return useQuery({ + queryKey: [QUERY_KEYS.ROOM_COMPARE, roomId1, roomId2], + queryFn: async () => await getRoomCompare(roomId1, roomId2), + staleTime: STALE_TIME, + }); +}; + +export default useGetCompareRoomsQuery; diff --git a/frontend/src/hooks/query/useGetRoomCategoryDetail.ts b/frontend/src/hooks/query/useGetRoomCategoryDetail.ts new file mode 100644 index 000000000..af71aaa51 --- /dev/null +++ b/frontend/src/hooks/query/useGetRoomCategoryDetail.ts @@ -0,0 +1,16 @@ +import { useQuery } from '@tanstack/react-query'; + +import { getRoomCategoryDetail } from '@/apis/room'; +import { QUERY_KEYS } from '@/constants/queryKeys'; +import { STALE_TIME } from '@/constants/system'; +import { RoomCategoryDetail } from '@/types/RoomCompare'; + +const useGetRoomCategoryDetailQuery = ({ roomId, categoryId }: { roomId: number; categoryId: number }) => { + return useQuery({ + queryKey: [QUERY_KEYS.ROOM_CATEGORY_DETAIL, roomId, categoryId], + queryFn: async () => await getRoomCategoryDetail({ roomId, categoryId }), + staleTime: STALE_TIME, + }); +}; + +export default useGetRoomCategoryDetailQuery; diff --git a/frontend/src/mocks/fixtures/checklistList.ts b/frontend/src/mocks/fixtures/checklistList.ts index 8b0bfa385..8bdf1c4c4 100644 --- a/frontend/src/mocks/fixtures/checklistList.ts +++ b/frontend/src/mocks/fixtures/checklistList.ts @@ -4,7 +4,7 @@ export const checklistList: { checklists: ChecklistPreview[] } = { checklists: [ { checklistId: 1, - roomName: '건대역 오픈형', + roomName: '예시용 체크리스트', address: '서울 광진구 구의동 센트럴빌', deposit: 800, rent: 65, @@ -43,20 +43,5 @@ export const checklistList: { checklists: ChecklistPreview[] } = { summary: '방은 좁으나 싼 가격이 장점!', isLiked: false, }, - { - checklistId: 4, - roomName: '사당역 분리형', - address: '서울 동작구 사당동', - deposit: 500, - rent: 45, - station: { - stationName: '사당', - stationLine: ['4호선', '2호선'], - walkingTime: 10, - }, - createdAt: '2024-08-21T10:00:00Z', - summary: '방은 좁으나 싼 가격이 장점!', - isLiked: false, - }, ], }; diff --git a/frontend/src/mocks/fixtures/roomCategoryDetail.ts b/frontend/src/mocks/fixtures/roomCategoryDetail.ts new file mode 100644 index 000000000..41f35ff41 --- /dev/null +++ b/frontend/src/mocks/fixtures/roomCategoryDetail.ts @@ -0,0 +1,46 @@ +export const roomCategoryDetail = { + questions: { + good: [ + { + questionId: 1, + title: '곰팡이가 핀 곳 없이 깨끗한가요?', + subtitle: '천장, 벽면, 가구 뒤, 장판을 확인하세요.', + highlights: ['곰팡이'], + }, + { + questionId: 3, + title: '벌레가 나온 흔적 없이 깔끔한가요?', + subtitle: '벌레 퇴치약이 부착되어 있는지 확인하세요.', + highlights: ['벌레'], + }, + ], + bad: [ + { + questionId: 2, + title: '불쾌한 냄새 없이 쾌적한가요?', + subtitle: null, + highlights: ['불쾌한 냄새'], + }, + { + questionId: 1, + title: '곰팡이가 핀 곳 없이 깨끗한가요?', + subtitle: '천장, 벽면, 가구 뒤, 장판을 확인하세요.', + highlights: ['곰팡이'], + }, + { + questionId: 3, + title: '벌레가 나온 흔적 없이 깔끔한가요?', + subtitle: '벌레 퇴치약이 부착되어 있는지 확인하세요.', + highlights: ['벌레'], + }, + ], + none: [ + { + questionId: 4, + title: '화장실은 깔끔한가요?', + subtitle: null, + highlights: ['화장실'], + }, + ], + }, +}; diff --git a/frontend/src/mocks/fixtures/roomCompare.ts b/frontend/src/mocks/fixtures/roomCompare.ts index 5b81e87a2..e36071e1a 100644 --- a/frontend/src/mocks/fixtures/roomCompare.ts +++ b/frontend/src/mocks/fixtures/roomCompare.ts @@ -1,109 +1,115 @@ import { nearSubway } from '@/mocks/fixtures/subway'; -import { ChecklistCompare } from '@/types/checklistCompare'; +import { RoomCompare } from '@/types/RoomCompare'; -export const roomsForCompare: ChecklistCompare[] = [ - { - checklistId: 1, - roomName: '건대입구역 10분거리 방', - address: '서울 송파구 올림픽로35다길 42', - buildingName: '한국루터회관', - deposit: 1000, - rent: 50, - maintenanceFee: 5, - contractTerm: 12, - floor: 5, - realEstate: undefined, - structure: '오픈형 원룸', - size: 25, - floorLevel: '지상', - occupancyMonth: 9, - occupancyPeriod: '중순', - includedMaintenances: [2], - createdAt: '2024-02-01T10:00:00Z', - options: [1, 2, 3], - nearSubwayStations: nearSubway, - categories: [ - { - categoryId: 1, - categoryName: '청결', - score: 70, +export const roomsForCompare: { checklists: RoomCompare[] } = { + checklists: [ + { + checklistId: 1, + roomName: '건대입구역 10분거리 방', + address: '서울 송파구 올림픽로35다길 42', + buildingName: '한국루터회관', + deposit: 1000, + rent: 50, + maintenanceFee: 5, + contractTerm: 12, + floor: 5, + realEstate: undefined, + structure: '오픈형 원룸', + size: 25, + floorLevel: '지상', + occupancyMonth: 9, + occupancyPeriod: '중순', + includedMaintenances: [2], + createdAt: '2024-02-01T10:00:00Z', + options: [1, 2, 3], + stations: { stations: nearSubway }, + categories: { + categories: [ + { + categoryId: 1, + categoryName: '청결', + score: 70, + }, + { + categoryId: 2, + categoryName: '편의시설', + score: 60, + }, + { + categoryId: 3, + categoryName: '화장실', + score: 40, + }, + { + categoryId: 4, + categoryName: '보안', + score: 20, + }, + { + categoryId: 4, + categoryName: '보안', + score: null, + }, + ], }, - { - categoryId: 2, - categoryName: '편의시설', - score: 60, + geolocation: { + latitude: 37.5061912, + longitude: 127.0508228, }, - { - categoryId: 3, - categoryName: '화장실', - score: 40, - }, - { - categoryId: 4, - categoryName: '보안', - score: 20, - }, - { - categoryId: 4, - categoryName: '보안', - score: null, - }, - ], - geolocation: { - latitude: 37.5061912, - longitude: 127.0508228, }, - }, - { - checklistId: 1, - roomName: '건대입구역 10분거리 방', - address: '서울 송파구 올림픽로35다길 42', - buildingName: '한국루터회관', - deposit: undefined, - rent: 50, - maintenanceFee: 5, - contractTerm: 12, - floor: 5, - realEstate: undefined, - structure: '오픈형 원룸', - size: 25, - floorLevel: '지상', - occupancyMonth: 9, - occupancyPeriod: '중순', - includedMaintenances: [2], - createdAt: '2024-02-01T10:00:00Z', - options: [1, 2, 3], - nearSubwayStations: nearSubway, - categories: [ - { - categoryId: 1, - categoryName: '청결', - score: 20, - }, - { - categoryId: 4, - categoryName: '보안', - score: null, - }, - { - categoryId: 2, - categoryName: '편의시설', - score: 50, - }, - { - categoryId: 3, - categoryName: '화장실', - score: 90, + { + checklistId: 1, + roomName: '건대입구역 10분거리 방', + address: '서울 송파구 올림픽로35다길 42', + buildingName: '한국루터회관', + deposit: undefined, + rent: 50, + maintenanceFee: 5, + contractTerm: 12, + floor: 5, + realEstate: undefined, + structure: '오픈형 원룸', + size: 25, + floorLevel: '지상', + occupancyMonth: 9, + occupancyPeriod: '중순', + includedMaintenances: [2], + createdAt: '2024-02-01T10:00:00Z', + options: [4, 5], + stations: { stations: nearSubway }, + categories: { + categories: [ + { + categoryId: 1, + categoryName: '청결', + score: 70, + }, + { + categoryId: 2, + categoryName: '편의시설', + score: 60, + }, + { + categoryId: 3, + categoryName: '화장실', + score: 40, + }, + { + categoryId: 4, + categoryName: '보안', + score: 20, + }, + { + categoryId: 4, + categoryName: '보안', + score: null, + }, + ], }, - { - categoryId: 4, - categoryName: '보안', - score: 95, + geolocation: { + latitude: 37.5061912, + longitude: 127.2508228, }, - ], - geolocation: { - latitude: 37.5061912, - longitude: 127.2508228, }, - }, -]; + ], +}; diff --git a/frontend/src/mocks/handlers/roomCompare.ts b/frontend/src/mocks/handlers/roomCompare.ts index 22c04876c..961ffa944 100644 --- a/frontend/src/mocks/handlers/roomCompare.ts +++ b/frontend/src/mocks/handlers/roomCompare.ts @@ -1,10 +1,14 @@ import { http, HttpResponse } from 'msw'; import { BASE_URL, ENDPOINT } from '@/apis/url'; +import { roomCategoryDetail } from '@/mocks/fixtures/roomCategoryDetail'; import { roomsForCompare } from '@/mocks/fixtures/roomCompare'; export const roomCompareHandlers = [ - http.get(BASE_URL + ENDPOINT.CHECKLIST_COMPARE(1, 2), () => { + http.get(BASE_URL + ENDPOINT.ROOM_COMPARE(1, 2), () => { return HttpResponse.json(roomsForCompare, { status: 200 }); }), + http.get(BASE_URL + ENDPOINT.ROOM_CATEGORY_DETAIL(1, 1), () => { + return HttpResponse.json(roomCategoryDetail, { status: 200 }); + }), ]; diff --git a/frontend/src/pages/CategoryChoosePage.tsx b/frontend/src/pages/CategoryChoosePage.tsx deleted file mode 100644 index d6f691a4d..000000000 --- a/frontend/src/pages/CategoryChoosePage.tsx +++ /dev/null @@ -1,137 +0,0 @@ -import styled from '@emotion/styled'; -import { useEffect, useState } from 'react'; -import { Link, useNavigate } from 'react-router-dom'; - -import { getCategory, postCategory } from '@/apis/category'; -import { CloseIcon, LampIcon } from '@/assets/assets'; -import Badge from '@/components/_common/Badge/Badge'; -import Button from '@/components/_common/Button/Button'; -import Header from '@/components/_common/Header/Header'; -import Layout from '@/components/_common/layout/Layout'; -import { TOAST_MESSAGE } from '@/constants/messages/message'; -import { ROUTE_PATH } from '@/constants/routePath'; -import { MAX_SELECT_CATEGORY_COUNT } from '@/constants/system'; -import useToast from '@/hooks/useToast'; -import { flexColumn, flexRow, title2 } from '@/styles/common'; -import { Category } from '@/types/category'; - -const CategoryChoosePage = () => { - const [categories, setCategories] = useState([]); - const [selectedCategory, setSelectedCategory] = useState([]); - const { showToast } = useToast(); - - const navigate = useNavigate(); - - useEffect(() => { - const fetchCategory = async () => { - const categories = await getCategory(); - setCategories(categories); - }; - fetchCategory(); - }, []); - - const handleClick = (id: number) => { - setSelectedCategory(prev => { - if (prev.includes(id)) { - return prev.filter(category => category !== id); - } else if (prev.length < MAX_SELECT_CATEGORY_COUNT) { - return [...prev, id]; - } - showToast({ message: TOAST_MESSAGE.MAX_SELECT }); - return prev; - }); - }; - - const handleSubmit = () => { - const addCategory = async () => { - await postCategory(selectedCategory); - navigate(ROUTE_PATH.checklistList); - }; - addCategory(); - }; - - return ( - <> -
- - - } - /> - - - - - - 방을 선택할 때
- {`중요한 요소를 최대 ${MAX_SELECT_CATEGORY_COUNT}개 선택해주세요!`} -
- 선택하신 기준을 통해 최적의 방을 추천해드려요. -
- - {categories?.map(category => ( - handleClick(category.categoryId)} - isSelected={selectedCategory.includes(category.categoryId)} - /> - ))} - -
- - - - {selectedCategory.length === 0 ? ( -