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 (
-
- 체크리스트 질문 템플릿
+ {Icon}
+ {title}
-
+
);
};
@@ -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}>
+
+
+
+
+
+
+
+ {formattedUndefined(address, 'string')}
+
+ {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 ? (
-
-
- >
- );
-};
-
-export default CategoryChoosePage;
-
-const S = {
- Wrapper: styled.div`
- ${flexColumn}
- justify-content: space-around;
- height: 80dvh;
- padding: 0.4rem;
- `,
- Content: styled.article`
- ${flexColumn}
- gap: 4rem;
- `,
- TitleSection: styled.div`
- margin-top: 3rem;
-
- ${flexColumn}
- `,
- Title: styled.h1`
- ${title2}
- `,
- SubTitle: styled.span`
- margin-top: 1rem;
-
- font-size: ${({ theme }) => theme.text.size.medium};
- `,
- ButtonWrapper: styled.div`
- display: flex;
- width: 90%;
- max-width: 35rem;
- height: auto;
- flex-wrap: wrap;
- gap: 1.5rem 1rem;
- `,
- IconWrapper: styled.div`
- ${flexRow}
- padding: 1.6rem;
- justify-content: flex-end;
- `,
-};
diff --git a/frontend/src/pages/ChecklistListPage.tsx b/frontend/src/pages/ChecklistListPage.tsx
index 87642fcca..35ad5f698 100644
--- a/frontend/src/pages/ChecklistListPage.tsx
+++ b/frontend/src/pages/ChecklistListPage.tsx
@@ -2,7 +2,7 @@ import styled from '@emotion/styled';
import { ErrorBoundary } from 'react-error-boundary';
import { useNavigate } from 'react-router-dom';
-import { PlusBlack } from '@/assets/assets';
+import { LampIcon, PencilIcon, PlusBlack } from '@/assets/assets';
import ListErrorFallback from '@/components/_common/errorBoundary/ListErrorFallback';
import TitleErrorFallback from '@/components/_common/errorBoundary/TitleErrorFallback';
import FlexBox from '@/components/_common/FlexBox/FlexBox';
@@ -23,7 +23,13 @@ const ChecklistListPage = () => {
useTrackPageView({ eventName: '[View] 체크리스트 리스트 페이지' });
const navigate = useNavigate();
+
const handleClickMoveCustomPage = () => navigate(ROUTE_PATH.checklistQuestionSelect);
+
+ const handleClickMoveQuestionSelectPage = () => {
+ navigate(ROUTE_PATH.roomCompareSelect);
+ };
+
const handleClickFloatingButton = () => {
trackAddChecklistButton();
navigate(ROUTE_PATH.checklistNew);
@@ -33,9 +39,24 @@ const ChecklistListPage = () => {
<>
체크리스트} />
-
-
-
+
+ }
+ title={'체크리스트 질문 템플릿'}
+ buttonColor={theme.palette.green500}
+ buttonText="편집하기"
+ buttonDetailText={'체크리스트 질문을 편집하려면 이 버튼을 누르세요.'}
+ />
+ }
+ title={'체크리스트를 비교해서 보기'}
+ buttonColor={theme.palette.yellow600}
+ buttonText="비교하기"
+ buttonDetailText={'체크리스트 질문을 편집하려면 이 버튼을 누르세요.'}
+ />
+
}>
diff --git a/frontend/src/pages/RoomComparePage.tsx b/frontend/src/pages/RoomComparePage.tsx
index 4273508b5..1b17a2d24 100644
--- a/frontend/src/pages/RoomComparePage.tsx
+++ b/frontend/src/pages/RoomComparePage.tsx
@@ -1,43 +1,69 @@
import styled from '@emotion/styled';
-import { useEffect, useState } from 'react';
-import { useNavigate } from 'react-router-dom';
+import { useNavigate, useSearchParams } from 'react-router-dom';
import Header from '@/components/_common/Header/Header';
import Layout from '@/components/_common/layout/Layout';
import RoomCompareMap from '@/components/_common/Map/RoomCompareMap';
+import { default as Marker } from '@/components/_common/Marker/Marker';
import CategoryDetailModal from '@/components/RoomCompare/CategoryDetailModal';
import CompareCard from '@/components/RoomCompare/CompareCard';
import OptionDetailModal from '@/components/RoomCompare/OptionDetailModal';
-import RoomMarker from '@/components/RoomCompare/RoomMarker';
+import { OPTIONS } from '@/constants/options';
import { ROUTE_PATH } from '@/constants/routePath';
+import useGetCompareRoomsQuery from '@/hooks/query/useGetCompareRoomsQuery';
import useModal from '@/hooks/useModal';
-import { roomsForCompare } from '@/mocks/fixtures/roomCompare';
import { flexCenter, flexRow } from '@/styles/common';
import theme from '@/styles/theme';
import { Position } from '@/types/address';
-import { ChecklistCompare } from '@/types/checklistCompare';
+
+export interface OptionDetail {
+ optionId: number;
+ optionName: string;
+ hasOption: [boolean, boolean];
+}
const RoomComparePage = () => {
+ const [searchParams, setSearchParams] = useSearchParams();
const navigate = useNavigate();
- // const roomsId = { ...location.state };
+ const roomId1 = Number(searchParams.get('roomId1'));
+ const roomId2 = Number(searchParams.get('roomId2'));
+
const { isModalOpen: isOptionModalOpen, openModal: openOptionModal, closeModal: closeOptionModal } = useModal();
const { isModalOpen: isCategoryModalOpen, openModal: openCategoryModal, closeModal: closeCategoryModal } = useModal();
- const [roomList, setRoomList] = useState([]);
+ if (!roomId1 || !roomId2) throw new Error('잘못된 비교입니다.');
+ const { data: rooms } = useGetCompareRoomsQuery(roomId1, roomId2);
- //TODO: 나중에 비교 데이터 요청해서 받아오는 로직으로 수정
- useEffect(() => {
- setRoomList(roomsForCompare);
- });
+ if (!rooms) return;
+
+ const formattedOptionDetail = () => {
+ const optionsState: OptionDetail[] = OPTIONS.map(option => ({
+ optionId: option.id,
+ optionName: option.displayName,
+ hasOption: [false, false],
+ }));
+
+ rooms.forEach((room, index) => {
+ room.options.forEach(optionId => {
+ const targetOption = optionsState.find(option => option.optionId === optionId)!;
+ targetOption.hasOption[index] = true;
+ });
+ });
+ return optionsState;
+ };
const handleOpenCategoryDetailModal = (roomId: number, categoryId: number) => {
openCategoryModal();
- navigate(ROUTE_PATH.roomCompare + `?roomId=${roomId}&categoryId=${categoryId}`);
+ searchParams.append('targetRoomId', String(roomId));
+ searchParams.append('categoryId', String(categoryId));
+ setSearchParams(searchParams);
};
const handleCloseategoryDetailModal = () => {
closeCategoryModal();
- navigate(ROUTE_PATH.roomCompare);
+ searchParams.delete('targetRoomId');
+ searchParams.delete('categoryId');
+ setSearchParams(searchParams);
};
const handleClickBackward = () => {
@@ -48,16 +74,7 @@ const RoomComparePage = () => {
{ latitude: 37.5061912, longitude: 127.1266228 },
];
- const optionMock = [
- { optionName: '세탁기', hasRoom1: true, hasRoom2: false },
- { optionName: '세탁기', hasRoom1: true, hasRoom2: false },
- { optionName: '세탁기', hasRoom1: true, hasRoom2: false },
- { optionName: '세탁기', hasRoom1: true, hasRoom2: false },
- { optionName: '세탁기', hasRoom1: true, hasRoom2: false },
- { optionName: '세탁기', hasRoom1: true, hasRoom2: false },
- ];
-
- if (!roomList.length) return loading
;
+ if (!rooms.length) return loading
;
return (
<>
@@ -69,18 +86,18 @@ const RoomComparePage = () => {
- {roomList[0].roomName}
-
+ {rooms[0].roomName}
+
- {roomList[1].roomName}
-
+ {rooms[1].roomName}
+
- {roomList?.map((room, index) => (
+ {rooms?.map((room, index) => (
{
{/*방 옵션 비교 모달*/}
{isOptionModalOpen && (
@@ -127,6 +145,7 @@ const S = {
`,
Title: styled.span`
display: inline;
+ width: calc(100% - 3rem);
padding: 0.8rem 0;
font-weight: ${({ theme }) => theme.text.weight.bold};
diff --git a/frontend/src/pages/RoomCompareSelectPage.tsx b/frontend/src/pages/RoomCompareSelectPage.tsx
new file mode 100644
index 000000000..87fd21fa5
--- /dev/null
+++ b/frontend/src/pages/RoomCompareSelectPage.tsx
@@ -0,0 +1,100 @@
+import styled from '@emotion/styled';
+import { useEffect, useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+
+import Button from '@/components/_common/Button/Button';
+import CounterBox from '@/components/_common/CounterBox/CounterBox';
+import Header from '@/components/_common/Header/Header';
+import Layout from '@/components/_common/layout/Layout';
+import CompareSelectCardList from '@/components/RoomCompare/CompareSelectCardList';
+import { ROUTE_PATH } from '@/constants/routePath';
+import useGetChecklistList from '@/hooks/useGetChecklistList';
+import useToast from '@/hooks/useToast';
+import theme from '@/styles/theme';
+
+const MIN_ROOM_COMPARE_COUNT = 2;
+
+const RoomCompareSelectPage = () => {
+ const navigate = useNavigate();
+ const { showToast } = useToast();
+ const [selectedRoomIds, setSelectedRoomIds] = useState>(new Set());
+ const { data: checklistList, isLoading } = useGetChecklistList();
+
+ const userChecklists = checklistList?.filter(checklist => checklist.checklistId !== 1) || [];
+
+ useEffect(() => {
+ if (userChecklists.length < MIN_ROOM_COMPARE_COUNT) {
+ showToast({ message: '비교 기능은 작성한 체크리스트가 2개 이상일 때만 가능합니다.' });
+ navigate(ROUTE_PATH.checklistList);
+ }
+ }, [userChecklists.length, navigate, showToast]);
+
+ const handleToggleSelectChecklist = (roomId: number) => {
+ setSelectedRoomIds(prev => {
+ const updatedSet = new Set(prev);
+
+ if (prev.has(roomId)) {
+ updatedSet.delete(roomId);
+ return updatedSet;
+ }
+
+ if (prev.size >= 2) {
+ showToast({ message: '방은 2개까지만 선택할 수 있습니다.', type: 'info' });
+ return prev;
+ }
+
+ updatedSet.add(roomId);
+ return updatedSet;
+ });
+ };
+
+ const handleNavigateToCompare = () => {
+ if (selectedRoomIds.size !== 2) {
+ showToast({ message: '비교할 2개의 방을 선택해주세요.', type: 'info' });
+ return;
+ }
+
+ const [roomId1, roomId2] = [...selectedRoomIds];
+ const searchParams = new URLSearchParams({
+ roomId1: String(roomId1),
+ roomId2: String(roomId2),
+ });
+
+ navigate(`${ROUTE_PATH.roomCompare}?${searchParams}`);
+ };
+
+ return (
+ <>
+ navigate(ROUTE_PATH.checklistList)} />}
+ center={비교할 방 선택하기}
+ right={
+
+ }
+ />
+
+
+
+
+
+
+ >
+ );
+};
+
+const S = {
+ CounterContainer: styled.div`
+ display: flex;
+ width: 100%;
+ height: 3rem;
+ margin-top: 1rem;
+ justify-content: flex-end;
+ `,
+};
+
+export default RoomCompareSelectPage;
diff --git a/frontend/src/routers/router.tsx b/frontend/src/routers/router.tsx
index e627cd1e9..1250f54fd 100644
--- a/frontend/src/routers/router.tsx
+++ b/frontend/src/routers/router.tsx
@@ -5,6 +5,7 @@ import FooterLayout from '@/components/_common/layout/FooterLayout';
import { ROUTE_PATH } from '@/constants/routePath';
import ResetPasswordPage from '@/pages/ResetPasswordPage';
import RoomComparePage from '@/pages/RoomComparePage';
+import RoomCompareSelectPage from '@/pages/RoomCompareSelectPage';
import SignInPage from '@/pages/SignInPage';
import SignUpPage from '@/pages/SignUpPage';
@@ -92,6 +93,10 @@ const router = createBrowserRouter([
element: ,
path: ROUTE_PATH.roomCompare,
},
+ {
+ element: ,
+ path: ROUTE_PATH.roomCompareSelect,
+ },
{
element: ,
path: '*',
diff --git a/frontend/src/types/RoomCompare.ts b/frontend/src/types/RoomCompare.ts
new file mode 100644
index 000000000..0cccf9d04
--- /dev/null
+++ b/frontend/src/types/RoomCompare.ts
@@ -0,0 +1,22 @@
+import { Position } from '@/types/address';
+import { ChecklistQuestion } from '@/types/checklist';
+import { RoomInfo } from '@/types/room';
+import { SubwayStation } from '@/types/subway';
+
+export interface CategoryScore {
+ categoryId: number;
+ categoryName: string;
+ score: number | null;
+}
+
+export interface RoomCompare extends RoomInfo {
+ checklistId: number;
+ options: number[];
+ categories: { categories: CategoryScore[] };
+ stations: { stations: SubwayStation[] };
+ geolocation: Position;
+}
+
+export type SmallAnswerType = 'good' | 'bad' | 'none';
+
+export type RoomCategoryDetail = Record;
diff --git a/frontend/src/types/checklistCompare.ts b/frontend/src/types/checklistCompare.ts
deleted file mode 100644
index 1ff504334..000000000
--- a/frontend/src/types/checklistCompare.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { Position } from '@/types/address';
-import { RoomInfo } from '@/types/room';
-import { SubwayStation } from '@/types/subway';
-
-export interface CategoryScore {
- categoryId: number;
- categoryName: string;
- score: number | null;
-}
-
-export interface ChecklistCompare extends RoomInfo {
- checklistId: number;
- options: number[];
- categories: CategoryScore[];
- nearSubwayStations: SubwayStation[];
- geolocation: Position;
-}
diff --git a/frontend/src/utils/loadScript.ts b/frontend/src/utils/loadScript.ts
index ff62b54ab..a10843b55 100644
--- a/frontend/src/utils/loadScript.ts
+++ b/frontend/src/utils/loadScript.ts
@@ -1,8 +1,11 @@
type ScriptType = 'kakaoMap' | 'daumAddress';
-/* eslint-disable @typescript-eslint/no-explicit-any */
+interface ScriptInfo {
+ url: string;
+ loaded: boolean;
+}
-const scripts = {
+const scripts: Record = {
kakaoMap: {
url: `https://dapi.kakao.com/v2/maps/sdk.js?appkey=${process.env.KAKAO_MAP_KEY}&autoload=false&libraries=services`,
loaded: false,
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
index 27e0b132f..ec90f430c 100644
--- a/frontend/tsconfig.json
+++ b/frontend/tsconfig.json
@@ -4,7 +4,7 @@
"noImplicitAny": true,
"esModuleInterop": true,
"module": "esnext",
- "target": "es5",
+ "target": "ES2015",
"allowJs": true,
"moduleResolution": "node",
"jsx": "react-jsx",