-
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.
Merge pull request #62 from Invincible-Backend-Study/fe/dev
feat: 관리자 질문 등록 기능 구현
- Loading branch information
Showing
40 changed files
with
1,012 additions
and
5 deletions.
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
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,15 @@ | ||
import {axiosInstance} from "@/api/AxiosInstance"; | ||
import {END_POINT} from "@/constants/api"; | ||
|
||
interface PostImageBody { | ||
thumbnail: FormData; | ||
} | ||
|
||
interface ImageUrl { | ||
imageUrl: string; | ||
} | ||
|
||
export const postImage = async (body: PostImageBody) => { | ||
const { data } = await axiosInstance.post<ImageUrl>(END_POINT.admin.IMAGE, body.thumbnail); | ||
return data.imageUrl; | ||
} |
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 {axiosInstance} from "@/api/AxiosInstance"; | ||
import {END_POINT} from "@/constants/api"; | ||
import {Pageable} from "@/types/api"; | ||
import {QuestionSetResponse} from "@/types/admin/questionSet"; | ||
|
||
|
||
export const getQuestionSetByAdmin = async (pageable: Pageable) => { | ||
const {data} = await axiosInstance.get<QuestionSetResponse>(END_POINT.admin.QUESTION_SETS(pageable)) | ||
return data; | ||
} | ||
|
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,13 @@ | ||
import {axiosInstance} from "@/api/AxiosInstance"; | ||
import {END_POINT} from "@/constants/api"; | ||
import {QuestionSearchResponse} from "@/types/admin/question"; | ||
|
||
|
||
export const getQuestionsByAdmin = async (questionSetId: number | undefined) => { | ||
if(questionSetId === undefined){ | ||
return [] | ||
} | ||
const {data} = await axiosInstance.get<QuestionSearchResponse>(END_POINT.admin.QUESTIONS({questionSetId})) | ||
|
||
return data.questions; | ||
} |
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,8 @@ | ||
import {Question} from "@/types/admin/question"; | ||
import {axiosInstance} from "@/api/AxiosInstance"; | ||
import {END_POINT} from "@/constants/api"; | ||
|
||
|
||
export const postAdminQuestion = async (question: Question) => { | ||
await axiosInstance.post(END_POINT.admin.QUESTION, {...question, action: "CREATE"}) ; | ||
} |
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,13 @@ | ||
import {axiosInstance} from "@/api/AxiosInstance"; | ||
import {END_POINT} from "@/constants/api"; | ||
import {QuestionSetSaveBody} from "@/types/admin/questionSet"; | ||
|
||
|
||
interface QuestionSaveResponse{ | ||
questionSetId: number; | ||
} | ||
|
||
export const postAdminQuestionSet = async (body: QuestionSetSaveBody) => { | ||
const { data } = await axiosInstance.post<QuestionSaveResponse>(END_POINT.admin.QUESTION_SET, body); | ||
return data; | ||
} |
18 changes: 18 additions & 0 deletions
18
frontend/src/components/AdminEditorButton/AdminEditorButton.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,18 @@ | ||
import {Button, ButtonGroup} from "@nextui-org/react"; | ||
import {CiFloppyDisk, CiFolderOn, CiSquarePlus} from "react-icons/ci"; | ||
|
||
|
||
interface AdminEditorButtonProps { | ||
add?: () => void; | ||
file?:() => void; | ||
save?: () => void; | ||
} | ||
|
||
const AdminEditorButton = ({add, file, save}: AdminEditorButtonProps) => { | ||
return <ButtonGroup className="pt-3 pb-3" variant="ghost" radius="none"> | ||
{add && <Button isIconOnly onClick={add}><CiSquarePlus size={26}/></Button>} | ||
{file && <Button isIconOnly onClick={file}><CiFolderOn size={26}/></Button>} | ||
{save && <Button isIconOnly onClick={save}><CiFloppyDisk size={26}/></Button> } | ||
</ButtonGroup> | ||
} | ||
export default AdminEditorButton; |
37 changes: 37 additions & 0 deletions
37
frontend/src/components/QuestionSetPreview/QuestionSetPreview.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,37 @@ | ||
import QuestionSetItem from "@/components/QuestionSetItem/QuestionSetItem"; | ||
import {QuestionSetRow} from "@/types/admin/questionSet"; | ||
|
||
interface QuestionSetPreviewProps { | ||
questionSet?: QuestionSetRow | ||
} | ||
|
||
const QuestionSetPreview = ({questionSet}: QuestionSetPreviewProps) => { | ||
return ( | ||
<div className='w-full h-[50%] p-3'> | ||
<span>미리보기</span> | ||
<div className="flex flex-row gap-3"> | ||
{questionSet && <QuestionSetItem | ||
questionSet={{ | ||
questionSetId: 1, | ||
count: 10, | ||
title: questionSet.title, | ||
tailQuestionDepth: 10, | ||
description: questionSet.description, | ||
thumbnailUrl: questionSet.thumbnailUrl | ||
}} | ||
openInterviewSetting={() => {}}/>} | ||
<QuestionSetItem questionSet={{ | ||
questionSetId: 1, | ||
count: 10, | ||
title: "여기서 편집해세요", | ||
tailQuestionDepth: 10, | ||
description: "여기서 자유롭게 편집하시죠", | ||
thumbnailUrl: "https://storage.googleapis.com/mubaegseu/jpa.png" | ||
}} openInterviewSetting={() => {}}/> | ||
</div> | ||
</div> | ||
) | ||
} | ||
|
||
|
||
export default QuestionSetPreview; |
11 changes: 11 additions & 0 deletions
11
frontend/src/components/QuestionSetTable/QuestionSetActionColumn.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,11 @@ | ||
import {Button} from "@nextui-org/react"; | ||
import {CiPen, CiTrash} from "react-icons/ci"; | ||
|
||
const QuestionSetActionColumn = () => { | ||
return <> | ||
<Button isIconOnly variant="light"><CiPen size={20}/></Button> | ||
<Button isIconOnly variant="light"><CiTrash size={20}/></Button> | ||
</> | ||
} | ||
|
||
export default QuestionSetActionColumn; |
12 changes: 12 additions & 0 deletions
12
frontend/src/components/QuestionSetTable/QuestionSetDateColumn.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,12 @@ | ||
import {dateToString} from "@/utils/Date"; | ||
|
||
|
||
interface QuestionSetDateColumnProps { | ||
date?: string | Date | ||
} | ||
|
||
const QuestionSetDateColumn = ({date} : QuestionSetDateColumnProps) => { | ||
return <div>{date === undefined ? "" : dateToString(new Date(date))}</div> | ||
} | ||
|
||
export default QuestionSetDateColumn; |
93 changes: 93 additions & 0 deletions
93
frontend/src/components/QuestionSetTable/QuestionSetEditForm.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,93 @@ | ||
import {Button, Image, Input, ModalBody, ModalContent, ModalHeader, Slider} from "@nextui-org/react"; | ||
import {QuestionSetRow} from "@/types/admin/questionSet"; | ||
import {ChangeQuestionForm} from "@/components/QuestionSetTable/useQuestionSetEditForm"; | ||
import {FileUploader} from "react-drag-drop-files"; | ||
import {useCallback} from "react"; | ||
import {useImageUploadMutation} from "@/hooks/api/image/useImageUploadMutation"; | ||
import {toast} from "sonner"; | ||
import imageCompression from "browser-image-compression"; | ||
|
||
|
||
interface QuestionSetEditFormProps { | ||
form: QuestionSetRow; | ||
change: ChangeQuestionForm; | ||
confirm:() => void; | ||
} | ||
|
||
const QuestionSetEditForm = ({confirm, change, form: {description, title, thumbnailUrl, defaultTailQuestionDepth}}: QuestionSetEditFormProps) => { | ||
const imageUploadMutation = useImageUploadMutation(); | ||
|
||
|
||
const compressImage = useCallback(async (originalImageFile: File) => { | ||
let imageFile: File; | ||
try { | ||
const compressedImageFile = await imageCompression( | ||
originalImageFile, | ||
{ | ||
maxSizeMB: 1.5 | ||
} | ||
); | ||
|
||
const fileName = originalImageFile.name; | ||
|
||
const fileType = compressedImageFile.type; | ||
|
||
imageFile = new File([compressedImageFile], fileName, { type: fileType }); | ||
} catch (e) { | ||
imageFile = originalImageFile; | ||
} | ||
|
||
return imageFile; | ||
}, []); | ||
|
||
|
||
const handleChange = useCallback(async(file: File) => { | ||
|
||
const compressedFile = await compressImage(file); | ||
const formData = new FormData(); | ||
formData.append("thumbnail", compressedFile); | ||
|
||
imageUploadMutation.mutate({thumbnail: formData}, { | ||
onSuccess: (thumbnailUrl) => { | ||
change("thumbnailUrl", thumbnailUrl) | ||
}, | ||
onError: () => { | ||
toast.error("이미지 업로드에 실패했습니다.") | ||
} | ||
}); | ||
}, []); | ||
|
||
|
||
|
||
|
||
|
||
return ( | ||
<ModalContent> | ||
{onClose => ( | ||
<> | ||
<ModalHeader>질문 집합 등록/편집 폼</ModalHeader> | ||
<ModalBody> | ||
<span className="text-sm">바디용</span> | ||
|
||
<Input placeholder={"제목"} value={title} onValueChange={(t) => change("title", t)}/> | ||
|
||
<Input placeholder={"설명"} value={description} onValueChange={t => change("description",t)}/> | ||
|
||
<Slider label="기본 꼬리질문 개수" minValue={0} maxValue={20} defaultValue={defaultTailQuestionDepth} onChange={(n) => typeof n === 'number' && change("defaultTailQuestionDepth", n)}/> | ||
|
||
<FileUploader label="이미지를 올려주세요" handleChange={handleChange} name="file" types={["PNG", "JPEG", "WEBP"]}/> | ||
|
||
{thumbnailUrl && <Image src={thumbnailUrl}/>} | ||
|
||
<Button onClick={() => { | ||
confirm(); | ||
onClose(); | ||
}}>등록</Button> | ||
|
||
</ModalBody> | ||
</> | ||
)} | ||
</ModalContent> | ||
) | ||
} | ||
export default QuestionSetEditForm; |
10 changes: 10 additions & 0 deletions
10
frontend/src/components/QuestionSetTable/QuestionSetIdColumn.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,10 @@ | ||
|
||
|
||
interface QuestionSetIdColumnProps { | ||
id: number; | ||
} | ||
|
||
const QuestionSetIdColumn = ({id}: QuestionSetIdColumnProps) => { | ||
return <div>{id >= 0 ? id : "NEW"}</div> | ||
} | ||
export default QuestionSetIdColumn; |
100 changes: 100 additions & 0 deletions
100
frontend/src/components/QuestionSetTable/QuestionSetTable.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,100 @@ | ||
import { | ||
ScrollShadow, | ||
Spinner, | ||
Table, | ||
TableBody, | ||
TableCell, | ||
TableColumn, | ||
TableHeader, | ||
TableRow | ||
} from "@nextui-org/react"; | ||
import {columns} from "@/components/QuestionSetTable/QuestionSetTableConstant"; | ||
import {QuestionSetRow} from "@/types/admin/questionSet"; | ||
import useQuestionSetTable from "@/components/QuestionSetTable/useQuestionSetTable"; | ||
import React, {useCallback, useMemo} from "react"; | ||
import QuestionSetIdColumn from "@/components/QuestionSetTable/QuestionSetIdColumn"; | ||
import QuestionSetActionColumn from "@/components/QuestionSetTable/QuestionSetActionColumn"; | ||
import QuestionSetTextColumn from "@/components/QuestionSetTable/QuestionSetTextColumn"; | ||
import InterviewHistoryPagination from "@/components/InterviewHistoryTable/InterviewHistoryPagination"; | ||
|
||
|
||
interface QuestionSetTableProps { | ||
rows: QuestionSetRow[], | ||
page: number; | ||
totalPages?: number; | ||
changePage?: (e: number) => void; | ||
isLoading: boolean, | ||
selectionChange: (e: number) => void; | ||
} | ||
|
||
|
||
const QuestionSetTable = ({rows, page, totalPages, isLoading, changePage, selectionChange}: QuestionSetTableProps) => { | ||
const {tableClassNames} = useQuestionSetTable(); | ||
|
||
const bottomContent = useMemo(() => { | ||
return <InterviewHistoryPagination page={page} totalPages={totalPages} changePage={changePage}/> | ||
},[page, totalPages]) | ||
|
||
const renderCell = useCallback((item: QuestionSetRow, cellType: React.Key) => { | ||
const cellValue = item[cellType as keyof QuestionSetRow]; | ||
|
||
if(cellType === "questionSetId") { | ||
return <QuestionSetIdColumn id={item.questionSetId}/> | ||
} | ||
|
||
if(cellType === "action") { | ||
return <QuestionSetActionColumn/> | ||
} | ||
|
||
return <QuestionSetTextColumn text={cellValue}/> | ||
}, []) | ||
|
||
return( | ||
<ScrollShadow className="max-h-full"> | ||
<Table | ||
aria-label="simple table" | ||
isCompact | ||
removeWrapper | ||
onSelectionChange={(selection) => { | ||
if([...selection].length === 0) { | ||
selectionChange(-1); | ||
} | ||
for(const k of selection) { | ||
selectionChange(Number(k)); | ||
} | ||
}} | ||
color="success" | ||
selectionMode="single" | ||
classNames={tableClassNames} | ||
|
||
bottomContent={bottomContent} | ||
bottomContentPlacement="outside" | ||
> | ||
<TableHeader columns={columns}> | ||
{(column) => ( | ||
<TableColumn | ||
key={column.uid} | ||
align={column.uid === "actions" ? "center" : "start"} | ||
width={"10%"} | ||
allowsSorting={column.sortable} | ||
> | ||
{column.name} | ||
</TableColumn> | ||
)} | ||
</TableHeader> | ||
<TableBody | ||
loadingContent={<Spinner label="Loading..." />} | ||
isLoading={isLoading} | ||
items={rows} emptyContent={"질문 이력이 없습니다."}> | ||
{(row) => ( | ||
<TableRow key={row.questionSetId}> | ||
{(columnKey) => <TableCell>{renderCell(row, columnKey)}</TableCell>} | ||
</TableRow> | ||
)} | ||
</TableBody> | ||
</Table> | ||
</ScrollShadow> | ||
) | ||
} | ||
|
||
export default QuestionSetTable; |
11 changes: 11 additions & 0 deletions
11
frontend/src/components/QuestionSetTable/QuestionSetTableConstant.ts
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 @@ | ||
|
||
|
||
|
||
export const columns = [ | ||
{name: "질문 집합 ID", uid: "questionSetId", sortable: true}, | ||
{name: "질문 집합 제목", uid: "title"}, | ||
{name: "질문 집합 설명", uid: "description"}, | ||
{name: "이미지 주소", uid: "thumbnailUrl"}, | ||
{name: "액션", uid:"action" } | ||
]; | ||
|
5 changes: 5 additions & 0 deletions
5
frontend/src/components/QuestionSetTable/QuestionSetTablePagination.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,5 @@ | ||
|
||
|
||
const QuestionSetTablePagination = () => { | ||
|
||
} |
Oops, something went wrong.