Skip to content

Commit

Permalink
feat: implement concert record form
Browse files Browse the repository at this point in the history
  • Loading branch information
nxnaxx committed Jan 7, 2025
1 parent 6dd8ccf commit df29f53
Show file tree
Hide file tree
Showing 12 changed files with 174 additions and 25 deletions.
6 changes: 6 additions & 0 deletions src/api/concertRecord.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { endPoint } from 'constants/endPoint';
import { tokenAxios } from 'utils';

export const requestPostConcertRecord = async (formData: FormData) => {
return await tokenAxios.post(`${endPoint.CREATE_CONCERT_RECORD}`, formData);
};
1 change: 1 addition & 0 deletions src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from './surveyApi';
export * from './rentalFormApi';
export * from './searchApi';
export * from './userApi';
export * from './concertRecord';
7 changes: 7 additions & 0 deletions src/constants/endPoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,11 @@ export const endPoint = {
GET_CONCERT_SEARCH_ALL: '/search/concert/list/all',
SEARCH_ARTISTS: '/artists/search',
SEARCH_POPULAR_KEYWORD: '/search/popular',

// Concert Record API
GET_CONCERT_RECORD_LIST: '/diaries/list',
GET_CONCERT_RECORD_DETAILS: (diaryId: string) => `/diaries/${diaryId}`,
CREATE_CONCERT_RECORD: '/diaries',
UPDATE_CONCERT_RECORD: '/diaries',
DELETE_CONCERT_RECORD: (diaryId: string) => `/diaries/${diaryId}`,
};
45 changes: 32 additions & 13 deletions src/pages/createConcertRecord/CreateConcertRecord.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,26 @@
import styled from '@emotion/styled';
import { zodResolver } from '@hookform/resolvers/zod';
import { motion } from 'framer-motion';
import { useEffect, useState } from 'react';
import { useState } from 'react';
import { Controller, FormProvider, useForm } from 'react-hook-form';
import { LuCalendar } from 'react-icons/lu';
import { TbChevronDown } from 'react-icons/tb';

import ConcertTime from './components/ConcertTime';
import RecordImageField from './components/RecordImageField';
import RecordSubmitDialog from './components/RecordSubmitDialog';

import BaseButton from 'components/buttons/BaseButton';
import SearchConcertItem from 'components/items/SearchConcertItem';
import SearchField from 'components/searchField/SearchField';
import DateSheet from 'components/sheets/DateSheet';
import SearchConcertSheet from 'components/sheets/SearchConcertSheet';
import { CONCERT_RECORD_PLACEHOLDER } from 'constants/placeholder';
import { concertRecordSchema, type ConcertRecordSchemaType } from 'schemas/concertRecordSchema';
import { useModalStore } from 'stores';
import { BodyMediumText, BodyRegularText } from 'styles/Typography';
import type { ConcertData } from 'types';
import type { ConcertRecord } from 'types/concertRecord';

interface FieldStyleProps {
isError: boolean;
Expand Down Expand Up @@ -52,7 +56,7 @@ const DateSelect = styled.div`
gap: 0.8rem;
width: 100%;
height: 4rem;
padding: 0 1.2rem 0 1.6rem;
padding: 0 1.2rem;
border-radius: 4px;
background-color: ${({ theme }) => theme.colors.dark[500]};
cursor: pointer;
Expand Down Expand Up @@ -144,9 +148,25 @@ const CreateConcertRecord = () => {
const { openModal } = useModalStore(['openModal']);
const [concertData, setConcertData] = useState<ConcertData | null>(null);

const methods = useForm();
const methods = useForm<ConcertRecordSchemaType>({
resolver: zodResolver(concertRecordSchema),
defaultValues: {
concertId: 0,
date: '',
episode: '',
content: '',
seatName: '',
images: [],
},
});

const { control, setValue, watch } = methods;
const {
control,
setValue,
watch,
handleSubmit,
formState: { isValid },
} = methods;

const handleConcertSelect = (concertData: ConcertData) => {
setConcertData(concertData);
Expand All @@ -157,13 +177,13 @@ const CreateConcertRecord = () => {
setValue('date', date);
};

useEffect(() => {
console.log(watch('image'));
}, [watch]);
const onSubmit = (recordData: ConcertRecord) => {
openModal('dialog', 'confirm', <RecordSubmitDialog recordData={recordData} />);
};

return (
<FormProvider {...methods}>
<ConcertRecordForm>
<ConcertRecordForm onSubmit={handleSubmit(onSubmit)}>
<FormFieldContainer>
<FormFieldLabel>언제 보셨나요?</FormFieldLabel>
<Controller
Expand All @@ -180,7 +200,7 @@ const CreateConcertRecord = () => {
}
>
<LuCalendar size={20} />
<DateSelectValue isValid={field.value}>
<DateSelectValue isValid={field.value !== ''}>
{field.value || CONCERT_RECORD_PLACEHOLDER.date}
</DateSelectValue>
<DropdownIcon size={24} />
Expand Down Expand Up @@ -254,17 +274,16 @@ const CreateConcertRecord = () => {
</FormFieldContainer>
<FormFieldContainer>
<FormFieldLabel>사진 첨부 (선택)</FormFieldLabel>
<RecordImageField />
<RecordImageField onUploadImages={(images) => setValue('images', images)} />
</FormFieldContainer>
</ActiveContent>
)}
<ButtonWrapper>
<BaseButton
color="primary"
isDisabled={false}
onClick={() => {}}
isDisabled={!isValid}
size="medium"
type="button"
type="submit"
variant="fill"
>
등록
Expand Down
10 changes: 4 additions & 6 deletions src/pages/createConcertRecord/components/RecordImageField.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import styled from '@emotion/styled';
import { useEffect, useState } from 'react';
import { useFormContext } from 'react-hook-form';
import { LuCamera } from 'react-icons/lu';
import { TbX } from 'react-icons/tb';

Expand All @@ -9,13 +8,12 @@ import { hexToRgba } from 'utils';

interface ImageFieldProps {
initialImages?: string[];
onUploadImages?: (images: File[]) => void;
}

const MAX_IMAGES = 5;

const RecordImageField = ({ initialImages = [] }: ImageFieldProps) => {
const { setValue } = useFormContext();

const RecordImageField = ({ initialImages = [], onUploadImages }: ImageFieldProps) => {
// 모든 이미지의 미리보기
const [previewImages, setPreviewImages] = useState<string[]>([]);
// 새로 추가된 파일들
Expand Down Expand Up @@ -49,7 +47,7 @@ const RecordImageField = ({ initialImages = [] }: ImageFieldProps) => {
setNewImageFiles((prev) => [...prev, ...files]);
setPreviewImages((prev) => [...prev, ...encodedImages]);

setValue('image', [...newImageFiles, ...files]);
onUploadImages?.([...newImageFiles, ...files]);
};

const handleImageDelete = (index: number) => {
Expand All @@ -63,7 +61,7 @@ const RecordImageField = ({ initialImages = [] }: ImageFieldProps) => {
// 새로 추가된 이미지 삭제
const newImageIndex = index - initialImages.length;
setNewImageFiles((prev) => prev.filter((_, i) => i !== newImageIndex));
setValue('image', [...newImageFiles.filter((_, i) => i !== newImageIndex)]);
onUploadImages?.([...newImageFiles.filter((_, i) => i !== newImageIndex)]);
}

setPreviewImages((prev) => prev.filter((_, i) => i !== index));
Expand Down
65 changes: 65 additions & 0 deletions src/pages/createConcertRecord/components/RecordSubmitDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import styled from '@emotion/styled';
import { useNavigate } from 'react-router-dom';

import BaseButton from 'components/buttons/BaseButton';
import Dialog from 'components/dialog/Dialog';
import { usePostConcertRecord } from 'queries/concertRecord';
import { useModalStore } from 'stores';
import { TitleText2 } from 'styles/Typography';
import type { ConcertRecord } from 'types';

interface RecordSubmitDialogProps {
recordData: ConcertRecord;
}

const DialogContainer = styled.div`
position: absolute;
z-index: 1004;
`;

const RecordSubmitDialog = ({ recordData }: RecordSubmitDialogProps) => {
const { closeModal } = useModalStore(['closeModal']);
const { mutate } = usePostConcertRecord();
const navigate = useNavigate();

const handleSubmitSuccess = () => {
navigate('/concert-record');
closeModal('dialog', 'confirm');
};

const handleApplyClick = () => {
mutate(recordData, { onSuccess: handleSubmitSuccess });
};

return (
<DialogContainer>
<Dialog>
<Dialog.Content>
<TitleText2>공연 기록을 등록하시겠습니까?</TitleText2>
</Dialog.Content>
<Dialog.Button>
<BaseButton
color="primary"
isFullWidth={false}
onClick={() => closeModal('dialog', 'confirm')}
size="small"
variant="outline"
>
취소
</BaseButton>
<BaseButton
color="primary"
isFullWidth={false}
onClick={handleApplyClick}
size="small"
variant="fill"
>
등록
</BaseButton>
</Dialog.Button>
</Dialog>
</DialogContainer>
);
};

export default RecordSubmitDialog;
1 change: 1 addition & 0 deletions src/queries/concertRecord/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './usePostConcertRecord';
31 changes: 31 additions & 0 deletions src/queries/concertRecord/usePostConcertRecord.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';

import { requestPostConcertRecord } from 'api';
import type { ConcertRecord } from 'types';

const postFormData = async (recordFormData: ConcertRecord) => {
const formData = new FormData();
const { images, ...rest } = recordFormData;

images.forEach((file) => {
formData.append('images', file);
});

formData.append('request', JSON.stringify(rest));

return await requestPostConcertRecord(formData);
};

export const usePostConcertRecord = () => {
const queryClient = useQueryClient();

return useMutation({
mutationFn: postFormData,
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ['recordList'] });
},
onError: (err) => {
console.log('폼 전송 오류: ', err);
},
});
};
24 changes: 24 additions & 0 deletions src/schemas/concertRecordSchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { z } from 'zod';

const requiredString = () => z.string().min(1, '필수 입력 항목입니다.');
const requiredNumber = () => z.number().positive('필수 입력 항목입니다.');
const requiredFiles = () =>
z
.array(
z.instanceof(File).refine((file) => file instanceof File, {
message: '파일을 업로드해야 합니다.',
})
)
.optional()
.default([]);

export const concertRecordSchema = z.object({
concertId: requiredNumber(),
date: requiredString(),
episode: requiredString(),
content: requiredString(),
seatName: requiredString(),
images: requiredFiles(),
});

export type ConcertRecordSchemaType = z.infer<typeof concertRecordSchema>;
2 changes: 1 addition & 1 deletion src/types/concertRecord.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ export interface ConcertRecord {
episode: string;
content: string;
seatName: string;
images: string[];
images: File[] | [];
}
1 change: 1 addition & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export * from './user';
export * from './api';
export * from './filter';
export * from './search';
export * from './concertRecord';
6 changes: 1 addition & 5 deletions src/types/rental.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { ApiResponse } from './api';
import type { Region } from './filter';
import type { RefundAccount } from './user';

// Rental List
export interface RentalList {
Expand Down Expand Up @@ -46,11 +47,6 @@ export interface BoardingDates {
isApplied?: boolean;
}

export interface RefundAccount {
bank: string;
number: string;
}

export interface RentalDetail {
concertName: string;
imageUrl: string;
Expand Down

0 comments on commit df29f53

Please sign in to comment.