Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file removed client/public/kakao_default_profile.jpeg
Binary file not shown.
140 changes: 140 additions & 0 deletions client/src/app/admin/community/boards/board.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
"use client"

import {
Card,
CardContent,
IconButton,
MenuItem,
Select,
Stack,
TextField,
Typography,
} from "@mui/material"
import DeleteIcon from "@mui/icons-material/Delete"
import EditIcon from "@mui/icons-material/Edit"
import SaveIcon from "@mui/icons-material/Save"
import { deleteBoard } from "@/app/community/community.api"
import { useState } from "react"
import axios from "@/config/axios"

export default function Board({
board,
load,
success,
error,
}: {
board: any
load: () => Promise<void>
success: (msg: string) => void
error: (msg: string) => void
}) {
const [editing, setEditing] = useState(false)
const [name, setName] = useState(board.name)
const [slug, setSlug] = useState(board.slug)
const [boardType, setBoardType] = useState(board.type)

async function handleEdit(id: string) {
setEditing(true)
}

async function handleDelete(id: string) {
const ok = confirm(
"정말로 게시판을 삭제하시겠습니까? (관련 게시글은 삭제됩니다)",
)
if (!ok) return
try {
await deleteBoard(id)
await load()
success("게시판이 삭제되었습니다.")
} catch (err) {
console.error(err)
error("게시판 삭제 실패")
}
}

async function handleSave(id: string) {
if (!name.trim() || !slug.trim()) {
error("이름과 슬러그를 입력하세요.")
return
}
try {
await axios.put(`/community/boards/${id}`, {
name,
slug,
})
setEditing(false)
await load()
success("게시판이 수정되었습니다.")
} catch (err) {
console.error(err)
error("게시판 수정 실패")
}
}

return (
<Card key={board.id}>
<CardContent>
<Stack
direction="row"
justifyContent="space-between"
alignItems="center"
>
<Stack
spacing={0.5}
direction="row"
justifyContent="center"
alignItems="center"
>
{editing ? (
<TextField
label="이름"
value={name}
onChange={(e) => setName(e.target.value)}
/>
) : (
<Typography fontWeight={800}>{name}</Typography>
)}
{editing ? (
<TextField
label="slug"
value={slug}
onChange={(e) => setSlug(e.target.value)}
/>
) : (
<Typography variant="caption" color="text.secondary">
{slug} · {board.visibility}
</Typography>
)}
</Stack>
<Stack direction="row" spacing={1} alignItems="center">
{editing ? (
<Select
value={boardType}
onChange={(e) => setBoardType(e.target.value)}
>
<MenuItem value="free">자유 게시판</MenuItem>
<MenuItem value="qna">Q&A 게시판</MenuItem>
</Select>
) : (
<Typography variant="caption" color="text.secondary">
{boardType === "free" ? "자유 게시판" : "Q&A 게시판"}
</Typography>
)}
</Stack>
</Stack>
{editing ? (
<IconButton color="success" onClick={() => handleSave(board.id)}>
<SaveIcon />
</IconButton>
) : (
<IconButton color="primary" onClick={() => handleEdit(board.id)}>
<EditIcon />
</IconButton>
)}
<IconButton color="error" onClick={() => handleDelete(board.id)}>
<DeleteIcon />
</IconButton>
</CardContent>
</Card>
)
}
163 changes: 163 additions & 0 deletions client/src/app/admin/community/boards/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
"use client"

import { useEffect, useState } from "react"
import {
Box,
Button,
Card,
CardContent,
CircularProgress,
FormControl,
InputLabel,
MenuItem,
Select,
SelectChangeEvent,
Stack,
TextField,
Typography,
IconButton,
} from "@mui/material"
import useAuth from "@/hooks/useAuth"
import { useNotification } from "@/hooks/useNotification"
import { fetchBoards, createBoard } from "@/app/community/community.api"
import { CommunityBoard } from "@/app/community/community.types"
import Board from "./board"

export default function AdminCommunityBoardsPage() {
const { isAdminIfNotExit } = useAuth()
const { success, error } = useNotification()
const [loading, setLoading] = useState(true)
const [boards, setBoards] = useState<CommunityBoard[]>([])

const [name, setName] = useState("")
const [slug, setSlug] = useState("")
const [boardType, setBoardType] = useState("free")
const [creating, setCreating] = useState(false)

useEffect(() => {
if (!isAdminIfNotExit("/admin/community/boards")) return
load()
}, [])

async function load() {
try {
setLoading(true)
const list = await fetchBoards()
setBoards(list)
} catch (err) {
console.error(err)
error("게시판 목록을 불러오지 못했습니다.")
} finally {
setLoading(false)
}
}

async function handleCreate() {
if (!name.trim() || !slug.trim()) {
error("이름과 슬러그를 입력하세요.")
return
}
try {
setCreating(true)
await createBoard({
name: name.trim(),
slug: slug.trim(),
description: "",
boardType: boardType as "free" | "qna",
})
setName("")
setSlug("")
setBoardType("free")
await load()
success("게시판이 생성되었습니다.")
} catch (err: any) {
console.error(err)
error(err?.response?.data?.error || "게시판 생성 실패")
} finally {
setCreating(false)
}
}

if (loading)
return (
<Stack
alignItems="center"
justifyContent="center"
sx={{ minHeight: 200 }}
>
<CircularProgress />
</Stack>
)

return (
<Box sx={{ px: { xs: 2, sm: 3 }, py: { xs: 2, sm: 3 } }}>
<Stack spacing={2} sx={{ maxWidth: 980, mx: "auto" }}>
<Card>
<CardContent>
<Stack spacing={1}>
<Typography variant="h5" fontWeight={800}>
게시판 관리
</Typography>
<Typography color="text.secondary">
관리자가 보드 생성/삭제를 할 수 있습니다.
</Typography>
</Stack>
</CardContent>
</Card>

<Card>
<CardContent>
<Stack
direction={{ xs: "column", sm: "row" }}
spacing={2}
alignItems="center"
>
<TextField
label="이름"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<TextField
label="slug"
value={slug}
onChange={(e) => setSlug(e.target.value)}
/>
<FormControl sx={{ minWidth: 140 }}>
<InputLabel>유형</InputLabel>
<Select
value={boardType}
label="유형"
onChange={(e: SelectChangeEvent) =>
setBoardType(e.target.value)
}
>
<MenuItem value="free">자유 게시판</MenuItem>
<MenuItem value="qna">Q&A 게시판</MenuItem>
</Select>
</FormControl>
<Button
variant="contained"
onClick={handleCreate}
disabled={creating}
>
생성
</Button>
</Stack>
</CardContent>
</Card>

<Stack spacing={1}>
{boards.map((b) => (
<Board
key={b.id}
board={b}
load={load}
success={success}
error={error}
/>
))}
</Stack>
</Stack>
</Box>
)
}
Loading
Loading