diff --git a/package-lock.json b/package-lock.json index 7affbb9a..e549826a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "@storybook/test": "^8.3.5", "@tanstack/react-query": "^5.59.16", "@tanstack/react-query-next-experimental": "^5.59.19", + "@vercel/speed-insights": "^1.1.0", "dayjs": "^1.11.13", "embla-carousel-react": "^7.1.0", "framer-motion": "^11.11.9", @@ -27,6 +28,7 @@ "react": "^18", "react-dom": "^18", "react-hook-form": "^7.53.1", + "react-hook-form-persist": "^3.0.0", "react-intersection-observer": "^9.13.1", "react-toastify": "^10.0.6", "zustand": "^5.0.1" @@ -4890,6 +4892,40 @@ "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", "dev": true }, + "node_modules/@vercel/speed-insights": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@vercel/speed-insights/-/speed-insights-1.1.0.tgz", + "integrity": "sha512-rAXxuhhO4mlRGC9noa5F7HLMtGg8YF1zAN6Pjd1Ny4pII4cerhtwSG4vympbCl+pWkH7nBS9kVXRD4FAn54dlg==", + "hasInstallScript": true, + "peerDependencies": { + "@sveltejs/kit": "^1 || ^2", + "next": ">= 13", + "react": "^18 || ^19 || ^19.0.0-rc", + "svelte": ">= 4", + "vue": "^3", + "vue-router": "^4" + }, + "peerDependenciesMeta": { + "@sveltejs/kit": { + "optional": true + }, + "next": { + "optional": true + }, + "react": { + "optional": true + }, + "svelte": { + "optional": true + }, + "vue": { + "optional": true + }, + "vue-router": { + "optional": true + } + } + }, "node_modules/@vitest/expect": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.0.5.tgz", @@ -12480,6 +12516,15 @@ "react": "^16.8.0 || ^17 || ^18 || ^19" } }, + "node_modules/react-hook-form-persist": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/react-hook-form-persist/-/react-hook-form-persist-3.0.0.tgz", + "integrity": "sha512-6nwW65JyFpBem9RjLYAWvIFxOLoCk0E13iB9e5yeF5jeHlwx1ua0M77FvwhPpD8eaCz7hG4ziCdOxRcnJVUSxQ==", + "peerDependencies": { + "react": ">= 16.3", + "react-hook-form": ">= 6" + } + }, "node_modules/react-intersection-observer": { "version": "9.13.1", "resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.13.1.tgz", diff --git a/package.json b/package.json index 565c46af..1765cdb4 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@storybook/test": "^8.3.5", "@tanstack/react-query": "^5.59.16", "@tanstack/react-query-next-experimental": "^5.59.19", + "@vercel/speed-insights": "^1.1.0", "dayjs": "^1.11.13", "embla-carousel-react": "^7.1.0", "framer-motion": "^11.11.9", @@ -34,6 +35,7 @@ "react": "^18", "react-dom": "^18", "react-hook-form": "^7.53.1", + "react-hook-form-persist": "^3.0.0", "react-intersection-observer": "^9.13.1", "react-toastify": "^10.0.6", "zustand": "^5.0.1" diff --git a/src/_apis/crew/crew.ts b/src/_apis/crew/crew.ts index f8fb8d20..169faa8c 100644 --- a/src/_apis/crew/crew.ts +++ b/src/_apis/crew/crew.ts @@ -1,19 +1,21 @@ import { fetchApi } from '@/src/utils/api'; -import { CreateCrewRequestTypes, CreateCrewResponseTypes } from '@/src/types/create-crew'; +import { + CreateCrewRequestTypes, + CreateCrewResponseTypes, + EditCrewRequestTypes, + EditCrewResponseTypes, +} from '@/src/types/create-crew'; export async function createCrew(data: CreateCrewRequestTypes) { try { - const response: { data: CreateCrewResponseTypes; status: number } = await fetchApi( - `/api/crews`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - credentials: 'include', // Include authentication credentials - body: JSON.stringify(data), + const response: { data: CreateCrewResponseTypes } = await fetchApi(`/api/crews`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', }, - ); + credentials: 'include', // Include authentication credentials + body: JSON.stringify(data), + }); if (!response.data) { throw new Error('Failed to create crew: No data received'); } @@ -23,3 +25,18 @@ export async function createCrew(data: CreateCrewRequestTypes) { throw error; } } + +export async function editCrew(id: number, data: EditCrewRequestTypes) { + try { + await fetchApi(`/api/crews/${id}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include', // Include authentication credentials + body: JSON.stringify(data), + }); + } catch (error) { + throw error; + } +} diff --git a/src/_queries/crew/crew-detail-queries.ts b/src/_queries/crew/crew-detail-queries.ts index e203945e..f357186a 100644 --- a/src/_queries/crew/crew-detail-queries.ts +++ b/src/_queries/crew/crew-detail-queries.ts @@ -1,5 +1,9 @@ -import { useQuery } from '@tanstack/react-query'; +import { toast } from 'react-toastify'; +import { useRouter } from 'next/navigation'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { createCrew, editCrew } from '@/src/_apis/crew/crew'; import { getCrewDetail } from '@/src/_apis/crew/crew-detail-apis'; +import { CreateCrewRequestTypes, EditCrewRequestTypes } from '@/src/types/create-crew'; export function useGetCrewDetailQuery(id: number) { return useQuery({ @@ -7,3 +11,43 @@ export function useGetCrewDetailQuery(id: number) { queryFn: () => getCrewDetail(id), }); } + +export function useCreateCrewQuery() { + const router = useRouter(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (data: CreateCrewRequestTypes) => createCrew(data), + onSuccess: (data) => { + if (data === null || data === undefined) { + return; + } + queryClient.invalidateQueries({ queryKey: ['crewLists', 'crewDetail'] }); + toast.success('크루가 생성되었습니다.'); + router.push(`/crew/detail/${data.crewId}`); + }, + onError: (error) => { + toast.error(error.message); + }, + }); +} + +export function useEditCrewQuery(id: number) { + const router = useRouter(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (data: EditCrewRequestTypes) => editCrew(id, data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['crewDetail'] }); + toast.success('크루 정보가 수정되었습니다.'); + if (router) { + router.push(`/crew/detail/${id}`); + } + }, + onError: (error) => { + toast.error(error.message); + }, + retry: false, + }); +} diff --git a/src/app/_components/category/category-container/index.tsx b/src/app/(crew)/_components/category/category-container/index.tsx similarity index 86% rename from src/app/_components/category/category-container/index.tsx rename to src/app/(crew)/_components/category/category-container/index.tsx index 67c1ed3d..d257d18e 100644 --- a/src/app/_components/category/category-container/index.tsx +++ b/src/app/(crew)/_components/category/category-container/index.tsx @@ -1,7 +1,7 @@ import { useState } from 'react'; import category from '@/src/data/category.json'; -import InternalCategory from '@/src/app/_components/category/internal-category'; -import MainCategory from '@/src/app/_components/category/main-category'; +import InternalCategory from '@/src/app/(crew)/_components/category/internal-category'; +import MainCategory from '@/src/app/(crew)/_components/category/main-category'; export interface CategoryContainerProps { mainCategory: string; diff --git a/src/app/_components/category/internal-category/index.tsx b/src/app/(crew)/_components/category/internal-category/index.tsx similarity index 100% rename from src/app/_components/category/internal-category/index.tsx rename to src/app/(crew)/_components/category/internal-category/index.tsx diff --git a/src/app/_components/category/internal-category/internal-category.stories.tsx b/src/app/(crew)/_components/category/internal-category/internal-category.stories.tsx similarity index 100% rename from src/app/_components/category/internal-category/internal-category.stories.tsx rename to src/app/(crew)/_components/category/internal-category/internal-category.stories.tsx diff --git a/src/app/_components/category/main-category/index.tsx b/src/app/(crew)/_components/category/main-category/index.tsx similarity index 100% rename from src/app/_components/category/main-category/index.tsx rename to src/app/(crew)/_components/category/main-category/index.tsx diff --git a/src/app/_components/hero/hero-crew.tsx b/src/app/(crew)/_components/hero/hero-crew.tsx similarity index 100% rename from src/app/_components/hero/hero-crew.tsx rename to src/app/(crew)/_components/hero/hero-crew.tsx diff --git a/src/app/(crew)/crew/_components/create-crew-form/create-crew-form.stories.tsx b/src/app/(crew)/crew/create/_components/create-crew-form/create-crew-form.stories.tsx similarity index 93% rename from src/app/(crew)/crew/_components/create-crew-form/create-crew-form.stories.tsx rename to src/app/(crew)/crew/create/_components/create-crew-form/create-crew-form.stories.tsx index fb8cefe9..780a30a9 100644 --- a/src/app/(crew)/crew/_components/create-crew-form/create-crew-form.stories.tsx +++ b/src/app/(crew)/crew/create/_components/create-crew-form/create-crew-form.stories.tsx @@ -31,7 +31,7 @@ export default { } as Meta; const Template: StoryFn = function CreateCrewPageStory() { - return ; + return ; }; export const Default = Template.bind({}); diff --git a/src/app/(crew)/crew/_components/create-crew-form/index.tsx b/src/app/(crew)/crew/create/_components/create-crew-form/index.tsx similarity index 82% rename from src/app/(crew)/crew/_components/create-crew-form/index.tsx rename to src/app/(crew)/crew/create/_components/create-crew-form/index.tsx index a55d0ee3..d7a85b08 100644 --- a/src/app/(crew)/crew/_components/create-crew-form/index.tsx +++ b/src/app/(crew)/crew/create/_components/create-crew-form/index.tsx @@ -2,8 +2,10 @@ import { useEffect, useState } from 'react'; import { Controller, useForm, useWatch } from 'react-hook-form'; +import useFormPersist from 'react-hook-form-persist'; import { useRouter } from 'next/navigation'; import { NumberInput } from '@mantine/core'; +import { getImageUrl } from '@/src/_apis/image/get-image-url'; import categoryData from '@/src/data/category.json'; import regionData from '@/src/data/region.json'; import Button from '@/src/components/common/input/button'; @@ -11,18 +13,20 @@ import DropDown from '@/src/components/common/input/drop-down'; import FileInputWrap from '@/src/components/common/input/file-input-wrap'; import TextInput from '@/src/components/common/input/text-input'; import Textarea from '@/src/components/common/input/textarea'; -import { CreateCrewFormTypes } from '@/src/types/create-crew'; +import { CreateCrewFormTypes, EditCrewResponseTypes } from '@/src/types/create-crew'; import ImgCrewSampleUrls from '@/public/assets/images/crew-sample'; export interface CreateCrewFormProps { - data: CreateCrewFormTypes; + type: 'create' | 'edit'; + data: CreateCrewFormTypes | EditCrewResponseTypes; isEdit?: boolean; onEdit?: (data: CreateCrewFormTypes) => void; onSubmit?: (data: CreateCrewFormTypes) => void; } export default function CreateCrewForm({ - isEdit = false, + type, + isEdit, onEdit = () => {}, onSubmit = () => {}, data, @@ -34,20 +38,40 @@ export default function CreateCrewForm({ setValue, trigger, clearErrors, - formState: { errors, isValid, isSubmitting }, + watch, + formState: { errors, isValid, isSubmitting, isSubmitSuccessful }, } = useForm({ defaultValues: data, mode: 'onBlur', }); + useFormPersist(type === 'create' ? 'createCrew' : 'editCrew', { + watch, + setValue, + storage: typeof window !== 'undefined' ? window.localStorage : undefined, + }); + const [categoryIndex, setCategoryIndex] = useState(0); const [regionIndex, setRegionIndex] = useState(0); const title = useWatch({ control, name: 'title' }); const mainCategory = useWatch({ control, name: 'mainCategory' }); const mainLocation = useWatch({ control, name: 'mainLocation' }); + const subLocation = useWatch({ control, name: 'subLocation' }); const introduce = useWatch({ control, name: 'introduce' }); + const setInitialValues = () => { + setValue('title', ''); + setValue('mainCategory', ''); + setValue('subCategory', ''); + setValue('imageUrl', ''); + setValue('mainLocation', ''); + setValue('subLocation', ''); + setValue('totalCount', 4); + setValue('introduce', ''); + localStorage.removeItem('createCrew'); + }; + const handleMainCategoryChange = (newValue: string | null) => { setValue('mainCategory', newValue || ''); setValue('subCategory', null); @@ -59,13 +83,36 @@ export default function CreateCrewForm({ setValue('subLocation', null); clearErrors('subLocation'); }; + + const handleFileChange = async ( + file: File | string | null, + onChange: (value: string | File) => void, + ) => { + if (file instanceof File) { + const imgResponse = await getImageUrl(file, 'CREW'); + onChange(imgResponse?.imageUrl || ''); + } + }; + + const handleClear = () => { + setInitialValues(); + router.back(); + }; + useEffect(() => { setCategoryIndex(categoryData.findIndex((category) => category.title.label === mainCategory)); setRegionIndex(regionData.findIndex((region) => region.main.label === mainLocation)); - }, [mainCategory, mainLocation]); + + if (isEdit && subLocation === '') { + setValue('subLocation', '전체'); + } + if (!isEdit && isSubmitSuccessful) { + setInitialValues(); + } + }, [mainCategory, mainLocation, isSubmitSuccessful]); return ( -
+
@@ -105,7 +152,6 @@ export default function CreateCrewForm({ )} />
-
- {introduce.length}/100 + {introduce?.length}/100
- {isEdit ? '수정' : '확인'} + {type === 'create' ? '만들기' : '수정'}
- +
); } diff --git a/src/app/(crew)/crew/_components/create-gathering-form/create-gathering-form.stories.tsx b/src/app/(crew)/crew/detail/[id]/_components/create-gathering/create-gathering-form/create-gathering-form.stories.tsx similarity index 100% rename from src/app/(crew)/crew/_components/create-gathering-form/create-gathering-form.stories.tsx rename to src/app/(crew)/crew/detail/[id]/_components/create-gathering/create-gathering-form/create-gathering-form.stories.tsx diff --git a/src/app/(crew)/crew/_components/create-gathering-form/index.tsx b/src/app/(crew)/crew/detail/[id]/_components/create-gathering/create-gathering-form/index.tsx similarity index 100% rename from src/app/(crew)/crew/_components/create-gathering-form/index.tsx rename to src/app/(crew)/crew/detail/[id]/_components/create-gathering/create-gathering-form/index.tsx diff --git a/src/app/(crew)/crew/_components/create-gathering-modal/container.tsx b/src/app/(crew)/crew/detail/[id]/_components/create-gathering/create-gathering-modal/container.tsx similarity index 100% rename from src/app/(crew)/crew/_components/create-gathering-modal/container.tsx rename to src/app/(crew)/crew/detail/[id]/_components/create-gathering/create-gathering-modal/container.tsx diff --git a/src/app/(crew)/crew/_components/create-gathering-modal/presenter.tsx b/src/app/(crew)/crew/detail/[id]/_components/create-gathering/create-gathering-modal/presenter.tsx similarity index 90% rename from src/app/(crew)/crew/_components/create-gathering-modal/presenter.tsx rename to src/app/(crew)/crew/detail/[id]/_components/create-gathering/create-gathering-modal/presenter.tsx index 7c5e7b75..71f1ea4e 100644 --- a/src/app/(crew)/crew/_components/create-gathering-modal/presenter.tsx +++ b/src/app/(crew)/crew/detail/[id]/_components/create-gathering/create-gathering-modal/presenter.tsx @@ -1,5 +1,5 @@ import { Modal, ScrollArea } from '@mantine/core'; -import CreateGatheringForm from '@/src/app/(crew)/crew/_components/create-gathering-form'; +import CreateGatheringForm from '@/src/app/(crew)/crew/detail/[id]/_components/create-gathering/create-gathering-form'; import { CreateGatheringFormTypes } from '@/src/types/gathering-data'; export interface GatheringDetailModalProps { diff --git a/src/app/(crew)/crew/detail/[id]/_components/create-gathering.tsx b/src/app/(crew)/crew/detail/[id]/_components/create-gathering/index.tsx similarity index 93% rename from src/app/(crew)/crew/detail/[id]/_components/create-gathering.tsx rename to src/app/(crew)/crew/detail/[id]/_components/create-gathering/index.tsx index dfcec562..53248d9e 100644 --- a/src/app/(crew)/crew/detail/[id]/_components/create-gathering.tsx +++ b/src/app/(crew)/crew/detail/[id]/_components/create-gathering/index.tsx @@ -3,7 +3,7 @@ import { useRouter } from 'next/navigation'; import { useDisclosure } from '@mantine/hooks'; import { useAuthStore } from '@/src/store/use-auth-store'; -import CreateGatheringModalContainer from '@/src/app/(crew)/crew/_components/create-gathering-modal/container'; +import CreateGatheringModalContainer from '@/src/app/(crew)/crew/detail/[id]/_components/create-gathering/create-gathering-modal/container'; import Button from '@/src/components/common/input/button'; import { CreateGatheringFormTypes } from '@/src/types/gathering-data'; diff --git a/src/app/(crew)/crew/_components/gathering-detail-modal/container.tsx b/src/app/(crew)/crew/detail/[id]/_components/gathering-detail-modal/container.tsx similarity index 100% rename from src/app/(crew)/crew/_components/gathering-detail-modal/container.tsx rename to src/app/(crew)/crew/detail/[id]/_components/gathering-detail-modal/container.tsx diff --git a/src/app/(crew)/crew/_components/gathering-detail-modal/gathering-detail-modal.stories.tsx b/src/app/(crew)/crew/detail/[id]/_components/gathering-detail-modal/gathering-detail-modal.stories.tsx similarity index 100% rename from src/app/(crew)/crew/_components/gathering-detail-modal/gathering-detail-modal.stories.tsx rename to src/app/(crew)/crew/detail/[id]/_components/gathering-detail-modal/gathering-detail-modal.stories.tsx diff --git a/src/app/(crew)/crew/_components/gathering-detail-modal/presenter.tsx b/src/app/(crew)/crew/detail/[id]/_components/gathering-detail-modal/presenter.tsx similarity index 100% rename from src/app/(crew)/crew/_components/gathering-detail-modal/presenter.tsx rename to src/app/(crew)/crew/detail/[id]/_components/gathering-detail-modal/presenter.tsx diff --git a/src/app/(crew)/crew/detail/[id]/edit/page.tsx b/src/app/(crew)/crew/detail/[id]/edit/page.tsx index 1ba58730..78348720 100644 --- a/src/app/(crew)/crew/detail/[id]/edit/page.tsx +++ b/src/app/(crew)/crew/detail/[id]/edit/page.tsx @@ -2,34 +2,41 @@ import Image from 'next/image'; import { useParams } from 'next/navigation'; -import { useGetCrewDetailQuery } from '@/src/_queries/crew/crew-detail-queries'; -import CreateCrewForm from '@/src/app/(crew)/crew/_components/create-crew-form'; -import { CreateCrewFormTypes } from '@/src/types/create-crew'; +import { Loader } from '@mantine/core'; +import { getImageUrl } from '@/src/_apis/image/get-image-url'; +import { useEditCrewQuery, useGetCrewDetailQuery } from '@/src/_queries/crew/crew-detail-queries'; +import CreateCrewForm from '@/src/app/(crew)/crew/create/_components/create-crew-form'; +import { CreateCrewFormTypes, EditCrewRequestTypes } from '@/src/types/create-crew'; import IcoCreateCrew from '@/public/assets/icons/ic-create-crew.svg'; export default function EditCrewPage() { const { id } = useParams(); const { data, isLoading, error } = useGetCrewDetailQuery(Number(id)); - + const { isPending, mutate } = useEditCrewQuery(Number(id)); if (data === undefined) return null; - // TODO : 테스트중 - const initialValue: CreateCrewFormTypes = { - title: '', - mainCategory: '', - subCategory: '', - imageUrl: - 'https://crewcrew.s3.ap-northeast-2.amazonaws.com/crew/0e05d971-15a8-4a32-bf03-80d12cae392e', - mainLocation: '', - subLocation: '', - totalCount: 4, - introduce: '', - }; + const handleEdit = async (editedData: CreateCrewFormTypes) => { + const newData: EditCrewRequestTypes = { + title: editedData.title, + mainCategory: editedData.mainCategory, + subCategory: editedData.subCategory ?? '', + imageUrl: (editedData.imageUrl as string) ?? '', + mainLocation: editedData.mainLocation, + subLocation: editedData.subLocation === '전체' ? '' : (editedData.subLocation ?? ''), + totalCount: editedData.totalCount, + introduce: editedData.introduce, + }; - const handleEdit = () => { - // TODO : PATCH API 연결 + mutate(newData); + localStorage.removeItem('editCrew'); }; + if (isLoading || isPending) + return ( +
+ +
+ ); return (
@@ -43,7 +50,7 @@ export default function EditCrewPage() {

크루 수정하기

- +
); } diff --git a/src/app/(crew)/my-crew/hosted/page.tsx b/src/app/(crew)/my-crew/hosted/page.tsx index 8e0fa069..4f0f330c 100644 --- a/src/app/(crew)/my-crew/hosted/page.tsx +++ b/src/app/(crew)/my-crew/hosted/page.tsx @@ -6,7 +6,7 @@ import { useInfiniteScroll } from '@/src/hooks/use-infinite-scroll'; import CrewCardList from '@/src/components/common/crew-list/crew-card-list'; export default function MyCrewHostedPage() { - const { data, status, ref, isFetchingNextPage } = useInfiniteScroll( + const { data, isLoading, error, ref, isFetchingNextPage } = useInfiniteScroll( useGetMyCrewHostedQuery({ pageable: { page: 0, size: 6, sort: ['createdAt,desc'] }, }), @@ -14,14 +14,14 @@ export default function MyCrewHostedPage() { return (
- {status === 'pending' || isFetchingNextPage ? ( + {isLoading || isFetchingNextPage ? (
) : (
)} - {status === 'error' &&

에러가 발생했습니다.

} + {error &&

에러가 발생했습니다.

}
); } diff --git a/src/app/(crew)/my-crew/joined/page.tsx b/src/app/(crew)/my-crew/joined/page.tsx index 4af69e63..e016ecae 100644 --- a/src/app/(crew)/my-crew/joined/page.tsx +++ b/src/app/(crew)/my-crew/joined/page.tsx @@ -6,7 +6,7 @@ import { useInfiniteScroll } from '@/src/hooks/use-infinite-scroll'; import CrewCardList from '@/src/components/common/crew-list/crew-card-list'; export default function MyCrewJoinedPage() { - const { data, status, ref, isFetchingNextPage } = useInfiniteScroll( + const { data, isLoading, error, ref, isFetchingNextPage } = useInfiniteScroll( useGetMyCrewJoinedQuery({ pageable: { page: 0, size: 6, sort: ['createdAt,desc'] }, }), @@ -14,14 +14,14 @@ export default function MyCrewJoinedPage() { return (
- {status === 'pending' || isFetchingNextPage ? ( + {isLoading || isFetchingNextPage ? (
) : (
)} - {status === 'error' &&

에러가 발생했습니다.

} + {error &&

에러가 발생했습니다.

}
); } diff --git a/src/app/(crew)/page.tsx b/src/app/(crew)/page.tsx index 1fd3cbc1..f7bdca76 100644 --- a/src/app/(crew)/page.tsx +++ b/src/app/(crew)/page.tsx @@ -6,8 +6,8 @@ import { Divider, Loader, TextInput } from '@mantine/core'; import { useGetCrewListQuery } from '@/src/_queries/crew/crew-list-queries'; import regionData from '@/src/data/region.json'; import { useInfiniteScroll } from '@/src/hooks/use-infinite-scroll'; -import CategoryContainer from '@/src/app/_components/category/category-container'; -import HeroCrew from '@/src/app/_components/hero/hero-crew'; +import CategoryContainer from '@/src/app/(crew)/_components/category/category-container'; +import HeroCrew from '@/src/app/(crew)/_components/hero/hero-crew'; import CrewCardList from '@/src/components/common/crew-list/crew-card-list'; import Button from '@/src/components/common/input/button'; import DropDown from '@/src/components/common/input/drop-down'; @@ -23,7 +23,6 @@ export default function HomePage() { const handleRegionChange = (newValue: string) => { const selectedRegion = regionData.find((dataItem) => dataItem.main.label === newValue); - if (selectedRegion?.main.label === '지역 전체') return ''; return selectedRegion ? selectedRegion.main.label : ''; }; @@ -35,7 +34,7 @@ export default function HomePage() { } }; - const { data, status, isFetchingNextPage, ref } = useInfiniteScroll( + const { data, isLoading, error, isFetchingNextPage, ref } = useInfiniteScroll( useGetCrewListQuery({ condition: { keyword: search, @@ -133,14 +132,14 @@ export default function HomePage() {
{data && } - {status === 'pending' || isFetchingNextPage ? ( + {isLoading || isFetchingNextPage ? (
) : (
)} - {status === 'error' &&

에러가 발생했습니다.

} + {error &&

에러가 발생했습니다.

}
); diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 30e570db..d2d77a43 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,6 +1,7 @@ import type { Metadata } from 'next'; import { ColorSchemeScript, MantineProvider } from '@mantine/core'; import '@mantine/core/styles.css'; +import { SpeedInsights } from '@vercel/speed-insights/next'; import ClientProvider from '@/src/components/client-provider'; import { pretendard } from '@/src/fonts/pretendard/pretendard'; import '@/src/styles/globals.css'; @@ -23,7 +24,10 @@ export default function RootLayout({ - {children} + + {children} + + diff --git a/src/components/common/crew-list/crew-card-list.stories.tsx b/src/components/common/crew-list/crew-card-list.stories.tsx index a283efd8..386f973f 100644 --- a/src/components/common/crew-list/crew-card-list.stories.tsx +++ b/src/components/common/crew-list/crew-card-list.stories.tsx @@ -28,7 +28,7 @@ export default meta; type Story = StoryObj; function RenderCrewCardList() { - const { data, status, isFetchingNextPage, ref } = useInfiniteScroll( + const { data, isLoading, error, isFetchingNextPage, ref } = useInfiniteScroll( useGetCrewListQuery({ condition: { keyword: '', @@ -45,14 +45,14 @@ function RenderCrewCardList() { return (
{data && } - {status === 'pending' || isFetchingNextPage ? ( + {isLoading || isFetchingNextPage ? (
) : (
)} - {status === 'error' &&

에러가 발생했습니다.

} + {error &&

에러가 발생했습니다.

}
); } diff --git a/src/components/common/gathering-card/container.tsx b/src/components/common/gathering-card/container.tsx index 40505485..29d296b3 100644 --- a/src/components/common/gathering-card/container.tsx +++ b/src/components/common/gathering-card/container.tsx @@ -5,7 +5,7 @@ import { toast } from 'react-toastify'; import { useDisclosure } from '@mantine/hooks'; import { useGetGatheringDetailQuery } from '@/src/_queries/gathering/gathering-detail-queries'; import { ApiError } from '@/src/utils/api'; -import GatheringDetailModalContainer from '@/src/app/(crew)/crew/_components/gathering-detail-modal/container'; +import GatheringDetailModalContainer from '@/src/app/(crew)/crew/detail/[id]/_components/gathering-detail-modal/container'; import { GatheringType } from '@/src/types/gathering-data'; import GatheringCardPresenter from './presenter'; diff --git a/src/components/common/gathering-card/scheduled-gathering-card/container.tsx b/src/components/common/gathering-card/scheduled-gathering-card/container.tsx index fa0fb373..33de86a9 100644 --- a/src/components/common/gathering-card/scheduled-gathering-card/container.tsx +++ b/src/components/common/gathering-card/scheduled-gathering-card/container.tsx @@ -1,5 +1,5 @@ import { useCallback, useState } from 'react'; -import GatheringDetailModalContainer from '@/src/app/(crew)/crew/_components/gathering-detail-modal/container'; +import GatheringDetailModalContainer from '@/src/app/(crew)/crew/detail/[id]/_components/gathering-detail-modal/container'; import ScheduledGatheringCardPresenter from './presenter'; interface ScheduledGatheringCardContainerProps { diff --git a/src/components/common/input/drop-down/index.tsx b/src/components/common/input/drop-down/index.tsx index 3f22cb72..7be4276f 100644 --- a/src/components/common/input/drop-down/index.tsx +++ b/src/components/common/input/drop-down/index.tsx @@ -27,7 +27,7 @@ export default function DropDown({ error, ...rest }: DropDownProps) { - const [currentValue, setCurrentValue] = useState(null); + const [currentValue, setCurrentValue] = useState(value); const [isFocused, setIsFocused] = useState(false); const inputRef = useRef(null); @@ -44,12 +44,12 @@ export default function DropDown({ if (variant === 'default') { return isFocused ? 'focus:bg-black focus:text-white focus:placeholder:text-white' - : 'bg-white text-gray-800 placeholder-gray-800'; + : 'text-gray-800 placeholder-gray-800'; } return isFocused ? 'focus:bg-black focus:text-white focus:placeholder:text-white sort-bg-on pl-10' - : 'bg-white text-gray-800 placeholder-gray-800 sort-bg pl-10'; + : 'text-gray-800 placeholder-gray-800 sort-bg pl-10'; }; const handleChange = (newValue: string | null, option: { value: string; label: string }) => { diff --git a/src/components/common/input/file-input-wrap/file-input/file-input.stories.tsx b/src/components/common/input/file-input-wrap/file-input/file-input.stories.tsx index 2112b0c1..11da99fd 100644 --- a/src/components/common/input/file-input-wrap/file-input/file-input.stories.tsx +++ b/src/components/common/input/file-input-wrap/file-input/file-input.stories.tsx @@ -1,5 +1,4 @@ import { useState } from 'react'; -import { StaticImageData } from 'next/image'; import { action } from '@storybook/addon-actions'; import { Meta, StoryFn } from '@storybook/react'; import FileInput, { FileInputProps } from '.'; @@ -26,7 +25,7 @@ const meta: Meta = { export default meta; const Template: StoryFn = function FileInputStory() { - const [fileValue, setFileValue] = useState(null); + const [fileValue, setFileValue] = useState(null); const [isBlur, setIsBlur] = useState(false); return ( @@ -55,7 +54,7 @@ export const FileInput01 = Template.bind({}); FileInput01.args = { value: null, isBlur: false, - onChange: (newValue: File | StaticImageData | null) => { + onChange: (newValue: File | string | null) => { action('onChange')({ image: newValue }); }, }; diff --git a/src/components/common/input/file-input-wrap/file-input/index.tsx b/src/components/common/input/file-input-wrap/file-input/index.tsx index 0b6fe9fc..a0c1dc66 100644 --- a/src/components/common/input/file-input-wrap/file-input/index.tsx +++ b/src/components/common/input/file-input-wrap/file-input/index.tsx @@ -2,15 +2,23 @@ import { ChangeEvent, useEffect, useRef, useState } from 'react'; import Image, { StaticImageData } from 'next/image'; import IcoPlus from '@/public/assets/icons/ic-plus.svg'; import IcoX from '@/public/assets/icons/ic-x.svg'; +import ImgCrewSampleUrls from '@/public/assets/images/crew-sample'; +import ImgGatheringSampleUrls from '@/public/assets/images/gathering-sample'; export interface FileInputProps { - value: File | StaticImageData | string | null; + value: File | string | null; onChange: (value: File | null) => void; isBlur: boolean; } +const isSample = (value: File | string | null) => { + if (typeof value === 'string') { + return !!(ImgCrewSampleUrls.includes(value) || ImgGatheringSampleUrls.includes(value)); + } + return false; +}; export default function FileInput({ value, isBlur, onChange }: FileInputProps) { - const [preview, setPreview] = useState(typeof value === 'string' ? value : null); + const [preview, setPreview] = useState(isSample(value) ? null : (value as string)); const [fileReader, setFileReader] = useState(null); const fileInput = useRef(null); const timerRef = useRef(null); diff --git a/src/data/region.json b/src/data/region.json index b38ea22d..4a3712a7 100644 --- a/src/data/region.json +++ b/src/data/region.json @@ -1,11 +1,8 @@ [ - { - "main": { "label": "지역 전체", "value": "" }, - "areas": [] - }, { "main": { "label": "서울특별시", "value": "seoul" }, "areas": [ + { "label": "전체", "value": "" }, { "label": "강남구", "value": "gangnam" }, { "label": "강동구", "value": "gangdong" }, { "label": "강북구", "value": "gangbuk" }, @@ -36,6 +33,7 @@ { "main": { "label": "부산광역시", "value": "busan" }, "areas": [ + { "label": "전체", "value": "" }, { "label": "강서구", "value": "gangseo" }, { "label": "금정구", "value": "geumjeong" }, { "label": "남구", "value": "nam" }, @@ -56,6 +54,7 @@ { "main": { "label": "대구광역시", "value": "daegu" }, "areas": [ + { "label": "전체", "value": "" }, { "label": "남구", "value": "nam" }, { "label": "달서구", "value": "dalseo" }, { "label": "달성군", "value": "dalseong" }, @@ -69,6 +68,7 @@ { "main": { "label": "인천광역시", "value": "incheon" }, "areas": [ + { "label": "전체", "value": "" }, { "label": "강화군", "value": "ganghwa" }, { "label": "계양구", "value": "gyeyang" }, { "label": "남동구", "value": "namdong" }, @@ -83,6 +83,7 @@ { "main": { "label": "광주광역시", "value": "gwangju" }, "areas": [ + { "label": "전체", "value": "" }, { "label": "광산구", "value": "gwangsan" }, { "label": "남구", "value": "nam" }, { "label": "동구", "value": "dong" }, @@ -93,6 +94,7 @@ { "main": { "label": "대전광역시", "value": "daejeon" }, "areas": [ + { "label": "전체", "value": "" }, { "label": "대덕구", "value": "daedeok" }, { "label": "동구", "value": "dong" }, { "label": "서구", "value": "seo" }, @@ -103,6 +105,7 @@ { "main": { "label": "울산광역시", "value": "ulsan" }, "areas": [ + { "label": "전체", "value": "" }, { "label": "남구", "value": "nam" }, { "label": "동구", "value": "dong" }, { "label": "북구", "value": "buk" }, @@ -112,11 +115,14 @@ }, { "main": { "label": "세종특별자치시", "value": "sejong" }, - "areas": [] + "areas": [ + { "label": "전체", "value": "" } + ] }, { "main": { "label": "경기도", "value": "gyeonggi" }, "areas": [ + { "label": "전체", "value": "" }, { "label": "가평군", "value": "gapyeong" }, { "label": "고양시", "value": "goyang" }, { "label": "과천시", "value": "gwacheon" }, @@ -145,6 +151,7 @@ { "main": { "label": "강원도", "value": "gangwon" }, "areas": [ + { "label": "전체", "value": "" }, { "label": "강릉시", "value": "gangneung" }, { "label": "고성군", "value": "goseong" }, { "label": "동해시", "value": "donghae" }, @@ -168,6 +175,7 @@ { "main": { "label": "충청북도", "value": "chungbuk" }, "areas": [ + { "label": "전체", "value": "" }, { "label": "괴산군", "value": "goesan" }, { "label": "단양군", "value": "danyang" }, { "label": "보은군", "value": "boeun" }, @@ -184,6 +192,7 @@ { "main": { "label": "충청남도", "value": "chungnam" }, "areas": [ + { "label": "전체", "value": "" }, { "label": "계룡시", "value": "gyeryong" }, { "label": "공주시", "value": "gongju" }, { "label": "금산군", "value": "geumsan" }, @@ -204,6 +213,7 @@ { "main": { "label": "전라북도", "value": "jeonbuk" }, "areas": [ + { "label": "전체", "value": "" }, { "label": "고창군", "value": "gochang" }, { "label": "군산시", "value": "gunsan" }, { "label": "김제시", "value": "gimje" }, @@ -223,6 +233,7 @@ { "main": { "label": "전라남도", "value": "jeonnam" }, "areas": [ + { "label": "전체", "value": "" }, { "label": "강진군", "value": "gangjin" }, { "label": "고흥군", "value": "goheung" }, { "label": "곡성군", "value": "gokseong" }, @@ -250,6 +261,7 @@ { "main": { "label": "경상북도", "value": "gyeongbuk" }, "areas": [ + { "label": "전체", "value": "" }, { "label": "경산시", "value": "gyeongsan" }, { "label": "경주시", "value": "gyeongju" }, { "label": "고령군", "value": "goryeong" }, @@ -278,6 +290,7 @@ { "main": { "label": "경상남도", "value": "gyeongnam" }, "areas": [ + { "label": "전체", "value": "" }, { "label": "거제시", "value": "geoje" }, { "label": "거창군", "value": "geochang" }, { "label": "고성군", "value": "goseong" }, @@ -301,6 +314,7 @@ { "main": { "label": "제주특별자치도", "value": "jeju" }, "areas": [ + { "label": "전체", "value": "" }, { "label": "서귀포시", "value": "seogwipo" }, { "label": "제주시", "value": "jeju" } ] diff --git a/src/hooks/use-infinite-scroll.ts b/src/hooks/use-infinite-scroll.ts index a1c9f415..fc1951c7 100644 --- a/src/hooks/use-infinite-scroll.ts +++ b/src/hooks/use-infinite-scroll.ts @@ -10,7 +10,7 @@ export const useInfiniteScroll = ({ queryFn: ({ pageParam }: { pageParam?: number }) => Promise; getNextPageParam: (lastPage: TData, pages: TData[]) => number | undefined; }) => { - const { data, status, fetchNextPage, hasNextPage, isFetchingNextPage, refetch } = + const { data, isLoading, error, fetchNextPage, hasNextPage, isFetchingNextPage, refetch } = useInfiniteQuery({ queryKey, queryFn, @@ -26,5 +26,5 @@ export const useInfiniteScroll = ({ fetchNextPage(); } - return { data, status, ref, isFetchingNextPage, refetch }; + return { data, error, isLoading, ref, isFetchingNextPage, refetch }; }; diff --git a/src/types/create-crew.d.ts b/src/types/create-crew.d.ts index 7920e8a9..76765d5c 100644 --- a/src/types/create-crew.d.ts +++ b/src/types/create-crew.d.ts @@ -13,6 +13,17 @@ export interface CreateCrewFormTypes { imageUrl: File | string | null; } +export interface EditCrewResponseTypes { + title: string; + mainCategory: string; + subCategory: string; + mainLocation: string; + subLocation: string; + totalCount: number; + introduce: string; + imageUrl: string; +} + export interface CreateCrewRequestTypes { title: string; mainCategory: string; @@ -24,6 +35,8 @@ export interface CreateCrewRequestTypes { imageUrl: string; } +export interface EditCrewRequestTypes extends CreateCrewRequestTypes {} + export interface CreateCrewResponseTypes { crewId: number; } diff --git a/src/types/crew-card.d.ts b/src/types/crew-card.d.ts index 964c0a9b..3a60707f 100644 --- a/src/types/crew-card.d.ts +++ b/src/types/crew-card.d.ts @@ -42,6 +42,8 @@ export interface CrewMember { export interface CrewDetail { id: number; title: string; + mainCategory?: string; + subCategory?: string; mainLocation: string; subLocation: string; participantCount: number; @@ -50,6 +52,7 @@ export interface CrewDetail { totalGatheringCount: number; crewMembers: CrewMember[]; confirmed: boolean; + introduce?: string; } export interface MyCrewList { diff --git a/src/utils/api.ts b/src/utils/api.ts index dc2f8769..6373900f 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -57,8 +57,9 @@ export async function fetchApi( if (error instanceof Error) { if (error.name === 'AbortError') throw new ApiError(408, 'Request timeout'); if (error instanceof ApiError) throw error; + throw new ApiError(0, error.message || 'An unexpected error occurred'); } - throw new ApiError(0, 'Network error or request failed'); + throw new ApiError(0, 'Unknown error occurred'); } finally { clearTimeout(id); }