diff --git a/src/pages/ProjectDetail.tsx b/src/pages/ProjectDetail.tsx index a0176f4..985cba7 100644 --- a/src/pages/ProjectDetail.tsx +++ b/src/pages/ProjectDetail.tsx @@ -3,7 +3,7 @@ import { useParams, Link } from "react-router-dom"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { ArrowLeft, Plus, GanttChartSquare, Columns3, CalendarDays, Trash2, Upload, Download, Lock, Pencil, - Mail, ChevronDown, ChevronRight, RefreshCw, EyeOff, Eye, + Mail, ChevronDown, ChevronRight, RefreshCw, EyeOff, Eye, ExternalLink, } from "lucide-react"; import { getProject, getProjectMembers, getContacts, getTasks, getContract, getContractFiles, @@ -499,12 +499,23 @@ function Contacts({ projectId }: { projectId: string }) { function MailTab({ projectId }: { projectId: string }) { const { nameOf } = useDirectory(); const qc = useQueryClient(); - const q = useQuery({ queryKey: ["mails", projectId], queryFn: () => getProjectMails(projectId) }); const [showHidden, setShowHidden] = useState(false); + const [polling, setPolling] = useState(false); + const q = useQuery({ + queryKey: ["mails", projectId], + queryFn: () => getProjectMails(projectId), + refetchInterval: (query) => (polling || query.state.data?.syncing ? 3000 : false), + }); const sync = useMutation({ mutationFn: () => syncProjectMails(projectId), - onSuccess: () => setTimeout(() => qc.invalidateQueries({ queryKey: ["mails", projectId] }), 2500), + onMutate: () => setPolling(true), + onError: () => { setPolling(false); alert("동기화 요청에 실패했습니다. 잠시 후 다시 시도해 주세요."); }, + onSettled: () => { + qc.invalidateQueries({ queryKey: ["mails", projectId] }); + setTimeout(() => setPolling(false), 30000); // 백필 동안 자동 폴링 후 중단 + }, }); + const busy = sync.isPending || polling || !!q.data?.syncing; if (q.isLoading) return ; const data = q.data; @@ -527,8 +538,8 @@ function MailTab({ projectId }: { projectId: string }) { @{data.domain} 와(과) 주고받은 메일 · 내가 수신·참조(CC)된 메일만

- {data.lastSyncedAt ? `마지막 동기화 ${formatDateTime(data.lastSyncedAt)}` : data.syncing ? "처음 동기화 중…" : "아직 동기화 안 됨"} - {data.error && · 일부 메일함 오류} + {busy ? "동기화 중…" : data.lastSyncedAt ? `마지막 동기화 ${formatDateTime(data.lastSyncedAt)}` : "아직 동기화 안 됨"} + {data.error && · 동기화 오류(연동 설정 확인)}

@@ -537,8 +548,8 @@ function MailTab({ projectId }: { projectId: string }) { {showHidden ? "숨긴 메일 가리기" : `숨긴 메일 보기 (${hiddenCount})`} )} - +
{visible.length === 0 ? ( @@ -553,6 +564,11 @@ function MailTab({ projectId }: { projectId: string }) { ); } +// RFC822 Message-ID로 보는 사람 본인 Gmail에서 해당 메일을 연다. +function gmailUrl(messageId: string): string { + return `https://mail.google.com/mail/u/0/#search/rfc822msgid:${encodeURIComponent(messageId)}`; +} + // "Name " 또는 "a@b" → 표시용 짧은 이름(이름 또는 @앞) function addrName(a: string): string { a = a.trim(); @@ -589,9 +605,11 @@ function MailRow({ projectId, mail, nameOf }: { projectId: string; mail: Project - +
{when}