Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

여행 정보(이름, 날짜, 도시, 설명, 대표사진) 수정 모달 구현 #132

Merged
merged 20 commits into from
Jul 27, 2023
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
ede2e1d
feat:여행정보 수정 모달 마크업
Dahyeeee Jul 20, 2023
3a2a277
feat:여행정보 수정 서버로 put하는 로직 작성
Dahyeeee Jul 20, 2023
ee623e2
refactor: 필수입력 마크들어가도록 수정, 모달 불필요한 padding삭제
Dahyeeee Jul 21, 2023
cd68319
Merge branch 'develop' of https://github.com/woowacourse-teams/2023-h…
Dahyeeee Jul 24, 2023
7385eb5
feat:여행정보 수정 가능 기능
Dahyeeee Jul 24, 2023
d7cfd54
feat:서버로 수정한 정보 보내는 기능
Dahyeeee Jul 24, 2023
238ecd2
feat:기간이 기존값보다 작을 때 경고 출력기능
Dahyeeee Jul 24, 2023
0ffc2f6
refactor: rename 함수
Dahyeeee Jul 24, 2023
c401fce
refactor:기간 줄어드는 경고 삭제
Dahyeeee Jul 25, 2023
93f2955
refactor: 수정모달 기간 조정에 관한 경고문구 삽입
Dahyeeee Jul 25, 2023
245a795
refactor:도시 미선택시 경고문구 출력
Dahyeeee Jul 25, 2023
4c3ce0a
Merge branch 'develop' of https://github.com/woowacourse-teams/2023-h…
Dahyeeee Jul 25, 2023
a282296
refactor:코드 컨밴션에 맞게 수정
Dahyeeee Jul 25, 2023
15803f4
refactor:여행정보수정모달 css변경
Dahyeeee Jul 25, 2023
5999579
Merge branch 'develop' of https://github.com/woowacourse-teams/2023-h…
Dahyeeee Jul 26, 2023
54fa0bb
refactor:여행수정 폼 제출할 때 validate하도록 리팩토링
Dahyeeee Jul 26, 2023
26603f8
refactor: city불러오는 위치 App으로 변경
Dahyeeee Jul 26, 2023
e26542d
refactor:사용자입력값 업데이트 로직 커스텀훅내부로 이동
Dahyeeee Jul 26, 2023
50f65a5
refactor:구조분해할당 프롭스에서 바로 하도록 수정
Dahyeeee Jul 27, 2023
af8c2ad
refactor: 시안에 맞게 디자인 수정
Dahyeeee Jul 27, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions frontend/src/api/trip/putTrip.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { END_POINTS } from '@constants/api';
import { TripPutData } from '@type/trip';
Dahyeeee marked this conversation as resolved.
Show resolved Hide resolved

import { axiosInstance } from '@api/axiosInstance';

export interface PutTripParams extends TripPutData {
tripId: number;
}

export const putTrip =
() =>
({ tripId, ...tripInformation }: PutTripParams) => {
return axiosInstance.put<TripPutData>(END_POINTS.TRIP(tripId), {
...tripInformation,
});
};
13 changes: 7 additions & 6 deletions frontend/src/components/common/CitySearchBar/CitySearchBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,19 @@ import {
import CitySuggestion from '@components/common/CitySuggestion/CitySuggestion';

interface CitySearchBarProps {
initialCityTags?: CityData[];
setCityData: (cities: CityData[]) => void;
initialCities?: CityData[];
updateCityInfo: (cities: CityData[]) => void;
required?: boolean;
}

const CitySearchBar = ({ initialCityTags, setCityData }: CitySearchBarProps) => {
const CitySearchBar = ({ initialCities, updateCityInfo, required = false }: CitySearchBarProps) => {
const [queryWord, setQueryWord] = useState('');
const { cityTags, addCityTag, deleteCityTag } = useCityTags(initialCityTags ?? []);
const { cityTags, addCityTag, deleteCityTag } = useCityTags(initialCities ?? []);
const { isOpen: isSuggestionOpen, open: openSuggestion, close: closeSuggestion } = useOverlay();
const inputRef = useRef<HTMLInputElement>(null);

useEffect(() => {
setCityData(cityTags);
updateCityInfo(cityTags);
}, [cityTags]);

const handleInputChange = (event: FormEvent<HTMLInputElement>) => {
Expand Down Expand Up @@ -88,7 +89,7 @@ const CitySearchBar = ({ initialCityTags, setCityData }: CitySearchBarProps) =>
return (
Dahyeeee marked this conversation as resolved.
Show resolved Hide resolved
<Menu closeMenu={closeSuggestion}>
<div css={containerStyling} onClick={focusInput}>
<Label>방문 도시</Label>
<Label required={required}>방문 도시</Label>
<div css={wrapperStyling}>
<SearchPinIcon aria-label="지도표시 아이콘" css={searchPinIconStyling} />
<div css={tagListStyling}>
Expand Down
10 changes: 6 additions & 4 deletions frontend/src/components/common/DateInput/DateInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,21 @@ import {

interface DateInputProps {
initialDateRange?: DateRangeData;
setDateData: (dateRange: DateRangeData) => void;
updateDateInfo: (dateRange: DateRangeData) => void;
required?: boolean;
}

const DateInput = ({
initialDateRange = { start: null, end: null },
setDateData,
updateDateInfo,
required = false,
}: DateInputProps) => {
const [inputValue, setInputValue] = useState(dateRangeToString(initialDateRange));
const [selectedDateRange, setSelectedDateRange] = useState(initialDateRange);
const { isOpen: isCalendarOpen, close: closeCalendar, toggle: toggleCalendar } = useOverlay();

useEffect(() => {
setDateData(selectedDateRange);
updateDateInfo(selectedDateRange);
}, [selectedDateRange]);

const handleDateClick = (dateRange: DateRangeData) => {
Expand All @@ -37,7 +39,7 @@ const DateInput = ({

return (
<Flex styles={{ direction: 'column', width: '400px', margin: '0 auto', align: 'flex-start' }}>
<Label>방문 기간</Label>
<Label required={required}>방문 기간</Label>
<Menu closeMenu={closeCalendar} css={containerStyling}>
<Box onClick={toggleCalendar} css={getInputStyling(isCalendarOpen)}>
<Input
Expand Down
70 changes: 38 additions & 32 deletions frontend/src/components/common/TripInformation/TripInformation.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import DefaultThumbnail from '@assets/png/trip-information_default-thumbnail.png';
import type { TripData } from '@type/trip';
import { Badge, Box, Button, Flex, Heading, Text, Theme } from 'hang-log-design-system';
import { Badge, Box, Button, Flex, Heading, Text, Theme, useOverlay } from 'hang-log-design-system';

import { formatDate } from '@utils/formatter';

Expand All @@ -12,42 +12,48 @@ import {
sectionStyling,
titleStyling,
} from '@components/common/TripInformation/TripInformation.style';
import TripInfoEditModal from '@components/trip/TripInfoEditModal/TripInfoEditModal';

type TripInformationProps = Omit<TripData, 'dayLogs'>;

const TripInformation = ({ ...information }: TripInformationProps) => {
const { isOpen: isEditModalOpen, close: closeEditModal, open: openEditModal } = useOverlay();

return (
<section css={sectionStyling}>
<Box css={imageWrapperStyling}>
<div />
<img src={information.imageUrl ?? DefaultThumbnail} alt="여행 대표 이미지" />
</Box>
<Box>
<Flex styles={{ gap: Theme.spacer.spacing1 }}>
{information.cities.map(({ id, name }) => (
<Badge key={id}>{name}</Badge>
))}
</Flex>
<Heading css={titleStyling} size="large">
{information.title}
</Heading>
<Text>
{formatDate(information.startDate)} - {formatDate(information.endDate)}
</Text>
<Text css={descriptionStyling} size="small">
{information.description}
</Text>
</Box>
<Box css={buttonContainerStyling}>
{/* 수정 모드일 때만 보인다 */}
<Button css={editButtonStyling} variant="outline" size="small">
여행 정보 수정
</Button>
<Button variant="primary" size="small">
저장
</Button>
</Box>
</section>
<>
<section css={sectionStyling}>
<Box css={imageWrapperStyling}>
<div />
<img src={information.imageUrl ?? DefaultThumbnail} alt="여행 대표 이미지" />
</Box>
<Box>
<Flex styles={{ gap: Theme.spacer.spacing1 }}>
{information.cities.map(({ id, name }) => (
<Badge key={id}>{name}</Badge>
))}
</Flex>
<Heading css={titleStyling} size="large">
{information.title}
</Heading>
<Text>
{formatDate(information.startDate)} - {formatDate(information.endDate)}
</Text>
<Text css={descriptionStyling} size="small">
{information.description}
</Text>
</Box>
<Box css={buttonContainerStyling}>
{/* 수정 모드일 때만 보인다 */}
<Button onClick={openEditModal} css={editButtonStyling} variant="outline" size="small">
여행 정보 수정
</Button>
<Button variant="primary" size="small">
저장
</Button>
</Box>
</section>
<TripInfoEditModal isOpen={isEditModalOpen} onClose={closeEditModal} {...information} />
</>
);
};

Expand Down
16 changes: 8 additions & 8 deletions frontend/src/components/newTrip/NewTripForm/NewTripForm.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
import { useCityDateForm } from '@/hooks/common/useCityDateForm';
Dahyeeee marked this conversation as resolved.
Show resolved Hide resolved
import { PATH } from '@constants/path';
import { Button } from 'hang-log-design-system';
import type { FormEvent } from 'react';
import { useNavigate } from 'react-router-dom';

import { useNewTripMutation } from '@hooks/api/useNewTripMutation';
import { useNewTripForm } from '@hooks/newTrip/useNewTripForm';

import CitySearchBar from '@components/common/CitySearchBar/CitySearchBar';
import DateInput from '@components/common/DateInput/DateInput';
import { formStyling } from '@components/newTrip/NewTripForm/NewTripForm.style';

const NewTripForm = () => {
const { newTripData, setCityData, setDateData, isAllInputFilled } = useNewTripForm();
const TripAddForm = () => {
Dahyeeee marked this conversation as resolved.
Show resolved Hide resolved
const { cityDateInfo, updateCityInfo, updateDateInfo, isCityDateValid } = useCityDateForm();
const newTripMutation = useNewTripMutation();
const navigate = useNavigate();

const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();

newTripMutation.mutate(newTripData, {
newTripMutation.mutate(cityDateInfo, {
onSuccess: goToTripEditPageWithId,
});
};
Expand All @@ -30,13 +30,13 @@ const NewTripForm = () => {

return (
<form css={formStyling} onSubmit={handleSubmit}>
<CitySearchBar setCityData={setCityData} />
<DateInput setDateData={setDateData} />
<Button variant="primary" disabled={!isAllInputFilled}>
<CitySearchBar updateCityInfo={updateCityInfo} />
<DateInput updateDateInfo={updateDateInfo} />
<Button variant="primary" disabled={!isCityDateValid}>
기록하기
</Button>
</form>
);
};

export default NewTripForm;
export default TripAddForm;
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { css } from '@emotion/react';
import { Theme } from 'hang-log-design-system';

export const formStyling = css({
display: 'flex',
flexDirection: 'column',
gap: Theme.spacer.spacing3,

'> button': {
width: '400px',
},
});

export const cityInputErrorTextStyling = css({
lineHeight: 0,
color: Theme.color.red200,
});

export const dateInputTextStyling = css({
maxWidth: '400px',
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { useTripInfoForm } from '@/hooks/trip/useTripInfoForm';
Dahyeeee marked this conversation as resolved.
Show resolved Hide resolved
import type { TripData, TripPutData } from '@type/trip';
import {
Button,
Flex,
Dahyeeee marked this conversation as resolved.
Show resolved Hide resolved
ImageUploadInput,
Input,
Modal,
SupportingText,
} from 'hang-log-design-system';
import type { ChangeEvent, FormEvent } from 'react';
Dahyeeee marked this conversation as resolved.
Show resolved Hide resolved

import CitySearchBar from '@components/common/CitySearchBar/CitySearchBar';
import DateInput from '@components/common/DateInput/DateInput';
import {
cityInputErrorTextStyling,
dateInputTextStyling,
formStyling,
} from '@components/trip/TripInfoEditModal/TripInfoEditModal.style';

interface TripInfoEditModalProps extends Omit<TripData, 'dayLogs'> {
isOpen: boolean;
onClose: () => void;
}

const TripInfoEditModal = ({ isOpen, onClose, ...information }: TripInfoEditModalProps) => {
const {
tripInfo,
isCityInputError,
updateInputValue,
updateCityInfo,
updateDateInfo,
handleSubmit,
} = useTripInfoForm(information, onClose);

const handleChangeValue = (key: keyof TripPutData) => (e: ChangeEvent<HTMLInputElement>) => {
ashleysyheo marked this conversation as resolved.
Show resolved Hide resolved
updateInputValue(key, e.currentTarget.value);
};

return (
<Modal isOpen={isOpen} closeModal={onClose} hasCloseButton>
<form onSubmit={handleSubmit} css={formStyling}>
Dahyeeee marked this conversation as resolved.
Show resolved Hide resolved
<CitySearchBar
required
initialCities={information.cities}
updateCityInfo={updateCityInfo}
/>
{isCityInputError && (
<SupportingText css={cityInputErrorTextStyling}>
ashleysyheo marked this conversation as resolved.
Show resolved Hide resolved
방문 도시는 최소 한개 이상 선택해야 합니다
</SupportingText>
)}
<DateInput
required
initialDateRange={{ start: tripInfo.startDate, end: tripInfo.endDate }}
updateDateInfo={updateDateInfo}
/>
<SupportingText css={dateInputTextStyling}>
⚠︎ 방문 기간을 단축하면 마지막 날짜부터 작성한 기록들이 <br />
Dahyeeee marked this conversation as resolved.
Show resolved Hide resolved
삭제됩니다.
</SupportingText>
<Input
label="여행 제목"
value={tripInfo.title}
required
onChange={handleChangeValue('title')}
/>
<Input
Dahyeeee marked this conversation as resolved.
Show resolved Hide resolved
label="여행 설명"
value={tripInfo.description ?? ''}
onChange={handleChangeValue('description')}
/>
<ImageUploadInput
label="대표 이미지 업로드"
imageUrls={tripInfo.imageUrl === null ? null : [tripInfo.imageUrl]}
imageAltText="여행 대표 이미지 업로드"
Dahyeeee marked this conversation as resolved.
Show resolved Hide resolved
maxUploadCount={1}
onRemove={() => {}}
/>
<Button variant="primary" type="submit">
Dahyeeee marked this conversation as resolved.
Show resolved Hide resolved
여행 정보 수정
</Button>
</form>
</Modal>
);
};

export default TripInfoEditModal;
19 changes: 19 additions & 0 deletions frontend/src/hooks/api/useEditTripMutation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';

import { putTrip } from '@api/trip/putTrip';

export const useEditTripMutation = () => {
Dahyeeee marked this conversation as resolved.
Show resolved Hide resolved
const queryClient = useQueryClient();

const tripMutation = useMutation(putTrip(), {
Dahyeeee marked this conversation as resolved.
Show resolved Hide resolved
onSuccess: () => {
// 여행 정보 수정 성공시 Trip 정보 재요청
queryClient.invalidateQueries({ queryKey: ['trip'] });
},
onError: (err, _, context) => {
Dahyeeee marked this conversation as resolved.
Show resolved Hide resolved
alert('오류가 발생했습니다. 잠시 후 다시 시도해주세요.');
},
});

return tripMutation;
};
3 changes: 1 addition & 2 deletions frontend/src/hooks/api/useNewTripMutation.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { postNewTrip } from '@/api/trips/postNewTrip';
Dahyeeee marked this conversation as resolved.
Show resolved Hide resolved
import { NETWORK } from '@constants/api';
import { useMutation } from '@tanstack/react-query';

import { postNewTrip } from '@api/trips/newTrip';

export const useNewTripMutation = () => {
const newTripMutation = useMutation(postNewTrip(), {
onError: (err, _, context) => {
Dahyeeee marked this conversation as resolved.
Show resolved Hide resolved
Dahyeeee marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
38 changes: 38 additions & 0 deletions frontend/src/hooks/common/useCityDateForm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { CityData } from '@type/city';
import type { DateRangeData, NewTripData } from '@type/trips';
import { useEffect, useState } from 'react';

const defaultTripData = {
startDate: null,
endDate: null,
cityIds: [],
};

export const useCityDateForm = (initialTripData?: NewTripData) => {
const [cityDateInfo, setCityDateInfo] = useState<NewTripData>(initialTripData ?? defaultTripData);
const [isCityDateValid, setIsCityDateValid] = useState(false);

useEffect(() => {
validateInputs();
}, [cityDateInfo]);

const updateCityInfo = (cities: CityData[]) => {
const cityIds = cities.map((city) => city.id);

setCityDateInfo((prev) => ({ ...prev, cityIds }));
};
Dahyeeee marked this conversation as resolved.
Show resolved Hide resolved

const updateDateInfo = (dateRange: DateRangeData) => {
const { start: startDate, end: endDate } = dateRange;

setCityDateInfo((prev) => ({ ...prev, startDate, endDate }));
};

const validateInputs = () => {
const { startDate, endDate, cityIds } = cityDateInfo;

setIsCityDateValid(!!startDate && !!endDate && !!cityIds.length);
};
Dahyeeee marked this conversation as resolved.
Show resolved Hide resolved

return { cityDateInfo, updateCityInfo, updateDateInfo, isCityDateValid };
};
Loading