Skip to content
Merged
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
346 changes: 346 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"react-dom": "^18.3.1",
"react-error-boundary": "^6.0.0",
"react-router-dom": "^7.8.2",
"recharts": "^3.2.0",
"tailwindcss": "^4.1.12",
"wx-react-gantt": "^1.3.1",
"zustand": "^5.0.8"
Expand Down
45 changes: 45 additions & 0 deletions src/api/dashboardAPI.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import api from './client.js';

/**
* 대시보드 프로젝트 리스트를 가져오는 API 함수
* @returns {Promise<Object>} 프로젝트 데이터가 포함된 응답 객체
*/
export const fetchDashboardProjects = async () => {
try {
const response = await api.get('/dashboards');
return response.data;
} catch (error) {
console.error('대시보드 프로젝트 데이터 로딩 실패:', error);
throw error;
}
};

// /**
// * 특정 프로젝트의 상세 정보를 가져오는 API 함수
// * @param {number} projectId - 프로젝트 ID
// * @returns {Promise<Object>} 프로젝트 상세 데이터
// */
// export const fetchProjectDetail = async (projectId) => {
// try {
// const response = await api.get(`/projects/${projectId}`);
// return response.data;
// } catch (error) {
// console.error('프로젝트 상세 데이터 로딩 실패:', error);
// throw error;
// }
// };

/**
* 특정 프로젝트의 대시보드 데이터를 가져오는 API 함수
* @param {number} projectId - 프로젝트 ID
* @returns {Promise<Object>} 프로젝트 대시보드 데이터 (차트, 일정, 우선순위 등)
*/
export const fetchProjectDashboard = async (projectId) => {
try {
const response = await api.get(`/dashboards/${projectId}`);
return response.data;
} catch (error) {
console.error('프로젝트 대시보드 데이터 로딩 실패:', error);
throw error;
}
};
53 changes: 53 additions & 0 deletions src/components/common/ConfirmDialog.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
const ConfirmDialog = ({
isOpen,
title,
message,
confirmText = "확인",
cancelText = "취소",
onConfirm,
onCancel,
type = "warning",
}) => {
if (!isOpen) return null;

return (
<>
{/* 다이얼로그 */}
<div className="modal select-none">
<div className="bg-white rounded-lg p-6 min-w-80 max-w-md shadow-xl">
{/* 헤더 */}
<div className="mb-4">
<h3 className="text-lg font-semibold text-gray-900">{title}</h3>
</div>

{/* 메시지 */}
<div className="mb-6">
<p className="text-gray-700 whitespace-pre-line">{message}</p>
</div>

{/* 버튼들 */}
<div className="flex justify-end gap-3">
<button
onClick={onCancel}
className="px-4 py-2 rounded bg-gray-200 hover:bg-gray-300 text-gray-800 transition-colors"
>
{cancelText}
</button>
<button
onClick={onConfirm}
className={`px-4 py-2 rounded text-white transition-colors ${
type === "danger"
? "bg-red-300 hover:bg-red-600"
: "bg-blue-300 hover:bg-blue-600"
}`}
>
{confirmText}
</button>
</div>
</div>
</div>
</>
);
};

export default ConfirmDialog;
215 changes: 56 additions & 159 deletions src/components/common/loading/LoadingComponents.jsx
Original file line number Diff line number Diff line change
@@ -1,173 +1,70 @@
import { Replay, Warning} from "@mui/icons-material";
import { Skeleton } from "@mui/material";

// 기본 Skeleton 컴포넌트 - 개선된 애니메이션
export function SkeletonBox({ className = "", width = "w-full", height = "h-4" }) {
// ProjectList용 Skeleton UI
export function ProjectListSkeleton({ count = 4 }) {
return (
<div
className={`rounded ${width} ${height} ${className}`}
style={{
background: 'linear-gradient(90deg, #f3f4f6 25%, #e5e7eb 50%, #f3f4f6 75%)',
backgroundSize: '200% 100%',
animation: 'shimmer 1.5s ease-in-out infinite'
}}
/>
);
}
<div className="bg-white border border-gray-200 rounded-lg">
{/* 헤더 스켈레톤 */}
<div className="px-4 py-3 border-b border-gray-100 bg-gray-50">
<div className="flex items-center justify-between">
<Skeleton variant="text" width={120} height={24} />
<Skeleton variant="text" width={40} height={20} />
</div>
</div>

// 회의 테이블용 Skeleton UI
export function MeetingTableSkeleton({ rows = 10 }) {
// 제목 컬럼용 다양한 너비 배열
const titleWidths = ['w-48', 'w-56', 'w-44', 'w-52', 'w-40'];

return (
<div className="bg-white border-y border-gray-200 overflow-hidden">
<div>
<table className="min-w-full">
{/* 헤더 */}
<thead className="border-b border-gray-200">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
제목
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
작성자
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
타입
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
진행 날짜
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
장소
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
참여 인원
</th>
</tr>
</thead>

{/* 스켈레톤 로우들 */}
<tbody className="bg-white">
{Array.from({ length: rows }).map((_, index) => (
<tr
key={index}
className={`${
index % 2 === 0 ? 'bg-white' : 'bg-gray-50'
} border-b border-gray-100`}
>
{/* 제목 컬럼 */}
<td className="px-4 py-3">
<div className="flex items-center gap-3">
<SkeletonBox width="w-4" height="h-4" />
<div className="flex-1">
<SkeletonBox width={titleWidths[index % titleWidths.length]} height="h-4" />
</div>
</div>
</td>

{/* 작성자 컬럼 */}
<td className="px-4 py-3">
<div className="flex items-center gap-2">
<SkeletonBox width="w-8" height="h-8" className="rounded-full" />
<SkeletonBox width="w-16" height="h-4" />
</div>
</td>

{/* 타입 컬럼 */}
<td className="px-4 py-3">
<SkeletonBox width="w-20" height="h-6" className="rounded" />
</td>

{/* 진행 날짜 컬럼 */}
<td className="px-4 py-3">
<div className="flex items-center gap-2">
<SkeletonBox width="w-4" height="h-4" />
<div>
<SkeletonBox width="w-24" height="h-4" className="mb-1" />
<SkeletonBox width="w-16" height="h-3" />
</div>
{/* 프로젝트 목록 스켈레톤 */}
<div className="divide-y divide-gray-100">
{Array.from({ length: count }).map((_, index) => (
<div key={index} className="p-4">
{/* 프로젝트 헤더 */}
<div className="flex items-start justify-between mb-3">
<div className="flex-1 min-w-0">
<Skeleton variant="text" width="60%" height={20} className="mb-2" />
<div className="flex items-center gap-3 mt-1">
<div className="flex items-center gap-1">
<Skeleton variant="circular" width={16} height={16} />
<Skeleton variant="text" width={30} height={16} />
</div>
</td>

{/* 장소 컬럼 */}
<td className="px-4 py-3">
<SkeletonBox width="w-20" height="h-4" />
</td>

{/* 참여 인원 컬럼 */}
<td className="px-4 py-3">
<div className="flex items-center gap-2">
<SkeletonBox width="w-4" height="h-4" />
<SkeletonBox width="w-8" height="h-4" />
<div className="flex items-center gap-1">
<Skeleton variant="circular" width={16} height={16} />
<Skeleton variant="text" width={50} height={16} />
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}
</div>
</div>
<Skeleton variant="rounded" width={40} height={24} />
</div>

// 범용 테이블 Skeleton UI (다른 테이블에서도 사용 가능)
export function TableSkeleton({
rows = 5,
columns = 4,
showHeader = true,
headerTitles = []
}) {
const defaultWidths = ['w-32', 'w-24', 'w-20', 'w-28', 'w-16'];

return (
<div className="bg-white border-y border-gray-200 overflow-hidden">
<div>
<table className="min-w-full">
{/* 헤더 */}
{showHeader && (
<thead className="border-b border-gray-200">
<tr>
{Array.from({ length: columns }).map((_, colIndex) => (
<th
key={colIndex}
className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
{headerTitles[colIndex] || (
<SkeletonBox width="w-16" height="h-4" />
)}
</th>
))}
</tr>
</thead>
)}

{/* 스켈레톤 로우들 */}
<tbody className="bg-white">
{Array.from({ length: rows }).map((_, rowIndex) => (
<tr
key={rowIndex}
className={`${
rowIndex % 2 === 0 ? 'bg-white' : 'bg-gray-50'
} border-b border-gray-100`}
>
{Array.from({ length: columns }).map((_, colIndex) => (
<td key={colIndex} className="px-4 py-3">
<SkeletonBox
width={defaultWidths[colIndex % defaultWidths.length]}
height="h-4"
/>
</td>
))}
</tr>
))}
</tbody>
</table>
{/* 진행률 스켈레톤 */}
<div className="mb-3">
<div className="flex items-center justify-between mb-2">
<Skeleton variant="text" width={40} height={16} />
<Skeleton variant="text" width={30} height={16} />
</div>
<Skeleton variant="rounded" width="100%" height={8} />
</div>

{/* 마감일 및 팀원 스켈레톤 */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-1">
<Skeleton variant="circular" width={16} height={16} />
<Skeleton variant="text" width={80} height={16} />
<Skeleton variant="text" width={50} height={16} />
</div>
<div className="flex items-center gap-1">
<Skeleton variant="circular" width={24} height={24} />
<Skeleton variant="circular" width={24} height={24} />
<Skeleton variant="circular" width={24} height={24} />
</div>
</div>
</div>
))}
</div>
</div>
);
}


export function LoadingSpinner({ message = "데이터를 불러오는 중..." }) {
return (
<div className="flex flex-col justify-center items-center py-8 space-y-4">
Expand All @@ -180,7 +77,7 @@ export function LoadingSpinner({ message = "데이터를 불러오는 중..." })
// 에러 경계 컴포넌트
export function ErrorFallback({ error, onRetry }) {
return (
<div className="min-h-[50vh] flex items-center justify-center">
<div className="min-h-[50vh] bg-white rounded-lg shadow-sm flex items-center justify-center">
<div className="flex flex-col items-center py-8 space-y-4">
<div className="text-red-500 text-center">
<Warning fontSize="large" className="text-yellow-500"/>
Expand Down
Loading
Loading