feat: 신규 페이지 구현 및 HR/설정 기능 개선
신규 페이지: - 회계관리: 거래처, 예상비용, 청구서, 발주서 - 게시판: 공지사항, 자료실, 커뮤니티 - 고객센터: 문의/FAQ - 설정: 계정, 알림, 출퇴근, 팝업, 구독, 결제내역 - 리포트 (차트 시각화) - 개발자 테스트 URL 페이지 기능 개선: - HR 직원관리/휴가관리/카드관리 강화 - IntegratedListTemplateV2 확장 - AuthenticatedLayout 패딩 표준화 - 로그인 페이지 UI 개선 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
228
src/components/board/BoardDetail/index.tsx
Normal file
228
src/components/board/BoardDetail/index.tsx
Normal file
@@ -0,0 +1,228 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 게시글 상세 보기 컴포넌트
|
||||
*
|
||||
* 디자인 스펙 기준:
|
||||
* - 페이지 타이틀: 게시글 상세
|
||||
* - 페이지 설명: 게시글을 조회합니다.
|
||||
* - 삭제/수정 버튼 (본인 글만 표시)
|
||||
* - 게시판명 라벨
|
||||
* - 제목
|
||||
* - 메타 정보 (작성자 | 날짜 | 조회수)
|
||||
* - 내용 (HTML 렌더링)
|
||||
* - 첨부파일 다운로드 링크
|
||||
* - 댓글 섹션 (댓글 사용함 설정 시만 표시)
|
||||
*/
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { format } from 'date-fns';
|
||||
import { FileText, Download, ArrowLeft } from 'lucide-react';
|
||||
import { PageLayout } from '@/components/organisms/PageLayout';
|
||||
import { PageHeader } from '@/components/organisms/PageHeader';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
} from '@/components/ui/card';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import { CommentSection } from '../CommentSection';
|
||||
import type { Post, Comment } from '../types';
|
||||
|
||||
interface BoardDetailProps {
|
||||
post: Post;
|
||||
comments: Comment[];
|
||||
currentUserId: string;
|
||||
}
|
||||
|
||||
export function BoardDetail({ post, comments: initialComments, currentUserId }: BoardDetailProps) {
|
||||
const router = useRouter();
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||
const [comments, setComments] = useState<Comment[]>(initialComments);
|
||||
|
||||
const isMyPost = post.authorId === currentUserId;
|
||||
|
||||
// ===== 액션 핸들러 =====
|
||||
const handleBack = useCallback(() => {
|
||||
router.push('/ko/board');
|
||||
}, [router]);
|
||||
|
||||
const handleEdit = useCallback(() => {
|
||||
router.push(`/ko/board/${post.id}/edit`);
|
||||
}, [router, post.id]);
|
||||
|
||||
const handleConfirmDelete = useCallback(() => {
|
||||
// TODO: API 호출
|
||||
console.log('Delete post:', post.id);
|
||||
setShowDeleteDialog(false);
|
||||
router.push('/ko/board');
|
||||
}, [post.id, router]);
|
||||
|
||||
// ===== 댓글 핸들러 =====
|
||||
const handleAddComment = useCallback((content: string) => {
|
||||
const newComment: Comment = {
|
||||
id: `comment-${Date.now()}`,
|
||||
postId: post.id,
|
||||
authorId: currentUserId,
|
||||
authorName: '홍길동', // 실제로는 사용자 정보에서 가져옴
|
||||
authorDepartment: '개발팀',
|
||||
authorPosition: '과장',
|
||||
content,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
setComments((prev) => [...prev, newComment]);
|
||||
// TODO: API 호출
|
||||
}, [post.id, currentUserId]);
|
||||
|
||||
const handleUpdateComment = useCallback((id: string, content: string) => {
|
||||
setComments((prev) =>
|
||||
prev.map((c) =>
|
||||
c.id === id ? { ...c, content, updatedAt: new Date().toISOString() } : c
|
||||
)
|
||||
);
|
||||
// TODO: API 호출
|
||||
}, []);
|
||||
|
||||
const handleDeleteComment = useCallback((id: string) => {
|
||||
setComments((prev) => prev.filter((c) => c.id !== id));
|
||||
// TODO: API 호출
|
||||
}, []);
|
||||
|
||||
// 파일 크기 포맷
|
||||
const formatFileSize = (bytes: number) => {
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
};
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
{/* 헤더 */}
|
||||
<PageHeader
|
||||
title="게시글 상세"
|
||||
description="게시글을 조회합니다."
|
||||
icon={FileText}
|
||||
actions={
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={handleBack}>
|
||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||
목록
|
||||
</Button>
|
||||
{isMyPost && (
|
||||
<>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="text-red-500 border-red-200 hover:bg-red-50 hover:text-red-600"
|
||||
onClick={() => setShowDeleteDialog(true)}
|
||||
>
|
||||
삭제
|
||||
</Button>
|
||||
<Button onClick={handleEdit}>수정</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* 게시글 카드 */}
|
||||
<Card className="mb-6">
|
||||
<CardHeader className="pb-4">
|
||||
{/* 게시판명 라벨 */}
|
||||
<Badge variant="secondary" className="w-fit mb-2">
|
||||
{post.boardName}
|
||||
</Badge>
|
||||
|
||||
{/* 제목 */}
|
||||
<h2 className="text-xl font-bold text-gray-900">{post.title}</h2>
|
||||
|
||||
{/* 메타 정보: 작성자 | 날짜 | 조회수 */}
|
||||
<div className="flex items-center gap-2 text-sm text-gray-500 mt-2">
|
||||
<span>{post.authorName}</span>
|
||||
<span className="text-gray-300">|</span>
|
||||
<span>{format(new Date(post.createdAt), 'yyyy-MM-dd HH:mm')}</span>
|
||||
<span className="text-gray-300">|</span>
|
||||
<span>조회수 {post.viewCount}</span>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-6">
|
||||
{/* 내용 (HTML 렌더링) */}
|
||||
<div
|
||||
className="prose prose-sm max-w-none"
|
||||
dangerouslySetInnerHTML={{ __html: post.content }}
|
||||
/>
|
||||
|
||||
{/* 첨부파일 */}
|
||||
{post.attachments.length > 0 && (
|
||||
<div className="space-y-2 pt-4 border-t border-gray-100">
|
||||
{post.attachments.map((file) => (
|
||||
<a
|
||||
key={file.id}
|
||||
href={file.fileUrl}
|
||||
download={file.fileName}
|
||||
className="flex items-center gap-2 p-2 bg-gray-50 rounded-md hover:bg-gray-100 transition-colors"
|
||||
>
|
||||
<Download className="h-4 w-4 text-gray-500" />
|
||||
<span className="text-sm text-blue-600 hover:underline">
|
||||
{file.fileName}
|
||||
</span>
|
||||
<span className="text-xs text-gray-400">
|
||||
({formatFileSize(file.fileSize)})
|
||||
</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 댓글 섹션 (댓글 사용함 설정 시만 표시) */}
|
||||
{post.allowComments && (
|
||||
<CommentSection
|
||||
postId={post.id}
|
||||
comments={comments}
|
||||
currentUserId={currentUserId}
|
||||
onAddComment={handleAddComment}
|
||||
onUpdateComment={handleUpdateComment}
|
||||
onDeleteComment={handleDeleteComment}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 삭제 확인 다이얼로그 */}
|
||||
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>게시글 삭제</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
정말 삭제하시겠습니까?
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleConfirmDelete}
|
||||
className="bg-red-600 hover:bg-red-700"
|
||||
>
|
||||
삭제
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export default BoardDetail;
|
||||
Reference in New Issue
Block a user