feat(mail): 메모를 메일 리스트의 독립 열로(클릭 전에도 보임·인라인 편집) + 수신자 요약
All checks were successful
build-and-push / build (push) Successful in 31s
All checks were successful
build-and-push / build (push) Successful in 31s
- 각 메일 행 우측에 '메모 열' 상시 표시, 바로 요약 메모 입력(blur 저장)·마지막 수정자 표시 - 보낸이→받는이 표시에서 수신자 많으면 '외 N명'으로 축약, hover(title)로 전체 노출 - 펼침 상세에 전체 받는사람/참조/시각/메일함 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
05eb82c635
commit
6bbdf4311d
@ -540,6 +540,23 @@ function MailTab({ projectId }: { projectId: string }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// "Name <a@b>" 또는 "a@b" → 표시용 짧은 이름(이름 또는 @앞)
|
||||||
|
function addrName(a: string): string {
|
||||||
|
a = a.trim();
|
||||||
|
const m = a.match(/^"?([^"<]+?)"?\s*<[^>]*>$/);
|
||||||
|
if (m) return m[1].trim();
|
||||||
|
const at = a.indexOf("@");
|
||||||
|
return at > 0 ? a.slice(0, at) : a;
|
||||||
|
}
|
||||||
|
// 수신자(받는사람+참조)가 많으면 "둘 + 외 N명"으로 줄이고, 전체는 title(hover)로.
|
||||||
|
function recipientSummary(to: string, cc: string) {
|
||||||
|
const parts = `${to},${cc}`.split(",").map((s) => s.trim()).filter(Boolean);
|
||||||
|
const names = parts.map(addrName);
|
||||||
|
const full = parts.join(", ");
|
||||||
|
const short = names.length <= 2 ? names.join(", ") : `${names.slice(0, 2).join(", ")} 외 ${names.length - 2}명`;
|
||||||
|
return { short: short || "—", full: full || "—" };
|
||||||
|
}
|
||||||
|
|
||||||
function MailRow({ projectId, mail, nameOf }: { projectId: string; mail: ProjectMail; nameOf: (e?: string | null) => string }) {
|
function MailRow({ projectId, mail, nameOf }: { projectId: string; mail: ProjectMail; nameOf: (e?: string | null) => string }) {
|
||||||
const qc = useQueryClient();
|
const qc = useQueryClient();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
@ -549,52 +566,56 @@ function MailRow({ projectId, mail, nameOf }: { projectId: string; mail: Project
|
|||||||
const save = useMutation({ mutationFn: () => putMailNote(projectId, mail.messageId, memo), onSuccess: invalidate });
|
const save = useMutation({ mutationFn: () => putMailNote(projectId, mail.messageId, memo), onSuccess: invalidate });
|
||||||
const hide = useMutation({ mutationFn: () => hideMail(projectId, mail.messageId, !mail.hidden), onSuccess: invalidate });
|
const hide = useMutation({ mutationFn: () => hideMail(projectId, mail.messageId, !mail.hidden), onSuccess: invalidate });
|
||||||
const when = mail.ts ? formatDateTime(new Date(mail.ts).toISOString()) : mail.date;
|
const when = mail.ts ? formatDateTime(new Date(mail.ts).toISOString()) : mail.date;
|
||||||
|
const rcpt = recipientSummary(mail.to, mail.cc);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className={classNames("overflow-hidden", mail.hidden && "opacity-60")}>
|
<Card className={classNames("overflow-hidden", mail.hidden && "opacity-60")}>
|
||||||
<div className="flex items-start gap-1 px-2 sm:px-3 py-3">
|
<div className="flex flex-col sm:flex-row sm:items-stretch">
|
||||||
|
{/* 메일 본문 요약 (제목·보낸이→받는이·스니펫) */}
|
||||||
|
<div className="flex items-start gap-1 min-w-0 flex-1 px-2 sm:px-3 py-3">
|
||||||
<button onClick={() => setOpen((o) => !o)} className="mt-0.5 text-ink-muted shrink-0 p-1 rounded hover:bg-canvas">
|
<button onClick={() => setOpen((o) => !o)} className="mt-0.5 text-ink-muted shrink-0 p-1 rounded hover:bg-canvas">
|
||||||
{open ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
|
{open ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
|
||||||
</button>
|
</button>
|
||||||
<button onClick={() => setOpen((o) => !o)} className="min-w-0 flex-1 text-left">
|
<button onClick={() => setOpen((o) => !o)} className="min-w-0 flex-1 text-left">
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<span className="text-sm font-semibold text-ink truncate">{mail.subject || "(제목 없음)"}</span>
|
<span className="text-sm font-semibold text-ink truncate">{mail.subject || "(제목 없음)"}</span>
|
||||||
{mail.note && <span className="text-[10px] bg-chip-bg text-navy rounded-pill px-1.5 py-0.5 shrink-0">메모</span>}
|
|
||||||
{mail.hidden && <span className="text-[10px] bg-divider text-ink-muted rounded-pill px-1.5 py-0.5 shrink-0">숨김</span>}
|
{mail.hidden && <span className="text-[10px] bg-divider text-ink-muted rounded-pill px-1.5 py-0.5 shrink-0">숨김</span>}
|
||||||
</span>
|
</span>
|
||||||
<span className="block text-xs text-ink-muted truncate mt-0.5">{mail.from}</span>
|
<span className="block text-xs text-ink-muted truncate mt-0.5">
|
||||||
<span className="block text-sm text-ink-secondary truncate mt-1">{mail.snippet}</span>
|
{addrName(mail.from)} <span className="text-ink-muted/70">→</span>{" "}
|
||||||
{/* 메일 리스트에 메모도 함께 표시 */}
|
<span title={rcpt.full} className="cursor-help underline decoration-dotted decoration-ink-muted/40 underline-offset-2">{rcpt.short}</span>
|
||||||
{mail.note && (
|
|
||||||
<span className="mt-2 block text-xs text-ink bg-navy-subtle/40 border border-navy-subtle rounded-control px-2.5 py-1.5 line-clamp-2">
|
|
||||||
📝 {mail.note}
|
|
||||||
</span>
|
</span>
|
||||||
)}
|
<span className="block text-sm text-ink-secondary truncate mt-1">{mail.snippet}</span>
|
||||||
</button>
|
</button>
|
||||||
<div className="flex items-center gap-1 shrink-0">
|
<div className="flex items-center gap-1 shrink-0">
|
||||||
<span className="text-[11px] text-ink-muted tabular hidden sm:block">{when}</span>
|
<span className="text-[11px] text-ink-muted tabular hidden md:block">{when}</span>
|
||||||
<button onClick={() => hide.mutate()} title={mail.hidden ? "다시 보이기" : "숨기기"}
|
<button onClick={() => hide.mutate()} title={mail.hidden ? "다시 보이기" : "숨기기"}
|
||||||
className="p-1.5 rounded text-ink-muted hover:text-ink hover:bg-canvas">
|
className="p-1.5 rounded text-ink-muted hover:text-ink hover:bg-canvas">
|
||||||
{mail.hidden ? <Eye size={15} /> : <EyeOff size={15} />}
|
{mail.hidden ? <Eye size={15} /> : <EyeOff size={15} />}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{open && (
|
{/* 메모 열 — 클릭 전에도 항상 보이고 바로 요약을 적을 수 있다 */}
|
||||||
<div className="px-4 pb-4 pt-1 border-t border-divider space-y-3">
|
<div className="sm:w-72 shrink-0 border-t sm:border-t-0 sm:border-l border-divider px-3 py-2 flex flex-col bg-canvas/30">
|
||||||
<div className="text-xs text-ink-muted grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-1">
|
<textarea
|
||||||
<div><span className="text-ink-secondary font-medium">보낸사람</span> {mail.from}</div>
|
value={memo}
|
||||||
<div><span className="text-ink-secondary font-medium">받는사람</span> {mail.to}</div>
|
onChange={(e) => setMemo(e.target.value)}
|
||||||
{mail.cc && <div className="sm:col-span-2"><span className="text-ink-secondary font-medium">참조</span> {mail.cc}</div>}
|
|
||||||
<div className="sm:col-span-2 sm:hidden"><span className="text-ink-secondary font-medium">시각</span> {when}</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="flex items-center justify-between mb-1">
|
|
||||||
<span className="form-label !mb-0">공동 메모 <span className="text-ink-muted font-normal">· 프로젝트 구성원 누구나 수정</span></span>
|
|
||||||
{mail.noteEditedBy && <span className="text-[11px] text-ink-muted">마지막 수정 {nameOf(mail.noteEditedBy)}</span>}
|
|
||||||
</div>
|
|
||||||
<Textarea value={memo} onChange={(e) => setMemo(e.target.value)}
|
|
||||||
onBlur={() => { if (memo !== (mail.note ?? "")) save.mutate(); }}
|
onBlur={() => { if (memo !== (mail.note ?? "")) save.mutate(); }}
|
||||||
placeholder="이 메일에 대한 팀 메모를 남기세요. (대응 방향·할 일·합의 내용 등)" style={{ minHeight: 80 }} />
|
placeholder="요약 메모 입력…"
|
||||||
|
rows={2}
|
||||||
|
className="w-full text-xs leading-snug text-ink bg-transparent rounded-control px-2 py-1.5 resize-none border border-transparent hover:border-border focus:border-navy focus:bg-surface focus:outline-none min-h-[44px]"
|
||||||
|
/>
|
||||||
|
{mail.noteEditedBy && <span className="text-[10px] text-ink-muted mt-1 px-2 truncate">✎ {nameOf(mail.noteEditedBy)}</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{open && (
|
||||||
|
<div className="px-4 pb-4 pt-2 border-t border-divider">
|
||||||
|
<div className="text-xs text-ink-muted grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-1">
|
||||||
|
<div className="sm:col-span-2"><span className="text-ink-secondary font-medium">보낸사람</span> {mail.from}</div>
|
||||||
|
<div className="sm:col-span-2"><span className="text-ink-secondary font-medium">받는사람</span> {mail.to || "—"}</div>
|
||||||
|
{mail.cc && <div className="sm:col-span-2"><span className="text-ink-secondary font-medium">참조</span> {mail.cc}</div>}
|
||||||
|
<div><span className="text-ink-secondary font-medium">시각</span> {when}</div>
|
||||||
|
<div><span className="text-ink-secondary font-medium">발견 메일함</span> {nameOf(mail.mailbox)}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user