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:
182
src/components/board/CommentSection/CommentItem.tsx
Normal file
182
src/components/board/CommentSection/CommentItem.tsx
Normal file
@@ -0,0 +1,182 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 개별 댓글 컴포넌트
|
||||
*
|
||||
* 디자인 스펙 기준:
|
||||
* - 프로필 이미지, 부서명 이름 직책, 등록일시, 댓글 내용
|
||||
* - 수정/삭제 버튼 (본인 댓글만)
|
||||
* - 수정 클릭 시 인풋박스에 기존 댓글 내용 입력 상태로 변경
|
||||
* - 삭제 클릭 시 "정말 삭제하시겠습니까?" 확인 Alert
|
||||
*/
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
import { format } from 'date-fns';
|
||||
import { User, Pencil, Trash2 } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import type { Comment } from '../types';
|
||||
|
||||
interface CommentItemProps {
|
||||
comment: Comment;
|
||||
currentUserId: string;
|
||||
onUpdate: (id: string, content: string) => void;
|
||||
onDelete: (id: string) => void;
|
||||
}
|
||||
|
||||
export function CommentItem({
|
||||
comment,
|
||||
currentUserId,
|
||||
onUpdate,
|
||||
onDelete,
|
||||
}: CommentItemProps) {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [editContent, setEditContent] = useState(comment.content);
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||
|
||||
const isMyComment = comment.authorId === currentUserId;
|
||||
|
||||
// 수정 모드 토글
|
||||
const handleEditClick = useCallback(() => {
|
||||
setIsEditing(true);
|
||||
setEditContent(comment.content);
|
||||
}, [comment.content]);
|
||||
|
||||
// 수정 취소
|
||||
const handleCancelEdit = useCallback(() => {
|
||||
setIsEditing(false);
|
||||
setEditContent(comment.content);
|
||||
}, [comment.content]);
|
||||
|
||||
// 수정 저장
|
||||
const handleSaveEdit = useCallback(() => {
|
||||
if (editContent.trim()) {
|
||||
onUpdate(comment.id, editContent.trim());
|
||||
setIsEditing(false);
|
||||
}
|
||||
}, [comment.id, editContent, onUpdate]);
|
||||
|
||||
// 삭제 확인
|
||||
const handleConfirmDelete = useCallback(() => {
|
||||
onDelete(comment.id);
|
||||
setShowDeleteDialog(false);
|
||||
}, [comment.id, onDelete]);
|
||||
|
||||
// 작성자 정보 포맷
|
||||
const authorInfo = [
|
||||
comment.authorDepartment,
|
||||
comment.authorName,
|
||||
comment.authorPosition,
|
||||
].filter(Boolean).join(' ');
|
||||
|
||||
return (
|
||||
<div className="flex gap-3 py-4 border-b border-gray-100 last:border-0">
|
||||
{/* 프로필 이미지 */}
|
||||
<div className="flex-shrink-0">
|
||||
{comment.authorProfileImage ? (
|
||||
<img
|
||||
src={comment.authorProfileImage}
|
||||
alt={comment.authorName}
|
||||
className="w-10 h-10 rounded-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-10 h-10 rounded-full bg-gray-200 flex items-center justify-center">
|
||||
<User className="w-5 h-5 text-gray-500" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 댓글 내용 */}
|
||||
<div className="flex-1 min-w-0">
|
||||
{/* 작성자 정보 + 날짜 + 버튼 */}
|
||||
<div className="flex items-center justify-between 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">
|
||||
{format(new Date(comment.createdAt), 'yyyy-MM-dd HH:mm')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 본인 댓글만 수정/삭제 버튼 표시 */}
|
||||
{isMyComment && !isEditing && (
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 px-2 text-gray-500 hover:text-gray-700"
|
||||
onClick={handleEditClick}
|
||||
>
|
||||
<Pencil className="h-3 w-3 mr-1" />
|
||||
수정
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 px-2 text-red-500 hover:text-red-600"
|
||||
onClick={() => setShowDeleteDialog(true)}
|
||||
>
|
||||
<Trash2 className="h-3 w-3 mr-1" />
|
||||
삭제
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 댓글 본문 또는 수정 폼 */}
|
||||
{isEditing ? (
|
||||
<div className="space-y-2">
|
||||
<Textarea
|
||||
value={editContent}
|
||||
onChange={(e) => setEditContent(e.target.value)}
|
||||
placeholder="댓글을 입력해주세요"
|
||||
className="min-h-[80px]"
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" onClick={handleSaveEdit}>
|
||||
저장
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={handleCancelEdit}>
|
||||
취소
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-gray-700 whitespace-pre-wrap">{comment.content}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 삭제 확인 다이얼로그 */}
|
||||
<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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default CommentItem;
|
||||
90
src/components/board/CommentSection/index.tsx
Normal file
90
src/components/board/CommentSection/index.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 댓글 섹션 컴포넌트
|
||||
*
|
||||
* 디자인 스펙 기준:
|
||||
* - 댓글 등록 Textarea + 버튼
|
||||
* - 댓글 수 표시 ("댓글 N")
|
||||
* - 댓글 목록
|
||||
*/
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { CommentItem } from './CommentItem';
|
||||
import type { Comment } from '../types';
|
||||
|
||||
interface CommentSectionProps {
|
||||
postId: string;
|
||||
comments: Comment[];
|
||||
currentUserId: string;
|
||||
onAddComment: (content: string) => void;
|
||||
onUpdateComment: (id: string, content: string) => void;
|
||||
onDeleteComment: (id: string) => void;
|
||||
}
|
||||
|
||||
export function CommentSection({
|
||||
postId,
|
||||
comments,
|
||||
currentUserId,
|
||||
onAddComment,
|
||||
onUpdateComment,
|
||||
onDeleteComment,
|
||||
}: CommentSectionProps) {
|
||||
const [newComment, setNewComment] = useState('');
|
||||
|
||||
// 댓글 등록 핸들러
|
||||
const handleSubmit = useCallback(() => {
|
||||
if (newComment.trim()) {
|
||||
onAddComment(newComment.trim());
|
||||
setNewComment('');
|
||||
}
|
||||
}, [newComment, onAddComment]);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 댓글 등록 */}
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-lg font-semibold flex items-center gap-2">
|
||||
<span className="w-2 h-2 bg-red-500 rounded-full" />
|
||||
댓글 등록
|
||||
</h3>
|
||||
<Textarea
|
||||
value={newComment}
|
||||
onChange={(e) => setNewComment(e.target.value)}
|
||||
placeholder="댓글을 입력해주세요"
|
||||
className="min-h-[100px]"
|
||||
/>
|
||||
<div className="flex justify-end">
|
||||
<Button onClick={handleSubmit}>등록</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 댓글 목록 */}
|
||||
{comments.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-lg font-semibold flex items-center gap-2">
|
||||
<span className="w-2 h-2 bg-red-500 rounded-full" />
|
||||
댓글 {comments.length}
|
||||
</h3>
|
||||
<div className="bg-white rounded-lg border border-gray-200">
|
||||
<div className="p-4">
|
||||
{comments.map((comment) => (
|
||||
<CommentItem
|
||||
key={comment.id}
|
||||
comment={comment}
|
||||
currentUserId={currentUserId}
|
||||
onUpdate={onUpdateComment}
|
||||
onDelete={onDeleteComment}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default CommentSection;
|
||||
Reference in New Issue
Block a user