diff --git a/frontend/playwright.api.config.ts b/frontend/playwright.api.config.ts index 1de4307fa..ee11c47f1 100644 --- a/frontend/playwright.api.config.ts +++ b/frontend/playwright.api.config.ts @@ -19,7 +19,7 @@ export default defineConfig({ /* Retry on CI only */ retries: process.env.CI ? 2 : 0, /* Opt out of parallel tests on CI. */ - workers: process.env.CI ? 1 : undefined, + workers: process.env.CI ? 4 : undefined, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: 'html', /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ @@ -41,11 +41,11 @@ export default defineConfig({ dependencies: ['setup'], }, - { - name: 'firefox', - use: { ...devices['Desktop Firefox'], storageState: 'playwright/.auth/user.json' }, - dependencies: ['setup'], - }, + // { + // name: 'firefox', + // use: { ...devices['Desktop Firefox'], storageState: 'playwright/.auth/user.json' }, + // dependencies: ['setup'], + // }, { name: 'webkit', diff --git a/frontend/playwright.mock.config.ts b/frontend/playwright.mock.config.ts index d737aceb8..cfbd11fed 100644 --- a/frontend/playwright.mock.config.ts +++ b/frontend/playwright.mock.config.ts @@ -19,7 +19,7 @@ export default defineConfig({ /* Retry on CI only */ retries: process.env.CI ? 2 : 0, /* Opt out of parallel tests on CI. */ - workers: process.env.CI ? 1 : undefined, + workers: process.env.CI ? 4 : undefined, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: 'html', /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ @@ -38,10 +38,10 @@ export default defineConfig({ use: { ...devices['Desktop Chrome'] }, }, - { - name: 'firefox', - use: { ...devices['Desktop Firefox'] }, - }, + // { + // name: 'firefox', + // use: { ...devices['Desktop Firefox'] }, + // }, { name: 'webkit', diff --git a/frontend/playwright/tests/api/auth.setup.ts b/frontend/playwright/tests/api/auth.setup.ts index b05c0f5ad..c42ccff9e 100644 --- a/frontend/playwright/tests/api/auth.setup.ts +++ b/frontend/playwright/tests/api/auth.setup.ts @@ -1,16 +1,18 @@ import { test as setup } from '@playwright/test'; import path from 'path'; +import { ROUTE_PATH } from '@/constants/routePath'; + const authFile = path.join(__dirname, '../../../playwright/.auth/user.json'); setup('authenticate', async ({ page }) => { const username = process.env.EMAIL || ''; const password = process.env.PASSWORD || ''; - await page.goto('/sign-in'); + await page.goto(ROUTE_PATH.signIn); await page.locator('input[name="email"]').fill(username); await page.locator('input[name="password"]').fill(password); await page.getByRole('button', { name: '로그인 하기' }).click(); - await page.waitForURL('/home'); + await page.waitForURL(ROUTE_PATH.home); await page.context().storageState({ path: authFile }); }); diff --git a/frontend/playwright/tests/api/postNewChecklist.spec.ts b/frontend/playwright/tests/api/postNewChecklist.spec.ts index 12217038f..12520d170 100644 --- a/frontend/playwright/tests/api/postNewChecklist.spec.ts +++ b/frontend/playwright/tests/api/postNewChecklist.spec.ts @@ -1,9 +1,11 @@ import { expect, test } from '@playwright/test'; +import { ROUTE_PATH } from '@/constants/routePath'; + import { DefaultChecklistTabsNames, FirstCategoryQuestion } from '../constants/constants'; test('체크리스트가 인풋을 채우고 제출할 수 있다.', async ({ page }) => { - await page.goto('/checklist/new'); + await page.goto(ROUTE_PATH.checklistNew); const tabs = page.locator('.tab'); const roomInfoTab = tabs.nth(0); diff --git a/frontend/playwright/tests/api/renderTab.spec.ts b/frontend/playwright/tests/api/renderTab.spec.ts index 607927827..bc25eabbf 100644 --- a/frontend/playwright/tests/api/renderTab.spec.ts +++ b/frontend/playwright/tests/api/renderTab.spec.ts @@ -1,5 +1,7 @@ import test, { expect } from '@playwright/test'; +import { ROUTE_PATH } from '@/constants/routePath'; + import { DefaultChecklistTabsNames, DefaultQuestionSelectTabsNames, @@ -8,7 +10,7 @@ import { } from '../constants/constants'; test('체크리스트 생성 페이지에 들어가면 탭과 질문들이 잘 렌더링된다.', async ({ page }) => { - await page.goto('/checklist/new'); + await page.goto(ROUTE_PATH.checklistNew); const tabs = page.locator('.tab'); await expect(tabs).toHaveCount(6, { timeout: 3000 }); @@ -37,7 +39,7 @@ test('체크리스트 질문 선택 페이지에 들어가면 탭과 질문들 }); test('체크리스트 편집 페이지에 들어가면 탭과 질문들이 잘 렌더링된다.', async ({ page }) => { - await page.goto('/checklist'); + await page.goto(ROUTE_PATH.checklistList); await page.getByTestId('checklist-card').nth(0).click(); const checklistEditButton = page.locator('button[id="checklistEditButton"]'); await checklistEditButton.click(); diff --git a/frontend/playwright/tests/mock/postNewChecklist.spec.ts b/frontend/playwright/tests/mock/postNewChecklist.spec.ts index 18b4747c2..d6c507b28 100644 --- a/frontend/playwright/tests/mock/postNewChecklist.spec.ts +++ b/frontend/playwright/tests/mock/postNewChecklist.spec.ts @@ -1,8 +1,10 @@ import { test } from '@playwright/test'; +import { ROUTE_PATH } from '@/constants/routePath'; + test('빈 체크리스트를 제출할 수 있다.', async ({ page }) => { - await page.goto('/checklist/new'); + await page.goto(ROUTE_PATH.checklistNew); await page.getByRole('button', { name: '저장' }).click(); await page.getByRole('button', { name: '체크리스트 저장하기' }).click(); - await page.waitForURL('/checklist'); + await page.waitForURL(ROUTE_PATH.checklistList); }); diff --git a/frontend/public/index.html b/frontend/public/index.html index e538d0011..4e8d10c59 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -9,7 +9,7 @@ - + - - - - - + + + + + diff --git a/frontend/src/apis/checklist.ts b/frontend/src/apis/checklist.ts index 0d16a9c0b..7e18eca45 100644 --- a/frontend/src/apis/checklist.ts +++ b/frontend/src/apis/checklist.ts @@ -18,13 +18,14 @@ export const getChecklistAllQuestions = async () => { export const getChecklistDetail = async (id: number) => { const response = await fetcher.get({ url: BASE_URL + ENDPOINT.CHECKLIST_ID_V1(id) }); - const data = await response.json(); - return data as ChecklistInfo; + const data = (await response.json()) as ChecklistInfo; + data.room = Object.fromEntries(Object.entries(data.room).filter(([, value]) => value !== null)); + return data; }; export const getChecklists = async (isLikeFiltered: boolean = false) => { const response = await fetcher.get({ - url: BASE_URL + (isLikeFiltered ? ENDPOINT.CHECKLISTS_LIKE : ENDPOINT.CHECKLISTS), + url: BASE_URL + (isLikeFiltered ? ENDPOINT.CHECKLISTS_LIKE_V1 : ENDPOINT.CHECKLISTS_V1), }); const data = await response.json(); return data.checklists.map(mapObjNullToUndefined).slice(0, 10); @@ -40,7 +41,7 @@ export const postChecklist = async (checklist: ChecklistPostForm) => { export const putChecklist = async (id: number, checklist: ChecklistPostForm) => { const mappedRoomInfo = roomInfoApiMapper(checklist.room); const mappedChecklist = { ...checklist, room: mappedRoomInfo }; - const response = await fetcher.put({ url: BASE_URL + ENDPOINT.CHECKLIST_ID(id), body: mappedChecklist }); + const response = await fetcher.put({ url: BASE_URL + ENDPOINT.CHECKLIST_ID_V1(id), body: mappedChecklist }); return response; }; diff --git a/frontend/src/apis/fetcher.ts b/frontend/src/apis/fetcher.ts index 4cde556e8..48cf4bca7 100644 --- a/frontend/src/apis/fetcher.ts +++ b/frontend/src/apis/fetcher.ts @@ -1,3 +1,5 @@ +import { captureException } from '@sentry/react'; + import APIError from '@/apis/error/APIError'; import { deleteToken, postReissueAccessToken } from '@/apis/user'; import { API_ERROR_MESSAGE } from '@/constants/messages/apiErrorMessage'; @@ -38,7 +40,9 @@ const handleError = async (response: Response, requestProps: RequestProps) => { return handleUnauthorizedError(response, requestProps, errorCode); } - throw new APIError(response.status, errorCode); + const apiError = new APIError(response.status, errorCode); + captureException(apiError); + throw apiError; }; const handleUnauthorizedError = async (response: Response, requestProps: RequestProps, errorCode: string) => { diff --git a/frontend/src/apis/url.ts b/frontend/src/apis/url.ts index 2108f462a..6b56d34db 100644 --- a/frontend/src/apis/url.ts +++ b/frontend/src/apis/url.ts @@ -7,6 +7,7 @@ export const ENDPOINT = { // checklist CHECKLISTS: '/checklists', CHECKLISTS_LIKE: '/checklists/like', + CHECKLISTS_LIKE_V1: '/v1/checklists/like', CHECKLISTS_V1: '/v1/checklists', CHECKLIST_QUESTION: '/checklists/questions', CHECKLIST_ALL_QUESTION: '/custom-checklist/all', diff --git a/frontend/src/assets/assets.tsx b/frontend/src/assets/assets.tsx index b842e2bc5..60cdffe81 100644 --- a/frontend/src/assets/assets.tsx +++ b/frontend/src/assets/assets.tsx @@ -16,7 +16,6 @@ import Retry from '@/assets/icons/common/retry.svg'; import SmallCheck from '@/assets/icons/common/small-check.svg'; import PlusBlack from '@/assets/icons/plusMinus/plus-black.svg'; import PlusWhite from '@/assets/icons/plusMinus/plus-white.svg'; - // room import Building from '@/assets/icons/room/building.svg'; import Calendar from '@/assets/icons/room/calendar.svg'; diff --git a/frontend/src/assets/icons/kakao/kakao-logo.svg b/frontend/src/assets/icons/kakao/kakao-logo.svg index d67677e39..a2a684a24 100644 --- a/frontend/src/assets/icons/kakao/kakao-logo.svg +++ b/frontend/src/assets/icons/kakao/kakao-logo.svg @@ -1,3 +1,3 @@ - + diff --git a/frontend/src/assets/icons/map/search.svg b/frontend/src/assets/icons/map/search.svg index 25f0c33c3..90af2c09a 100644 --- a/frontend/src/assets/icons/map/search.svg +++ b/frontend/src/assets/icons/map/search.svg @@ -1,3 +1,3 @@ - + diff --git a/frontend/src/components/ArticleList/ArticleCard.tsx b/frontend/src/components/ArticleList/ArticleCard.tsx index 03a74c7eb..e49b0054d 100644 --- a/frontend/src/components/ArticleList/ArticleCard.tsx +++ b/frontend/src/components/ArticleList/ArticleCard.tsx @@ -47,6 +47,10 @@ const S = { background-color: ${({ theme }) => theme.palette.white}; ${boxShadow}; cursor: pointer; + + :hover { + background-color: ${({ theme }) => theme.palette.grey200}; + } `, Keyword: styled.span<{ bgColor: string }>` padding: 0.4rem 0.8rem; diff --git a/frontend/src/components/ChecklistDetail/ChecklistDetailSection.tsx b/frontend/src/components/ChecklistDetail/ChecklistDetailSection.tsx index 2ac002781..672f9ca8d 100644 --- a/frontend/src/components/ChecklistDetail/ChecklistDetailSection.tsx +++ b/frontend/src/components/ChecklistDetail/ChecklistDetailSection.tsx @@ -20,12 +20,11 @@ const ChecklistDetailSection = () => { return ( <> diff --git a/frontend/src/components/ChecklistDetail/RoomInfoSection.tsx b/frontend/src/components/ChecklistDetail/RoomInfoSection.tsx index 5122735f4..f6d36a963 100644 --- a/frontend/src/components/ChecklistDetail/RoomInfoSection.tsx +++ b/frontend/src/components/ChecklistDetail/RoomInfoSection.tsx @@ -16,8 +16,7 @@ import LikeButton from '@/components/_common/Like/LikeButton'; import AddressMap from '@/components/_common/Map/AddressMap'; import SubwayStations from '@/components/_common/Subway/SubwayStations'; import { IncludedMaintenancesData } from '@/constants/roomInfo'; -import { flexColumn, flexRow, flexSpaceBetween, title2 } from '@/styles/common'; -import { ChecklistInfo } from '@/types/checklist'; +import { flexColumn, flexRow, flexSpaceBetween, title2, title3, title4 } from '@/styles/common'; import { Option } from '@/types/option'; import { RoomInfo } from '@/types/room'; import { SubwayStation } from '@/types/subway'; @@ -30,10 +29,9 @@ interface Props { checklistId: number; isLiked: boolean; nearSubways: SubwayStation[]; - checklist: ChecklistInfo; } -const RoomInfoSection = ({ checklist, nearSubways, room, options, checklistId, isLiked }: Props) => { +const RoomInfoSection = ({ nearSubways, room, options, checklistId, isLiked }: Props) => { const { roomName, deposit, @@ -61,6 +59,7 @@ const RoomInfoSection = ({ checklist, nearSubways, room, options, checklistId, i {roomName} +
@@ -70,59 +69,106 @@ const RoomInfoSection = ({ checklist, nearSubways, room, options, checklistId, i - - - - {formattedUndefined(structure, 'string', '방 구조')} / {formattedUndefined(size)} 평 - - - - {floorLevel === '지상' - ? `${formattedUndefined(floor)}층` - : formattedUndefined(floorLevel, 'string', '방 종류')} - - + - 관리비 포함 항목 : - {includedMaintenances - ?.map(id => IncludedMaintenancesData.find(item => item.id === id)?.displayName) - .filter(Boolean) - .join(', ')} - {!includedMaintenances?.length && formattedUndefined(includedMaintenances?.length, 'string', '')} + + + 한줄평 + + {formattedUndefined(summary, 'string')} + - - {formattedUndefined(contractTerm)}개월 계약
- 입주 가능일 : {formattedUndefined(occupancyMonth)}월 {occupancyPeriod} + + 방 둘러본 날 + + {formattedDate(createdAt ?? '', '.')}
+ - - + + + 방 구조
/ 방 평수 +
+ + {formattedUndefined(structure, 'string')}
/ {formattedUndefined(size)} 평 +
- - - - {formattedUndefined(realEstate, 'string', '부동산')} - - - - {formattedDate(createdAt ?? '', '.')} - - + + + + 방 층수 + + + {floorLevel === '지상' ? `${formattedUndefined(floor)}층` : formattedUndefined(floorLevel, 'string')} + + + - - {options.length - ? options.map(option => option.optionName).join(', ') - : formattedUndefined(options.length, 'string', '옵션')} + + + 관리비 포함 항목 + + + {includedMaintenances + ?.map(id => IncludedMaintenancesData.find(item => item.id === id)?.displayName) + .filter(Boolean) + .join(', ')} + {!includedMaintenances?.length && formattedUndefined(includedMaintenances?.length, 'string')} + + - - {formattedUndefined(summary, 'string', '한줄평')} + + + 계약 기간
/ 입주 가능일 +
+ + {formattedUndefined(contractTerm)}개월 계약
/{formattedUndefined(occupancyMonth)}월 {occupancyPeriod} +
+ - - {formattedUndefined(address, 'string', '주소')}
{buildingName} + + + 옵션 + + + {options.length + ? options.map(option => option.optionName).join(', ') + : formattedUndefined(options.length, 'string')} +
+ + + + + 부동산 + + {formattedUndefined(realEstate, 'string')} + + + + + + 주소 + + + {formattedUndefined(address, 'string')}
+ {buildingName} +
+
+ + + + + 가까운 지하철 + + + + + + ); @@ -149,14 +195,31 @@ const S = { background-color: ${({ theme }) => theme.palette.green500}; color: ${({ theme }) => theme.palette.white}; - font-size: ${({ theme }) => theme.text.size.medium}; + font-size: ${({ theme }) => theme.text.size.small}; box-sizing: border-box; border-radius: 1.6rem; `, + Label: styled.div` + width: 100%; + ${flexRow} + gap: 1rem; + + font-weight: ${({ theme }) => theme.text.weight.bold}; + `, + Text: styled.div` + ${flexRow} + width: 100%; + `, Row: styled.div` + width: 100%; ${flexRow} gap: 1rem; `, + Column: styled.div` + width: 100%; + ${flexColumn} + gap: 1rem; + `, GapBox: styled.div` display: flex; gap: 30%; @@ -174,7 +237,8 @@ const S = { MoneyText: styled.div` width: 100%; - font-size: ${({ theme }) => theme.text.size.small}; + /* ${title4} */ + ${title3} ${flexRow} ${flexSpaceBetween} `, diff --git a/frontend/src/components/ChecklistList/ChecklistCard.tsx b/frontend/src/components/ChecklistList/ChecklistCard.tsx index 70712ab8b..b13acc56f 100644 --- a/frontend/src/components/ChecklistList/ChecklistCard.tsx +++ b/frontend/src/components/ChecklistList/ChecklistCard.tsx @@ -25,7 +25,7 @@ const ChecklistCard = ({ checklist }: Props) => { @@ -37,7 +37,7 @@ const ChecklistCard = ({ checklist }: Props) => { - {`"${formattedUndefined(summary, 'string', '한줄평')}"`} + {`"${formattedUndefined(summary, 'string')}"`} {formattedDate(createdAt ?? '')} @@ -58,6 +58,10 @@ const S = { background-color: ${({ theme }) => theme.palette.white}; ${boxShadow}; cursor: pointer; + + :hover { + background-color: ${({ theme }) => theme.palette.grey200}; + } `, Row: styled.div` ${flexSpaceBetween} diff --git a/frontend/src/components/ChecklistList/CustomBanner.tsx b/frontend/src/components/ChecklistList/CustomBanner.tsx index d9eaf3f57..ef092a01e 100644 --- a/frontend/src/components/ChecklistList/CustomBanner.tsx +++ b/frontend/src/components/ChecklistList/CustomBanner.tsx @@ -1,6 +1,7 @@ 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 { @@ -12,9 +13,9 @@ const CustomBanner = ({ onClick }: Props) => { - 편집하기 + ); }; @@ -46,15 +47,15 @@ const S = { Title: styled.span` ${flexCenter} `, - Button: styled.button` - padding: 0.4rem 0.8rem; + Button: styled(Button)` + padding: 0.6rem 1rem; background-color: ${({ theme }) => theme.palette.green500}; color: ${({ theme }) => theme.palette.white}; border-radius: 0.8rem; - font-size: ${({ theme }) => theme.text.size.xSmall}; + font-size: ${({ theme }) => theme.text.size.small}; cursor: pointer; `, diff --git a/frontend/src/components/ChecklistList/LikeFilterButton.tsx b/frontend/src/components/ChecklistList/LikeFilterButton.tsx new file mode 100644 index 000000000..0ba66c4ee --- /dev/null +++ b/frontend/src/components/ChecklistList/LikeFilterButton.tsx @@ -0,0 +1,41 @@ +import styled from '@emotion/styled'; + +import Like from '@/assets/icons/like/Like'; +import useGetIsUserValidQuery from '@/hooks/query/useGetIsUserValid'; +import useGetChecklistList from '@/hooks/useGetChecklistList'; +import { boxShadow, flexRow } from '@/styles/common'; +import theme from '@/styles/theme'; + +const LikeFilterButton = () => { + const { isLikeFiltered: isEnabled, toggleFilter } = useGetChecklistList(); + const { data: user, isError, isPending } = useGetIsUserValidQuery(); + + const notLoggedIn = !(!isError && !isPending && user.isAccessTokenExist && user.isRefreshTokenExist); + if (notLoggedIn) return null; + + return ( + + + 좋아요 + + ); +}; + +export default LikeFilterButton; + +const S = { + LikeFilterBox: styled.section<{ $isChecked: boolean }>` + ${flexRow} + flex: 0 0 auto; + align-items: center; + gap: 1rem; + box-sizing: border-box; + border-radius: 1.5rem; + height: 3rem; + padding: 1.2rem 1.6rem; + + background-color: ${({ theme, $isChecked }) => ($isChecked ? theme.palette.red200 : theme.palette.white)}; + ${boxShadow}; + cursor: pointer; + `, +}; diff --git a/frontend/src/components/ChecklistQuestionSelect/QuestionSelectCard/QuestionSelectCard.tsx b/frontend/src/components/ChecklistQuestionSelect/QuestionSelectCard/QuestionSelectCard.tsx index c4d32034a..15ae7b863 100644 --- a/frontend/src/components/ChecklistQuestionSelect/QuestionSelectCard/QuestionSelectCard.tsx +++ b/frontend/src/components/ChecklistQuestionSelect/QuestionSelectCard/QuestionSelectCard.tsx @@ -23,6 +23,7 @@ const QuestionSelectCard = ({ question }: { question: ChecklistQuestionWithIsSel onClick={handleCheckQuestion} aria-label={`${title} ${subtitle ?? ''} 해당 질문을 선택하려면 두번 탭하세요.`} tabIndex={0} + className="question" >