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

// 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>
);
}