-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat: 프로젝트 문의 페이지 초안 작성 * fix: inquiry 타입 추가 * feat: TitleRow 컴포넌트 추가 * feat: 프로젝트 문의 api 연결
- Loading branch information
Showing
9 changed files
with
376 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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%; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
127
src/components/pages/AdminInquiriesEditForm/AdminInquiriesEditForm.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} |
134 changes: 134 additions & 0 deletions
134
src/components/pages/AdminInquiriesListSection/AdminInquiriesListSection.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
</> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> {} |