From d254ea43c70d98c6fa0125bee1a7f1b2efcdaabe Mon Sep 17 00:00:00 2001 From: kikiyeom Date: Tue, 5 Mar 2024 11:17:35 +0900 Subject: [PATCH] =?UTF-8?q?Feat:=20=EC=8A=A4=EC=BC=80=EC=A4=84=EC=97=90=20?= =?UTF-8?q?=EC=9E=A5=EC=86=8C=EA=B0=80=20=ED=8F=AC=ED=95=A8=EB=90=9C?= =?UTF-8?q?=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...stcode-npm-2.0.3-f480b4f904-72283bd2c3.zip | Bin 0 -> 6323 bytes package.json | 1 + public/index.html | 1 + .../ScheduleTemplate.component.tsx | 66 +++++++++++++++++- .../ScheduleTemplate.styled.ts | 30 +++++++- .../ScheduleInfoList.component.tsx | 14 +++- .../common/RadioButton/RadioButton.styled.ts | 2 +- .../ScheduleDetail/ScheduleDetail.page.tsx | 2 + src/types/dto/schedule.ts | 14 ++++ src/utils/schedule.ts | 28 +++++++- yarn.lock | 8 +++ 11 files changed, 158 insertions(+), 8 deletions(-) create mode 100644 .yarn/cache/@types-daum-postcode-npm-2.0.3-f480b4f904-72283bd2c3.zip diff --git a/.yarn/cache/@types-daum-postcode-npm-2.0.3-f480b4f904-72283bd2c3.zip b/.yarn/cache/@types-daum-postcode-npm-2.0.3-f480b4f904-72283bd2c3.zip new file mode 100644 index 0000000000000000000000000000000000000000..66715a552121886b2e0291b0ae4a7127ade96a4f GIT binary patch literal 6323 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 literal 0 HcmV?d00001 diff --git a/package.json b/package.json index 280f4e70..18914cd6 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "@testing-library/react": "^12.1.2", "@testing-library/react-hooks": "^7.0.2", "@testing-library/user-event": "^13.5.0", + "@types/daum-postcode": "^2.0.3", "@types/editorjs__header": "^2.6.0", "@types/jest": "^27.4.0", "@types/lodash-es": "^4.17.6", diff --git a/public/index.html b/public/index.html index 25433363..5b9493d7 100644 --- a/public/index.html +++ b/public/index.html @@ -40,6 +40,7 @@ /> +
diff --git a/src/components/Schedule/ScheduleTemplate/ScheduleTemplate.component.tsx b/src/components/Schedule/ScheduleTemplate/ScheduleTemplate.component.tsx index 45b7104c..e61862c2 100644 --- a/src/components/Schedule/ScheduleTemplate/ScheduleTemplate.component.tsx +++ b/src/components/Schedule/ScheduleTemplate/ScheduleTemplate.component.tsx @@ -1,7 +1,7 @@ import React, { useMemo } from 'react'; import { useFieldArray, useFormContext } from 'react-hook-form'; import { useRecoilValue } from 'recoil'; -import { DatePickerField, InputField, SelectField } from '@/components'; +import { Button, DatePickerField, InputField, RadioButtonField, SelectField } from '@/components'; import { InputSize } from '@/components/common/Input/Input.component'; import * as Styled from './ScheduleTemplate.styled'; import { $generations } from '@/store'; @@ -9,7 +9,7 @@ import { SelectOption } from '@/components/common/Select/Select.component'; import { SessionTemplate } from '../SessionTemplate'; import Plus from '@/assets/svg/plus-20.svg'; import { EventCreateRequest } from '@/types'; -import { ScheduleFormValues } from '@/utils'; +import { LocationType, ScheduleFormValues } from '@/utils'; const DEFAULT_SESSION: EventCreateRequest = { startedAt: '', @@ -19,9 +19,12 @@ const DEFAULT_SESSION: EventCreateRequest = { }; const ScheduleTemplate = () => { - const { register, control, formState, getValues } = useFormContext(); + const { register, control, formState, getValues, watch, setValue } = + useFormContext(); const generations = useRecoilValue($generations); + const locationType = watch('locationType'); + const { fields, append, remove } = useFieldArray({ name: 'sessions', control, @@ -37,6 +40,31 @@ const ScheduleTemplate = () => { const defaultOption = generationOptions.find( (option) => option.value === getValues('generationNumber')?.toString(), ); + const handleClickAddressSearch = () => { + new window.daum.Postcode({ + async oncomplete(data: { address: string }) { + const res = await fetch( + `https://dapi.kakao.com/v2/local/search/address?query=${data.address}`, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: 'KakaoAK cc4af66dc10aa1a20830f3cc62c40a87', + }, + }, + ); + const json = await res.json(); + const roadAddress = json.documents[0].road_address; + setValue('locationInfo', { + address: roadAddress.address_name, + latitude: roadAddress.y, + longitude: roadAddress.x, + placeName: roadAddress.building_name ?? roadAddress.address_name, + }); + setValue('placeName', roadAddress.building_name); + }, + }).open(); + }; return ( <> @@ -66,6 +94,38 @@ const ScheduleTemplate = () => { defaultDate={getValues('date')} {...register('date', { required: true })} /> +
+ + 장소 + + + + + + + {locationType === LocationType.OFFLINE && ( + + + + + )} +
세션 정보 diff --git a/src/components/Schedule/ScheduleTemplate/ScheduleTemplate.styled.ts b/src/components/Schedule/ScheduleTemplate/ScheduleTemplate.styled.ts index 85092199..4dd16bc6 100644 --- a/src/components/Schedule/ScheduleTemplate/ScheduleTemplate.styled.ts +++ b/src/components/Schedule/ScheduleTemplate/ScheduleTemplate.styled.ts @@ -6,7 +6,6 @@ export const ScheduleContent = styled.div` display: flex; flex-direction: column; gap: 2rem; - height: 36.9rem; padding: 2.4rem; background-color: ${theme.colors.white}; border: 0.1rem solid ${theme.colors.gray30}; @@ -43,3 +42,32 @@ export const AddButton = styled.button` margin-top: 2.4rem; background-color: transparent; `; + +export const InputLabel = styled.label` + ${({ theme }) => css` + ${theme.fonts.medium15} + display: flex; + margin-bottom: 0.6rem; + color: ${theme.colors.gray70}; + `} +`; + +export const RequiredDot = styled.span` + width: 0.6rem; + min-width: 0.6rem; + height: 0.6rem; + margin: 0.8rem 0 0 0.6rem; + background-color: #eb6963; + border-radius: 50%; +`; + +export const RadioButtonGroup = styled.div` + display: flex; + gap: 2rem; + margin-bottom: 1rem; +`; + +export const InputWithButton = styled.div` + display: flex; + gap: 1rem; +`; diff --git a/src/components/ScheduleDetail/ScheduleInfoList/ScheduleInfoList.component.tsx b/src/components/ScheduleDetail/ScheduleInfoList/ScheduleInfoList.component.tsx index b49e2aad..37dd9dd4 100644 --- a/src/components/ScheduleDetail/ScheduleInfoList/ScheduleInfoList.component.tsx +++ b/src/components/ScheduleDetail/ScheduleInfoList/ScheduleInfoList.component.tsx @@ -11,6 +11,10 @@ interface ScheduleInfoListProps { startedAt: string; publishedAt?: string; status: ValueOf; + location: { + address: string | null; + placeName: string; + }; } const ScheduleInfoList = ({ @@ -20,6 +24,7 @@ const ScheduleInfoList = ({ createdAt, publishedAt, status, + location, }: ScheduleInfoListProps) => { const scheduleInfoListItem = useMemo(() => { return [ @@ -39,6 +44,13 @@ const ScheduleInfoList = ({ label: '등록 일시', value: formatDate(createdAt, 'YYYY년 M월 D일 A hh시 mm분'), }, + { + label: '장소', + value: + location.address === null + ? location.placeName + : `${location.placeName}, ${location.address}`, + }, { label: '배포 일시', value: formatDate(publishedAt, 'YYYY년 M월 D일 A hh시 mm분'), @@ -48,7 +60,7 @@ const ScheduleInfoList = ({ value: getScheduleStatusText(status), }, ]; - }, [createdAt, generationNumber, name, publishedAt, startedAt, status]); + }, [createdAt, generationNumber, name, publishedAt, startedAt, status, location]); return ( diff --git a/src/components/common/RadioButton/RadioButton.styled.ts b/src/components/common/RadioButton/RadioButton.styled.ts index 1ea0ad75..ee44d722 100644 --- a/src/components/common/RadioButton/RadioButton.styled.ts +++ b/src/components/common/RadioButton/RadioButton.styled.ts @@ -67,6 +67,6 @@ export const RadioButtonMark = styled.span` export const RadioButtonText = styled.span` ${({ theme }) => css` ${theme.fonts.medium14} - padding-left: 3.8rem; + padding-left: 3rem; `} `; diff --git a/src/pages/ScheduleDetail/ScheduleDetail.page.tsx b/src/pages/ScheduleDetail/ScheduleDetail.page.tsx index e131dd3c..647cb07f 100644 --- a/src/pages/ScheduleDetail/ScheduleDetail.page.tsx +++ b/src/pages/ScheduleDetail/ScheduleDetail.page.tsx @@ -29,6 +29,7 @@ const ScheduleDetail = () => { publishedAt, status, eventList: sessionList, + location, } = useRecoilValue($scheduleDetail({ scheduleId: scheduleId ?? '' })); const isPublished = status === ScheduleStatus.PUBLIC; @@ -147,6 +148,7 @@ const ScheduleDetail = () => { createdAt={createdAt} publishedAt={publishedAt} status={status} + location={location} /> diff --git a/src/types/dto/schedule.ts b/src/types/dto/schedule.ts index 4fd79c8e..4bb70b4e 100644 --- a/src/types/dto/schedule.ts +++ b/src/types/dto/schedule.ts @@ -31,6 +31,10 @@ export interface ScheduleCreateRequest { name: string; startedAt: string; eventsCreateRequests: EventCreateRequest[]; + address?: string; + latitude?: number; + longitude?: number; + placeName?: string; } export interface ScheduleUpdateRequest { @@ -39,6 +43,10 @@ export interface ScheduleUpdateRequest { name: string; startedAt: string; eventsCreateRequests: EventCreateRequest[]; + address?: string; + latitude?: number; + longitude?: number; + placeName?: string; } export interface ScheduleResponse { @@ -51,6 +59,12 @@ export interface ScheduleResponse { publishedAt?: string; eventList: Session[]; status: ValueOf; + location: { + address: string | null; + latitude: number | null; + longitude: number | null; + placeName: string; + }; } export interface QRCodeRequest { diff --git a/src/utils/schedule.ts b/src/utils/schedule.ts index 399c45d0..caaa7a89 100644 --- a/src/utils/schedule.ts +++ b/src/utils/schedule.ts @@ -9,11 +9,24 @@ import { } from '@/types'; import { formatDate, toUtcFormat } from '.'; +export const LocationType = { + OFFLINE: 'offline', + ONLINE: 'online', +} as const; + export interface ScheduleFormValues { name: string; generationNumber: number; date: Dayjs; sessions: EventCreateRequest[]; + locationType: ValueOf; + placeName?: string; + locationInfo?: { + address: string; + latitude: string; + longitude: string; + placeName: string; + }; } export const getScheduleStatusText = (status: ValueOf) => { @@ -30,7 +43,7 @@ export const getScheduleStatusText = (status: ValueOf) => export const parseScheduleResponseToFormValues = ( response: ScheduleResponse, ): ScheduleFormValues => { - const { name, generationNumber, startedAt, eventList } = response; + const { name, generationNumber, startedAt, eventList, location } = response; const date: Dayjs = dayjs(startedAt, 'YYYY-MM-DD').startOf('day'); @@ -45,18 +58,22 @@ export const parseScheduleResponseToFormValues = ( })), })); + const isOffline = !!location.address; + return { name, generationNumber, date, sessions, + locationType: isOffline ? LocationType.OFFLINE : LocationType.ONLINE, + placeName: isOffline ? location.placeName : undefined, }; }; export const parseFormValuesToScheduleRequest = ( formValues: ScheduleFormValues, ): ScheduleCreateRequest | ScheduleUpdateRequest => { - const { generationNumber, date, sessions, name } = formValues; + const { generationNumber, date, sessions, name, locationType, locationInfo } = formValues; const formattedDate = date.format('YYYY-MM-DD'); @@ -81,5 +98,12 @@ export const parseFormValuesToScheduleRequest = ( eventsCreateRequests, }; + if (locationType === LocationType.OFFLINE && locationInfo) { + scheduleRequest.address = locationInfo.address; + scheduleRequest.latitude = Number(locationInfo.latitude); + scheduleRequest.longitude = Number(locationInfo.longitude); + scheduleRequest.placeName = locationInfo.placeName; + } + return scheduleRequest; }; diff --git a/yarn.lock b/yarn.lock index 289cee3c..9cdd74b6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3921,6 +3921,13 @@ __metadata: languageName: node linkType: hard +"@types/daum-postcode@npm:^2.0.3": + version: 2.0.3 + resolution: "@types/daum-postcode@npm:2.0.3" + checksum: 72283bd2c3bb17073eb1324089e2bc5f5ff6df89c8731e1b9123620328cca0f7b77ac6480a06b5e35c0436c652782af376a896af5044416e2dfb809d040ee728 + languageName: node + linkType: hard + "@types/editorjs__header@npm:^2.6.0": version: 2.6.0 resolution: "@types/editorjs__header@npm:2.6.0" @@ -12951,6 +12958,7 @@ __metadata: "@testing-library/react": ^12.1.2 "@testing-library/react-hooks": ^7.0.2 "@testing-library/user-event": ^13.5.0 + "@types/daum-postcode": ^2.0.3 "@types/editorjs__header": ^2.6.0 "@types/jest": ^27.4.0 "@types/lodash-es": ^4.17.6