|
|
/* 아리 — 회의 도우미 드로어 (일정 페이지)
|
|
|
사전 브리핑(upcoming) · 실시간 기록(live) · 회의 노트 + 액션 추출(done) · 1:1 어시스턴트
|
|
|
window.CalMeet = { MeetDrawer } */
|
|
|
(function () {
|
|
|
const { useState, useEffect } = React;
|
|
|
|
|
|
const MP = {
|
|
|
spark: "M12 3l1.9 5.2L19 10l-5.1 1.8L12 17l-1.9-5.2L5 10l5.1-1.8z|M19 4v3|M5 17v3",
|
|
|
x: "M18 6 6 18M6 6l12 12",
|
|
|
tick: "M20 6 9 17l-5-5",
|
|
|
check: "M9 11l3 3L22 4|M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11",
|
|
|
clock: "M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20z|M12 7v5l3 2",
|
|
|
mic: "M12 2a3 3 0 0 0-3 3v6a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3z|M19 10v1a7 7 0 0 1-14 0v-1|M12 18v4M8 22h8",
|
|
|
file: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z|M14 2v6h6M9 13h6M9 17h4",
|
|
|
users: "M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2|M9 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8z|M23 21v-2a4 4 0 0 0-3-3.87|M16 3.13a4 4 0 0 1 0 7.75",
|
|
|
list: "M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01",
|
|
|
msg: "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z",
|
|
|
target: "M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20z|M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8z|M12 12h.01",
|
|
|
flag: "M4 22V4M4 4s1.5-1 5-1 5 2 8 2 4-1 4-1v10s-1.5 1-4 1-5-2-8-2-5 1-5 1",
|
|
|
heart: "M19 14c1.5-1.5 3-3.4 3-5.5A4.5 4.5 0 0 0 12 5.5 4.5 4.5 0 0 0 2 8.5c0 2.1 1.5 4 3 5.5l7 7z",
|
|
|
plus: "M12 5v14M5 12h14",
|
|
|
zap: "M13 2 3 14h9l-1 8 10-12h-9z",
|
|
|
};
|
|
|
function MI({ d, w }) {
|
|
|
return (
|
|
|
<svg className="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
|
|
strokeWidth={w || 1.9} strokeLinecap="round" strokeLinejoin="round">
|
|
|
{MP[d].split("|").map((s, i) => <path key={i} d={s} />)}
|
|
|
</svg>
|
|
|
);
|
|
|
}
|
|
|
|
|
|
function Sec({ icon, title, hint, children }) {
|
|
|
return (
|
|
|
<section className="md-sec">
|
|
|
<div className="md-sh"><MI d={icon} /><b>{title}</b>{hint && <span className="md-hint">{hint}</span>}</div>
|
|
|
{children}
|
|
|
</section>
|
|
|
);
|
|
|
}
|
|
|
|
|
|
/* 액션 아이템 한 줄 — '작업으로' 보내기 */
|
|
|
function ActionRow({ a, onAdd }) {
|
|
|
return (
|
|
|
<div className={"md-act" + (a.added ? " added" : "")}>
|
|
|
<span className="md-act-check"><MI d="tick" w="3" /></span>
|
|
|
<span className="md-act-text">{a.text}</span>
|
|
|
<span className="md-act-who">{a.who} · {a.when}</span>
|
|
|
{a.added
|
|
|
? <span className="md-act-done"><MI d="tick" w="2.6" />작업에 있음</span>
|
|
|
: <button className="md-act-add" onClick={onAdd}><MI d="plus" />작업으로</button>}
|
|
|
</div>
|
|
|
);
|
|
|
}
|
|
|
|
|
|
function MeetDrawer({ ev, meet, calName, tone, onClose }) {
|
|
|
const [actions, setActions] = useState(meet.actions || []);
|
|
|
useEffect(() => setActions(meet.actions || []), [ev.id]);
|
|
|
useEffect(() => {
|
|
|
const h = (e) => { if (e.key === "Escape") onClose(); };
|
|
|
window.addEventListener("keydown", h); return () => window.removeEventListener("keydown", h);
|
|
|
}, [onClose]);
|
|
|
|
|
|
const addOne = (i) => setActions((as) => as.map((a, j) => (j === i ? { ...a, added: true } : a)));
|
|
|
const addAll = () => setActions((as) => as.map((a) => ({ ...a, added: true })));
|
|
|
const remaining = actions.filter((a) => !a.added).length;
|
|
|
|
|
|
const phaseBadge = meet.phase === "done"
|
|
|
? <span className="md-phase done"><MI d="tick" w="2.6" />회의 노트 정리됨</span>
|
|
|
: meet.phase === "live"
|
|
|
? <span className="md-phase live"><span className="md-pulse" />기록 중</span>
|
|
|
: <span className="md-phase up"><MI d="clock" />{meet.startsIn}</span>;
|
|
|
|
|
|
return (
|
|
|
<>
|
|
|
<div className="dp-backdrop" onClick={onClose} />
|
|
|
<aside className={"mdrawer tint-" + tone} role="dialog" aria-label="회의 도우미">
|
|
|
<div className="md-head">
|
|
|
<div className="md-ic"><MI d={meet.oneOnOne ? "heart" : "spark"} /></div>
|
|
|
<div className="md-ht">
|
|
|
<span className="md-k">{meet.oneOnOne ? "1:1 어시스턴트" : meet.phase === "upcoming" ? "사전 브리핑" : "회의 도우미"}</span>
|
|
|
<b>{ev.title}</b>
|
|
|
<span className="md-sub">{ev.start}–{ev.end}{ev.loc ? " · " + ev.loc : ""} · {calName}</span>
|
|
|
</div>
|
|
|
<button className="md-x" onClick={onClose} aria-label="닫기"><MI d="x" /></button>
|
|
|
</div>
|
|
|
<div className="md-bar">{phaseBadge}{meet.meta && <span className="md-meta">{meet.meta}</span>}</div>
|
|
|
|
|
|
<div className="md-body">
|
|
|
{/* ---- 1:1 — 지난 대화 · 약속 · 시그널 · 대화 주제 ---- */}
|
|
|
{meet.oneOnOne && (
|
|
|
<>
|
|
|
<Sec icon="msg" title="지난 1:1 요약" hint={meet.lastMeeting.when}>
|
|
|
<p className="md-note">{meet.lastMeeting.note}</p>
|
|
|
</Sec>
|
|
|
<Sec icon="flag" title="지난번 약속한 것">
|
|
|
{meet.promises.map((p, i) => (
|
|
|
<div key={i} className={"md-chk" + (p.done ? " done" : "")}>
|
|
|
<span className="md-cbx"><MI d="tick" w="3" /></span>{p.text}
|
|
|
{!p.done && <span className="md-tag warn">아직</span>}
|
|
|
</div>
|
|
|
))}
|
|
|
</Sec>
|
|
|
<Sec icon="zap" title={meet.person.name + "님 시그널"} hint="아리가 작업·메일에서 정리">
|
|
|
{meet.signals.map((s, i) => (
|
|
|
<div key={i} className="md-sig">
|
|
|
<span className="md-sig-dot" style={{ background: `var(--${s.tone})` }} />
|
|
|
<span className="md-sig-k">{s.k}</span>
|
|
|
<span className="md-sig-v">{s.v}</span>
|
|
|
</div>
|
|
|
))}
|
|
|
</Sec>
|
|
|
<Sec icon="spark" title="이런 이야기를 나눠보세요">
|
|
|
<ol className="md-points">
|
|
|
{meet.talkingPoints.map((t, i) => <li key={i}>{t}</li>)}
|
|
|
</ol>
|
|
|
</Sec>
|
|
|
</>
|
|
|
)}
|
|
|
|
|
|
{/* ---- 예정 회의 — 안건 · 지난 회의 · 지난 액션 · 인사이트 ---- */}
|
|
|
{!meet.oneOnOne && meet.phase === "upcoming" && (
|
|
|
<>
|
|
|
<Sec icon="list" title="오늘 안건">
|
|
|
<ol className="md-points">{meet.agenda.map((a, i) => <li key={i}>{a}</li>)}</ol>
|
|
|
</Sec>
|
|
|
<Sec icon="msg" title="지난 회의에서" hint={meet.lastMeeting.when}>
|
|
|
<p className="md-note">{meet.lastMeeting.note}</p>
|
|
|
</Sec>
|
|
|
<Sec icon="target" title="지난 액션 아이템"
|
|
|
hint={meet.lastActions.filter((a) => a.done).length + "/" + meet.lastActions.length + " 완료"}>
|
|
|
{meet.lastActions.map((a, i) => (
|
|
|
<div key={i} className={"md-chk" + (a.done ? " done" : "")}>
|
|
|
<span className="md-cbx"><MI d="tick" w="3" /></span>{a.text}
|
|
|
<span className="md-who">{a.who}</span>
|
|
|
{!a.done && <span className="md-tag warn">미완</span>}
|
|
|
</div>
|
|
|
))}
|
|
|
</Sec>
|
|
|
<Sec icon="spark" title="아리 인사이트">
|
|
|
{meet.insights.map((t, i) => (
|
|
|
<p key={i} className="md-insight" dangerouslySetInnerHTML={{ __html: t }} />
|
|
|
))}
|
|
|
</Sec>
|
|
|
{meet.docs && (
|
|
|
<div className="md-docs">
|
|
|
{meet.docs.map((d, i) => <span key={i} className="md-doc"><MI d="file" />{d}</span>)}
|
|
|
</div>
|
|
|
)}
|
|
|
</>
|
|
|
)}
|
|
|
|
|
|
{/* ---- 끝난/진행 중 회의 — 요약 · 결정 · 액션 추출 ---- */}
|
|
|
{!meet.oneOnOne && meet.phase !== "upcoming" && (
|
|
|
<>
|
|
|
<Sec icon={meet.phase === "live" ? "mic" : "msg"}
|
|
|
title={meet.phase === "live" ? "지금까지 논의" : "핵심 논의 요약"}
|
|
|
hint={meet.phase === "live" ? "실시간" : "음성 기록 기반"}>
|
|
|
<ul className="md-sum">{meet.summary.map((s, i) => <li key={i}>{s}</li>)}</ul>
|
|
|
</Sec>
|
|
|
{meet.decisions.length > 0 && (
|
|
|
<Sec icon="flag" title="결정 사항">
|
|
|
{meet.decisions.map((d, i) => (
|
|
|
<div key={i} className="md-decision"><MI d="tick" w="2.6" />{d}</div>
|
|
|
))}
|
|
|
</Sec>
|
|
|
)}
|
|
|
<Sec icon="target" title="액션 아이템" hint="아리가 대화에서 추출">
|
|
|
{actions.map((a, i) => <ActionRow key={i} a={a} onAdd={() => addOne(i)} />)}
|
|
|
{remaining > 0 ? (
|
|
|
<button className="md-addall" onClick={addAll}>
|
|
|
<MI d="check" />남은 {remaining}건 모두 작업으로 보내기
|
|
|
</button>
|
|
|
) : (
|
|
|
<div className="md-allok"><MI d="tick" w="2.6" />모든 액션이 담당자 작업으로 등록됐어요</div>
|
|
|
)}
|
|
|
</Sec>
|
|
|
</>
|
|
|
)}
|
|
|
</div>
|
|
|
</aside>
|
|
|
</>
|
|
|
);
|
|
|
}
|
|
|
|
|
|
window.CalMeet = { MeetDrawer };
|
|
|
})();
|