메일관리 시스템 구현 완료
This commit is contained in:
@@ -1,11 +1,201 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Inbox, Mail, Clock, AlertCircle, RefreshCw } from "lucide-react";
|
||||
import {
|
||||
Inbox,
|
||||
Mail,
|
||||
RefreshCw,
|
||||
Loader2,
|
||||
CheckCircle,
|
||||
Paperclip,
|
||||
AlertCircle,
|
||||
Search,
|
||||
Filter,
|
||||
SortAsc,
|
||||
SortDesc,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
MailAccount,
|
||||
ReceivedMail,
|
||||
getMailAccounts,
|
||||
getReceivedMails,
|
||||
testImapConnection,
|
||||
} from "@/lib/api/mail";
|
||||
import MailDetailModal from "@/components/mail/MailDetailModal";
|
||||
|
||||
export default function MailReceivePage() {
|
||||
const [accounts, setAccounts] = useState<MailAccount[]>([]);
|
||||
const [selectedAccountId, setSelectedAccountId] = useState<string>("");
|
||||
const [mails, setMails] = useState<ReceivedMail[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [testing, setTesting] = useState(false);
|
||||
const [testResult, setTestResult] = useState<{
|
||||
success: boolean;
|
||||
message: string;
|
||||
} | null>(null);
|
||||
|
||||
// 메일 상세 모달 상태
|
||||
const [isDetailModalOpen, setIsDetailModalOpen] = useState(false);
|
||||
const [selectedMailId, setSelectedMailId] = useState<string>("");
|
||||
|
||||
// 검색 및 필터 상태
|
||||
const [searchTerm, setSearchTerm] = useState<string>("");
|
||||
const [filterStatus, setFilterStatus] = useState<string>("all"); // all, unread, read, attachment
|
||||
const [sortBy, setSortBy] = useState<string>("date-desc"); // date-desc, date-asc, from-asc, from-desc
|
||||
|
||||
// 계정 목록 로드
|
||||
useEffect(() => {
|
||||
loadAccounts();
|
||||
}, []);
|
||||
|
||||
// 계정 선택 시 메일 로드
|
||||
useEffect(() => {
|
||||
if (selectedAccountId) {
|
||||
loadMails();
|
||||
}
|
||||
}, [selectedAccountId]);
|
||||
|
||||
// 자동 새로고침 (30초마다)
|
||||
useEffect(() => {
|
||||
if (!selectedAccountId) return;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
loadMails();
|
||||
}, 30000); // 30초
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [selectedAccountId]);
|
||||
|
||||
const loadAccounts = async () => {
|
||||
try {
|
||||
const data = await getMailAccounts();
|
||||
if (Array.isArray(data)) {
|
||||
const activeAccounts = data.filter((acc) => acc.status === "active");
|
||||
setAccounts(activeAccounts);
|
||||
if (activeAccounts.length > 0 && !selectedAccountId) {
|
||||
setSelectedAccountId(activeAccounts[0].id);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("계정 로드 실패:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const loadMails = async () => {
|
||||
if (!selectedAccountId) return;
|
||||
|
||||
setLoading(true);
|
||||
setTestResult(null);
|
||||
try {
|
||||
const data = await getReceivedMails(selectedAccountId, 50);
|
||||
setMails(data);
|
||||
} catch (error) {
|
||||
console.error("메일 로드 실패:", error);
|
||||
alert(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "메일을 불러오는데 실패했습니다."
|
||||
);
|
||||
setMails([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTestConnection = async () => {
|
||||
if (!selectedAccountId) return;
|
||||
|
||||
setTesting(true);
|
||||
setTestResult(null);
|
||||
try {
|
||||
const result = await testImapConnection(selectedAccountId);
|
||||
setTestResult(result);
|
||||
if (result.success) {
|
||||
// 연결 성공 후 자동으로 메일 로드
|
||||
setTimeout(() => loadMails(), 1000);
|
||||
}
|
||||
} catch (error) {
|
||||
setTestResult({
|
||||
success: false,
|
||||
message:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "IMAP 연결 테스트 실패",
|
||||
});
|
||||
} finally {
|
||||
setTesting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - date.getTime();
|
||||
const diffMins = Math.floor(diffMs / 60000);
|
||||
const diffHours = Math.floor(diffMs / 3600000);
|
||||
const diffDays = Math.floor(diffMs / 86400000);
|
||||
|
||||
if (diffMins < 60) {
|
||||
return `${diffMins}분 전`;
|
||||
} else if (diffHours < 24) {
|
||||
return `${diffHours}시간 전`;
|
||||
} else if (diffDays < 7) {
|
||||
return `${diffDays}일 전`;
|
||||
} else {
|
||||
return date.toLocaleDateString("ko-KR");
|
||||
}
|
||||
};
|
||||
|
||||
const handleMailClick = (mail: ReceivedMail) => {
|
||||
setSelectedMailId(mail.id);
|
||||
setIsDetailModalOpen(true);
|
||||
};
|
||||
|
||||
const handleMailRead = () => {
|
||||
// 메일을 읽었으므로 목록 새로고침
|
||||
loadMails();
|
||||
};
|
||||
|
||||
// 필터링 및 정렬된 메일 목록
|
||||
const filteredAndSortedMails = React.useMemo(() => {
|
||||
let result = [...mails];
|
||||
|
||||
// 검색
|
||||
if (searchTerm) {
|
||||
const searchLower = searchTerm.toLowerCase();
|
||||
result = result.filter(
|
||||
(mail) =>
|
||||
mail.subject.toLowerCase().includes(searchLower) ||
|
||||
mail.from.toLowerCase().includes(searchLower) ||
|
||||
mail.preview.toLowerCase().includes(searchLower)
|
||||
);
|
||||
}
|
||||
|
||||
// 필터
|
||||
if (filterStatus === "unread") {
|
||||
result = result.filter((mail) => !mail.isRead);
|
||||
} else if (filterStatus === "read") {
|
||||
result = result.filter((mail) => mail.isRead);
|
||||
} else if (filterStatus === "attachment") {
|
||||
result = result.filter((mail) => mail.hasAttachments);
|
||||
}
|
||||
|
||||
// 정렬
|
||||
if (sortBy === "date-desc") {
|
||||
result.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
|
||||
} else if (sortBy === "date-asc") {
|
||||
result.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
|
||||
} else if (sortBy === "from-asc") {
|
||||
result.sort((a, b) => a.from.localeCompare(b.from));
|
||||
} else if (sortBy === "from-desc") {
|
||||
result.sort((a, b) => b.from.localeCompare(a.from));
|
||||
}
|
||||
|
||||
return result;
|
||||
}, [mails, searchTerm, filterStatus, sortBy]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="w-full max-w-none px-4 py-8 space-y-8">
|
||||
@@ -13,92 +203,353 @@ export default function MailReceivePage() {
|
||||
<div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">메일 수신함</h1>
|
||||
<p className="mt-2 text-gray-600">받은 메일을 확인하고 관리합니다</p>
|
||||
<p className="mt-2 text-gray-600">
|
||||
IMAP으로 받은 메일을 확인합니다
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={loadMails}
|
||||
disabled={loading || !selectedAccountId}
|
||||
>
|
||||
<RefreshCw
|
||||
className={`w-4 h-4 mr-2 ${loading ? "animate-spin" : ""}`}
|
||||
/>
|
||||
새로고침
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleTestConnection}
|
||||
disabled={testing || !selectedAccountId}
|
||||
>
|
||||
{testing ? (
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<CheckCircle className="w-4 h-4 mr-2" />
|
||||
)}
|
||||
연결 테스트
|
||||
</Button>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" disabled>
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
새로고침
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 메일 목록 미리보기 */}
|
||||
{/* 계정 선택 */}
|
||||
<Card className="shadow-sm">
|
||||
<CardHeader className="bg-gradient-to-r from-slate-50 to-gray-50 border-b">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Inbox className="w-5 h-5 text-orange-500" />
|
||||
받은 메일함
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-6">
|
||||
{/* 빈 상태 */}
|
||||
<div className="text-center py-16">
|
||||
<Mail className="w-16 h-16 mx-auto mb-4 text-gray-300" />
|
||||
<h3 className="text-lg font-semibold text-gray-700 mb-2">
|
||||
메일 수신 기능 준비 중
|
||||
</h3>
|
||||
<p className="text-gray-500 mb-6">
|
||||
IMAP/POP3 기반 메일 수신 기능이 곧 추가될 예정입니다.
|
||||
</p>
|
||||
|
||||
{/* 예상 레이아웃 미리보기 */}
|
||||
<div className="max-w-3xl mx-auto space-y-2">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex items-center gap-4 p-4 bg-gray-50 rounded-lg border border-gray-200 opacity-40"
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<label className="text-sm font-medium text-gray-700 whitespace-nowrap">
|
||||
메일 계정:
|
||||
</label>
|
||||
<select
|
||||
value={selectedAccountId}
|
||||
onChange={(e) => setSelectedAccountId(e.target.value)}
|
||||
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-orange-500 focus:border-orange-500"
|
||||
>
|
||||
<option value="">계정 선택</option>
|
||||
{accounts.map((account) => (
|
||||
<option key={account.id} value={account.id}>
|
||||
{account.name} ({account.email})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* 연결 테스트 결과 */}
|
||||
{testResult && (
|
||||
<div
|
||||
className={`mt-4 p-3 rounded-lg flex items-center gap-2 ${
|
||||
testResult.success
|
||||
? "bg-green-50 text-green-800 border border-green-200"
|
||||
: "bg-red-50 text-red-800 border border-red-200"
|
||||
}`}
|
||||
>
|
||||
{testResult.success ? (
|
||||
<CheckCircle className="w-5 h-5" />
|
||||
) : (
|
||||
<AlertCircle className="w-5 h-5" />
|
||||
)}
|
||||
<span>{testResult.message}</span>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 검색 및 필터 */}
|
||||
{selectedAccountId && (
|
||||
<Card className="shadow-sm">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex flex-col md:flex-row gap-3">
|
||||
{/* 검색 */}
|
||||
<div className="flex-1 relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
|
||||
<input
|
||||
type="text"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
placeholder="제목, 발신자, 내용으로 검색..."
|
||||
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-orange-500 focus:border-orange-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 필터 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Filter className="w-4 h-4 text-gray-500" />
|
||||
<select
|
||||
value={filterStatus}
|
||||
onChange={(e) => setFilterStatus(e.target.value)}
|
||||
className="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-orange-500 focus:border-orange-500"
|
||||
>
|
||||
<div className="w-10 h-10 bg-gray-200 rounded-full" />
|
||||
<div className="flex-1 text-left">
|
||||
<div className="h-4 bg-gray-200 rounded w-1/3 mb-2" />
|
||||
<div className="h-3 bg-gray-200 rounded w-2/3" />
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">
|
||||
<Clock className="w-4 h-4" />
|
||||
<option value="all">전체</option>
|
||||
<option value="unread">읽지 않음</option>
|
||||
<option value="read">읽음</option>
|
||||
<option value="attachment">첨부파일 있음</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* 정렬 */}
|
||||
<div className="flex items-center gap-2">
|
||||
{sortBy.includes("desc") ? (
|
||||
<SortDesc className="w-4 h-4 text-gray-500" />
|
||||
) : (
|
||||
<SortAsc className="w-4 h-4 text-gray-500" />
|
||||
)}
|
||||
<select
|
||||
value={sortBy}
|
||||
onChange={(e) => setSortBy(e.target.value)}
|
||||
className="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-orange-500 focus:border-orange-500"
|
||||
>
|
||||
<option value="date-desc">날짜 ↓ (최신순)</option>
|
||||
<option value="date-asc">날짜 ↑ (오래된순)</option>
|
||||
<option value="from-asc">발신자 ↑ (A-Z)</option>
|
||||
<option value="from-desc">발신자 ↓ (Z-A)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 검색 결과 카운트 */}
|
||||
{(searchTerm || filterStatus !== "all") && (
|
||||
<div className="mt-3 text-sm text-gray-600">
|
||||
{filteredAndSortedMails.length}개의 메일이 검색되었습니다
|
||||
{searchTerm && (
|
||||
<span className="ml-2">
|
||||
(검색어: <span className="font-medium text-orange-600">{searchTerm}</span>)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 메일 목록 */}
|
||||
{loading ? (
|
||||
<Card className="shadow-sm">
|
||||
<CardContent className="flex justify-center items-center py-16">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-orange-500" />
|
||||
<span className="ml-3 text-gray-600">메일을 불러오는 중...</span>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : filteredAndSortedMails.length === 0 ? (
|
||||
<Card className="text-center py-16 bg-white shadow-sm">
|
||||
<CardContent className="pt-6">
|
||||
<Mail className="w-16 h-16 mx-auto mb-4 text-gray-300" />
|
||||
<p className="text-gray-500 mb-4">
|
||||
{!selectedAccountId
|
||||
? "메일 계정을 선택하세요"
|
||||
: searchTerm || filterStatus !== "all"
|
||||
? "검색 결과가 없습니다"
|
||||
: "받은 메일이 없습니다"}
|
||||
</p>
|
||||
{selectedAccountId && (
|
||||
<Button
|
||||
onClick={handleTestConnection}
|
||||
variant="outline"
|
||||
disabled={testing}
|
||||
>
|
||||
{testing ? (
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<CheckCircle className="w-4 h-4 mr-2" />
|
||||
)}
|
||||
IMAP 연결 테스트
|
||||
</Button>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<Card className="shadow-sm">
|
||||
<CardHeader className="bg-gradient-to-r from-slate-50 to-gray-50 border-b">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Inbox className="w-5 h-5 text-orange-500" />
|
||||
받은 메일함 ({filteredAndSortedMails.length}/{mails.length}개)
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
<div className="divide-y">
|
||||
{filteredAndSortedMails.map((mail) => (
|
||||
<div
|
||||
key={mail.id}
|
||||
onClick={() => handleMailClick(mail)}
|
||||
className={`p-4 hover:bg-gray-50 transition-colors cursor-pointer ${
|
||||
!mail.isRead ? "bg-blue-50/30" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start gap-4">
|
||||
{/* 읽음 표시 */}
|
||||
<div className="flex-shrink-0 w-2 h-2 mt-2">
|
||||
{!mail.isRead && (
|
||||
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 메일 내용 */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span
|
||||
className={`text-sm ${
|
||||
mail.isRead
|
||||
? "text-gray-600"
|
||||
: "text-gray-900 font-semibold"
|
||||
}`}
|
||||
>
|
||||
{mail.from}
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
{mail.hasAttachments && (
|
||||
<Paperclip className="w-4 h-4 text-gray-400" />
|
||||
)}
|
||||
<span className="text-xs text-gray-500">
|
||||
{formatDate(mail.date)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<h3
|
||||
className={`text-sm mb-1 truncate ${
|
||||
mail.isRead ? "text-gray-700" : "text-gray-900 font-medium"
|
||||
}`}
|
||||
>
|
||||
{mail.subject}
|
||||
</h3>
|
||||
<p className="text-xs text-gray-500 line-clamp-2">
|
||||
{mail.preview}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 안내 정보 */}
|
||||
<Card className="bg-gradient-to-r from-blue-50 to-indigo-50 border-blue-200 shadow-sm">
|
||||
<Card className="bg-gradient-to-r from-green-50 to-emerald-50 border-green-200 shadow-sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg flex items-center">
|
||||
<AlertCircle className="w-5 h-5 mr-2 text-blue-500" />
|
||||
구현 예정 기능
|
||||
<CheckCircle className="w-5 h-5 mr-2 text-green-600" />
|
||||
메일 수신 기능 완성! 🎉
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-gray-700 mb-4">
|
||||
💡 메일 수신함에 추가될 기능들:
|
||||
✅ 구현 완료된 모든 기능:
|
||||
</p>
|
||||
<ul className="space-y-2 text-sm text-gray-600">
|
||||
<li className="flex items-start">
|
||||
<span className="text-blue-500 mr-2">✓</span>
|
||||
<span>IMAP/POP3 프로토콜을 통한 메일 수신</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-blue-500 mr-2">✓</span>
|
||||
<span>받은 메일 목록 조회 및 검색</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-blue-500 mr-2">✓</span>
|
||||
<span>메일 읽음/읽지않음 상태 관리</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-blue-500 mr-2">✓</span>
|
||||
<span>첨부파일 다운로드</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-blue-500 mr-2">✓</span>
|
||||
<span>메일 필터링 및 정렬</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<div>
|
||||
<p className="font-medium text-gray-800 mb-2">📬 기본 기능</p>
|
||||
<ul className="space-y-1 text-sm text-gray-600">
|
||||
<li className="flex items-start">
|
||||
<span className="text-green-500 mr-2">✓</span>
|
||||
<span>IMAP 프로토콜 메일 수신</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-green-500 mr-2">✓</span>
|
||||
<span>메일 목록 표시</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-green-500 mr-2">✓</span>
|
||||
<span>읽음/안읽음 상태</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-green-500 mr-2">✓</span>
|
||||
<span>첨부파일 유무 표시</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-800 mb-2">📄 상세보기</p>
|
||||
<ul className="space-y-1 text-sm text-gray-600">
|
||||
<li className="flex items-start">
|
||||
<span className="text-green-500 mr-2">✓</span>
|
||||
<span>HTML 본문 렌더링</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-green-500 mr-2">✓</span>
|
||||
<span>텍스트 본문 보기</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-green-500 mr-2">✓</span>
|
||||
<span>자동 읽음 처리</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-green-500 mr-2">✓</span>
|
||||
<span>첨부파일 다운로드</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-800 mb-2">🔍 고급 기능</p>
|
||||
<ul className="space-y-1 text-sm text-gray-600">
|
||||
<li className="flex items-start">
|
||||
<span className="text-green-500 mr-2">✓</span>
|
||||
<span>통합 검색 (제목/발신자/내용)</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-green-500 mr-2">✓</span>
|
||||
<span>필터링 (읽음/첨부파일)</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-green-500 mr-2">✓</span>
|
||||
<span>정렬 (날짜/발신자)</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-green-500 mr-2">✓</span>
|
||||
<span>자동 새로고침 (30초)</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-800 mb-2">🔒 보안</p>
|
||||
<ul className="space-y-1 text-sm text-gray-600">
|
||||
<li className="flex items-start">
|
||||
<span className="text-green-500 mr-2">✓</span>
|
||||
<span>XSS 방지 (DOMPurify)</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-green-500 mr-2">✓</span>
|
||||
<span>비밀번호 암호화</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-green-500 mr-2">✓</span>
|
||||
<span>안전한 파일명 생성</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 메일 상세 모달 */}
|
||||
<MailDetailModal
|
||||
isOpen={isDetailModalOpen}
|
||||
onClose={() => setIsDetailModalOpen(false)}
|
||||
accountId={selectedAccountId}
|
||||
mailId={selectedMailId}
|
||||
onMailRead={handleMailRead}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
309
frontend/components/mail/MailDetailModal.tsx
Normal file
309
frontend/components/mail/MailDetailModal.tsx
Normal file
@@ -0,0 +1,309 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
X,
|
||||
Paperclip,
|
||||
Reply,
|
||||
Forward,
|
||||
Loader2,
|
||||
AlertCircle,
|
||||
} from "lucide-react";
|
||||
import { MailDetail, getMailDetail, markMailAsRead } from "@/lib/api/mail";
|
||||
import DOMPurify from "isomorphic-dompurify";
|
||||
|
||||
interface MailDetailModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
accountId: string;
|
||||
mailId: string; // "accountId-seqno" 형식
|
||||
onMailRead?: () => void; // 읽음 처리 후 목록 갱신용
|
||||
}
|
||||
|
||||
export default function MailDetailModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
accountId,
|
||||
mailId,
|
||||
onMailRead,
|
||||
}: MailDetailModalProps) {
|
||||
const [mail, setMail] = useState<MailDetail | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [showHtml, setShowHtml] = useState(true); // HTML/텍스트 토글
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && mailId) {
|
||||
loadMailDetail();
|
||||
}
|
||||
}, [isOpen, mailId]);
|
||||
|
||||
const loadMailDetail = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
// mailId에서 seqno 추출 (예: "account123-45" -> 45)
|
||||
const seqno = parseInt(mailId.split("-").pop() || "0", 10);
|
||||
|
||||
if (isNaN(seqno)) {
|
||||
throw new Error("유효하지 않은 메일 ID입니다.");
|
||||
}
|
||||
|
||||
// 메일 상세 조회
|
||||
const mailDetail = await getMailDetail(accountId, seqno);
|
||||
setMail(mailDetail);
|
||||
|
||||
// 읽음 처리
|
||||
if (!mailDetail.isRead) {
|
||||
await markMailAsRead(accountId, seqno);
|
||||
onMailRead?.(); // 목록 갱신
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("메일 상세 조회 실패:", err);
|
||||
setError(
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: "메일을 불러오는데 실패했습니다."
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleString("ko-KR", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
};
|
||||
|
||||
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`;
|
||||
};
|
||||
|
||||
const sanitizeHtml = (html: string) => {
|
||||
return DOMPurify.sanitize(html, {
|
||||
ALLOWED_TAGS: [
|
||||
"p",
|
||||
"br",
|
||||
"strong",
|
||||
"em",
|
||||
"u",
|
||||
"a",
|
||||
"ul",
|
||||
"ol",
|
||||
"li",
|
||||
"h1",
|
||||
"h2",
|
||||
"h3",
|
||||
"h4",
|
||||
"h5",
|
||||
"h6",
|
||||
"img",
|
||||
"div",
|
||||
"span",
|
||||
"table",
|
||||
"tr",
|
||||
"td",
|
||||
"th",
|
||||
"thead",
|
||||
"tbody",
|
||||
],
|
||||
ALLOWED_ATTR: ["href", "src", "alt", "title", "style", "class"],
|
||||
});
|
||||
};
|
||||
|
||||
const handleDownloadAttachment = async (index: number, filename: string) => {
|
||||
try {
|
||||
const seqno = parseInt(mailId.split("-").pop() || "0", 10);
|
||||
|
||||
// 다운로드 URL
|
||||
const downloadUrl = `http://localhost:8080/api/mail/receive/${accountId}/${seqno}/attachment/${index}`;
|
||||
|
||||
// 다운로드 트리거
|
||||
const link = document.createElement('a');
|
||||
link.href = downloadUrl;
|
||||
link.download = filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
} catch (err) {
|
||||
console.error('첨부파일 다운로드 실패:', err);
|
||||
alert('첨부파일 다운로드에 실패했습니다.');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh] overflow-hidden flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center justify-between pr-6">
|
||||
<span className="text-xl font-bold truncate">메일 상세</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onClose}
|
||||
className="absolute right-4 top-4"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex justify-center items-center py-16">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-orange-500" />
|
||||
<span className="ml-3 text-gray-600">메일을 불러오는 중...</span>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="flex flex-col items-center justify-center py-16">
|
||||
<AlertCircle className="w-12 h-12 text-red-500 mb-4" />
|
||||
<p className="text-red-600">{error}</p>
|
||||
<Button onClick={loadMailDetail} variant="outline" className="mt-4">
|
||||
다시 시도
|
||||
</Button>
|
||||
</div>
|
||||
) : mail ? (
|
||||
<div className="flex-1 overflow-y-auto space-y-4">
|
||||
{/* 메일 헤더 */}
|
||||
<div className="border-b pb-4 space-y-2">
|
||||
<h2 className="text-2xl font-bold text-gray-900">
|
||||
{mail.subject}
|
||||
</h2>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-1 text-sm">
|
||||
<div>
|
||||
<span className="font-medium text-gray-700">보낸사람:</span>{" "}
|
||||
<span className="text-gray-900">{mail.from}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium text-gray-700">받는사람:</span>{" "}
|
||||
<span className="text-gray-600">{mail.to}</span>
|
||||
</div>
|
||||
{mail.cc && (
|
||||
<div>
|
||||
<span className="font-medium text-gray-700">참조:</span>{" "}
|
||||
<span className="text-gray-600">{mail.cc}</span>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<span className="font-medium text-gray-700">날짜:</span>{" "}
|
||||
<span className="text-gray-600">
|
||||
{formatDate(mail.date)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" size="sm">
|
||||
<Reply className="w-4 h-4 mr-2" />
|
||||
답장
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
<Forward className="w-4 h-4 mr-2" />
|
||||
전달
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 첨부파일 */}
|
||||
{mail.attachments && mail.attachments.length > 0 && (
|
||||
<div className="bg-gray-50 rounded-lg p-4">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Paperclip className="w-4 h-4 text-gray-600" />
|
||||
<span className="font-medium text-gray-700">
|
||||
첨부파일 ({mail.attachments.length})
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{mail.attachments.map((attachment, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center justify-between bg-white rounded px-3 py-2 border hover:border-orange-300 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Paperclip className="w-4 h-4 text-gray-400" />
|
||||
<span className="text-sm text-gray-900">
|
||||
{attachment.filename}
|
||||
</span>
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{formatFileSize(attachment.size)}
|
||||
</Badge>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDownloadAttachment(index, attachment.filename)}
|
||||
className="hover:bg-orange-50 hover:text-orange-600"
|
||||
>
|
||||
다운로드
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* HTML/텍스트 토글 */}
|
||||
{mail.htmlBody && mail.textBody && (
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant={showHtml ? "default" : "outline"}
|
||||
size="sm"
|
||||
onClick={() => setShowHtml(true)}
|
||||
className={
|
||||
showHtml ? "bg-orange-500 hover:bg-orange-600" : ""
|
||||
}
|
||||
>
|
||||
HTML 보기
|
||||
</Button>
|
||||
<Button
|
||||
variant={!showHtml ? "default" : "outline"}
|
||||
size="sm"
|
||||
onClick={() => setShowHtml(false)}
|
||||
className={
|
||||
!showHtml ? "bg-orange-500 hover:bg-orange-600" : ""
|
||||
}
|
||||
>
|
||||
텍스트 보기
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 메일 본문 */}
|
||||
<div className="border rounded-lg p-6 bg-white min-h-[300px]">
|
||||
{showHtml && mail.htmlBody ? (
|
||||
<div
|
||||
className="prose max-w-none"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: sanitizeHtml(mail.htmlBody),
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<pre className="whitespace-pre-wrap font-sans text-sm text-gray-800">
|
||||
{mail.textBody || "본문 내용이 없습니다."}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -359,3 +359,73 @@ function camelToKebab(str: string): string {
|
||||
return str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 📥 메일 수신 API (Step 2)
|
||||
// ============================================
|
||||
|
||||
export interface ReceivedMail {
|
||||
id: string;
|
||||
messageId: string;
|
||||
from: string;
|
||||
to: string;
|
||||
subject: string;
|
||||
date: string;
|
||||
preview: string;
|
||||
isRead: boolean;
|
||||
hasAttachments: boolean;
|
||||
}
|
||||
|
||||
export interface MailDetail extends ReceivedMail {
|
||||
htmlBody: string;
|
||||
textBody: string;
|
||||
cc?: string;
|
||||
bcc?: string;
|
||||
attachments: Array<{
|
||||
filename: string;
|
||||
contentType: string;
|
||||
size: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 받은 메일 목록 조회
|
||||
*/
|
||||
export async function getReceivedMails(
|
||||
accountId: string,
|
||||
limit: number = 50
|
||||
): Promise<ReceivedMail[]> {
|
||||
return fetchApi<ReceivedMail[]>(`/mail/receive/${accountId}?limit=${limit}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 메일 상세 조회
|
||||
*/
|
||||
export async function getMailDetail(
|
||||
accountId: string,
|
||||
seqno: number
|
||||
): Promise<MailDetail> {
|
||||
return fetchApi<MailDetail>(`/mail/receive/${accountId}/${seqno}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 메일을 읽음으로 표시
|
||||
*/
|
||||
export async function markMailAsRead(
|
||||
accountId: string,
|
||||
seqno: number
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
return fetchApi(`/mail/receive/${accountId}/${seqno}/mark-read`, {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* IMAP 연결 테스트
|
||||
*/
|
||||
export async function testImapConnection(
|
||||
accountId: string
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
return fetchApi(`/mail/receive/${accountId}/test-imap`, {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
568
frontend/package-lock.json
generated
568
frontend/package-lock.json
generated
@@ -40,6 +40,7 @@
|
||||
"cmdk": "^1.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"docx-preview": "^0.3.6",
|
||||
"isomorphic-dompurify": "^2.28.0",
|
||||
"lucide-react": "^0.525.0",
|
||||
"mammoth": "^1.11.0",
|
||||
"next": "15.4.4",
|
||||
@@ -88,6 +89,170 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/css-color": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.0.5.tgz",
|
||||
"integrity": "sha512-lMrXidNhPGsDjytDy11Vwlb6OIGrT3CmLg3VWNFyWkLWtijKl7xjvForlh8vuj0SHGjgl4qZEQzUmYTeQA2JFQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@csstools/css-calc": "^2.1.4",
|
||||
"@csstools/css-color-parser": "^3.1.0",
|
||||
"@csstools/css-parser-algorithms": "^3.0.5",
|
||||
"@csstools/css-tokenizer": "^3.0.4",
|
||||
"lru-cache": "^11.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/dom-selector": {
|
||||
"version": "6.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.5.7.tgz",
|
||||
"integrity": "sha512-cvdTPsi2qC1c22UppvuVmx/PDwuc6+QQkwt9OnwQD6Uotbh//tb2XDF0OoK2V0F4b8d02LIwNp3BieaDMAhIhA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@asamuzakjp/nwsapi": "^2.3.9",
|
||||
"bidi-js": "^1.0.3",
|
||||
"css-tree": "^3.1.0",
|
||||
"is-potential-custom-element-name": "^1.0.1",
|
||||
"lru-cache": "^11.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/nwsapi": {
|
||||
"version": "2.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz",
|
||||
"integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@csstools/color-helpers": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
|
||||
"integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT-0",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-calc": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz",
|
||||
"integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@csstools/css-parser-algorithms": "^3.0.5",
|
||||
"@csstools/css-tokenizer": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-color-parser": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz",
|
||||
"integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@csstools/color-helpers": "^5.1.0",
|
||||
"@csstools/css-calc": "^2.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@csstools/css-parser-algorithms": "^3.0.5",
|
||||
"@csstools/css-tokenizer": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-parser-algorithms": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz",
|
||||
"integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@csstools/css-tokenizer": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-syntax-patches-for-csstree": {
|
||||
"version": "1.0.14",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.14.tgz",
|
||||
"integrity": "sha512-zSlIxa20WvMojjpCSy8WrNpcZ61RqfTfX3XTaOeVlGJrt/8HF3YbzgFZa01yTbT4GWQLwfTcC3EB8i3XnB647Q==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT-0",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"postcss": "^8.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-tokenizer": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
|
||||
"integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@date-fns/tz": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.4.1.tgz",
|
||||
@@ -2872,6 +3037,13 @@
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/trusted-types": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@types/use-sync-external-store": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
|
||||
@@ -3515,6 +3687,15 @@
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/agent-base": {
|
||||
"version": "7.1.4",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
|
||||
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/ajv": {
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
@@ -3834,6 +4015,15 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bidi-js": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
|
||||
"integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"require-from-string": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/bluebird": {
|
||||
"version": "3.4.7",
|
||||
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz",
|
||||
@@ -4213,6 +4403,33 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/css-tree": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz",
|
||||
"integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mdn-data": "2.12.2",
|
||||
"source-map-js": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cssstyle": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.1.tgz",
|
||||
"integrity": "sha512-g5PC9Aiph9eiczFpcgUhd9S4UUO3F+LHGRIi5NUMZ+4xtoIYbHNZwZnWA2JsFGe8OU8nl4WyaEFiZuGuxlutJQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@asamuzakjp/css-color": "^4.0.3",
|
||||
"@csstools/css-syntax-patches-for-csstree": "^1.0.14",
|
||||
"css-tree": "^3.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
@@ -4413,6 +4630,19 @@
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/data-urls": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.0.tgz",
|
||||
"integrity": "sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"whatwg-mimetype": "^4.0.0",
|
||||
"whatwg-url": "^15.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/data-view-buffer": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz",
|
||||
@@ -4487,7 +4717,6 @@
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
@@ -4501,6 +4730,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/decimal.js": {
|
||||
"version": "10.6.0",
|
||||
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
|
||||
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/decimal.js-light": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
|
||||
@@ -4627,6 +4862,15 @@
|
||||
"jszip": ">=3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz",
|
||||
"integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==",
|
||||
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||
"optionalDependencies": {
|
||||
"@types/trusted-types": "^2.0.7"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "16.6.1",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
||||
@@ -4705,6 +4949,18 @@
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
|
||||
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/es-abstract": {
|
||||
"version": "1.24.0",
|
||||
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz",
|
||||
@@ -5916,6 +6172,56 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/html-encoding-sniffer": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
|
||||
"integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"whatwg-encoding": "^3.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/http-proxy-agent": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
|
||||
"integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "^7.1.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/https-proxy-agent": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
||||
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "^7.1.2",
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ignore": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||
@@ -6255,6 +6561,12 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-potential-custom-element-name": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
|
||||
"integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-regex": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
|
||||
@@ -6414,6 +6726,19 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/isomorphic-dompurify": {
|
||||
"version": "2.28.0",
|
||||
"resolved": "https://registry.npmjs.org/isomorphic-dompurify/-/isomorphic-dompurify-2.28.0.tgz",
|
||||
"integrity": "sha512-9G5v8g4tYoix5odskjG704Khm1zNrqqqOC4YjCwEUhx0OvuaijRCprAV2GwJ9iw/01c6H1R+rs/2AXPZLlgDaQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dompurify": "^3.2.7",
|
||||
"jsdom": "^27.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/iterator.prototype": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz",
|
||||
@@ -6462,6 +6787,45 @@
|
||||
"js-yaml": "bin/js-yaml.js"
|
||||
}
|
||||
},
|
||||
"node_modules/jsdom": {
|
||||
"version": "27.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.0.0.tgz",
|
||||
"integrity": "sha512-lIHeR1qlIRrIN5VMccd8tI2Sgw6ieYXSVktcSHaNe3Z5nE/tcPQYQWOq00wxMvYOsz+73eAkNenVvmPC6bba9A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@asamuzakjp/dom-selector": "^6.5.4",
|
||||
"cssstyle": "^5.3.0",
|
||||
"data-urls": "^6.0.0",
|
||||
"decimal.js": "^10.5.0",
|
||||
"html-encoding-sniffer": "^4.0.0",
|
||||
"http-proxy-agent": "^7.0.2",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"is-potential-custom-element-name": "^1.0.1",
|
||||
"parse5": "^7.3.0",
|
||||
"rrweb-cssom": "^0.8.0",
|
||||
"saxes": "^6.0.0",
|
||||
"symbol-tree": "^3.2.4",
|
||||
"tough-cookie": "^6.0.0",
|
||||
"w3c-xmlserializer": "^5.0.0",
|
||||
"webidl-conversions": "^8.0.0",
|
||||
"whatwg-encoding": "^3.1.1",
|
||||
"whatwg-mimetype": "^4.0.0",
|
||||
"whatwg-url": "^15.0.0",
|
||||
"ws": "^8.18.2",
|
||||
"xml-name-validator": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"canvas": "^3.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"canvas": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/json-buffer": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
|
||||
@@ -6863,6 +7227,15 @@
|
||||
"underscore": "^1.13.1"
|
||||
}
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "11.2.2",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz",
|
||||
"integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/lucide-react": {
|
||||
"version": "0.525.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.525.0.tgz",
|
||||
@@ -6924,6 +7297,12 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/mdn-data": {
|
||||
"version": "2.12.2",
|
||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz",
|
||||
"integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==",
|
||||
"license": "CC0-1.0"
|
||||
},
|
||||
"node_modules/merge2": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||
@@ -7019,7 +7398,6 @@
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
@@ -7393,6 +7771,18 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/parse5": {
|
||||
"version": "7.3.0",
|
||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
|
||||
"integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"entities": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/path-exists": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||
@@ -7488,7 +7878,6 @@
|
||||
"version": "8.5.6",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -7705,7 +8094,6 @@
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
@@ -8064,6 +8452,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/require-from-string": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/reselect": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
|
||||
@@ -8122,6 +8519,12 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/rrweb-cssom": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
|
||||
"integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/run-parallel": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||
@@ -8207,6 +8610,24 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/saxes": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
|
||||
"integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"xmlchars": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=v12.22.7"
|
||||
}
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.26.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz",
|
||||
@@ -8700,6 +9121,12 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/symbol-tree": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
|
||||
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/synckit": {
|
||||
"version": "0.11.11",
|
||||
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz",
|
||||
@@ -8825,6 +9252,24 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/tldts": {
|
||||
"version": "7.0.16",
|
||||
"resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.16.tgz",
|
||||
"integrity": "sha512-5bdPHSwbKTeHmXrgecID4Ljff8rQjv7g8zKQPkCozRo2HWWni+p310FSn5ImI+9kWw9kK4lzOB5q/a6iv0IJsw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tldts-core": "^7.0.16"
|
||||
},
|
||||
"bin": {
|
||||
"tldts": "bin/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/tldts-core": {
|
||||
"version": "7.0.16",
|
||||
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.16.tgz",
|
||||
"integrity": "sha512-XHhPmHxphLi+LGbH0G/O7dmUH9V65OY20R7vH8gETHsp5AZCjBk9l8sqmRKLaGOxnETU7XNSDUPtewAy/K6jbA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
@@ -8838,6 +9283,30 @@
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tough-cookie": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz",
|
||||
"integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"tldts": "^7.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/tr46": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz",
|
||||
"integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"punycode": "^2.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-api-utils": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
|
||||
@@ -9142,6 +9611,61 @@
|
||||
"d3-timer": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/w3c-xmlserializer": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
|
||||
"integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"xml-name-validator": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.0.tgz",
|
||||
"integrity": "sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-encoding": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
|
||||
"integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"iconv-lite": "0.6.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-mimetype": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
|
||||
"integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-url": {
|
||||
"version": "15.1.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.1.0.tgz",
|
||||
"integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tr46": "^6.0.0",
|
||||
"webidl-conversions": "^8.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
@@ -9275,6 +9799,27 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.18.3",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
|
||||
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/xlsx": {
|
||||
"version": "0.18.5",
|
||||
"resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
|
||||
@@ -9326,6 +9871,15 @@
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/xml-name-validator": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
|
||||
"integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/xmlbuilder": {
|
||||
"version": "10.1.1",
|
||||
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz",
|
||||
@@ -9335,6 +9889,12 @@
|
||||
"node": ">=4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/xmlchars": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
|
||||
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
"cmdk": "^1.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"docx-preview": "^0.3.6",
|
||||
"isomorphic-dompurify": "^2.28.0",
|
||||
"lucide-react": "^0.525.0",
|
||||
"mammoth": "^1.11.0",
|
||||
"next": "15.4.4",
|
||||
|
||||
Reference in New Issue
Block a user