diff --git a/src/app/mypage/settings/_components/MypageSettingsKeywords.tsx b/src/app/mypage/settings/_components/MypageSettingsKeywords.tsx new file mode 100644 index 0000000..c265d94 --- /dev/null +++ b/src/app/mypage/settings/_components/MypageSettingsKeywords.tsx @@ -0,0 +1,118 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { FC } from "react"; +import { Controller, useForm } from "react-hook-form"; + +import { + FestivalCategory, + FestivalCompanion, + FestivalMood, + FestivalPriority, +} from "@/apis/onboarding/onboardingType"; +import { BasicButton } from "@/components/core/Button"; +import { + CategoryKeywordInput, + MoodKeywordInput, +} from "@/components/core/Input"; +import { PriorityKeywordInput } from "@/components/core/Input/KeywordInput"; +import CompanionKeywordInput from "@/components/core/Input/KeywordInput/ConpanionKeywordInput"; +import { + ProfileUpdateSchema, + ProfileUpdateSchemaType, +} from "@/validations/ProfileUpdateSchema"; + +interface Props { + categories: Array; + companions: Array; + priorities: Array; + moods: Array; +} + +const MypageSettingsKeywords: FC = ({ + categories, + companions, + priorities, + moods, +}) => { + const { handleSubmit, control } = useForm({ + defaultValues: { + categoryIds: [], + moodIds: [], + companionIds: [], + priorityIds: [], + }, + resolver: zodResolver(ProfileUpdateSchema), + }); + + const onSubmit = (data: ProfileUpdateSchemaType) => { + console.log(data); + }; + + return ( +
+ ( + + )} + /> + + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + + + + ); +}; + +export default MypageSettingsKeywords; diff --git a/src/app/mypage/settings/_components/MypageSettingsProfile.tsx b/src/app/mypage/settings/_components/MypageSettingsProfile.tsx new file mode 100644 index 0000000..ad9f22e --- /dev/null +++ b/src/app/mypage/settings/_components/MypageSettingsProfile.tsx @@ -0,0 +1,61 @@ +"use client"; + +import React from "react"; +import { Controller, useForm } from "react-hook-form"; + +import { BasicButton } from "@/components/core/Button"; +import { TextInput } from "@/components/core/Input"; + +const MypageSettingsProfile = () => { + const { control, handleSubmit } = useForm({ + values: { + nickname: "", + statusMessage: "", + }, + }); + + const onSubmit = () => {}; + + return ( +
+ ( + + )} + /> + + ( + + )} + /> + + + + ); +}; + +export default MypageSettingsProfile; diff --git a/src/app/mypage/settings/_components/MypageSettingsTab.tsx b/src/app/mypage/settings/_components/MypageSettingsTab.tsx new file mode 100644 index 0000000..78d6f50 --- /dev/null +++ b/src/app/mypage/settings/_components/MypageSettingsTab.tsx @@ -0,0 +1,85 @@ +"use client"; + +import * as Tabs from "@radix-ui/react-tabs"; +import { FC } from "react"; + +import { + FestivalCategory, + FestivalCompanion, + FestivalMood, + FestivalPriority, +} from "@/apis/onboarding/onboardingType"; + +import MypageSettingsKeywords from "./MypageSettingsKeywords"; +import MypageSettingsProfile from "./MypageSettingsProfile"; + +interface Props { + categories: Array; + companions: Array; + priorities: Array; + moods: Array; +} + +const MypageSettingsTab: FC = ({ + categories, + companions, + priorities, + moods, +}) => { + const TabList = [ + { + name: "기본정보", + contentComponent: , + }, + { + name: "맞춤 필터", + contentComponent: ( + + ), + }, + ]; + + const handleTabChange = () => { + const tabElement = document.getElementById(`tab`); + if (tabElement) { + tabElement.scrollIntoView({ behavior: "smooth" }); + } + }; + + return ( + + + {TabList.map(({ name }, index) => ( + + {name} + + ))} + + {TabList.map(({ name, contentComponent }) => ( + + {contentComponent} + + ))} + + ); +}; + +export default MypageSettingsTab; diff --git a/src/app/mypage/settings/page.tsx b/src/app/mypage/settings/page.tsx new file mode 100644 index 0000000..f3e7dc4 --- /dev/null +++ b/src/app/mypage/settings/page.tsx @@ -0,0 +1,21 @@ +import { getOnboardingData } from "@/apis/onboarding/onboarding"; +import { DefaultHeader } from "@/layout/Mobile/MobileHeader"; + +import MypageSettingsView from "./view"; + +export default async function MypageSettingsPage() { + const { moods, categories, companions, priorities } = + await getOnboardingData(); + + return ( +
+ + +
+ ); +} diff --git a/src/app/mypage/settings/view.tsx b/src/app/mypage/settings/view.tsx new file mode 100644 index 0000000..7a925e0 --- /dev/null +++ b/src/app/mypage/settings/view.tsx @@ -0,0 +1,35 @@ +import { FC } from "react"; + +import { + FestivalCategory, + FestivalCompanion, + FestivalMood, + FestivalPriority, +} from "@/apis/onboarding/onboardingType"; + +import MypageSettingsTab from "./_components/MypageSettingsTab"; + +interface Props { + categories: Array; + companions: Array; + priorities: Array; + moods: Array; +} + +const MypageSettingsView: FC = ({ + categories, + companions, + priorities, + moods, +}) => { + return ( + + ); +}; + +export default MypageSettingsView; diff --git a/src/components/core/Chip/BasicChip/BasicChip.tsx b/src/components/core/Chip/BasicChip/BasicChip.tsx index d8f51f0..ff2fb8f 100644 --- a/src/components/core/Chip/BasicChip/BasicChip.tsx +++ b/src/components/core/Chip/BasicChip/BasicChip.tsx @@ -7,6 +7,7 @@ interface Props extends ComponentPropsWithoutRef<"button"> { active?: boolean; label: string; onClick?: () => void; + disabled?: boolean; } const BasicChip: FC = ({ @@ -14,6 +15,7 @@ const BasicChip: FC = ({ label, className, onClick, + disabled, }) => { return ( diff --git a/src/components/core/Input/KeywordInput/CategoryKeywordInput.tsx b/src/components/core/Input/KeywordInput/CategoryKeywordInput.tsx index bc095c1..a011d60 100644 --- a/src/components/core/Input/KeywordInput/CategoryKeywordInput.tsx +++ b/src/components/core/Input/KeywordInput/CategoryKeywordInput.tsx @@ -3,48 +3,62 @@ import { FC } from "react"; import { FestivalCategory } from "@/apis/onboarding/onboardingType"; +import { cn } from "@/utils"; import { BasicChip } from "../../Chip"; interface Props { - moods: Array; - selectedMoods: Array; + categories: Array; + selectedCategories: Array; onChange: (keyword: Array) => void; maxCount?: number; + isPrimaryLabel?: boolean; + label?: string; } const CategoryKeywordInput: FC = ({ - moods, - selectedMoods, + categories, + label = "주제", + isPrimaryLabel = false, + selectedCategories, maxCount = 2, onChange, }) => { const handleHandleToggle = (isSelected: boolean, id: number) => { if (isSelected) { - onChange(selectedMoods.filter((m) => m !== id)); + onChange(selectedCategories.filter((m) => m !== id)); return; } - onChange([...selectedMoods, id]); + onChange([...selectedCategories, id]); }; return (
- + 최대 2개
- {moods.map((mood) => { + {categories.map((mood) => { const { categoryId, name } = mood; - const isSelected = selectedMoods.some((id) => id === categoryId); + const isSelected = selectedCategories.some((id) => id === categoryId); return ( = maxCount && !isSelected} onClick={() => handleHandleToggle(isSelected, categoryId)} /> ); diff --git a/src/components/core/Input/KeywordInput/ConpanionKeywordInput.tsx b/src/components/core/Input/KeywordInput/ConpanionKeywordInput.tsx new file mode 100644 index 0000000..ab085a2 --- /dev/null +++ b/src/components/core/Input/KeywordInput/ConpanionKeywordInput.tsx @@ -0,0 +1,68 @@ +"use client"; +import { FC } from "react"; + +import { FestivalCompanion } from "@/apis/onboarding/onboardingType"; +import { cn } from "@/utils"; + +import { BasicChip } from "../../Chip"; + +interface Props { + companions: Array; + isPrimaryLabel?: boolean; + selectedCompanions: Array; + onChange: (keyword: Array) => void; + label?: string; +} + +const CompanionKeywordInput: FC = ({ + companions, + isPrimaryLabel = false, + selectedCompanions, + onChange, + label = "페스티벌 일행", +}) => { + const handleHandleToggle = (isSelected: boolean, id: number) => { + if (isSelected) { + onChange(selectedCompanions.filter((m) => m !== id)); + return; + } + + onChange([...selectedCompanions, id]); + }; + + return ( +
+
+ +
+
+ {companions.map((companion) => { + const { companionId, companionType } = companion; + const isSelected = selectedCompanions.some( + (id) => id === companionId, + ); + return ( + handleHandleToggle(isSelected, companionId)} + /> + ); + })} +
+
+ ); +}; + +export default CompanionKeywordInput; diff --git a/src/components/core/Input/KeywordInput/MoodKeywordInput.tsx b/src/components/core/Input/KeywordInput/MoodKeywordInput.tsx index d614302..d01ac0c 100644 --- a/src/components/core/Input/KeywordInput/MoodKeywordInput.tsx +++ b/src/components/core/Input/KeywordInput/MoodKeywordInput.tsx @@ -2,6 +2,7 @@ import { FC } from "react"; import { FestivalMood } from "@/apis/onboarding/onboardingType"; +import { cn } from "@/utils"; import { BasicChip } from "../../Chip"; @@ -10,6 +11,8 @@ interface Props { selectedMoods: Array; onChange: (keyword: Array) => void; maxCount?: number; + isPrimaryLabel?: boolean; + label?: string; } const MoodKeywordInput: FC = ({ @@ -17,6 +20,8 @@ const MoodKeywordInput: FC = ({ selectedMoods, maxCount = 2, onChange, + isPrimaryLabel = false, + label = "페스티벌 일행", }) => { const handleHandleToggle = (isSelected: boolean, id: number) => { if (isSelected) { @@ -30,8 +35,19 @@ const MoodKeywordInput: FC = ({ return (
- - 최대 2개 + + + 최대 {maxCount}개 +
{moods.map((mood) => { diff --git a/src/components/core/Input/KeywordInput/PriorityKeywordInput.tsx b/src/components/core/Input/KeywordInput/PriorityKeywordInput.tsx new file mode 100644 index 0000000..105ef64 --- /dev/null +++ b/src/components/core/Input/KeywordInput/PriorityKeywordInput.tsx @@ -0,0 +1,72 @@ +"use client"; +import { FC } from "react"; + +import { FestivalPriority } from "@/apis/onboarding/onboardingType"; +import { cn } from "@/utils"; + +import { BasicChip } from "../../Chip"; + +interface Props { + priorities: Array; + selectedPriorities: Array; + onChange: (keyword: Array) => void; + maxCount?: number; + isPrimaryLabel?: boolean; + label?: string; +} + +const PriorityKeywordInput: FC = ({ + priorities, + selectedPriorities, + maxCount = 3, + onChange, + isPrimaryLabel = false, + label = "페스티벌 우선순위", +}) => { + const handleHandleToggle = (isSelected: boolean, id: number) => { + if (isSelected) { + onChange(selectedPriorities.filter((m) => m !== id)); + return; + } + + onChange([...selectedPriorities, id]); + }; + + return ( +
+
+ + + 최대 {maxCount}개 + +
+
+ {priorities.map((priorityItem) => { + const { priority, priorityId } = priorityItem; + const isSelected = selectedPriorities.some((id) => id === priorityId); + return ( + handleHandleToggle(isSelected, priorityId)} + /> + ); + })} +
+
+ ); +}; + +export default PriorityKeywordInput; diff --git a/src/components/core/Input/KeywordInput/index.ts b/src/components/core/Input/KeywordInput/index.ts index bc6e961..80cdf29 100644 --- a/src/components/core/Input/KeywordInput/index.ts +++ b/src/components/core/Input/KeywordInput/index.ts @@ -1,2 +1,5 @@ export { default as CategoryKeywordInput } from "./CategoryKeywordInput"; +export { default as ConpanionKeywordInput } from "./ConpanionKeywordInput"; export { default as MoodKeywordInput } from "./MoodKeywordInput"; +export { default as PriorityKeywordInput } from "./PriorityKeywordInput"; +export { default as ReviewKeywordInput } from "./ReviewKeywordInput"; diff --git a/src/middleware.ts b/src/middleware.ts index d64daf6..1df27ab 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -36,7 +36,7 @@ export async function middleware(request: NextRequest) { return NextResponse.redirect(new URL("/onboarding", request.url)); } - !!session + return !!session ? NextResponse.redirect(new URL("/", request.url)) : NextResponse.next(); } diff --git a/src/validations/ProfileUpdateSchema.ts b/src/validations/ProfileUpdateSchema.ts new file mode 100644 index 0000000..dfef722 --- /dev/null +++ b/src/validations/ProfileUpdateSchema.ts @@ -0,0 +1,26 @@ +import { z } from "zod"; + +import { ONBOARDING_SETTING } from "@/config"; + +const MoodsSchema = z + .array(z.number()) + .min(ONBOARDING_SETTING.MOOD_MIN, "at least three items"); + +const PrioritiesSchema = z + .array(z.number()) + .min(ONBOARDING_SETTING.PRIORITY_MIN, "at least one item"); + +const companionsSchema = z.array(z.number()).min(1, "at least one item"); + +const CategoriesSchema = z + .array(z.number()) + .min(ONBOARDING_SETTING.CATEGORY_MIN, "at least two items"); + +export const ProfileUpdateSchema = z.object({ + categoryIds: CategoriesSchema, + moodIds: MoodsSchema, + companionIds: companionsSchema, + priorityIds: PrioritiesSchema, +}); + +export type ProfileUpdateSchemaType = z.output;