feat: [게시판] 게시판 관리 UI 개선

- BoardDetail, BoardForm, DynamicBoard 폼 개선
- CommentItem, BoardDetailClientV2 UI 개선
- 게시판 페이지 라우팅 개선

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
유병철
2026-02-25 22:33:06 +09:00
parent 81a016ada9
commit dc7e152311
9 changed files with 130 additions and 97 deletions

View File

@@ -10,7 +10,8 @@ import { DynamicBoardEditForm } from '@/components/board/DynamicBoard/DynamicBoa
import { useState, useEffect, useCallback, Suspense } from 'react';
import { DetailPageSkeleton } from '@/components/ui/skeleton';
import { format } from 'date-fns';
import { ArrowLeft, Pencil, Trash2, MessageSquare, Eye } from 'lucide-react';
import { ArrowLeft, Edit, Trash2, MessageSquare, Eye } from 'lucide-react';
import { useMenuStore } from '@/stores/menuStore';
import { PageLayout } from '@/components/organisms/PageLayout';
import { PageHeader } from '@/components/organisms/PageHeader';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
@@ -100,6 +101,7 @@ function DetailModeRouter() {
// 실제 상세 컴포넌트 (자체 hooks 사용)
function DynamicBoardDetailContent({ boardCode, postId }: { boardCode: string; postId: string }) {
const router = useRouter();
const sidebarCollapsed = useMenuStore((state) => state.sidebarCollapsed);
// 게시판 정보
const [boardName, setBoardName] = useState<string>('게시판');
@@ -261,6 +263,7 @@ function DynamicBoardDetailContent({ boardCode, postId }: { boardCode: string; p
icon={MessageSquare}
/>
<div className="pb-24">
{/* 게시글 상세 */}
<Card className="mb-6">
<CardHeader>
@@ -282,9 +285,9 @@ function DynamicBoardDetailContent({ boardCode, postId }: { boardCode: string; p
</div>
</div>
{isAuthor && (
<div className="flex items-center gap-2">
<div className="hidden md:flex items-center gap-2">
<Button variant="outline" size="sm" onClick={handleEdit}>
<Pencil className="h-4 w-4 mr-1" />
<Edit className="h-4 w-4 mr-1" />
</Button>
<Button variant="destructive" size="sm" onClick={() => setShowDeleteDialog(true)}>
@@ -400,13 +403,33 @@ function DynamicBoardDetailContent({ boardCode, postId }: { boardCode: string; p
</div>
</CardContent>
</Card>
</div>
{/* 하단 버튼 */}
<div className="mt-6 flex justify-start">
<Button variant="outline" onClick={handleBack}>
<ArrowLeft className="h-4 w-4 mr-2" />
{/* 하단 액션 버튼 (sticky) */}
<div
className={`fixed bottom-4 left-4 right-4 px-4 py-3 bg-background/95 backdrop-blur rounded-xl border shadow-lg z-50 transition-all duration-300 md:bottom-6 md:px-6 md:right-[48px] ${sidebarCollapsed ? 'md:left-[156px]' : 'md:left-[316px]'} flex items-center justify-between`}
>
<Button variant="outline" onClick={handleBack} size="sm" className="md:size-default">
<ArrowLeft className="w-4 h-4 md:mr-2" />
<span className="hidden md:inline"></span>
</Button>
{isAuthor && (
<div className="flex items-center gap-1 md:gap-2">
<Button
variant="outline"
onClick={() => setShowDeleteDialog(true)}
size="sm"
className="text-destructive hover:bg-destructive hover:text-destructive-foreground md:size-default"
>
<Trash2 className="w-4 h-4 md:mr-2" />
<span className="hidden md:inline"></span>
</Button>
<Button onClick={handleEdit} size="sm" className="md:size-default">
<Edit className="w-4 h-4 md:mr-2" />
<span className="hidden md:inline"></span>
</Button>
</div>
)}
</div>
{/* 게시글 삭제 확인 다이얼로그 */}

View File

@@ -223,7 +223,7 @@ function DynamicBoardListContent({ boardCode }: { boardCode: string }) {
const handleRowClick = useCallback(
(item: BoardPost) => {
router.push(`/ko/boards/${boardCode}/${item.id}`);
router.push(`/ko/boards/${boardCode}/${item.id}?mode=view`);
},
[router, boardCode]
);

View File

@@ -125,17 +125,11 @@ export function BoardDetail({ post, comments: initialComments, currentUserId }:
</h2>
{/* 메타 정보: 작성자 | 날짜 | 조회수 */}
<div className="flex items-center gap-2 text-sm text-gray-500 mt-2">
<span>{post.authorName}</span>
{post.authorDepartment && (
<>
<span className="text-gray-300">|</span>
<span>{post.authorDepartment}</span>
</>
)}
<span className="text-gray-300">|</span>
<div className="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-2 text-sm text-gray-500 mt-2">
<span>{post.authorName}{post.authorDepartment ? ` / ${post.authorDepartment}` : ''}</span>
<span className="hidden sm:inline text-gray-300">|</span>
<span>{format(new Date(post.createdAt), 'yyyy-MM-dd HH:mm')}</span>
<span className="text-gray-300">|</span>
<span className="hidden sm:inline text-gray-300">|</span>
<span> {post.viewCount}</span>
</div>
</CardHeader>

View File

@@ -7,6 +7,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { ClipboardList, ArrowLeft, Edit, Trash2 } from 'lucide-react';
import { useMenuStore } from '@/stores/menuStore';
import type { Board } from './types';
import {
BOARD_STATUS_LABELS,
@@ -33,6 +34,7 @@ const formatDateTime = (dateString: string): string => {
export function BoardDetail({ board, onEdit, onDelete }: BoardDetailProps) {
const router = useRouter();
const sidebarCollapsed = useMenuStore((state) => state.sidebarCollapsed);
const handleBack = () => {
router.push('/ko/board/board-management');
@@ -54,7 +56,7 @@ export function BoardDetail({ board, onEdit, onDelete }: BoardDetailProps) {
icon={ClipboardList}
/>
<div className="space-y-6">
<div className="space-y-6 pb-24">
{/* 게시판 정보 */}
<Card>
<CardHeader className="flex flex-row items-center justify-between">
@@ -93,28 +95,36 @@ export function BoardDetail({ board, onEdit, onDelete }: BoardDetailProps) {
</CardContent>
</Card>
{/* 버튼 영역 */}
<div className="flex items-center justify-between">
<Button variant="outline" onClick={handleBack}>
<ArrowLeft className="w-4 h-4 mr-2" />
</div>
{/* 하단 액션 버튼 (sticky) */}
<div
className={`fixed bottom-4 left-4 right-4 px-4 py-3 bg-background/95 backdrop-blur rounded-xl border shadow-lg z-50 transition-all duration-300 md:bottom-6 md:px-6 md:right-[48px] ${sidebarCollapsed ? 'md:left-[156px]' : 'md:left-[316px]'} flex items-center justify-between`}
>
<Button variant="outline" onClick={handleBack} size="sm" className="md:size-default">
<ArrowLeft className="w-4 h-4 md:mr-2" />
<span className="hidden md:inline"></span>
</Button>
<div className="flex items-center gap-2">
<div className="flex items-center gap-1 md:gap-2">
{onDelete && (
<Button variant="outline" onClick={onDelete} className="text-destructive hover:bg-destructive hover:text-destructive-foreground">
<Trash2 className="w-4 h-4 mr-2" />
<Button
variant="outline"
onClick={onDelete}
size="sm"
className="text-destructive hover:bg-destructive hover:text-destructive-foreground md:size-default"
>
<Trash2 className="w-4 h-4 md:mr-2" />
<span className="hidden md:inline"></span>
</Button>
)}
{onEdit && (
<Button onClick={onEdit}>
<Edit className="w-4 h-4 mr-2" />
<Button onClick={onEdit} size="sm" className="md:size-default">
<Edit className="w-4 h-4 md:mr-2" />
<span className="hidden md:inline"></span>
</Button>
)}
</div>
</div>
</div>
</PageLayout>
);
}

View File

@@ -93,9 +93,10 @@ export function BoardDetailClientV2({ boardId, initialMode }: BoardDetailClientV
// URL 쿼리 변경 감지
useEffect(() => {
if (!isNewMode && modeFromQuery === 'edit') {
if (isNewMode) return;
if (modeFromQuery === 'edit') {
setMode('edit');
} else if (!isNewMode && !modeFromQuery) {
} else {
setMode('view');
}
}, [modeFromQuery, isNewMode]);

View File

@@ -17,7 +17,8 @@ import {
SelectValue,
} from '@/components/ui/select';
import { Checkbox } from '@/components/ui/checkbox';
import { ClipboardList, ArrowLeft, Save } from 'lucide-react';
import { ClipboardList, ArrowLeft, Save, X } from 'lucide-react';
import { useMenuStore } from '@/stores/menuStore';
import type { Board, BoardFormData, BoardTarget, BoardStatus } from './types';
import { BOARD_TARGETS, BOARD_STATUS_LABELS } from './types';
@@ -62,6 +63,7 @@ const getCurrentDateTime = (): string => {
export function BoardForm({ mode, board, onSubmit }: BoardFormProps) {
const router = useRouter();
const sidebarCollapsed = useMenuStore((state) => state.sidebarCollapsed);
const [formData, setFormData] = useState<BoardFormData>({
target: 'all',
targetName: '',
@@ -129,7 +131,7 @@ export function BoardForm({ mode, board, onSubmit }: BoardFormProps) {
icon={ClipboardList}
/>
<form onSubmit={handleSubmit} className="space-y-6">
<form onSubmit={handleSubmit} className="space-y-6 pb-24">
{/* 게시판 정보 */}
<Card>
<CardHeader>
@@ -254,18 +256,21 @@ export function BoardForm({ mode, board, onSubmit }: BoardFormProps) {
</CardContent>
</Card>
{/* 버튼 영역 */}
<div className="flex items-center justify-between">
<Button type="button" variant="outline" onClick={handleBack}>
<ArrowLeft className="w-4 h-4 mr-2" />
</form>
{/* 하단 액션 버튼 (sticky) */}
<div
className={`fixed bottom-4 left-4 right-4 px-4 py-3 bg-background/95 backdrop-blur rounded-xl border shadow-lg z-50 transition-all duration-300 md:bottom-6 md:px-6 md:right-[48px] ${sidebarCollapsed ? 'md:left-[156px]' : 'md:left-[316px]'} flex items-center justify-between`}
>
<Button type="button" variant="outline" onClick={handleBack} size="sm" className="md:size-default">
<X className="w-4 h-4 md:mr-2" />
<span className="hidden md:inline"></span>
</Button>
<Button type="submit">
<Save className="w-4 h-4 mr-2" />
{mode === 'create' ? '등록' : '저장'}
<Button onClick={() => onSubmit(formData)} size="sm" className="md:size-default">
<Save className="w-4 h-4 md:mr-2" />
<span className="hidden md:inline">{mode === 'create' ? '등록' : '저장'}</span>
</Button>
</div>
</form>
</PageLayout>
);
}

View File

@@ -12,7 +12,7 @@
import { useState, useCallback, memo } from 'react';
import { format } from 'date-fns';
import { User, Pencil, Trash2 } from 'lucide-react';
import { User, Edit, Trash2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog';
@@ -90,7 +90,7 @@ export const CommentItem = memo(function CommentItem({
{/* 댓글 내용 */}
<div className="flex-1 min-w-0">
{/* 작성자 정보 + 날짜 + 버튼 */}
<div className="flex items-center justify-between gap-2 mb-1">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-1 sm:gap-2 mb-1">
<div className="flex items-center gap-2 text-sm">
<span className="font-medium text-gray-900">{authorInfo}</span>
<span className="text-gray-400">
@@ -107,7 +107,7 @@ export const CommentItem = memo(function CommentItem({
className="h-7 px-2 text-gray-500 hover:text-gray-700"
onClick={handleEditClick}
>
<Pencil className="h-3 w-3 mr-1" />
<Edit className="h-3 w-3 mr-1" />
</Button>
<Button

View File

@@ -7,7 +7,7 @@
import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { ArrowLeft, Save, MessageSquare } from 'lucide-react';
import { X, Save, MessageSquare } from 'lucide-react';
import { PageLayout } from '@/components/organisms/PageLayout';
import { PageHeader } from '@/components/organisms/PageHeader';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
@@ -16,6 +16,7 @@ import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Checkbox } from '@/components/ui/checkbox';
import { useMenuStore } from '@/stores/menuStore';
import { createDynamicBoardPost } from '@/components/board/DynamicBoard/actions';
import { getBoardByCode } from '@/components/board/BoardManagement/actions';
@@ -25,6 +26,7 @@ interface DynamicBoardCreateFormProps {
export function DynamicBoardCreateForm({ boardCode }: DynamicBoardCreateFormProps) {
const router = useRouter();
const sidebarCollapsed = useMenuStore((state) => state.sidebarCollapsed);
// 게시판 정보
const [boardName, setBoardName] = useState<string>('게시판');
@@ -71,7 +73,7 @@ export function DynamicBoardCreateForm({ boardCode }: DynamicBoardCreateFormProp
});
if (result.success && result.data) {
router.push(`/ko/boards/${boardCode}/${result.data.id}`);
router.push(`/ko/boards/${boardCode}/${result.data.id}?mode=view`);
} else {
setError(result.error || '게시글 등록에 실패했습니다.');
setIsSubmitting(false);
@@ -91,7 +93,7 @@ export function DynamicBoardCreateForm({ boardCode }: DynamicBoardCreateFormProp
icon={MessageSquare}
/>
<form onSubmit={handleSubmit}>
<form onSubmit={handleSubmit} className="pb-24">
<Card>
<CardHeader>
<CardTitle className="text-base"> </CardTitle>
@@ -143,23 +145,21 @@ export function DynamicBoardCreateForm({ boardCode }: DynamicBoardCreateFormProp
</CardContent>
</Card>
{/* 버튼 영역 */}
<div className="mt-6 flex items-center justify-between">
<Button
type="button"
variant="outline"
onClick={handleCancel}
disabled={isSubmitting}
</form>
{/* 하단 액션 버튼 (sticky) */}
<div
className={`fixed bottom-4 left-4 right-4 px-4 py-3 bg-background/95 backdrop-blur rounded-xl border shadow-lg z-50 transition-all duration-300 md:bottom-6 md:px-6 md:right-[48px] ${sidebarCollapsed ? 'md:left-[156px]' : 'md:left-[316px]'} flex items-center justify-between`}
>
<ArrowLeft className="h-4 w-4 mr-2" />
<Button type="button" variant="outline" onClick={handleCancel} disabled={isSubmitting} size="sm" className="md:size-default">
<X className="w-4 h-4 md:mr-2" />
<span className="hidden md:inline"></span>
</Button>
<Button type="submit" disabled={isSubmitting}>
<Save className="h-4 w-4 mr-2" />
{isSubmitting ? '등록 중...' : '등록'}
<Button onClick={(e) => { e.preventDefault(); const form = document.querySelector('form'); form?.requestSubmit(); }} disabled={isSubmitting} size="sm" className="md:size-default">
<Save className="w-4 h-4 md:mr-2" />
<span className="hidden md:inline">{isSubmitting ? '등록 중...' : '등록'}</span>
</Button>
</div>
</form>
</PageLayout>
);
}

View File

@@ -7,7 +7,7 @@
import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { ArrowLeft, Save, MessageSquare } from 'lucide-react';
import { ArrowLeft, X, Save, MessageSquare } from 'lucide-react';
import { DetailPageSkeleton } from '@/components/ui/skeleton';
import { PageLayout } from '@/components/organisms/PageLayout';
import { PageHeader } from '@/components/organisms/PageHeader';
@@ -17,6 +17,7 @@ import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Checkbox } from '@/components/ui/checkbox';
import { useMenuStore } from '@/stores/menuStore';
import { getDynamicBoardPost, updateDynamicBoardPost } from '@/components/board/DynamicBoard/actions';
import { getBoardByCode } from '@/components/board/BoardManagement/actions';
import type { PostApiData } from '@/components/customer-center/shared/types';
@@ -59,6 +60,7 @@ interface DynamicBoardEditFormProps {
export function DynamicBoardEditForm({ boardCode, postId }: DynamicBoardEditFormProps) {
const router = useRouter();
const sidebarCollapsed = useMenuStore((state) => state.sidebarCollapsed);
// 게시판 정보
const [boardName, setBoardName] = useState<string>('게시판');
@@ -134,7 +136,7 @@ export function DynamicBoardEditForm({ boardCode, postId }: DynamicBoardEditForm
});
if (result.success) {
router.push(`/ko/boards/${boardCode}/${postId}`);
router.push(`/ko/boards/${boardCode}/${postId}?mode=view`);
} else {
setError(result.error || '게시글 수정에 실패했습니다.');
setIsSubmitting(false);
@@ -143,7 +145,7 @@ export function DynamicBoardEditForm({ boardCode, postId }: DynamicBoardEditForm
// 취소
const handleCancel = () => {
router.push(`/ko/boards/${boardCode}/${postId}`);
router.push(`/ko/boards/${boardCode}/${postId}?mode=view`);
};
// 로딩 상태
@@ -178,7 +180,7 @@ export function DynamicBoardEditForm({ boardCode, postId }: DynamicBoardEditForm
icon={MessageSquare}
/>
<form onSubmit={handleSubmit}>
<form onSubmit={handleSubmit} className="pb-24">
<Card>
<CardHeader>
<CardTitle className="text-base"> </CardTitle>
@@ -230,23 +232,21 @@ export function DynamicBoardEditForm({ boardCode, postId }: DynamicBoardEditForm
</CardContent>
</Card>
{/* 버튼 영역 */}
<div className="mt-6 flex items-center justify-between">
<Button
type="button"
variant="outline"
onClick={handleCancel}
disabled={isSubmitting}
</form>
{/* 하단 액션 버튼 (sticky) */}
<div
className={`fixed bottom-4 left-4 right-4 px-4 py-3 bg-background/95 backdrop-blur rounded-xl border shadow-lg z-50 transition-all duration-300 md:bottom-6 md:px-6 md:right-[48px] ${sidebarCollapsed ? 'md:left-[156px]' : 'md:left-[316px]'} flex items-center justify-between`}
>
<ArrowLeft className="h-4 w-4 mr-2" />
<Button type="button" variant="outline" onClick={handleCancel} disabled={isSubmitting} size="sm" className="md:size-default">
<X className="w-4 h-4 md:mr-2" />
<span className="hidden md:inline"></span>
</Button>
<Button type="submit" disabled={isSubmitting}>
<Save className="h-4 w-4 mr-2" />
{isSubmitting ? '저장 중...' : '저장'}
<Button onClick={(e) => { e.preventDefault(); const form = document.querySelector('form'); form?.requestSubmit(); }} disabled={isSubmitting} size="sm" className="md:size-default">
<Save className="w-4 h-4 md:mr-2" />
<span className="hidden md:inline">{isSubmitting ? '저장 중...' : '저장'}</span>
</Button>
</div>
</form>
</PageLayout>
);
}