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 (
+
+
+
+
+
+
+ 식당 제보에 실패했어요
+
+
+ 다시 시도해 주세요.
+
+
+
+
+
+ );
+}
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 (
+
+
+
+
+
+
+ 식당을 제보했어요
+
+
+ 제보해주셔서 감사합니다.
+
+
+
+
+
+ );
+}
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: "혼밥 식당 제보",
+ },
+ },
};
/**