Skip to content

Commit

Permalink
[FEAT] 프로젝트 문의 페이지 추가 (#110)
Browse files Browse the repository at this point in the history
* feat: 프로젝트 문의 페이지 초안 작성

* fix: inquiry 타입 추가

* feat: TitleRow 컴포넌트 추가

* feat: 프로젝트 문의 api 연결
  • Loading branch information
hynseok authored Nov 14, 2024
1 parent cedb6db commit 287fd2d
Show file tree
Hide file tree
Showing 9 changed files with 376 additions and 1 deletion.
11 changes: 11 additions & 0 deletions src/app/(admin)/admin/projects/inquiries/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { PageHeader } from "@/components/common/PageHeader";
import { AdminInquiriesEditForm } from "@/components/pages/AdminInquiriesEditForm/AdminInquiriesEditForm";

export default function AdminInquiriesPage({ params }: { params: { id: string } }) {
return (
<main>
<PageHeader title={"프로젝트 문의 답변"} />
<AdminInquiriesEditForm id={params.id} />
</main>
);
}
10 changes: 9 additions & 1 deletion src/app/(admin)/admin/projects/inquiries/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
import { PageHeader } from "@/components/common/PageHeader";
import AdminInquiriesListSection from "@/components/pages/AdminInquiriesListSection/AdminInquiriesListSection";

export default function AdminInquiriesPage() {
return <main>Hello, world!</main>;
return (
<main>
<PageHeader title={"프로젝트 문의 게시판 관리"} />
<AdminInquiriesListSection />
</main>
);
}
14 changes: 14 additions & 0 deletions src/components/common/TitleRow/TitleRow.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
.title {
font-weight: var(--mantine-other-font-weights-bold);
font-size: 24px;
}

.wrapper {
min-width: 500px;
padding-left: 18px;
padding-right: 20px;
margin-top: 12px;
margin-bottom: 12px;
height: 56px;
width: 100%;
}
24 changes: 24 additions & 0 deletions src/components/common/TitleRow/TitleRow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Group, MantineStyleProp, Text } from "@mantine/core";
import { ReactNode } from "react";
import classes from "./TitleRow.module.css";

interface Props {
title: ReactNode;
badge?: ReactNode;
subString?: ReactNode;
style?: MantineStyleProp;
}

function TitleRow({ title, badge, subString, style }: Props) {
return (
<Group gap={0} className={classes.wrapper} justify="space-between" style={style}>
<Group>
<Text className={classes.title}>{title}</Text>
{badge}
</Group>
<Text c="dimmed">{subString}</Text>
</Group>
);
}

export default TitleRow;
127 changes: 127 additions & 0 deletions src/components/pages/AdminInquiriesEditForm/AdminInquiriesEditForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
"use client";

import { PrimaryButton } from "@/components/common/Buttons";
import { Row } from "@/components/common/Row";
import { Section } from "@/components/common/Section";
import { Group, Stack, Textarea, TextInput } from "@mantine/core";
import { isNotEmpty, useForm } from "@mantine/form";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import TitleRow from "@/components/common/TitleRow/TitleRow";
import { Inquiry } from "@/types/inquiry";
import { CommonAxios } from "@/utils/CommonAxios";

interface InquiryEditFormInputs {
title: string;
content: string;
}

export function AdminInquiriesEditForm({ id }: { id: string }) {
/* next 라우터, 페이지 이동에 이용 */
const { push } = useRouter();

const [inquiry, setInquiry] = useState<Inquiry | null>(null);
const [prevReplyFlag, setPrevReplyFlag] = useState<boolean>(false);

// 문의 답변 등록 및 수정을 위한 mantine form hook
const { onSubmit, getInputProps, setValues } = useForm<InquiryEditFormInputs>({
initialValues: {
title: "",
content: "",
},
validate: {
title: isNotEmpty("제목을 입력해주세요."),
content: isNotEmpty("내용을 입력해주세요."),
},
});

/* id를 통해 데이터 패칭 */
useEffect(() => {
const fetchInquiry = async () => {
const response = await CommonAxios.get(`/inquiries/${id}`);
setInquiry(response.data);
console.log(response.data);
};

const fetchPrevReply = async () => {
const response = await CommonAxios.get(`/inquiries/${id}/reply`);
if (response.data.title.length > 0) {
setPrevReplyFlag(true);
setValues(response.data);
}
};

if (id) {
fetchInquiry();
try {
fetchPrevReply();
} catch (error) {
console.error(error);
}
}
}, [id]);

// 문의 답변 등록/수정 request 함수
const handleSubmit = async (values: InquiryEditFormInputs) => {
try {
if (prevReplyFlag) {
await CommonAxios.put(`/inquiries/${id}/reply`, values);
} else {
await CommonAxios.post(`/inquiries/${id}/reply`, values);
}
push("../inquiries");
} catch (error) {
console.error(error);
}
};

return (
<Section>
<TitleRow title="문의 내용" />
<Stack gap="lg">
<Row field="프로젝트명" fieldSize={150}>
{inquiry?.projectName}
</Row>
<Row field="작성자" fieldSize={150}>
{inquiry?.authorName}
</Row>
<Row field="제목" fieldSize={150}>
{inquiry?.title}
</Row>
<Row field="내용" fieldSize={150}>
{inquiry?.content}
</Row>
</Stack>
<TitleRow title="문의 답변" />
<form onSubmit={onSubmit(handleSubmit)}>
<Stack gap="lg">
<Row field="제목" fieldSize={150}>
<TextInput id="input-title" {...getInputProps("title")} w={"90%"} />
</Row>
<Row field="내용" fieldSize={150}>
<Textarea
id="input-content"
w={"90%"}
minRows={20}
autosize
resize="vertical"
{...getInputProps("content")}
/>
</Row>
<Group justify="center">
<PrimaryButton
onClick={() => {
push("../inquiries");
}}
>
목록으로
</PrimaryButton>
<PrimaryButton key="register" type="submit">
답변 등록
</PrimaryButton>
</Group>
</Stack>
</form>
</Section>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
"use client";

import { DangerButton } from "@/components/common/Buttons";
import { DataTable } from "@/components/common/DataTable";
import { DataTableData } from "@/components/common/DataTable/elements/DataTableData";
import { DataTableRow } from "@/components/common/DataTable/elements/DataTableRow";
import { INQUIRIES_TABLE_HEADERS } from "@/constants/DataTableHeaders";
import { Button, Checkbox, Group, Stack } from "@mantine/core";
import { useInquiries } from "@/hooks/swr/useInquiries";
import { ChangeEvent, useState } from "react";
import { useTableSort } from "@/hooks/useTableSort";
import { PAGE_SIZES } from "@/constants/PageSize";
import { CommonAxios } from "@/utils/CommonAxios";
import { SearchInput } from "@/components/common/SearchInput";
import { handleChangeSearch } from "@/utils/handleChangeSearch";
import { useDebouncedState } from "@mantine/hooks";
import { PagedInquiriesRequestParams } from "@/types/inquiry";
import { useRouter } from "next/navigation";

export default function AdminInquiriesListSection() {
const { push } = useRouter();

/* 페이지당 행 개수 */
const [pageSize, setPageSize] = useState<string | null>(String(PAGE_SIZES[0]));
/* 페이지네이션 페이지 넘버*/
const [pageNumber, setPageNumber] = useState(1);
/* 데이터 정렬 훅 */
const { sortBy, order, handleSortButton } = useTableSort();

/* 쿼리 debounced state, 검색창에 이용 */
const [query, setQuery] = useDebouncedState<PagedInquiriesRequestParams>(
{
page: pageNumber - 1,
size: Number(pageSize),
},
300
);

/* SWR 훅을 사용하여 공지사항 목록 패칭 */
// TODO: 백엔드 수정 이후 sort 파라미터 추가
const { data, pageData, mutate } = useInquiries({
params: { ...query, page: pageNumber - 1, size: Number(pageSize) },
});

/* 체크박스 전체선택, 일괄선택 다루는 파트 */
const [selectedInquiries, setSelectedInquiries] = useState<number[]>([]);
const allChecked = selectedInquiries.length === data?.length;
const indeterminate = selectedInquiries.length > 0 && !allChecked;
// 전체선택 함수
const handleSelectAll = () => {
if (data) {
if (allChecked) {
setSelectedInquiries([]);
} else {
setSelectedInquiries(data.map((application) => application.id));
}
}
};
// 개별선택 함수
const handleSelect = (id: number) => {
setSelectedInquiries((prev) =>
prev.includes(id) ? prev.filter((applicationId) => applicationId !== id) : [...prev, id]
);
};

/* 삭제 버튼 핸들러 */
const handleDelete = () => {
// TODO: 삭제 확인하는 모달 추가
Promise.all(selectedInquiries.map((id) => CommonAxios.delete(`/inquiries/${id}`))).then(() => {
setSelectedInquiries([]);
mutate();
});
};

/* 검색창 핸들러 */
const onSearchChange = (event: ChangeEvent<HTMLInputElement>) => {
handleChangeSearch<string, PagedInquiriesRequestParams>({
name: "title",
value: event.target.value,
setQuery,
});
};

return (
<>
<Stack>
<Group justify="flex-end">
<SearchInput placeholder="제목 입력" w={300} onChange={onSearchChange} />
</Group>
<Group justify="flex-start">
<DangerButton onClick={handleDelete}>선택 삭제</DangerButton>
</Group>
<DataTable
headers={INQUIRIES_TABLE_HEADERS}
sortBy={sortBy}
order={order}
handleSortButton={handleSortButton}
totalElements={String(pageData?.totalElements)}
pageSize={pageSize}
setPageSize={setPageSize}
totalPages={pageData?.totalPages}
pageNumber={pageNumber}
setPageNumber={setPageNumber}
withCheckbox
checkboxProps={{ checked: allChecked, indeterminate, onChange: handleSelectAll }}
>
{data?.map((inquiry, index) => (
<DataTableRow key={index}>
<DataTableData text={false}>
<Checkbox
checked={selectedInquiries.includes(inquiry.id)}
onChange={() => handleSelect(inquiry.id)}
/>
</DataTableData>
<DataTableData>{index + 1 + (pageNumber - 1) * Number(pageSize)}</DataTableData>
<DataTableData>{inquiry.title}</DataTableData>
<DataTableData>{inquiry.authorName}</DataTableData>
<DataTableData>{inquiry.createdAt}</DataTableData>
<DataTableData text={false}>
<Button
onClick={() => {
push(`inquiries/${inquiry.id}`);
}}
>
수정
</Button>
</DataTableData>
</DataTableRow>
))}
</DataTable>
</Stack>
</>
);
}
8 changes: 8 additions & 0 deletions src/constants/DataTableHeaders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,11 @@ export const QUIZ_TABLE_HEADERS: DataTableHeaderProps[] = [
{ label: "전화번호", widthPercentage: 15, sort: true, selector: "phone" },
{ label: "푼 문제 개수", widthPercentage: 15, sort: true, selector: "successCount" },
];

export const INQUIRIES_TABLE_HEADERS: DataTableHeaderProps[] = [
{ label: "순번", widthPercentage: 7, sort: true, selector: "id" },
{ label: "제목", widthPercentage: 15, sort: true, selector: "title" },
{ label: "작성자", widthPercentage: 7, sort: false },
{ label: "작성일", widthPercentage: 7, sort: false },
{ label: "관리", widthPercentage: 7, sort: false },
];
30 changes: 30 additions & 0 deletions src/hooks/swr/useInquiries.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"use client";

import { PagedInquiriesRequestParams, PagedInquiriesResponse } from "@/types/inquiry";
import useSWR from "swr";

export function useInquiries({ params }: { params: PagedInquiriesRequestParams }) {
const result = useSWR<PagedInquiriesResponse>({
url: "/inquiries",
query: params,
});

return {
data: result.data?.content,
pageData: result.data && {
pageSize: result.data.size,
pageNumber: result.data.number,
totalElements: result.data.totalElements,
totalPages: result.data.totalPages,
},
get isLoading() {
return result.isLoading;
},
get error() {
return result.error;
},
get mutate() {
return result.mutate;
},
};
}
19 changes: 19 additions & 0 deletions src/types/inquiry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { PagedApiRequestParams, PagedApiResponse } from "./common";

export interface PagedInquiriesRequestParams extends PagedApiRequestParams {
title?: string;
sort?: string;
}

export interface Inquiry {
id: number;
authorName: string;
projectId: number;
projectName: string;
title: string;
content: string;
createdAt: string;
updatedAt: string;
}

export interface PagedInquiriesResponse extends PagedApiResponse<Inquiry> {}

0 comments on commit 287fd2d

Please sign in to comment.