You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
211 lines
6.6 KiB
TypeScript
211 lines
6.6 KiB
TypeScript
// frontend/components/mail/MailSidebar.tsx — 좌측 사이드바
|
|
// 계정 루트 트리: 전체 받은편지함 + 각 계정 아래 '실제' 폴더/라벨(Gmail 라벨·Outlook 폴더)을 서브트리로.
|
|
"use client";
|
|
import { useState } from "react";
|
|
import { Icon } from "@/components/Icon";
|
|
import type { IconName } from "@/components/icons/paths";
|
|
import type { AccountFolder, MailAccount } from "@/lib/types";
|
|
|
|
// 한 계정(또는 통합) 가지: 부모 행 + 실제 폴더 자식들.
|
|
function Branch({
|
|
nodeKey,
|
|
scope,
|
|
unread,
|
|
label,
|
|
sub,
|
|
dot,
|
|
open,
|
|
folders,
|
|
acctFilter,
|
|
folder,
|
|
onToggle,
|
|
onSelectScope,
|
|
}: {
|
|
nodeKey: string;
|
|
scope: string | null; // null=통합, 그 외=계정 id
|
|
unread: number;
|
|
label: string;
|
|
sub?: string;
|
|
dot?: string; // 계정 색
|
|
open: boolean;
|
|
folders: AccountFolder[]; // 이 가지에 표시할 실제 폴더/라벨
|
|
acctFilter: string | null;
|
|
folder: string;
|
|
onToggle: (key: string) => void;
|
|
onSelectScope: (acctFilter: string | null, folder: string) => void;
|
|
}) {
|
|
const scopeActive = acctFilter === scope && folder !== "smart";
|
|
return (
|
|
<div className="mt-branch">
|
|
<div className={"mt-parent" + (scopeActive ? " scope" : "")}>
|
|
<button
|
|
className={"mt-twist" + (open ? " open" : "")}
|
|
aria-label={open ? "접기" : "펼치기"}
|
|
onClick={() => onToggle(nodeKey)}
|
|
>
|
|
<Icon name="chev" />
|
|
</button>
|
|
<button
|
|
className="mt-parent-main"
|
|
onClick={() => {
|
|
onSelectScope(scope, "inbox");
|
|
if (!open) onToggle(nodeKey);
|
|
}}
|
|
>
|
|
{dot ? <span className="acc-dot" style={{ background: dot }} /> : <Icon name="layers" />}
|
|
<span className="mt-parent-txt">
|
|
<b>{label}</b>
|
|
{sub && <span>{sub}</span>}
|
|
</span>
|
|
{unread > 0 && <span className="fl-cnt">{unread}</span>}
|
|
</button>
|
|
</div>
|
|
{open && (
|
|
<div className="mt-kids">
|
|
{folders.map((f) => {
|
|
const on = acctFilter === scope && folder === f.slug;
|
|
return (
|
|
<button
|
|
key={f.slug}
|
|
className={"fl-row mt-child" + (on ? " on" : "")}
|
|
onClick={() => onSelectScope(scope, f.slug)}
|
|
title={f.name}
|
|
>
|
|
<Icon name={(f.icon || "folder") as IconName} />
|
|
<span className="fl-name">{f.name}</span>
|
|
{f.unread > 0 && <span className="fl-cnt">{f.unread}</span>}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function MailSidebar({
|
|
accounts,
|
|
folder,
|
|
acctFilter,
|
|
smartCount,
|
|
unifiedUnread,
|
|
accUnread,
|
|
unifiedFolders,
|
|
foldersByAccount,
|
|
accColor,
|
|
onCompose,
|
|
onSelectSmart,
|
|
onSelectScope,
|
|
onToast,
|
|
}: {
|
|
accounts: MailAccount[];
|
|
folder: string;
|
|
acctFilter: string | null;
|
|
smartCount: number;
|
|
unifiedUnread: number;
|
|
accUnread: (id: string) => number;
|
|
unifiedFolders: AccountFolder[];
|
|
foldersByAccount: Record<string, AccountFolder[]>;
|
|
accColor: (id: string) => string;
|
|
onCompose: () => void;
|
|
onSelectSmart: () => void;
|
|
onSelectScope: (acctFilter: string | null, folder: string) => void;
|
|
onToast: (msg: string, tone: string) => void;
|
|
}) {
|
|
// 펼친 계정 집합(기본은 전부 접힘). 키: 계정 id.
|
|
const [openAccts, setOpenAccts] = useState<Set<string>>(new Set());
|
|
const toggle = (key: string) =>
|
|
setOpenAccts((prev) => {
|
|
const next = new Set(prev);
|
|
if (next.has(key)) next.delete(key);
|
|
else next.add(key);
|
|
return next;
|
|
});
|
|
|
|
return (
|
|
<aside className="subnav">
|
|
<button className="compose-btn" onClick={onCompose}>
|
|
<Icon name="pen" />새 메일
|
|
</button>
|
|
<div className="sn-scroll" role="region" aria-label="메일 폴더·계정" tabIndex={0}>
|
|
<button
|
|
className={"sn-smart" + (folder === "smart" ? " on" : "")}
|
|
onClick={onSelectSmart}
|
|
>
|
|
<div className="sp-ico">
|
|
<Icon name="spark" />
|
|
</div>
|
|
<div className="sp-txt">
|
|
<b>아리 정리</b>
|
|
<span>새 메일 자동 분류</span>
|
|
</div>
|
|
{smartCount > 0 && <span className="sp-cnt">{smartCount}</span>}
|
|
</button>
|
|
|
|
{/* 전체 받은편지함 — 공통 시스템 폴더 플랫 목록(트리 아님). 계정 통합. */}
|
|
<div className="sn-unified-folders">
|
|
{unifiedFolders.map((f) => {
|
|
// 받은편지함 행은 집중/기타(others) 탭을 포함하므로 둘 다일 때 활성.
|
|
const isInbox = f.slug === "inbox";
|
|
const on =
|
|
acctFilter === null && (folder === f.slug || (isInbox && folder === "others"));
|
|
const label = isInbox ? "전체 받은편지함" : f.name;
|
|
const cnt = isInbox ? unifiedUnread : f.unread;
|
|
return (
|
|
<button
|
|
key={f.slug}
|
|
className={"fl-row" + (on ? " on" : "")}
|
|
onClick={() => onSelectScope(null, f.slug)}
|
|
title={label}
|
|
>
|
|
<Icon name={(f.icon || "folder") as IconName} />
|
|
<span className="fl-name">{label}</span>
|
|
{cnt > 0 && <span className="fl-cnt">{cnt}</span>}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
<div className="sn-label">
|
|
계정{" "}
|
|
<button
|
|
className="add"
|
|
aria-label="계정 추가"
|
|
onClick={() => onToast("계정 연결 화면을 여는 중…", "blue")}
|
|
>
|
|
<Icon name="plus" />
|
|
</button>
|
|
</div>
|
|
{accounts.map((a) => (
|
|
<Branch
|
|
key={a.id}
|
|
nodeKey={a.id}
|
|
scope={a.id}
|
|
unread={accUnread(a.id)}
|
|
label={a.name}
|
|
sub={a.email}
|
|
dot={accColor(a.id)}
|
|
open={openAccts.has(a.id)}
|
|
folders={foldersByAccount[a.id] ?? []}
|
|
acctFilter={acctFilter}
|
|
folder={folder}
|
|
onToggle={toggle}
|
|
onSelectScope={onSelectScope}
|
|
/>
|
|
))}
|
|
<button className="acc-add" onClick={() => onToast("계정 연결 화면을 여는 중…", "blue")}>
|
|
<Icon name="plus" />
|
|
계정 수입하기
|
|
</button>
|
|
</div>
|
|
<div className="sn-foot">
|
|
<div className="av-lg">지</div>
|
|
<div className="txt">
|
|
<b>지우님</b>
|
|
<span>Pro 플랜 · {accounts.length}개 계정</span>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
);
|
|
}
|