diff --git a/web/src/app/(main)/report/page.tsx b/web/src/app/(main)/report/page.tsx deleted file mode 100644 index 4a02e03..0000000 --- a/web/src/app/(main)/report/page.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function ReportPage() { - return
식당 제하
; -} diff --git a/web/src/app/(stack)/report/page.tsx b/web/src/app/(stack)/report/page.tsx new file mode 100644 index 0000000..daaf60e --- /dev/null +++ b/web/src/app/(stack)/report/page.tsx @@ -0,0 +1,56 @@ +"use client"; + +import { useReportForm } from "@/features/report/lib/hook/useReportForm"; +import { FailureModal } from "@/features/report/ui/FailureModal"; +import { ReportForm } from "@/features/report/ui/ReportForm"; +import { SuccessModal } from "@/features/report/ui/SuccessModal"; +import { CTA, TransitionLayout } from "@/shared/ui"; + +export default function ReportPage() { + const { + location, + setLocation, + name, + setName, + recommendedMenu, + setRecommendedMenu, + reason, + setReason, + seatTypes, + setSeatTypes, + isSuccessModalOpen, + setIsSuccessModalOpen, + isFailureModalOpen, + setIsFailureModalOpen, + handleSubmit, + } = useReportForm(); + + return ( + + +
+ +
+ + setIsSuccessModalOpen(false)} /> + setIsFailureModalOpen(false)} /> +
+ ); +} diff --git a/web/src/features/report/api/reportApi.ts b/web/src/features/report/api/reportApi.ts new file mode 100644 index 0000000..d7f0bd8 --- /dev/null +++ b/web/src/features/report/api/reportApi.ts @@ -0,0 +1,8 @@ +import { axiosInstance } from "@/shared/config"; +import type { ReportRequestBody } from "./types"; + +export const postReport = async (body: ReportRequestBody) => { + const response = await axiosInstance.post("/api/v1/report", body); + + return response; +}; diff --git a/web/src/features/report/api/types.ts b/web/src/features/report/api/types.ts new file mode 100644 index 0000000..62435d6 --- /dev/null +++ b/web/src/features/report/api/types.ts @@ -0,0 +1,12 @@ +import type { SeatTypes } from "@/entities/storeList/api"; + +export interface ReportRequestBody { + location: string; + name: string; + seatType: SeatTypes[]; + paymentMethods: string[]; + menuCategories: string[]; + recommendedMenu: string; + reason: string; + memberId: number; +} diff --git a/web/src/features/report/api/useReportMutation.ts b/web/src/features/report/api/useReportMutation.ts new file mode 100644 index 0000000..db32968 --- /dev/null +++ b/web/src/features/report/api/useReportMutation.ts @@ -0,0 +1,16 @@ +import { useMutation } from "@tanstack/react-query"; +import type { ApiResponse } from "@/shared/model"; +import { postReport } from "./reportApi"; + +interface UseReportMutationOptions { + onSuccess?: (data: ApiResponse) => void; + onError?: (error: Error) => void; +} + +export const useReportMutation = ({ onSuccess, onError }: UseReportMutationOptions = {}) => { + return useMutation({ + mutationFn: postReport, + onSuccess, + onError, + }); +}; diff --git a/web/src/features/report/lib/hook/useReportForm.ts b/web/src/features/report/lib/hook/useReportForm.ts new file mode 100644 index 0000000..0478b84 --- /dev/null +++ b/web/src/features/report/lib/hook/useReportForm.ts @@ -0,0 +1,61 @@ +import { useState } from "react"; +import type { SeatTypes } from "@/entities/storeList/api"; +import { useUserState } from "@/entities/user"; +import { useReportMutation } from "@/features/report/api/useReportMutation"; + +export const useReportForm = () => { + const { userState } = useUserState(); + + const [location, setLocation] = useState(""); + const [name, setName] = useState(""); + const [recommendedMenu, setRecommendedMenu] = useState(""); + const [reason, setReason] = useState(""); + const [seatTypes, setSeatTypes] = useState([]); + const [isSuccessModalOpen, setIsSuccessModalOpen] = useState(false); + const [isFailureModalOpen, setIsFailureModalOpen] = useState(false); + + const { mutate: reportMutate } = useReportMutation({ + onSuccess: () => { + setIsSuccessModalOpen(true); + }, + onError: () => { + setIsFailureModalOpen(true); + }, + }); + + const handleSubmit = () => { + if (!userState.memberId) { + alert("로그인이 필요합니다."); + return; + } + + reportMutate({ + location, + name, + seatType: seatTypes, + paymentMethods: [], + menuCategories: [], + recommendedMenu, + reason, + memberId: userState.memberId, + }); + }; + + return { + location, + setLocation, + name, + setName, + recommendedMenu, + setRecommendedMenu, + reason, + setReason, + seatTypes, + setSeatTypes, + isSuccessModalOpen, + setIsSuccessModalOpen, + isFailureModalOpen, + setIsFailureModalOpen, + handleSubmit, + }; +}; diff --git a/web/src/features/report/ui/FailureModal.tsx b/web/src/features/report/ui/FailureModal.tsx new file mode 100644 index 0000000..296b4f2 --- /dev/null +++ b/web/src/features/report/ui/FailureModal.tsx @@ -0,0 +1,36 @@ +import Image from "next/image"; +import Character from "@/shared/lib/assets/png/character/basic.png"; +import { Icon, Modal } from "@/shared/ui"; + +interface FailureModalProps { + isOpen: boolean; + onClose: () => void; +} + +export function FailureModal({ isOpen, onClose }: FailureModalProps) { + return ( + +
+ + character +
+ + 식당 제보에 실패했어요 + + + 다시 시도해 주세요. + +
+ +
+
+ ); +} diff --git a/web/src/features/report/ui/ReportForm.tsx b/web/src/features/report/ui/ReportForm.tsx new file mode 100644 index 0000000..54e5a61 --- /dev/null +++ b/web/src/features/report/ui/ReportForm.tsx @@ -0,0 +1,92 @@ +"use client"; + +import type { Dispatch, SetStateAction } from "react"; +import type { SeatTypes } from "@/entities/storeList/api"; +import { FilterSection, Input, InputReview } from "@/shared/ui"; + +const SEAT_TYPE_MAP: Record = { + CUBICLE: "칸막이", + BAR_TABLE: "바 좌석", + FOR_ONE: "1인석", + FOR_TWO: "2인석", + FOR_FOUR: "4인석", +}; + +interface ReportFormProps { + location: string; + setLocation: Dispatch>; + name: string; + setName: Dispatch>; + recommendedMenu: string; + setRecommendedMenu: Dispatch>; + reason: string; + setReason: Dispatch>; + seatTypes: SeatTypes[]; + setSeatTypes: Dispatch>; +} + +export const ReportForm = ({ + location, + setLocation, + name, + setName, + recommendedMenu, + setRecommendedMenu, + reason, + setReason, + seatTypes, + setSeatTypes, +}: ReportFormProps) => { + const handleSeatTypesChange = (selectedLabels: string[]) => { + const newSeatTypes = selectedLabels.map( + (label) => Object.entries(SEAT_TYPE_MAP).find(([, v]) => v === label)?.[0] as SeatTypes, + ); + setSeatTypes(newSeatTypes); + }; + + return ( +
+
+ 가게 위치 + +
+
+ 가게 정보 + +
+
+ 추천 메뉴 + +
+
+ 식당 추천 이유 + setReason(e.target.value)} + /> +
+
+ 좌석 형태 + SEAT_TYPE_MAP[type])} + onChange={handleSeatTypesChange} + /> +
+
+ ); +}; diff --git a/web/src/features/report/ui/SuccessModal.tsx b/web/src/features/report/ui/SuccessModal.tsx new file mode 100644 index 0000000..d5cfe5e --- /dev/null +++ b/web/src/features/report/ui/SuccessModal.tsx @@ -0,0 +1,44 @@ +import Image from "next/image"; +import { useRouter } from "next/navigation"; +import Character from "@/shared/lib/assets/png/character/basic.png"; +import { Icon, Modal } from "@/shared/ui"; + +interface SuccessModalProps { + isOpen: boolean; + onClose: () => void; +} + +export function SuccessModal({ isOpen, onClose }: SuccessModalProps) { + const router = useRouter(); + + const handleConfirm = () => { + onClose(); + router.push("/mypage"); + }; + + return ( + +
+ + character +
+ + 식당을 제보했어요 + + + 제보해주셔서 감사합니다. + +
+ +
+
+ ); +} diff --git a/web/src/shared/config/routeConfig.ts b/web/src/shared/config/routeConfig.ts index 5b5be56..22364f3 100644 --- a/web/src/shared/config/routeConfig.ts +++ b/web/src/shared/config/routeConfig.ts @@ -153,6 +153,13 @@ export const ROUTE_CONFIG: Record = { title: "정보 수정 제안", }, }, + "/report": { + group: "stack", + transition: "drill", + header: { + title: "혼밥 식당 제보", + }, + }, }; /**