diff --git a/src/app/api/support/notice/route.ts b/src/app/api/support/notice/route.ts new file mode 100644 index 00000000..f5379dac --- /dev/null +++ b/src/app/api/support/notice/route.ts @@ -0,0 +1,26 @@ +import { NextRequest } from "next/server"; + +/** + * 공지사항 목록 조회 + */ +export async function GET(request: NextRequest) { + const url = new URL(request.url); + const page = url.searchParams.get("page") || "1"; + + // 페이지 번호를 0 기반으로 조정합니다. + const zeroBasedPage = Number(page) - 1; + + const apiUrl = `${process.env.BACKEND_URL}/api/notice${zeroBasedPage > 0 ? `?page=${zeroBasedPage}` : ""}`; + + // 백엔드 API 호출 + const response = await fetch(apiUrl, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + cache: "no-cache", + }); + + return response; +} + diff --git a/src/app/api/support/qna/route.ts b/src/app/api/support/qna/route.ts new file mode 100644 index 00000000..a17b9191 --- /dev/null +++ b/src/app/api/support/qna/route.ts @@ -0,0 +1,102 @@ +import { NextRequest, NextResponse } from "next/server"; + +/** + * qna 등록 + */ +export async function POST(request: NextRequest) { + const access_cookie = request.cookies.get("access_token"); + if (!access_cookie) { + const refresh_cookie = request.cookies.get("refresh_token"); + if (!refresh_cookie) { + // 리프레시 토큰이 없으므로 요청 중단 + return new NextResponse("Refresh token not found", { status: 403 }); + } + // 리프레시 토큰으로 재발급 받아 재요청 보내기 위한 응답 + return new NextResponse("Refresh token not found", { status: 401 }); + } + const bodyData = await request.json(); + + const response = await fetch(`${process.env.BACKEND_URL}/api/qna`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Cookie: `${access_cookie?.name}=${access_cookie?.value}`, + }, + body: JSON.stringify(bodyData), + cache: "no-store", + }); + + return response; +} + +/** + * qna 리스트 페이지네이션 + */ +export async function GET(request: NextRequest) { + // 요청에서 access_token을 가져옵니다. + const access_cookie = request.cookies.get("access_token"); + + if (!access_cookie) { + // access_token이 없으면 refresh_token이 있는지 확인합니다. + const refresh_cookie = request.cookies.get("refresh_token"); + if (!refresh_cookie) { + // refresh_token이 없는 경우 요청을 중단하고 403 응답을 보냅니다. + return new NextResponse("Refresh token not found", { status: 403 }); + } + // refresh_token만으로는 유효하지 않으므로 401 응답을 보냅니다. + return new NextResponse("Access token not found", { status: 401 }); + } + + // 요청 URL에서 쿼리 파라미터를 가져옵니다. + const url = new URL(request.url); + const page = url.searchParams.get("page") || "1"; // 페이지가 없으면 기본값으로 1을 사용합니다. + + // 페이지 번호를 0 기반으로 조정합니다. + const zeroBasedPage = Number(page) - 1; + + // URL을 페이지 번호에 맞게 수정합니다. 페이지 번호가 0보다 큰 경우에만 쿼리 파라미터를 추가합니다. + const apiUrl = `${process.env.BACKEND_URL}/api/qna${zeroBasedPage > 0 ? `?page=${zeroBasedPage}` : ""}`; + + // 백엔드 API 호출 + const response = await fetch(apiUrl, { + method: "GET", + headers: { + "Content-Type": "application/json", + Cookie: `${access_cookie?.name}=${access_cookie?.value}`, + }, + cache: "no-store", + }); + + return response; +} + +/** + * qna 삭제 + */ +export async function DELETE(request: NextRequest) { + const access_cookie = request.cookies.get("access_token"); + if (!access_cookie) { + const refresh_cookie = request.cookies.get("refresh_token"); + if (!refresh_cookie) { + // 리프레시 토큰이 없으므로 요청 중단 + return new NextResponse("Refresh token not found", { status: 403 }); + } + // 리프레시 토큰으로 재발급 받아 재요청 보내기 위한 응답 + return new NextResponse("Refresh token not found", { status: 401 }); + } + const url = new URL(request.url); + + const response = await fetch( + `${process.env.BACKEND_URL}/api/qna/${url.searchParams.get("id")}`, + { + method: "DELETE", + headers: { + "Content-Type": "application/json", + Cookie: `${access_cookie?.name}=${access_cookie?.value}`, + }, + cache: "no-store", + }, + ); + + return response; +} diff --git a/src/app/api/support/question/route.ts b/src/app/api/support/question/route.ts new file mode 100644 index 00000000..941879fb --- /dev/null +++ b/src/app/api/support/question/route.ts @@ -0,0 +1,31 @@ +import { NextRequest, NextResponse } from "next/server"; + +/** + * qna 질문 등록 + */ +export async function POST(request: NextRequest) { + const access_cookie = request.cookies.get("access_token"); + if (!access_cookie) { + const refresh_cookie = request.cookies.get("refresh_token"); + if (!refresh_cookie) { + // 리프레시 토큰이 없으므로 요청 중단 + return new NextResponse("Refresh token not found", { status: 403 }); + } + // 리프레시 토큰으로 재발급 받아 재요청 보내기 위한 응답 + return new NextResponse("Refresh token not found", { status: 401 }); + } + + const bodyData = await request.json(); + + const response = await fetch(`${process.env.BACKEND_URL}/api/qna/question`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Cookie: `${access_cookie?.name}=${access_cookie?.value}`, + }, + body: JSON.stringify(bodyData), + cache: "no-store", + }); + + return response; +} diff --git a/src/app/support/notice/[id]/page.tsx b/src/app/support/notice/[id]/page.tsx new file mode 100644 index 00000000..c91e2cf1 --- /dev/null +++ b/src/app/support/notice/[id]/page.tsx @@ -0,0 +1,49 @@ +// app/page.tsx +import SupportNoticeDetail from "@/components/support/SupportNoticeDetail"; +import { Metadata } from "next"; +import { cookies } from "next/headers"; + +export const metadata: Metadata = { + title: "공지사항 상세조회", + description: "공지사항 상세조회", +}; + +interface PageProps { + params: { id: string }; +} + +async function fetchData(id: number) { + const cookie = cookies().get("access_token"); + + try { + const res = await fetch( + `${process.env.BACKEND_URL}/api/notice/${id}`, + { + headers: { + "Content-Type": "application/json", + Cookie: `${cookie?.name}=${cookie?.value}`, + }, + next: { revalidate: 3600 }, + }, + ); + if (!res.ok) { + throw new Error(`Failed to fetch data: ${res.statusText}`); + } + return res.json(); + } catch (error) { + return { error: "Failed to fetch data" }; + } +} + +export default async function Page({ params: { id } }: PageProps) { + const noticeId = Number(id); + if (noticeId <= 0 || !Number.isSafeInteger(noticeId)) { + throw Error("Not Found"); + } + + const data = await fetchData(noticeId); + + return ( + + ); +} diff --git a/src/app/support/qna/detail/[id]/page.tsx b/src/app/support/qna/detail/[id]/page.tsx index e06202f9..fa42d4be 100644 --- a/src/app/support/qna/detail/[id]/page.tsx +++ b/src/app/support/qna/detail/[id]/page.tsx @@ -1,19 +1,55 @@ import SupportQnADetailEditContainer from "@/containers/support/qna/SupportQnADetailEditContainer"; -import { Metadata } from "next"; +import { QnADetailType } from "@/types/QnADto"; +import { fetchWithAuth } from "@/utils/fetchWithAuth"; +import { cookies } from "next/headers"; -export const metadata: Metadata = { - title: "마이페이지", - description: "Solitour 사용자 마이페이지", -}; +interface Props { + params: { id: string }; +} + +export async function generateMetadata({ params: { id } }: Props) { + const qnaId = Number(id); + if (qnaId <= 0 || !Number.isSafeInteger(qnaId)) { + throw Error("Not Found"); + } + + return { + title: `공지사항 조회`, + description: "공지사항 상세조회", + }; +} + +async function fetchData(id: number) { + const cookie = cookies().get("access_token"); + + const res = await fetchWithAuth(`${process.env.BACKEND_URL}/api/qna/${id}`, { + headers: { + "Content-Type": "application/json", + Cookie: `${cookie?.name}=${cookie?.value}`, + }, + }); + + console.log("page.tsx 파일 : ",res.status); + console.log("page.tsx 파일 : ",cookie); + + if (!res.ok) { + throw new Error(`Failed to fetch data: ${res.statusText}`); + } + + return res.json(); +} + +export default async function Page({ params: { id } }: Props) { + const qnaId = Number(id); + if (qnaId <= 0 || !Number.isSafeInteger(qnaId)) { + throw Error("Not Found"); + } + + const data: QnADetailType = await fetchData(qnaId); -export default async function page() { return ( -
- +
+
); -} \ No newline at end of file +} diff --git a/src/components/common/Header.tsx b/src/components/common/Header.tsx index e84add97..0c24a5e0 100644 --- a/src/components/common/Header.tsx +++ b/src/components/common/Header.tsx @@ -127,7 +127,7 @@ const Header = ({ href="/support?menu=about" prefetch={userId > 0} > - 지원&안내 + 고객지원 diff --git a/src/components/common/HeaderSidebar.tsx b/src/components/common/HeaderSidebar.tsx index 98f2c509..650904bf 100644 --- a/src/components/common/HeaderSidebar.tsx +++ b/src/components/common/HeaderSidebar.tsx @@ -158,7 +158,7 @@ const HeaderSidebar = ({ height={22} /> )} -

지원&안내

+

고객지원

{signedIn ? (
diff --git a/src/components/support/SupportNotice.tsx b/src/components/support/SupportNotice.tsx deleted file mode 100644 index 7a9abc09..00000000 --- a/src/components/support/SupportNotice.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import Link from "next/link"; - -interface ISupportNotice { - data: { - id: number; - title: string; - createdAt: string; - content: string; - }[]; -} - -const SupportNotice = ({ data }: ISupportNotice) => { - return ( -
- {data.map((notice) => ( - -

{notice.title}

-

- {new Date(notice.createdAt).toLocaleDateString()} -

-

{notice.content}

- - ))} -
- ); -}; - -export default SupportNotice; diff --git a/src/components/support/SupportNoticeDetail.tsx b/src/components/support/SupportNoticeDetail.tsx new file mode 100644 index 00000000..2333503e --- /dev/null +++ b/src/components/support/SupportNoticeDetail.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { NoticeType } from "@/types/NoticeDto"; +import { NOTICE_DETAIL_BREADCRUMB_PATH } from "@/utils/constant/BreadCrumbDirectory"; +import { format } from "date-fns"; +import Breadcrumbs from "../common/Breadcrumb"; + +interface ISupportNoticeDetail { + data: NoticeType; +} + +const SupportNoticeDetail = ({ data }: ISupportNoticeDetail) => { + const categoryStyles: { [key: string]: string } = { + 이벤트: "bg-green-100 text-green-800", + 공지: "bg-blue-100 text-blue-800", + 점검: "bg-yellow-100 text-yellow-800", + 기타: "bg-gray-100 text-gray-800", + }; + + return ( +
+ +
+ {/* 카테고리 및 날짜 */} +
+
+ {data.categoryName} +
+ +
+ {format(new Date(data.createdAt), "yyyy-MM-dd")} +
+
+ + {/* 제목 */} +
+ {data.title} +
+ +
+ + {/* 본문 내용 */} +
{data.content}
+
+
+ ); +}; + +export default SupportNoticeDetail; diff --git a/src/components/support/SupportNoticeList.tsx b/src/components/support/SupportNoticeList.tsx new file mode 100644 index 00000000..5904c304 --- /dev/null +++ b/src/components/support/SupportNoticeList.tsx @@ -0,0 +1,50 @@ +import { NoticeType } from "@/types/NoticeDto"; +import { differenceInDays, format } from "date-fns"; +import Link from "next/link"; + +interface ISupportNoticeList { + data: NoticeType[]; + viewedNotices: number[]; + onClickNotice: (id: number) => void; +} + + const categoryStyles: { [key: string]: string } = { + 이벤트: "bg-green-100 text-green-800", + 공지: "bg-blue-100 text-blue-800", + 점검: "bg-yellow-100 text-yellow-800", + 기타: "bg-gray-100 text-gray-800", + }; + + +const SupportNoticeList = ({ data, viewedNotices, onClickNotice }: ISupportNoticeList) => { + return ( +
+ {data.map((notice) => ( + onClickNotice(notice.id)} + > + {differenceInDays(new Date(), new Date(notice.createdAt)) < 2 && + !viewedNotices.includes(notice.id) && ( +
+ New +
+ )} +
+ {notice.categoryName} +
+

{notice.title}

+

+ {format(new Date(notice.createdAt), "yyyy-MM-dd")} +

+ + ))} +
+ ); +}; + +export default SupportNoticeList; diff --git a/src/components/support/SupportQnA.tsx b/src/components/support/SupportQnA.tsx deleted file mode 100644 index 1ae603d7..00000000 --- a/src/components/support/SupportQnA.tsx +++ /dev/null @@ -1,166 +0,0 @@ -import Link from "next/link"; - -interface ISupportQnA { - id: number; - title: string; - createdAt: string; - status: "답변 대기중" | "답변 완료"; -} - -const qnaList: ISupportQnA[] = [ - { - id: 1, - title: "회원가입이 안됩니다.", - createdAt: "2024-08-26", - status: "답변 대기중", - }, - { - id: 2, - title: "비밀번호를 잊어버렸어요.", - createdAt: "2024-08-25", - status: "답변 완료", - }, - { - id: 3, - title: "결제 오류가 발생했습니다.", - createdAt: "2024-08-24", - status: "답변 대기중", - }, - { - id: 4, - title: "배송 조회가 안돼요.", - createdAt: "2024-08-23", - status: "답변 완료", - }, - { - id: 5, - title: "제품이 불량입니다.", - createdAt: "2024-08-22", - status: "답변 대기중", - }, - { - id: 6, - title: "환불이 지연되고 있어요.", - createdAt: "2024-08-21", - status: "답변 완료", - }, - { - id: 7, - title: "주문 내역이 보이지 않아요.", - createdAt: "2024-08-20", - status: "답변 대기중", - }, - { - id: 8, - title: "프로모션 코드가 적용되지 않아요.", - createdAt: "2024-08-19", - status: "답변 완료", - }, - { - id: 9, - title: "이메일 인증이 안됩니다.", - createdAt: "2024-08-18", - status: "답변 대기중", - }, - { - id: 10, - title: "주소 변경이 안돼요.", - createdAt: "2024-08-17", - status: "답변 완료", - }, - { - id: 11, - title: "고객센터 연결이 어려워요.", - createdAt: "2024-08-16", - status: "답변 대기중", - }, - { - id: 12, - title: "상품 상세 정보가 부족해요.", - createdAt: "2024-08-15", - status: "답변 완료", - }, - { - id: 13, - title: "배송이 너무 지연되고 있어요.", - createdAt: "2024-08-14", - status: "답변 대기중", - }, - { - id: 14, - title: "상품이 잘못 배송되었습니다.", - createdAt: "2024-08-13", - status: "답변 완료", - }, - { - id: 15, - title: "상품이 도착하지 않았습니다.", - createdAt: "2024-08-12", - status: "답변 대기중", - }, - { - id: 16, - title: "결제가 이중으로 되었습니다.", - createdAt: "2024-08-11", - status: "답변 완료", - }, - { - id: 17, - title: "계정을 삭제하고 싶습니다.", - createdAt: "2024-08-10", - status: "답변 대기중", - }, - { - id: 18, - title: "회원 등급이 잘못 적용되었습니다.", - createdAt: "2024-08-09", - status: "답변 완료", - }, - { - id: 19, - title: "자동 결제가 되지 않았어요.", - createdAt: "2024-08-08", - status: "답변 대기중", - }, - { - id: 20, - title: "포인트가 적립되지 않았습니다.", - createdAt: "2024-08-07", - status: "답변 완료", - }, -]; - -const SupportQnA = () => { - return ( -
-
- - - -
-
    - {qnaList.map((qna) => ( - -
  • -
    - {qna.title} - - {qna.status} - -
    -
    - {qna.createdAt} -
    -
  • - - ))} -
-
- ); -}; - -export default SupportQnA; \ No newline at end of file diff --git a/src/components/support/SupportQnAList.tsx b/src/components/support/SupportQnAList.tsx new file mode 100644 index 00000000..024881c4 --- /dev/null +++ b/src/components/support/SupportQnAList.tsx @@ -0,0 +1,93 @@ +import { QnAListElementType } from "@/types/QnADto"; +import { format } from "date-fns"; +import Link from "next/link"; + +interface ISupportQnAListProps { + elements: QnAListElementType[]; + loading: boolean; + userId: number; +} + + const STATUS: { [key: string]: { name: string; style: string } } = { + WAIT: { + name: "답변 대기중", + style: "bg-red-100 text-red-700", + }, + ANSWER: { + name: "답변 완료", + style: "bg-green-100 text-green-700", + }, + CLOSED: { + name: "답변 종료", + style: "bg-gray-400 text-black", + }, + }; + +const SupportQnAList = ({ elements, loading, userId }: ISupportQnAListProps) => { + + return ( +
+ {loading ? ( + <> +
    + {Array.from({ length: 10 }).map((_, index) => ( +
  • +
    +
    +
    +
    +
    +
    +
    +
  • + ))} +
+ + ) : ( + <> + {userId > 0 ? ( +
+ + + +
+ ) : ( +
+ 로그인이 필요합니다. +
+ )} +
    + {elements?.map((qna) => ( + +
  • +
    + + {qna.title} + + + {STATUS[qna.status].name} + +
    +
    + + {format(new Date(qna.createdAt), "yyyy-MM-dd")} + +
    +
  • + + ))} +
+ + )} +
+ ); +}; + +export default SupportQnAList; diff --git a/src/components/support/qna/SupportQnADetailEdit.tsx b/src/components/support/qna/SupportQnADetailEdit.tsx index 1656d6e7..20083628 100644 --- a/src/components/support/qna/SupportQnADetailEdit.tsx +++ b/src/components/support/qna/SupportQnADetailEdit.tsx @@ -1,133 +1,111 @@ import Breadcrumbs from "@/components/common/Breadcrumb"; +import { QnADetailType, QnAMessageType } from "@/types/QnADto"; +import { QNA_DETAIL_BREADCRUMB_PATH } from "@/utils/constant/BreadCrumbDirectory"; import { format } from "date-fns"; -import React, { useState } from "react"; +import React from "react"; interface ISupportQnADetailEdit { - // 필요한 props가 있으면 여기에 정의 + data: QnADetailType; + userId: number; // 현재 로그인한 사용자의 ID + changeInputHandler: (value: string) => void; + questionSubmitHandler: () => void; + messageList: QnAMessageType[]; + qnaCloseHandler: () => void; } -const SupportQnADetailEdit: React.FC = (props) => { - const [responses, setResponses] = useState([ - { - id: 1, - state: "question", - content: "React를 배우는 가장 좋은 방법은 무엇인가요?", - createdAt: "2024-08-27T10:00:00Z", - }, - { - id: 2, - state: "response", - content: - "React를 배우는 가장 좋은 방법은 공식 문서부터 시작하고 작은 프로젝트를 만드는 것입니다. 온라인 튜토리얼과 강의도 도움이 될 수 있습니다.", - createdAt: "2024-08-27T10:05:00Z", - }, - { - id: 3, - state: "response", - content: - "또 다른 좋은 방법은 커뮤니티나 포럼에 참여하여 질문을 하고 프로젝트에 대한 피드백을 받는 것입니다.", - createdAt: "2024-08-27T10:10:00Z", - }, - { - id: 4, - state: "question", - content: "React의 상태 관리를 위한 가장 좋은 라이브러리는 무엇인가요?", - createdAt: "2024-08-27T10:15:00Z", +const SupportQnADetailEdit: React.FC = ({ + data, + userId, + changeInputHandler, + questionSubmitHandler, + messageList, + qnaCloseHandler, +}) => { + const formatDate = (dateString: string) => { + return format(new Date(dateString), "yyyy-MM-dd HH:mm:ss"); + }; + + const STATUS: { [key: string]: { name: string; style: string } } = { + WAIT: { + name: "답변 대기 중", + style: "bg-red-100 text-red-700", }, - { - id: 5, - state: "response", - content: - "상태 관리를 위해서는 React의 내장 훅인 `useState`와 `useReducer`를 사용할 수 있습니다. 또한, 상태 관리를 더 복잡하게 하고 싶다면 Redux나 Zustand 같은 라이브러리를 고려할 수 있습니다.", - createdAt: "2024-08-27T10:20:00Z", + ANSWER: { + name: "답변 완료", + style: "bg-green-100 text-green-700", }, - { - id: 6, - state: "response", - content: - "Redux는 상태 관리를 위한 매우 강력한 라이브러리지만, 설정이 복잡할 수 있습니다. Zustand는 더 간단한 설정으로 상태 관리를 제공하므로 작은 프로젝트나 간단한 상태 관리에 적합할 수 있습니다.", - createdAt: "2024-08-27T10:25:00Z", + CLOSED: { + name: "답변 종료", + style: "bg-gray-400 text-black", }, - ]); - const [newResponse, setNewResponse] = useState(""); - - const handleResponseChange = ( - event: React.ChangeEvent, - ) => { - setNewResponse(event.target.value); }; - const handleResponseSubmit = () => { - if (newResponse.trim()) { - const newEntry = { - id: responses.length + 1, - state: "response", - content: newResponse, - createdAt: new Date().toISOString(), - }; - setResponses([...responses, newEntry]); - setNewResponse(""); // 제출 후 textarea를 비웁니다. - } - }; + return ( +
+ - const formatDate = (dateString: string) => { - const options: Intl.DateTimeFormatOptions = { - year: "numeric", - month: "long", - day: "numeric", - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - timeZoneName: "short", - }; - return new Date(dateString).toLocaleDateString(undefined, options); - }; - - const categories = [ - { label: "지원&안내", href: "/support" }, - { label: "QnA", href: "/support?menu=qna" }, - { label: "상세", href: "" }, - ]; +
+
+

+ {data.title} +

+
+
+ + {STATUS[data.status].name} + +
+
- return ( -
- -
- {responses.map((entry) => ( +
+ {messageList.map((entry) => (
-
-

- {entry.state === "question" ? "질문" : `답변 ${entry.id}`} -

+
+
+ {entry.userId === userId ? "Q" : "A"} +

{entry.content}

-

- {format(new Date(entry.createdAt), "yyyy-MM-dd hh:mm")} +

+ {format(new Date(entry.createdAt), "yyyy-MM-dd HH:mm")}

))}
-
-