diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json
index 6d163eb73..88b88bb65 100644
--- a/frontend/.eslintrc.json
+++ b/frontend/.eslintrc.json
@@ -22,7 +22,7 @@
"eqeqeq": "error",
"dot-notation": "warn",
"no-unused-vars": "off",
- "@typescript-eslint/no-unused-vars": ["error"],
+ "@typescript-eslint/no-unused-vars": ["error", { "varsIgnorePattern": "_" }],
"unused-imports/no-unused-imports": "error",
"unused-imports/no-unused-vars": [
"warn",
diff --git a/frontend/playwright-report/index.html b/frontend/playwright-report/index.html
deleted file mode 100644
index daa8bc7ed..000000000
--- a/frontend/playwright-report/index.html
+++ /dev/null
@@ -1,68 +0,0 @@
-
-
-
-
-
-
-
-
- Playwright Test Report
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/frontend/src/apis/checklist.ts b/frontend/src/apis/checklist.ts
index 7c5149875..90663d328 100644
--- a/frontend/src/apis/checklist.ts
+++ b/frontend/src/apis/checklist.ts
@@ -1,7 +1,8 @@
import fetcher from '@/apis/fetcher';
import { BASE_URL, ENDPOINT } from '@/apis/url';
+import { roomInfoApiMapper } from '@/store/roomInfoStore';
import { ChecklistInfo, ChecklistPostForm, ChecklistSelectedQuestions } from '@/types/checklist';
-import { mapObjNullToUndefined, mapObjUndefinedToNull } from '@/utils/typeFunctions';
+import { mapObjNullToUndefined } from '@/utils/typeFunctions';
export const getChecklistQuestions = async () => {
const response = await fetcher.get({ url: BASE_URL + ENDPOINT.CHECKLIST_QUESTION });
@@ -28,16 +29,17 @@ export const getChecklists = async () => {
};
export const postChecklist = async (checklist: ChecklistPostForm) => {
- checklist.room.structure = checklist.room.structure === 'NONE' ? undefined : checklist.room.structure;
- checklist.room = mapObjUndefinedToNull(checklist.room);
- const response = await fetcher.post({ url: BASE_URL + ENDPOINT.CHECKLISTS_V1, body: checklist });
+ const room = roomInfoApiMapper(checklist.room);
+ const response = await fetcher.post({
+ url: BASE_URL + ENDPOINT.CHECKLISTS_V1,
+ body: { ...checklist, room },
+ });
return response;
};
export const putChecklist = async (id: number, checklist: ChecklistPostForm) => {
- checklist.room.structure = checklist.room.structure === 'NONE' ? undefined : checklist.room.structure;
- checklist.room = mapObjUndefinedToNull(checklist.room);
- const response = await fetcher.put({ url: BASE_URL + ENDPOINT.CHECKLIST_ID(id), body: checklist });
+ const room = roomInfoApiMapper(checklist.room);
+ const response = await fetcher.put({ url: BASE_URL + ENDPOINT.CHECKLIST_ID(id), body: { ...checklist, room } });
return response;
};
diff --git a/frontend/src/components/NewChecklist/MemoModal/MemoModal.tsx b/frontend/src/components/NewChecklist/MemoModal/MemoModal.tsx
index 7f08f6c70..c94110dae 100644
--- a/frontend/src/components/NewChecklist/MemoModal/MemoModal.tsx
+++ b/frontend/src/components/NewChecklist/MemoModal/MemoModal.tsx
@@ -1,12 +1,11 @@
import styled from '@emotion/styled';
import { useRef } from 'react';
-import { useStore } from 'zustand';
import Button from '@/components/_common/Button/Button';
import Modal from '@/components/_common/Modal/Modal';
import Textarea from '@/components/_common/Textarea/Textarea';
import useInput from '@/hooks/useInput';
-import checklistRoomInfoStore from '@/store/checklistRoomInfoStore';
+import useRoomInfoValidated from '@/hooks/useRoomInfoValidated';
import { flexCenter, title3 } from '@/styles/common';
import theme from '@/styles/theme';
@@ -17,9 +16,8 @@ interface Props {
const MemoModal = ({ isModalOpen, modalClose }: Props) => {
const intervalRef = useRef(undefined);
- const checklistRoomInfoActions = useStore(checklistRoomInfoStore, state => state.actions);
- const memo = useStore(checklistRoomInfoStore, state => state.value.memo);
- const { value: memoValue, onChange } = useInput(memo || '');
+ const memo = useRoomInfoValidated('memo');
+ const { value: memoValue, onChange } = useInput(memo.rawValue || '');
const handleInputChange = (e: React.ChangeEvent) => {
onChange(e);
@@ -33,7 +31,7 @@ const MemoModal = ({ isModalOpen, modalClose }: Props) => {
};
const handleSubmit = (addModalClose: boolean) => {
- checklistRoomInfoActions.set('memo', memoValue);
+ memo.set(memoValue);
if (addModalClose) modalClose();
};
diff --git a/frontend/src/components/NewChecklist/NewChecklistContent.tsx b/frontend/src/components/NewChecklist/NewChecklistContent.tsx
index 5cefd2bd2..a4f5c3f21 100644
--- a/frontend/src/components/NewChecklist/NewChecklistContent.tsx
+++ b/frontend/src/components/NewChecklist/NewChecklistContent.tsx
@@ -9,6 +9,7 @@ import OptionChecklistTemplate from '@/components/NewChecklist/Option/OptionChec
import useInitialChecklist from '@/hooks/useInitialChecklist';
const NewChecklistContent = () => {
+ useInitialChecklist();
const { currentTabId } = useTabContext();
useInitialChecklist();
return (
diff --git a/frontend/src/components/NewChecklist/NewRoomInfoForm/DepositAndRent.tsx b/frontend/src/components/NewChecklist/NewRoomInfoForm/DepositAndRent.tsx
index d4c0f249b..e8c35b057 100644
--- a/frontend/src/components/NewChecklist/NewRoomInfoForm/DepositAndRent.tsx
+++ b/frontend/src/components/NewChecklist/NewRoomInfoForm/DepositAndRent.tsx
@@ -1,17 +1,12 @@
-import { useStore } from 'zustand';
-
import FormField from '@/components/_common/FormField/FormField';
import FormStyled from '@/components/NewChecklist/NewRoomInfoForm/styled';
-import checklistRoomInfoStore from '@/store/checklistRoomInfoStore';
+import useRoomInfoValidated from '@/hooks/useRoomInfoValidated';
const DepositAndRent = () => {
- const actions = useStore(checklistRoomInfoStore, state => state.actions);
- const deposit = useStore(checklistRoomInfoStore, state => state.rawValue.deposit);
- const rent = useStore(checklistRoomInfoStore, state => state.rawValue.rent);
- const errorMessageDeposit = useStore(checklistRoomInfoStore, state => state.errorMessage.deposit);
- const errorMessageRent = useStore(checklistRoomInfoStore, state => state.errorMessage.rent);
+ const deposit = useRoomInfoValidated('deposit');
+ const rent = useRoomInfoValidated('rent');
- const errorMessage = errorMessageDeposit || errorMessageRent;
+ const errorMessage = deposit.errorMessage || rent.errorMessage;
return (
@@ -19,18 +14,18 @@ const DepositAndRent = () => {
diff --git a/frontend/src/components/NewChecklist/NewRoomInfoForm/IncludedMaintenances.tsx b/frontend/src/components/NewChecklist/NewRoomInfoForm/IncludedMaintenances.tsx
index 8abbfd51a..633786daa 100644
--- a/frontend/src/components/NewChecklist/NewRoomInfoForm/IncludedMaintenances.tsx
+++ b/frontend/src/components/NewChecklist/NewRoomInfoForm/IncludedMaintenances.tsx
@@ -6,25 +6,24 @@ import FlexBox from '@/components/_common/FlexBox/FlexBox';
import FormField from '@/components/_common/FormField/FormField';
import FormStyled from '@/components/NewChecklist/NewRoomInfoForm/styled';
import { IncludedMaintenancesData } from '@/constants/roomInfo';
-import checklistRoomInfoStore from '@/store/checklistRoomInfoStore';
+import roomInfoStore from '@/store/roomInfoStore';
const IncludedMaintenances = () => {
- const actions = useStore(checklistRoomInfoStore, state => state.actions);
- const includedMaintenances = useStore(checklistRoomInfoStore, state => state.value.includedMaintenances);
+ // TODO : nonValidated 에서 관리해야함. 일단은 놔뒀음.
- const handleCheckIncluded = (id: number) => !!includedMaintenances?.includes(id);
+ const includedMaintenances = useStore(roomInfoStore, state => state.includedMaintenances).rawValue;
+ const actions = useStore(roomInfoStore, state => state.actions);
- const handleToggleButton = useCallback(
- (value: number) => {
- const isIncluded = handleCheckIncluded(value);
-
- const updatedValue = isIncluded
- ? includedMaintenances?.filter(id => id !== value)
- : [...(includedMaintenances || []), value];
+ const isIncluded = useCallback((id: number) => includedMaintenances.includes(id), [includedMaintenances]);
- actions.set('includedMaintenances', updatedValue);
+ const handleToggleButton = useCallback(
+ (clickedId: number) => {
+ const updatedValue = isIncluded(clickedId)
+ ? includedMaintenances.filter(id => id !== clickedId)
+ : [...(includedMaintenances || []), clickedId];
+ actions.set({ includedMaintenances: { rawValue: updatedValue, errorMessage: '' } });
},
- [includedMaintenances, actions],
+ [includedMaintenances, actions, isIncluded],
);
return (
@@ -37,7 +36,7 @@ const IncludedMaintenances = () => {
label={displayName}
name={displayName}
size="button"
- isSelected={handleCheckIncluded(id)}
+ isSelected={isIncluded(id)}
onClick={() => handleToggleButton(id)}
/>
))}
diff --git a/frontend/src/components/NewChecklist/NewRoomInfoForm/MaintenanceFee.tsx b/frontend/src/components/NewChecklist/NewRoomInfoForm/MaintenanceFee.tsx
index 73bf9f1b6..a81df9a7d 100644
--- a/frontend/src/components/NewChecklist/NewRoomInfoForm/MaintenanceFee.tsx
+++ b/frontend/src/components/NewChecklist/NewRoomInfoForm/MaintenanceFee.tsx
@@ -1,14 +1,10 @@
-import { useStore } from 'zustand';
-
import FormField from '@/components/_common/FormField/FormField';
import Input from '@/components/_common/Input/Input';
import FormStyled from '@/components/NewChecklist/NewRoomInfoForm/styled';
-import checklistRoomInfoStore from '@/store/checklistRoomInfoStore';
+import useRoomInfoValidated from '@/hooks/useRoomInfoValidated';
const MaintenanceFee = () => {
- const actions = useStore(checklistRoomInfoStore, state => state.actions);
- const maintenanceFee = useStore(checklistRoomInfoStore, state => state.rawValue.maintenanceFee);
- const errorMessage = useStore(checklistRoomInfoStore, state => state.errorMessage.maintenanceFee);
+ const maintenanceFee = useRoomInfoValidated('maintenanceFee');
return (
@@ -17,14 +13,14 @@ const MaintenanceFee = () => {
-
+
);
};
diff --git a/frontend/src/components/NewChecklist/NewRoomInfoForm/OccupancyMonth.tsx b/frontend/src/components/NewChecklist/NewRoomInfoForm/OccupancyMonth.tsx
index 7483c3c50..ab9c04c12 100644
--- a/frontend/src/components/NewChecklist/NewRoomInfoForm/OccupancyMonth.tsx
+++ b/frontend/src/components/NewChecklist/NewRoomInfoForm/OccupancyMonth.tsx
@@ -1,20 +1,14 @@
-import { useStore } from 'zustand';
-
import Dropdown from '@/components/_common/Dropdown/Dropdown';
import FlexBox from '@/components/_common/FlexBox/FlexBox';
import FormField from '@/components/_common/FormField/FormField';
import FormStyled from '@/components/NewChecklist/NewRoomInfoForm/styled';
import { roomOccupancyPeriods } from '@/constants/roomInfo';
-import checklistRoomInfoStore from '@/store/checklistRoomInfoStore';
+import useRoomInfoValidated from '@/hooks/useRoomInfoValidated';
const OccupancyMonth = () => {
- const actions = useStore(checklistRoomInfoStore, state => state.actions);
- const occupancyMonth = useStore(checklistRoomInfoStore, state => state.rawValue.occupancyMonth);
- const occupancyPeriod = useStore(checklistRoomInfoStore, state => state.rawValue.occupancyPeriod);
-
- const errorMessageOccupancyMonth = useStore(checklistRoomInfoStore, state => state.errorMessage.occupancyMonth);
- const errorMessageOccupancyPeriod = useStore(checklistRoomInfoStore, state => state.errorMessage.occupancyPeriod);
- const errorMessage = errorMessageOccupancyMonth || errorMessageOccupancyPeriod;
+ const occupancyMonth = useRoomInfoValidated('occupancyMonth');
+ const occupancyPeriod = useRoomInfoValidated('occupancyPeriod');
+ const errorMessage = occupancyMonth.errorMessage || occupancyPeriod.errorMessage;
return (
@@ -22,18 +16,16 @@ const OccupancyMonth = () => {
({ value }))}
- onSelectSetter={(level: string) => {
- actions.set('occupancyPeriod', level);
- }}
+ onSelectSetter={(level: string) => occupancyPeriod.set(level)}
/>
diff --git a/frontend/src/components/NewChecklist/NewRoomInfoForm/RealEstate.tsx b/frontend/src/components/NewChecklist/NewRoomInfoForm/RealEstate.tsx
index d847c6cf5..61b9f65b5 100644
--- a/frontend/src/components/NewChecklist/NewRoomInfoForm/RealEstate.tsx
+++ b/frontend/src/components/NewChecklist/NewRoomInfoForm/RealEstate.tsx
@@ -1,12 +1,8 @@
-import { useStore } from 'zustand';
-
import FormField from '@/components/_common/FormField/FormField';
-import checklistRoomInfoStore from '@/store/checklistRoomInfoStore';
+import useRoomInfoValidated from '@/hooks/useRoomInfoValidated';
const RealEstate = () => {
- const actions = useStore(checklistRoomInfoStore, state => state.actions);
- const realEstate = useStore(checklistRoomInfoStore, state => state.rawValue.realEstate);
- const errorMessage = useStore(checklistRoomInfoStore, state => state.errorMessage.realEstate);
+ const realEstate = useRoomInfoValidated('realEstate');
return (
@@ -15,12 +11,12 @@ const RealEstate = () => {
placeholder=""
width="full"
type={'string'}
- onChange={actions.onChange}
+ onChange={realEstate.onChange}
name={'realEstate'}
- value={realEstate}
+ value={realEstate.rawValue}
id="realEstate"
/>
-
+
);
};
diff --git a/frontend/src/components/NewChecklist/NewRoomInfoForm/RoomContractTerm.tsx b/frontend/src/components/NewChecklist/NewRoomInfoForm/RoomContractTerm.tsx
index 843e08204..84e335fcd 100644
--- a/frontend/src/components/NewChecklist/NewRoomInfoForm/RoomContractTerm.tsx
+++ b/frontend/src/components/NewChecklist/NewRoomInfoForm/RoomContractTerm.tsx
@@ -1,15 +1,12 @@
import styled from '@emotion/styled';
-import { useStore } from 'zustand';
import FormField from '@/components/_common/FormField/FormField';
import Input from '@/components/_common/Input/Input';
import FormStyled from '@/components/NewChecklist/NewRoomInfoForm/styled';
-import checklistRoomInfoStore from '@/store/checklistRoomInfoStore';
+import useRoomInfoValidated from '@/hooks/useRoomInfoValidated';
const RoomContractTerm = () => {
- const actions = useStore(checklistRoomInfoStore, state => state.actions);
- const contractTerm = useStore(checklistRoomInfoStore, state => state.rawValue.contractTerm);
- const errorMessage = useStore(checklistRoomInfoStore, state => state.errorMessage.contractTerm);
+ const contractTerm = useRoomInfoValidated('contractTerm');
return (
@@ -18,14 +15,14 @@ const RoomContractTerm = () => {
-
+
);
};
diff --git a/frontend/src/components/NewChecklist/NewRoomInfoForm/RoomFloor.tsx b/frontend/src/components/NewChecklist/NewRoomInfoForm/RoomFloor.tsx
index 8021d6ad6..25caadd23 100644
--- a/frontend/src/components/NewChecklist/NewRoomInfoForm/RoomFloor.tsx
+++ b/frontend/src/components/NewChecklist/NewRoomInfoForm/RoomFloor.tsx
@@ -1,22 +1,18 @@
-import { useStore } from 'zustand';
-
import Dropdown from '@/components/_common/Dropdown/Dropdown';
import FormField from '@/components/_common/FormField/FormField';
import Input from '@/components/_common/Input/Input';
import FormStyled from '@/components/NewChecklist/NewRoomInfoForm/styled';
import { roomFloorLevels } from '@/constants/roomInfo';
-import checklistRoomInfoStore from '@/store/checklistRoomInfoStore';
+import useRoomInfoValidated from '@/hooks/useRoomInfoValidated';
const RoomFloor = () => {
- const actions = useStore(checklistRoomInfoStore, state => state.actions);
- const floor = useStore(checklistRoomInfoStore, state => state.rawValue.floor);
- const floorLevel = useStore(checklistRoomInfoStore, state => state.rawValue.floorLevel);
- const errorMessageFloor = useStore(checklistRoomInfoStore, state => state.errorMessage.floor);
+ const floor = useRoomInfoValidated('floor');
+ const floorLevel = useRoomInfoValidated('floorLevel');
const handleClickDropdown = (level: string) => {
- actions.set('floorLevel', level);
+ floorLevel.set(level);
if (level === '반지하/지하') {
- actions.set('floor', '');
+ floor.set('');
}
};
return (
@@ -24,22 +20,22 @@ const RoomFloor = () => {
({ value }))}
onSelectSetter={handleClickDropdown}
/>
-
+
);
};
diff --git a/frontend/src/components/NewChecklist/NewRoomInfoForm/RoomName.tsx b/frontend/src/components/NewChecklist/NewRoomInfoForm/RoomName.tsx
index 8188a1bb2..7c0b622d8 100644
--- a/frontend/src/components/NewChecklist/NewRoomInfoForm/RoomName.tsx
+++ b/frontend/src/components/NewChecklist/NewRoomInfoForm/RoomName.tsx
@@ -1,21 +1,24 @@
import React from 'react';
-import { useStore } from 'zustand';
import FormField from '@/components/_common/FormField/FormField';
import useDefaultRoomName from '@/components/NewChecklist/NewRoomInfoForm/useDefaultRoomName';
-import checklistRoomInfoStore from '@/store/checklistRoomInfoStore';
+import useRoomInfoValidated from '@/hooks/useRoomInfoValidated';
const RoomName = () => {
+ const roomName = useRoomInfoValidated('roomName');
useDefaultRoomName();
- const actions = useStore(checklistRoomInfoStore, state => state.actions);
- const roomName = useStore(checklistRoomInfoStore, state => state.rawValue.roomName);
- const errorMessage = useStore(checklistRoomInfoStore, state => state.errorMessage.roomName);
return (
-
-
+
+
);
};
diff --git a/frontend/src/components/NewChecklist/NewRoomInfoForm/RoomNameNoDefault.tsx b/frontend/src/components/NewChecklist/NewRoomInfoForm/RoomNameNoDefault.tsx
index f449e20b1..a697a1337 100644
--- a/frontend/src/components/NewChecklist/NewRoomInfoForm/RoomNameNoDefault.tsx
+++ b/frontend/src/components/NewChecklist/NewRoomInfoForm/RoomNameNoDefault.tsx
@@ -1,19 +1,22 @@
import React from 'react';
-import { useStore } from 'zustand';
import FormField from '@/components/_common/FormField/FormField';
-import checklistRoomInfoStore from '@/store/checklistRoomInfoStore';
+import useRoomInfoValidated from '@/hooks/useRoomInfoValidated';
const RoomNameNoDefault = () => {
- const actions = useStore(checklistRoomInfoStore, state => state.actions);
- const roomName = useStore(checklistRoomInfoStore, state => state.rawValue.roomName);
- const errorMessage = useStore(checklistRoomInfoStore, state => state.errorMessage.roomName);
+ const roomName = useRoomInfoValidated('roomName');
return (
-
-
+
+
);
};
diff --git a/frontend/src/components/NewChecklist/NewRoomInfoForm/RoomSize.tsx b/frontend/src/components/NewChecklist/NewRoomInfoForm/RoomSize.tsx
index 6a5f97a94..0d85a8f13 100644
--- a/frontend/src/components/NewChecklist/NewRoomInfoForm/RoomSize.tsx
+++ b/frontend/src/components/NewChecklist/NewRoomInfoForm/RoomSize.tsx
@@ -1,23 +1,26 @@
-import { useStore } from 'zustand';
-
import FormField from '@/components/_common/FormField/FormField';
import Input from '@/components/_common/Input/Input';
import FormStyled from '@/components/NewChecklist/NewRoomInfoForm/styled';
-import checklistRoomInfoStore from '@/store/checklistRoomInfoStore';
+import useRoomInfoValidated from '@/hooks/useRoomInfoValidated';
const RoomSize = () => {
- const actions = useStore(checklistRoomInfoStore, state => state.actions);
- const roomSize = useStore(checklistRoomInfoStore, state => state.rawValue.size);
- const errorMessage = useStore(checklistRoomInfoStore, state => state.errorMessage.size);
+ const roomSize = useRoomInfoValidated('size');
return (
-
+
-
+
);
};
diff --git a/frontend/src/components/NewChecklist/NewRoomInfoForm/RoomStructure.tsx b/frontend/src/components/NewChecklist/NewRoomInfoForm/RoomStructure.tsx
index 370b454b3..ff46171e5 100644
--- a/frontend/src/components/NewChecklist/NewRoomInfoForm/RoomStructure.tsx
+++ b/frontend/src/components/NewChecklist/NewRoomInfoForm/RoomStructure.tsx
@@ -1,23 +1,18 @@
import { useCallback } from 'react';
-import { useStore } from 'zustand';
import Badge from '@/components/_common/Badge/Badge';
import FlexBox from '@/components/_common/FlexBox/FlexBox';
import FormField from '@/components/_common/FormField/FormField';
import FormStyled from '@/components/NewChecklist/NewRoomInfoForm/styled';
import { roomStructures } from '@/constants/roomInfo';
-import checklistRoomInfoStore from '@/store/checklistRoomInfoStore';
+import useRoomInfoValidated from '@/hooks/useRoomInfoValidated';
const RoomStructure = () => {
- const actions = useStore(checklistRoomInfoStore, state => state.actions);
- const roomStructure = useStore(checklistRoomInfoStore, state => state.rawValue.structure);
+ const roomStructure = useRoomInfoValidated('structure');
- const handleClickTagButton = useCallback(
- (value: string) => {
- actions.set('structure', value);
- },
- [actions],
- );
+ const handleClickTagButton = useCallback((value: string) => {
+ roomStructure.set(value);
+ }, []);
return (
@@ -29,7 +24,7 @@ const RoomStructure = () => {
label={structure}
name={structure}
size="button"
- isSelected={structure === roomStructure}
+ isSelected={structure === roomStructure.rawValue}
onClick={() => handleClickTagButton(structure)}
/>
diff --git a/frontend/src/components/NewChecklist/NewRoomInfoForm/useDefaultRoomName.ts b/frontend/src/components/NewChecklist/NewRoomInfoForm/useDefaultRoomName.ts
index 6faaf9853..9c90b4cdc 100644
--- a/frontend/src/components/NewChecklist/NewRoomInfoForm/useDefaultRoomName.ts
+++ b/frontend/src/components/NewChecklist/NewRoomInfoForm/useDefaultRoomName.ts
@@ -1,12 +1,11 @@
import { useEffect } from 'react';
-import { useStore } from 'zustand';
import useGetChecklistListQuery from '@/hooks/query/useGetChecklistListQuery';
-import checklistRoomInfoStore, { initialRoomInfo } from '@/store/checklistRoomInfoStore';
+import useRoomInfoValidated from '@/hooks/useRoomInfoValidated';
+import { initialRoomInfo } from '@/store/roomInfoStore';
const useDefaultRoomName = () => {
- const roomInfoActions = useStore(checklistRoomInfoStore, state => state.actions);
- const roomName = useStore(checklistRoomInfoStore, state => state.rawValue.roomName);
+ const roomName = useRoomInfoValidated('roomName');
const { data: checklistList } = useGetChecklistListQuery();
@@ -20,21 +19,10 @@ const useDefaultRoomName = () => {
).length;
const date = new Date();
- roomInfoActions.set('roomName', `${date.getMonth() + 1}월 ${date.getDate()}일 ${count}번째 방`);
+ roomName.set(`${date.getMonth() + 1}월 ${date.getDate()}일 ${count}번째 방`);
}, [checklistList]);
- // 그이후부터는 폼은 안건드리고, 내부 value에만 적용
- useEffect(() => {
- if (!checklistList) return;
- if (roomName !== initialRoomInfo.roomName) return;
-
- const count = checklistList.filter(
- checklist => new Date(checklist.createdAt).getUTCDay() === new Date().getUTCDay(),
- ).length;
-
- const date = new Date();
- roomInfoActions.setValueForced('roomName', `${date.getMonth() + 1}월 ${date.getDate()}일 ${count}번째 방`);
- }, [checklistList, roomName]);
+ // value를 직접건드려서 N번째방 value 넣어주던 로직지웠음.
};
export default useDefaultRoomName;
diff --git a/frontend/src/components/NewChecklist/SubmitModalWithSummary/SubmitModalWithSummary.tsx b/frontend/src/components/NewChecklist/SubmitModalWithSummary/SubmitModalWithSummary.tsx
index 42cf4ed19..595c34f3d 100644
--- a/frontend/src/components/NewChecklist/SubmitModalWithSummary/SubmitModalWithSummary.tsx
+++ b/frontend/src/components/NewChecklist/SubmitModalWithSummary/SubmitModalWithSummary.tsx
@@ -1,5 +1,4 @@
import styled from '@emotion/styled';
-import { useStore } from 'zustand';
import Button from '@/components/_common/Button/Button';
import CounterBox from '@/components/_common/CounterBox/CounterBox';
@@ -7,7 +6,7 @@ import FormField from '@/components/_common/FormField/FormField';
import Modal from '@/components/_common/Modal/Modal';
import { MODAL_MESSAGE } from '@/constants/message';
import useMutateChecklist from '@/hooks/useMutateChecklist';
-import checklistRoomInfoStore from '@/store/checklistRoomInfoStore';
+import useRoomInfoValidated from '@/hooks/useRoomInfoValidated';
import { flexColumn, title3 } from '@/styles/common';
import { MutateType } from '@/types/checklist';
@@ -28,8 +27,7 @@ const SubmitModalWithSummary = ({
mutateType,
checklistId,
}: Props) => {
- const summary = useStore(checklistRoomInfoStore, state => state.rawValue.summary);
- const actions = useStore(checklistRoomInfoStore, state => state.actions);
+ const summary = useRoomInfoValidated('summary');
// 체크리스트 작성 / 수정
const { handleSubmitChecklist } = useMutateChecklist(
@@ -63,14 +61,14 @@ const SubmitModalWithSummary = ({
-
+
diff --git a/frontend/src/components/_common/Subway/SubwayStations.tsx b/frontend/src/components/_common/Subway/SubwayStations.tsx
index 5b1647c8a..9807c04b4 100644
--- a/frontend/src/components/_common/Subway/SubwayStations.tsx
+++ b/frontend/src/components/_common/Subway/SubwayStations.tsx
@@ -6,7 +6,7 @@ import { SubwayStation } from '@/types/subway';
const SubwayStations = ({ stations }: { stations: SubwayStation[] }) => {
return (
- {stations.length ? (
+ {stations?.length ? (
stations?.map(station => )
) : (
diff --git a/frontend/src/hooks/useMutateChecklist.ts b/frontend/src/hooks/useMutateChecklist.ts
index 3cc350e9a..0f39e4b80 100644
--- a/frontend/src/hooks/useMutateChecklist.ts
+++ b/frontend/src/hooks/useMutateChecklist.ts
@@ -5,8 +5,8 @@ import { TOAST_MESSAGE } from '@/constants/message';
import useAddChecklistQuery from '@/hooks/query/useAddChecklistQuery';
import usePutChecklistQuery from '@/hooks/query/usePutChecklistQuery';
import useToast from '@/hooks/useToast';
-import checklistRoomInfoStore from '@/store/checklistRoomInfoStore';
import roomInfoNonValidatedStore from '@/store/roomInfoNonValidatedStore';
+import roomInfoStore from '@/store/roomInfoStore';
import useChecklistStore from '@/store/useChecklistStore';
import useSelectedOptionStore from '@/store/useSelectedOptionStore';
import { ChecklistCategoryWithAnswer, MutateType } from '@/types/checklist';
@@ -23,8 +23,9 @@ const useMutateChecklist = (
const { mutate: putChecklist } = usePutChecklistQuery();
// 방 기본 정보 - validated
- const roomInfoActions = useStore(checklistRoomInfoStore, state => state.actions);
- const roomInfoAnswer = useStore(checklistRoomInfoStore, state => state.value);
+ const roomInfoActions = useStore(roomInfoStore, state => state.actions);
+ const roomInfo = roomInfoActions.getParsedValues();
+
// 방 기본 정보 - nonValidated
const roomInfoUnvalidatedActions = useStore(roomInfoNonValidatedStore, state => state.actions);
const roomInfoUnvalidated = useStore(roomInfoNonValidatedStore, state => state);
@@ -35,7 +36,7 @@ const useMutateChecklist = (
const postData = {
room: {
- ...roomInfoAnswer,
+ ...roomInfo,
...roomInfoUnvalidatedActions.getFormValues(),
},
options: selectedOptions,
@@ -55,7 +56,7 @@ const useMutateChecklist = (
addChecklist(postData, {
onSuccess: res => {
showToast({ message: TOAST_MESSAGE.ADD });
- roomInfoActions.resetAll();
+ roomInfoActions.reset();
roomInfoUnvalidatedActions.resetAll();
if (onSuccessCallback) {
onSuccessCallback();
@@ -77,7 +78,7 @@ const useMutateChecklist = (
putChecklist(putData, {
onSuccess: res => {
showToast({ message: TOAST_MESSAGE.EDIT });
- roomInfoActions.resetAll();
+ roomInfoActions.reset();
roomInfoUnvalidatedActions.resetAll();
if (onSuccessCallback) {
onSuccessCallback();
diff --git a/frontend/src/hooks/useRoomInfoValidated.test.ts b/frontend/src/hooks/useRoomInfoValidated.test.ts
new file mode 100644
index 000000000..6b25182e3
--- /dev/null
+++ b/frontend/src/hooks/useRoomInfoValidated.test.ts
@@ -0,0 +1,87 @@
+import { renderHook } from '@testing-library/react';
+import { act } from 'react';
+
+import useRoomInfoValidated from '@/hooks/useRoomInfoValidated';
+import { initialRoomInfo, roomInfoStore } from '@/store/roomInfoStore';
+import { InputChangeEvent } from '@/types/event';
+
+const makeEvent = (rawValue: string) => ({ target: { value: rawValue } }) as InputChangeEvent;
+describe('새 스토어 테스트', () => {
+ beforeEach(() => {
+ roomInfoStore.getState().actions.reset();
+ });
+ it('store의 초기값이 잘 형성된다.', () => {
+ const { actions: _, ...roomInfo } = { ...roomInfoStore.getState() };
+ expect(roomInfo).toEqual(initialRoomInfo);
+ });
+ it('입력 변경이 작동한다.', () => {
+ const { result } = renderHook(() => useRoomInfoValidated('roomName'));
+
+ act(() => result.current.onChange(makeEvent('aa')));
+ expect(result.current.rawValue).toBe('aa');
+ });
+
+ describe('roomName', () => {
+ it('20자 이하 입력시 정삭 입력된다.', () => {
+ const { result: roomName } = renderHook(() => useRoomInfoValidated('roomName'));
+
+ act(() => roomName.current.onChange(makeEvent('a'.repeat(20))));
+
+ expect(roomName.current.rawValue).toBe('a'.repeat(20));
+ expect(roomName.current.errorMessage).toBe('');
+ });
+ it('roomName은 21자 이상의 입력은 방지한다', () => {
+ const { result: roomName } = renderHook(() => useRoomInfoValidated('roomName'));
+ expect(roomName.current.rawValue).toBe('');
+
+ act(() => roomName.current.onChange(makeEvent('a'.repeat(21))));
+
+ expect(roomName.current.rawValue).toBe('');
+ expect(roomName.current.errorMessage.length > 0).toBe(true);
+ });
+ });
+
+ describe('보증금', () => {
+ it('숫자형이 아닌 입력이 들어왔을 때 입력을 방지한다.', () => {
+ const { result: deposit } = renderHook(() => useRoomInfoValidated('deposit'));
+ expect(deposit.current.rawValue).toBe('');
+
+ act(() => deposit.current.onChange(makeEvent('ab')));
+
+ expect(deposit.current.rawValue).toBe('');
+ expect(deposit.current.errorMessage).not.toBe('');
+ });
+
+ it('숫자 입력 후 빈 문자열을 설정할 경우(모두 지울 경우), 빈 문자열로 설정된다.', () => {
+ const { result: deposit } = renderHook(() => useRoomInfoValidated('deposit'));
+ expect(deposit.current.rawValue).toBe('');
+
+ act(() => deposit.current.onChange(makeEvent('112')));
+ expect(deposit.current.rawValue).toBe('112');
+
+ act(() => deposit.current.onChange(makeEvent('')));
+ expect(deposit.current.rawValue).toBe('');
+ });
+ });
+
+ describe('층수', () => {
+ it('소수점을 포함해 입력("4.")해도 정상 입력된다.', () => {
+ const { result: floor } = renderHook(() => useRoomInfoValidated('floor'));
+
+ act(() => floor.current.onChange(makeEvent('4.')));
+
+ expect(floor.current.rawValue).toBe('4.');
+ expect(floor.current.errorMessage).toBe('');
+ });
+ });
+ describe('방 크기', () => {
+ it('소수("4.1")을 입력해도 정상 입력된다.', () => {
+ const { result: size } = renderHook(() => useRoomInfoValidated('size'));
+
+ act(() => size.current.onChange(makeEvent('4.1')));
+
+ expect(size.current.rawValue).toBe('4.1');
+ expect(size.current.errorMessage).toBe('');
+ });
+ });
+});
diff --git a/frontend/src/hooks/useRoomInfoValidated.ts b/frontend/src/hooks/useRoomInfoValidated.ts
new file mode 100644
index 000000000..7c0c1c152
--- /dev/null
+++ b/frontend/src/hooks/useRoomInfoValidated.ts
@@ -0,0 +1,105 @@
+import { useCallback } from 'react';
+import { useStore } from 'zustand';
+
+import roomInfoStore from '@/store/roomInfoStore';
+import { InputChangeEvent } from '@/types/event';
+import { RoomInfo } from '@/types/room';
+import {
+ inRangeValidator,
+ isIntegerValidator,
+ isNumericValidator,
+ lengthValidator,
+ nonNegativeValidator,
+ positiveValidator,
+ Validator,
+} from '@/utils/validators';
+
+const validators: Record = {
+ roomName: [lengthValidator(20)],
+ deposit: [isNumericValidator, nonNegativeValidator],
+ rent: [isNumericValidator, nonNegativeValidator],
+ maintenanceFee: [isNumericValidator, nonNegativeValidator],
+ contractTerm: [isNumericValidator, nonNegativeValidator],
+ type: [],
+ size: [isNumericValidator],
+ floor: [isIntegerValidator, positiveValidator],
+ floorLevel: [],
+ structure: [],
+ realEstate: [],
+ occupancyMonth: [isIntegerValidator, positiveValidator, inRangeValidator(1, 12)],
+ occupancyPeriod: [],
+
+ summary: [],
+ memo: [],
+
+ station: [],
+ walkingTime: [],
+ address: [],
+ buildingName: [],
+};
+
+const numerics = [
+ 'deposit',
+ 'rent',
+ 'maintenanceFee',
+ 'contractTerm',
+ 'size',
+ 'floor',
+ 'occupancyMonth',
+ 'occupancyPeriod',
+ 'walkingTime',
+] as const satisfies (keyof ValidatedRoomInfo)[];
+
+const isNumeric = new Set(numerics);
+
+type ValidatedRoomInfo = Omit;
+type Includes = U extends T[number] ? true : false;
+
+export const parseRoomInfo = (name: keyof RoomInfo, rawValue: string) =>
+ (isNumeric.has(name) ? Number(rawValue) : rawValue) as Includes extends true
+ ? number
+ : string;
+
+const useRoomInfoValidated = (name: Key) => {
+ const rawValue = useStore(roomInfoStore, state => state[name])!.rawValue!;
+ const errorMessage = useStore(roomInfoStore, state => state[name])!.errorMessage!;
+ const actions = useStore(roomInfoStore, state => state.actions);
+ const value = parseRoomInfo(name, rawValue);
+
+ const set = useCallback(
+ (rawValue: string) => actions.set({ [name]: { rawValue, errorMessage } }),
+ [name, errorMessage, actions],
+ );
+
+ // 에러메시지가 없을 경우만 set
+ const onChange = useCallback(
+ (e: InputChangeEvent) => {
+ if (e.target.value === '') {
+ actions.set({ [name]: { rawValue: '', errorMessage: '' } });
+ return;
+ }
+
+ const errorMessage = validation(e.target.value, validators[name]);
+ if (errorMessage) {
+ actions.set({ [name]: { rawValue, errorMessage } });
+ return;
+ }
+ actions.set({ [name]: { rawValue: e.target.value, errorMessage } });
+ },
+ [name, rawValue, actions],
+ );
+
+ return { rawValue, value, errorMessage, onChange, set };
+};
+
+const validation = (rawValue: string, validators: Validator[] | undefined) => {
+ const newErrorMessage =
+ validators
+ ?.slice()
+ .reverse()
+ .reduce((acc, { validate, errorMessage }) => (!validate(rawValue) ? errorMessage : acc), '') ?? '';
+
+ return newErrorMessage;
+};
+
+export default useRoomInfoValidated;
diff --git a/frontend/src/mocks/handlers/checklist.ts b/frontend/src/mocks/handlers/checklist.ts
index 494b6ab19..ea3b0e119 100644
--- a/frontend/src/mocks/handlers/checklist.ts
+++ b/frontend/src/mocks/handlers/checklist.ts
@@ -26,6 +26,9 @@ export const checklistHandlers = [
http.post(BASE_URL + ENDPOINT.CHECKLISTS, () => {
return HttpResponse.json({}, { status: 201 });
}),
+ http.post(BASE_URL + ENDPOINT.CHECKLISTS_V1, () => {
+ return HttpResponse.json({}, { status: 201 });
+ }),
http.get(BASE_URL + ENDPOINT.CHECKLIST_ALL_QUESTION, () => {
return HttpResponse.json(checklistAllQuestions, { status: 200 });
diff --git a/frontend/src/pages/EditChecklistPage.tsx b/frontend/src/pages/EditChecklistPage.tsx
index 09a70b02a..e0a76fc32 100644
--- a/frontend/src/pages/EditChecklistPage.tsx
+++ b/frontend/src/pages/EditChecklistPage.tsx
@@ -15,10 +15,12 @@ import { DEFAULT_CHECKLIST_TAB_PAGE } from '@/constants/system';
import useGetChecklistDetailQuery from '@/hooks/query/useGetChecklistDetailQuery';
import useChecklistTabs from '@/hooks/useChecklistTabs';
import useModal from '@/hooks/useModal';
-import checklistRoomInfoStore from '@/store/checklistRoomInfoStore';
+import useRoomInfoNonValidated from '@/hooks/useRoomInfoNonValidated';
import roomInfoNonValidatedStore from '@/store/roomInfoNonValidatedStore';
+import roomInfoStore from '@/store/roomInfoStore';
import useChecklistStore from '@/store/useChecklistStore';
import useSelectedOptionStore from '@/store/useSelectedOptionStore';
+import loadExternalScriptWithCallback from '@/utils/loadScript';
type RouteParams = {
checklistId: string;
@@ -28,10 +30,12 @@ const EditChecklistPage = () => {
const navigate = useNavigate();
const { checklistId } = useParams() as RouteParams;
const { data: checklist, isSuccess } = useGetChecklistDetailQuery(checklistId);
+
const { tabs } = useChecklistTabs();
const checklistActions = useChecklistStore(state => state.actions);
- const roomInfoActions = useStore(checklistRoomInfoStore, state => state.actions);
+ const { searchSubwayStationsByAddress } = useRoomInfoNonValidated();
+ const roomInfoActions = useStore(roomInfoStore, state => state.actions);
const roomInfoUnvalidatedActions = useStore(roomInfoNonValidatedStore, state => state.actions);
// 한줄평 모달
@@ -44,7 +48,7 @@ const EditChecklistPage = () => {
const selectedOptionActions = useSelectedOptionStore(state => state.actions);
const resetAndGoDetailPage = () => {
- roomInfoActions.resetAll();
+ roomInfoActions.reset();
roomInfoUnvalidatedActions.resetAll();
checklistActions.reset();
selectedOptionActions.reset();
@@ -55,10 +59,12 @@ const EditChecklistPage = () => {
const setChecklistDataToStore = async () => {
if (!isSuccess) return;
- roomInfoActions.setAll({
- rawValue: checklist.room,
- value: checklist.room,
- });
+ roomInfoActions.setRawValues(checklist.room);
+ roomInfoUnvalidatedActions.set('address', checklist.room.address!);
+ roomInfoUnvalidatedActions.set('buildingName', checklist.room.buildingName!);
+ //TODO: 가까운 지하철은 나중에 api 수정되면 저장
+
+ loadExternalScriptWithCallback('kakaoMap', () => searchSubwayStationsByAddress(checklist.room.address!));
selectedOptionActions.set(checklist.options.map(option => option.optionId));
checklistActions.set(checklist.categories);
@@ -89,6 +95,7 @@ const EditChecklistPage = () => {
)}
{/* 한줄평 모달*/}
+
{
- const { tabs } = useChecklistTabs();
const navigate = useNavigate();
+ const { tabs } = useChecklistTabs();
- const roomInfoActions = useStore(checklistRoomInfoStore, state => state.actions);
+ const roomInfoActions = useStore(roomInfoStore, state => state.actions);
const roomInfoNonValidatedActions = useStore(roomInfoNonValidatedStore, state => state.actions);
const checklistActions = useChecklistStore(state => state.actions);
const selectedOptionActions = useSelectedOptionStore(state => state.actions);
@@ -33,18 +33,12 @@ const NewChecklistPage = () => {
// 메모 모달
const { isModalOpen: isMemoModalOpen, openModal: openMemoModal, closeModal: closeMemoModal } = useModal();
-
- // 한줄평 모달
const { isModalOpen: isSubmitModalOpen, openModal: openSummaryModal, closeModal: closeSummaryModal } = useModal();
-
- // 뒤로가기 시 휘발 경고 모달
const { isModalOpen: isAlertModalOpen, openModal: openAlertModal, closeModal: closeAlertModal } = useModal();
-
- // 로그인 요청 모달
const { isModalOpen: isLoginModalOpen, openModal: openLoginModal, closeModal: closeLoginModal } = useModal();
const resetChecklist = () => {
- roomInfoActions.resetAll();
+ roomInfoActions.reset();
roomInfoNonValidatedActions.resetAll();
checklistActions.reset();
selectedOptionActions.reset();
diff --git a/frontend/src/store/checklistRoomInfoStore.test.ts b/frontend/src/store/checklistRoomInfoStore.test.ts
deleted file mode 100644
index 53e7d0d57..000000000
--- a/frontend/src/store/checklistRoomInfoStore.test.ts
+++ /dev/null
@@ -1,105 +0,0 @@
-import checklistRoomInfoStore from '@/store/checklistRoomInfoStore';
-import { InputChangeEvent } from '@/types/event';
-
-const store = checklistRoomInfoStore;
-const makeEvent = (name: string, value: string) => ({ target: { name, value } }) as InputChangeEvent;
-
-describe('useChecklistBasicInfoStore 테스트', () => {
- beforeEach(() => {
- store.getState().actions.resetAll();
- });
-
- it('모든 변수의 초기값이 적용된다.', () => {
- expect(store.getState()).toEqual(store.getInitialState());
- });
-
- describe('방 이름', () => {
- it('20자 이하 입력 시 정상 입력된다.', () => {
- store.getState().actions.onChange(makeEvent('roomName', '3'.repeat(20)));
-
- expect(store.getState().rawValue.roomName).toBe('3'.repeat(20));
- expect(store.getState().value.roomName).toBe('3'.repeat(20));
- expect(store.getState().errorMessage.roomName).toBe('');
- });
-
- it('20자 초과 입력 시 오류메시지가 발생한다.', () => {
- store.getState().actions.onChange(makeEvent('roomName', '3'.repeat(20)));
- store.getState().actions.onChange(makeEvent('roomName', '3'.repeat(21)));
-
- expect(store.getState().rawValue.roomName).toBe('3'.repeat(20));
- expect(store.getState().errorMessage.roomName).not.toBe('');
- });
- });
-
- describe('보증금', () => {
- it('숫자 입력 후 빈 문자열을 설정할 경우(모두 지울 경우), 빈 문자열로 설정된다.', () => {
- store.getState().actions.onChange(makeEvent('deposit', '112'));
- expect(store.getState().rawValue.deposit).toBe('112');
-
- store.getState().actions.onChange(makeEvent('deposit', ''));
- expect(store.getState().rawValue.deposit).toBe('');
- });
- });
-
- describe('관리비', () => {
- it('관리비 입력 필드에는 숫자만 입력 가능하다.', () => {
- store.getState().actions.onChange(makeEvent('maintenanceFee', '관리비'));
- expect(store.getState().rawValue.maintenanceFee).toBe('');
-
- store.getState().actions.onChange(makeEvent('maintenanceFee', '123'));
- expect(store.getState().rawValue.maintenanceFee).toBe('123');
- });
- });
-
- describe('관리비 포함 항목', () => {
- it('선택한 관리비 항목 종류만을 가지고 있는다.', () => {
- store.getState().actions.set('includedMaintenances', [1]);
- expect(store.getState().value.includedMaintenances).toStrictEqual([1]);
-
- store.getState().actions.set('includedMaintenances', [1, 2]);
- expect(store.getState().value.includedMaintenances).toStrictEqual([1, 2]);
- });
- });
-
- describe('층수', () => {
- it('4. 을 입력해도 입력상태가 지워지지 않는다.', () => {
- store.getState().actions.onChange(makeEvent('floor', '4.'));
-
- expect(store.getState().rawValue.floor).toBe('4.');
- });
- });
-
- describe('평수', () => {
- it('4를 입력할 수 있다.', () => {
- store.getState().actions.onChange(makeEvent('size', '4'));
-
- expect(store.getState().rawValue.size).toBe('4');
- expect(store.getState().value.size).toBe(4);
- expect(store.getState().errorMessage.size).toBe('');
- });
-
- it('4. 을 입력해도 입력상태가 지워지지 않는다.', () => {
- store.getState().actions.onChange(makeEvent('size', '4.'));
-
- expect(store.getState().rawValue.size).toBe('4.');
- expect(store.getState().errorMessage.size).toBe('');
- });
-
- it('4.1 입력시 정상 입력된다.', () => {
- store.getState().actions.onChange(makeEvent('size', '4.1'));
-
- expect(store.getState().rawValue.size).toBe('4.1');
- expect(store.getState().value.size).toBe(4.1);
- expect(store.getState().errorMessage.size).toBe('');
- });
-
- it('문자를 입력했을 때, 에러메시지가 발생하고 입력되지 않는다.', () => {
- store.getState().actions.onChange(makeEvent('size', '4.1'));
- store.getState().actions.onChange(makeEvent('size', '4.1e'));
-
- expect(store.getState().rawValue.size).toBe('4.1');
- expect(store.getState().value.size).toBe(4.1);
- expect(store.getState().errorMessage.size).not.toBe('');
- });
- });
-});
diff --git a/frontend/src/store/checklistRoomInfoStore.ts b/frontend/src/store/checklistRoomInfoStore.ts
deleted file mode 100644
index 936a493be..000000000
--- a/frontend/src/store/checklistRoomInfoStore.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import { roomFloorLevels, roomOccupancyPeriods } from '@/constants/roomInfo';
-import createFormStore, { FormSpec } from '@/store/createFormStore';
-import { RoomInfo } from '@/types/room';
-import { objectMap } from '@/utils/typeFunctions';
-import {
- inRangeValidator,
- isIntegerValidator,
- isNumericValidator,
- lengthValidator,
- nonNegativeValidator,
- positiveValidator,
-} from '@/utils/validators';
-
-const formSpec: FormSpec = {
- roomName: { initialValue: '', type: 'string', validators: [lengthValidator(20)] },
- deposit: { initialValue: '', type: 'number', validators: [isNumericValidator, nonNegativeValidator] },
- rent: { initialValue: '', type: 'number', validators: [isNumericValidator, nonNegativeValidator] },
- maintenanceFee: { initialValue: '', type: 'number', validators: [isNumericValidator, nonNegativeValidator] },
- includedMaintenances: { initialValue: '', type: 'number[]', validators: [] },
- contractTerm: { initialValue: '', type: 'number', validators: [isNumericValidator, nonNegativeValidator] },
- type: { initialValue: '', type: 'string', validators: [] },
- size: { initialValue: '', type: 'number', validators: [isNumericValidator] },
- floor: { initialValue: '', type: 'number', validators: [isIntegerValidator, positiveValidator] },
- floorLevel: { initialValue: roomFloorLevels[0], type: 'string', validators: [] },
- structure: { initialValue: 'NONE', type: 'string', validators: [] },
- realEstate: { initialValue: '', type: 'string', validators: [] },
- occupancyMonth: {
- initialValue: `${new Date().getMonth() + 1}`,
- type: 'number',
- validators: [isIntegerValidator, positiveValidator, inRangeValidator(1, 12)],
- },
- occupancyPeriod: { initialValue: roomOccupancyPeriods[0], type: 'string', validators: [] },
- summary: { initialValue: '', type: 'string', validators: [] },
- memo: { initialValue: '', type: 'string', validators: [] },
-};
-
-export const initialRoomInfo = objectMap(formSpec, ([key, val]) => [key, val.initialValue]);
-
-const checklistRoomInfoStore = createFormStore(formSpec, 'roomInfoForm');
-
-export default checklistRoomInfoStore;
diff --git a/frontend/src/store/createFormStore.ts b/frontend/src/store/createFormStore.ts
deleted file mode 100644
index 7e5008701..000000000
--- a/frontend/src/store/createFormStore.ts
+++ /dev/null
@@ -1,153 +0,0 @@
-import { createStore } from 'zustand';
-import { persist } from 'zustand/middleware';
-
-import { InputChangeEvent } from '@/types/event';
-import { objectMap } from '@/utils/typeFunctions';
-import { AllString } from '@/utils/utilityTypes';
-import { validation } from '@/utils/validation';
-import { Validator } from '@/utils/validators';
-
-const initialErrorMessages = (initial: Partial) =>
- Object.fromEntries(Object.entries(initial).map(([key]) => [key, ''])) as AllString;
-
-interface FormAction {
- onChange: (event: InputChangeEvent) => void;
- set: (name: keyof T, value: V) => void;
- setValueForced: (name: string, value: V) => void;
- setAll: (state: Partial>) => void;
- resetAll: () => void;
- _reset: (name: keyof T) => void;
- _update: (name: keyof AllString, value: V) => void;
- _updateErrorMsg: (field: keyof T, value: string) => void;
- _updateAfterValidation: (field: keyof T, value: string, validators: Validator[]) => void;
- _transform: (name: string, value: V) => void;
-}
-
-type FormState = { rawValue: Partial; value: Partial; errorMessage: AllString };
-
-export interface FormFieldSpec {
- initialValue: string;
- type: 'string' | 'number' | 'number[]';
- validators: Validator[];
-}
-
-export type FormSpec = {
- [k in keyof T as string]: FormFieldSpec;
-};
-
-const getInitialRaw = (formSpec: FormSpec) =>
- objectMap(formSpec, ([name, { initialValue }]) => [name, initialValue]) as Partial;
-const getValueType = (formSpec: FormSpec) =>
- objectMap(formSpec, ([name, { type }]) => [name, type]) as Partial>;
-const getValidationSet = (formSpec: FormSpec) =>
- objectMap(formSpec, ([name, { validators }]) => [name, validators]) as Record;
-
-const createFormStore = (formSpec: FormSpec, storageName: string) =>
- createStore<
- FormState & {
- actions: FormAction;
- }
- >()(
- persist(
- (set, get) => ({
- rawValue: getInitialRaw(formSpec),
-
- value: transformAll(getInitialRaw(formSpec), getValueType(formSpec)),
-
- errorMessage: initialErrorMessages(getInitialRaw(formSpec)),
-
- actions: {
- onChange: event => get().actions.set(event.target.name as keyof T, event.target.value),
-
- set: (name: keyof T, value: V) => {
- if (value === '') {
- get().actions._reset(name);
- return;
- }
-
- // 타입에 따라 다르게 처리
- if (Array.isArray(value)) {
- get().actions._update(name, value);
- } else if (typeof value === 'number') {
- get().actions._updateAfterValidation(name, value.toString(), getValidationSet(formSpec)[name]);
- } else {
- get().actions._updateAfterValidation(name, value as string, getValidationSet(formSpec)[name]);
- }
- },
-
- setValueForced: (name, value) => set({ value: { ...get().value, [name]: value } }),
-
- resetAll: () =>
- set({
- rawValue: getInitialRaw(formSpec),
- value: transformAll(getInitialRaw(formSpec), getValueType(formSpec)),
- errorMessage: initialErrorMessages(getInitialRaw(formSpec)),
- }),
-
- setAll: set,
-
- _reset: name => {
- get().actions._updateErrorMsg(name, '');
- get().actions._update(name, '');
- },
-
- _update: (name, value) => {
- set({ rawValue: { ...get().rawValue, [name]: value } });
- get().actions._transform(name as string, value ?? '');
- },
-
- _updateErrorMsg: (name, value) => set({ errorMessage: { ...get().errorMessage, [name]: value } }),
-
- _updateAfterValidation: (name, value, validators) => {
- validation(
- name as string,
- value,
- validators,
- (name: string, value: string) => {
- get().actions._update(name as keyof AllString, value);
- },
- (name: string, errorMessage: string) => {
- get().actions._updateErrorMsg(name as keyof T, errorMessage);
- },
- );
- },
-
- _transform: (name, value) =>
- set({
- value: {
- ...get().value,
- [name]: getValueType(formSpec)[name as keyof T] === 'number' ? Number(value) : value,
- },
- }),
- },
- }),
- {
- name: storageName,
- partialize: state => ({
- rawValue: state.rawValue,
- value: state.value,
- errorMessage: state.errorMessage,
- // actions는 저장하지 않음
- }),
- },
- ),
- );
-
-export default createFormStore;
-
-const transformAll = (rawValues: Partial, valueType: Partial>) =>
- objectMap(rawValues, ([key, value]) => {
- const valueTypeForKey = valueType[key as keyof T];
- if (valueTypeForKey === 'number') {
- return [key, typeof value === 'number' ? value : Number(value)];
- }
-
- if (valueTypeForKey === 'number[]') {
- try {
- return [key, Array.isArray(value) ? value : JSON.parse(value as string)];
- } catch (e) {
- return [key, []];
- }
- }
- return [key, value];
- }) as Partial;
diff --git a/frontend/src/store/roomInfoStore.ts b/frontend/src/store/roomInfoStore.ts
new file mode 100644
index 000000000..cad495fad
--- /dev/null
+++ b/frontend/src/store/roomInfoStore.ts
@@ -0,0 +1,106 @@
+import { createStore } from 'zustand';
+import { persist } from 'zustand/middleware';
+
+import { roomFloorLevels, roomOccupancyPeriods } from '@/constants/roomInfo';
+import { parseRoomInfo } from '@/hooks/useRoomInfoValidated';
+import { RoomInfo } from '@/types/room';
+import { Nullable } from '@/utils/utilityTypes';
+
+import { mapObjUndefinedToNull, objectMap } from '../utils/typeFunctions';
+
+type NumberToString = T extends number | string ? string : T;
+
+type RoomInfoStoreState = Required>;
+
+/** roomInfo 자료를 모두 담는 스토어입니다.
+ * rawValues: roomInfo 백엔드 스키마에 해당하는 자료를 모두 담을 수 있습니다. (다만 number 타입자료형만은 string으로 저장하고있습니다.)
+ * errorMessage: 기본값은 ''(에러없음) 입니다.
+ *
+ * 현재 구현상으로는 폼과 무관해서 안쓰더라도 errorMessage를 만들어줘야합니다.
+ */
+export const initialRoomInfo = {
+ roomName: { rawValue: '', errorMessage: '' },
+ deposit: { rawValue: '', errorMessage: '' },
+ rent: { rawValue: '', errorMessage: '' },
+ maintenanceFee: { rawValue: '', errorMessage: '' },
+ contractTerm: { rawValue: '', errorMessage: '' },
+ type: { rawValue: '', errorMessage: '' },
+ size: { rawValue: '', errorMessage: '' },
+ floor: { rawValue: '', errorMessage: '' },
+ floorLevel: { rawValue: roomFloorLevels[0], errorMessage: '' },
+ structure: { rawValue: '', errorMessage: '' },
+ realEstate: { rawValue: '', errorMessage: '' },
+ occupancyMonth: { rawValue: `${new Date().getMonth() + 1}`, errorMessage: '' },
+ occupancyPeriod: { rawValue: roomOccupancyPeriods[0], errorMessage: '' },
+ summary: { rawValue: '', errorMessage: '' },
+ memo: { rawValue: '', errorMessage: '' },
+
+ buildingName: { rawValue: '', errorMessage: '' },
+ station: { rawValue: '', errorMessage: '' },
+ walkingTime: { rawValue: '', errorMessage: '' },
+ address: { rawValue: '', errorMessage: '' },
+ includedMaintenances: { rawValue: [], errorMessage: '' },
+};
+
+export type oneItem = { rawValue: string; errorMessage: string };
+export type RawValues = { [k in keyof RoomInfoStoreState]: NumberToString };
+export type RoomInfoState = {
+ [k in keyof RoomInfoStoreState]: { rawValue: NumberToString; errorMessage: string };
+};
+
+interface RoomInfoActions {
+ set: (o: Partial) => void;
+ get: () => RoomInfoState;
+ reset: () => void;
+ setRawValues: (rawValues: Partial) => void;
+ getRawValues: () => RawValues;
+ getParsedValues: () => RoomInfo;
+}
+
+/**
+ * getParsedValues: store에 저장된 걸 백엔드에 POST하는 등 데이터가 필요할때 사용합니다.
+ * getRawValues: errorMessages말고 rawValues들만을 담은 객체를 반환합니다.
+ */
+export const roomInfoStore = createStore()(
+ persist(
+ (set, get) => ({
+ ...initialRoomInfo,
+ actions: {
+ set,
+ get: () => {
+ const { actions: _, ...state } = get();
+ return state;
+ },
+ setRawValues: (rawValues: Partial) => {
+ set({ ...objectMap(rawValues, ([key, value]) => [key, { rawValue: value, errorMessage: '' }]) });
+ },
+ getRawValues: () => {
+ const state = { ...get().actions.get() };
+ return objectMap(state, ([key, value]) => [key, value.rawValue]) as RawValues;
+ },
+ getParsedValues: () => {
+ const state = { ...get().actions.getRawValues() };
+ return objectMap(state, ([key, value]) => [
+ key,
+ typeof value === 'string' ? parseRoomInfo(key as keyof RoomInfo, value) : value,
+ ]) as RoomInfo;
+ },
+ reset: () => set({ ...initialRoomInfo }),
+ },
+ }),
+ {
+ name: 'roomInfo',
+ partialize: state => {
+ const { actions: _, ...roomInfo } = state;
+ return { ...roomInfo };
+ },
+ },
+ ),
+);
+
+export const roomInfoApiMapper = (values: Partial) => {
+ const result = { ...values, structure: values.structure === '' ? undefined : '' };
+ return mapObjUndefinedToNull(result) as Nullable>;
+};
+
+export default roomInfoStore;
diff --git a/frontend/src/types/checklist.ts b/frontend/src/types/checklist.ts
index ffeddabce..fa54d2b10 100644
--- a/frontend/src/types/checklist.ts
+++ b/frontend/src/types/checklist.ts
@@ -59,7 +59,7 @@ export interface ChecklistPreview {
export interface ChecklistInfo {
checklistId: number;
isLiked: boolean;
- room: RoomInfo;
+ room: Partial;
options: Option[];
categories: ChecklistCategoryWithAnswer[];
stations: SubwayStation[];
diff --git a/frontend/src/types/room.ts b/frontend/src/types/room.ts
index dcad91a45..6bb8a0b13 100644
--- a/frontend/src/types/room.ts
+++ b/frontend/src/types/room.ts
@@ -25,4 +25,5 @@ export type RoomInfo = Partial<{
buildingName: string;
includedMaintenances: number[]; // 관리비 포함항목
}>;
+
export type RoomInfoName = keyof RoomInfo;
diff --git a/frontend/src/utils/typeFunctions.ts b/frontend/src/utils/typeFunctions.ts
index 0b41fde8f..58d388984 100644
--- a/frontend/src/utils/typeFunctions.ts
+++ b/frontend/src/utils/typeFunctions.ts
@@ -1,20 +1,26 @@
-export const mapObjNullToUndefined = (obj: T) => {
+export const mapObjNullToUndefined = (obj: Record) => {
return objectMap(obj, ([key, value]) => [key, value ?? undefined]);
};
-export const mapObjUndefinedToNull = (obj: T) => {
+export const mapObjUndefinedToNull = (obj: Partial>) => {
return objectMap(obj, ([key, value]) => [key, value ?? null]);
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
-export const objectMap = (obj: object, func: ([key, value]: [string, any]) => any) =>
- Object.fromEntries(Object.entries(obj).map(func));
+export const objectMap = (
+ obj: Record,
+ func: ([key, value]: [PropertyKey, Value]) => [PropertyKey, Return],
+) => Object.fromEntries(Object.entries(obj).map(func));
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const objectFilter = (obj: object, func: ([key, value]: [string, any]) => boolean) =>
Object.fromEntries(Object.entries(obj).filter(func));
-export const objectMapToString = (obj: T) =>
+interface ToStringable {
+ toString(): string;
+}
+
+export const objectMapToString = >(obj: T) =>
objectMap(obj, ([key, value]) => [key, value.toString()]);
export const objectOmit = (obj: T, omit: Set) => objectFilter(obj, ([key]) => !omit.has(key));
diff --git a/frontend/src/utils/utilityTypes.ts b/frontend/src/utils/utilityTypes.ts
index 00e80e0f1..5e1d66952 100644
--- a/frontend/src/utils/utilityTypes.ts
+++ b/frontend/src/utils/utilityTypes.ts
@@ -1,5 +1,5 @@
export type Nullable = {
- [P in keyof T]?: T[P] | null;
+ [P in keyof T]: T[P] | null;
};
export type AllString = {