|
|
/* 아리 — 자동화 규칙 빌더 페이지
|
|
|
좌측 레일로 하위 화면 전환: 내 규칙 · 새 자동화 · 아리 제안 · 실행 기록 */
|
|
|
const { useState } = React;
|
|
|
const I = window.AriShell.Icon;
|
|
|
const Topbar = window.AriShell.Topbar;
|
|
|
const SubRail = window.AriShell.SubRail;
|
|
|
const AU = window.AriAuto;
|
|
|
|
|
|
const AU_TONE = { mail: "var(--blue)", cal: "var(--violet)", focus: "var(--coral)", life: "var(--green)" };
|
|
|
const AU_CAT = { mail: "메일", cal: "일정", focus: "집중", life: "라이프" };
|
|
|
|
|
|
function Flow({ p, small }) {
|
|
|
return (
|
|
|
<div className={"fl" + (small ? " rl-flow" : "")}>
|
|
|
<span className="fl-chip tg"><span className="k">트리거</span><span className="v">{p.trigger}</span></span>
|
|
|
{p.cond && (
|
|
|
<React.Fragment>
|
|
|
<span className="fl-arr"><I d="arrow" /></span>
|
|
|
<span className="fl-chip cd"><span className="k">조건</span><span className="v">{p.cond}</span></span>
|
|
|
</React.Fragment>
|
|
|
)}
|
|
|
<span className="fl-arr"><I d="arrow" /></span>
|
|
|
<span className="fl-chip ac"><span className="k">동작</span><span className="v">{p.action}</span></span>
|
|
|
</div>
|
|
|
);
|
|
|
}
|
|
|
|
|
|
/* ===== 내 규칙 ===== */
|
|
|
function RulesView({ rules, toggle }) {
|
|
|
const activeN = rules.filter((r) => r.on).length;
|
|
|
return (
|
|
|
<div className="board">
|
|
|
<section className="card sp2 r2">
|
|
|
<div className="ch">
|
|
|
<div className="ico"><I d="zap" /></div>
|
|
|
<div className="htext"><h3>내 규칙</h3><div className="sub">켜고 끄는 건 언제든지</div></div>
|
|
|
<span className="count">{activeN}/{rules.length} 켜짐</span>
|
|
|
</div>
|
|
|
<div className="rl-list">
|
|
|
{rules.map((r) => (
|
|
|
<div key={r.id} className={"rl-row" + (r.on ? "" : " off") + (r.fresh ? " rl-new" : "")}>
|
|
|
<button
|
|
|
className={"sw" + (r.on ? " on" : "")}
|
|
|
onClick={() => toggle(r.id)}
|
|
|
aria-label={r.name + (r.on ? " 끄기" : " 켜기")}
|
|
|
></button>
|
|
|
<div className="rl-body">
|
|
|
<div className="rl-top">
|
|
|
<span className="rl-name">{r.name}</span>
|
|
|
<span className="rl-cat" style={{ background: AU_TONE[r.cat] }}>{AU_CAT[r.cat]}</span>
|
|
|
</div>
|
|
|
<Flow small p={{ trigger: r.trigger, cond: r.cond, action: r.action }} />
|
|
|
<div className="rl-meta">
|
|
|
<span>마지막 실행 {r.last}</span>
|
|
|
<span>·</span>
|
|
|
<span>이번 주 {r.runs}회</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
))}
|
|
|
</div>
|
|
|
</section>
|
|
|
|
|
|
<section className="card sp2">
|
|
|
<div className="ch">
|
|
|
<div className="ico"><I d="shield" /></div>
|
|
|
<div className="htext"><h3>자동화 작동 방식</h3><div className="sub">안심하고 맡기세요</div></div>
|
|
|
</div>
|
|
|
<div className="how-list">
|
|
|
<div className="how"><span className="how-ic"><I d="tick" /></span><div><b>되돌릴 수 있어요</b><span>모든 자동 실행은 결재함에 기록되고 한 번에 되돌릴 수 있어요.</span></div></div>
|
|
|
<div className="how"><span className="how-ic"><I d="shield" /></span><div><b>위험한 건 물어봐요</b><span>보내기·결제·삭제는 규칙이 있어도 실행 전에 확인받아요.</span></div></div>
|
|
|
<div className="how"><span className="how-ic"><I d="brain" /></span><div><b>스스로 배워요</b><span>반복되는 손길을 발견하면 새 규칙을 먼저 제안해요.</span></div></div>
|
|
|
</div>
|
|
|
</section>
|
|
|
</div>
|
|
|
);
|
|
|
}
|
|
|
|
|
|
/* ===== 새 자동화 (작성 플로우) ===== */
|
|
|
function NewView({ onCreate }) {
|
|
|
const [text, setText] = useState("");
|
|
|
const match = AU.examples.find((e) => e.text === text.trim());
|
|
|
|
|
|
return (
|
|
|
<div className="board">
|
|
|
<section className="card sp2 hero">
|
|
|
<span className="hero-spark"><I d="zap" />새 자동화 만들기</span>
|
|
|
<p className="brief">하고 싶은 일을 <b>한 문장</b>으로 적으세요. 트리거·조건·동작은 아리가 찾아요.</p>
|
|
|
<div className="cmd">
|
|
|
<I d="pen" />
|
|
|
<input
|
|
|
value={text}
|
|
|
onChange={(e) => setText(e.target.value)}
|
|
|
onKeyDown={(e) => { if (e.key === "Enter" && match) onCreate(match); }}
|
|
|
placeholder="예: 출장 전날엔 저녁 일정 비워줘"
|
|
|
aria-label="새 규칙 문장"
|
|
|
autoFocus
|
|
|
/>
|
|
|
<button className="send" disabled={!match} onClick={() => match && onCreate(match)} aria-label="규칙 만들기"><I d="arrow" /></button>
|
|
|
</div>
|
|
|
<div className="chips">
|
|
|
{AU.examples.map((e) => (
|
|
|
<button key={e.text} className="chip" onClick={() => setText(e.text)}>
|
|
|
<I d="spark" />{e.text}
|
|
|
</button>
|
|
|
))}
|
|
|
</div>
|
|
|
</section>
|
|
|
|
|
|
{/* 미리보기 */}
|
|
|
<section className="card sp2">
|
|
|
<div className="ch">
|
|
|
<div className="ico"><I d="spark" /></div>
|
|
|
<div className="htext"><h3>미리보기</h3><div className="sub">{match ? "아리가 이렇게 이해했어요" : "문장을 적으면 여기에 규칙이 그려져요"}</div></div>
|
|
|
{match && <span className="rl-cat" style={{ background: AU_TONE[match.cat] }}>{AU_CAT[match.cat]}</span>}
|
|
|
</div>
|
|
|
{match ? (
|
|
|
<React.Fragment>
|
|
|
<div className="nv-name">{match.name}</div>
|
|
|
<Flow p={match.parse} />
|
|
|
<button className="rb-make-btn" onClick={() => onCreate(match)}>
|
|
|
<I d="plus" />이 규칙 만들기
|
|
|
</button>
|
|
|
<div className="nv-hint"><I d="shield" />만든 규칙은 ‘내 규칙’에서 언제든 끄거나 지울 수 있어요.</div>
|
|
|
</React.Fragment>
|
|
|
) : (
|
|
|
<div className="nv-empty">
|
|
|
<span className="nv-empty-ic"><I d="zap" /></span>
|
|
|
<p>예시 문장을 누르거나 직접 적어보세요. 아리가 <b>트리거 → 조건 → 동작</b>으로 나눠 보여드려요.</p>
|
|
|
</div>
|
|
|
)}
|
|
|
</section>
|
|
|
</div>
|
|
|
);
|
|
|
}
|
|
|
|
|
|
/* ===== 아리 제안 ===== */
|
|
|
function SuggestView({ suggests, accept, dismiss }) {
|
|
|
return (
|
|
|
<div className="board">
|
|
|
<section className="card sp2">
|
|
|
<div className="ch">
|
|
|
<div className="ico"><I d="brain" /></div>
|
|
|
<div className="htext"><h3>아리 제안</h3><div className="sub">반복 패턴을 발견했어요</div></div>
|
|
|
<span className="count">{suggests.length}건</span>
|
|
|
</div>
|
|
|
<div className="sg">
|
|
|
{suggests.map((s) => (
|
|
|
<div className="sg-card" key={s.id}>
|
|
|
<div className="sg-pat"><I d="spark" /><span>{s.pattern}</span></div>
|
|
|
<div className="sg-offer">
|
|
|
<div className="sg-name">{s.offer.name}</div>
|
|
|
<Flow small p={s.offer} />
|
|
|
</div>
|
|
|
<div className="sg-btns">
|
|
|
<button className="sg-yes" onClick={() => accept(s)}><I d="plus" />규칙 만들기</button>
|
|
|
<button className="sg-no" onClick={() => dismiss(s)}>무시</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
))}
|
|
|
{suggests.length === 0 && (
|
|
|
<div className="sg-empty"><I d="tick" />지금은 새 제안이 없어요. 패턴이 보이면 알려드릴게요.</div>
|
|
|
)}
|
|
|
</div>
|
|
|
</section>
|
|
|
</div>
|
|
|
);
|
|
|
}
|
|
|
|
|
|
/* ===== 실행 기록 ===== */
|
|
|
function LogView() {
|
|
|
return (
|
|
|
<div className="board">
|
|
|
<section className="card sp2">
|
|
|
<div className="ch">
|
|
|
<div className="ico"><I d="clock" /></div>
|
|
|
<div className="htext"><h3>실행 기록</h3><div className="sub">규칙이 움직인 순간들</div></div>
|
|
|
<span className="count">오늘 {AU.log.filter((l) => !l.time.startsWith("어제")).length}건</span>
|
|
|
</div>
|
|
|
<div className="rb-log">
|
|
|
{AU.log.map((l, i) => (
|
|
|
<div className="lg-row" key={i}>
|
|
|
<span className="lg-time">{l.time}</span>
|
|
|
<div className="lg-body">
|
|
|
<span className="lg-rule">{l.rule}</span>
|
|
|
<div className="lg-text">{l.text}</div>
|
|
|
</div>
|
|
|
<button className="lg-undo">되돌리기</button>
|
|
|
</div>
|
|
|
))}
|
|
|
</div>
|
|
|
</section>
|
|
|
</div>
|
|
|
);
|
|
|
}
|
|
|
|
|
|
const AU_RAIL = (suggestN) => [
|
|
|
{ id: "rules", icon: "zap", label: "내 규칙" },
|
|
|
{ id: "new", icon: "plus", label: "새 자동화" },
|
|
|
{ sep: true },
|
|
|
{ id: "suggest", icon: "brain", label: "아리 제안", dot: suggestN > 0 },
|
|
|
{ id: "log", icon: "clock", label: "실행 기록" },
|
|
|
];
|
|
|
|
|
|
const AU_HEAD = {
|
|
|
rules: { eye: ["규칙으로 굴러가는 하루"], title: "내 규칙", em: "켜두면 알아서 굴러가요" },
|
|
|
new: { eye: ["한 문장이면 충분해요"], title: "새 자동화", em: "말하듯 적으면 규칙이 돼요" },
|
|
|
suggest: { eye: ["아리가 지켜보다 발견한 것"], title: "아리 제안", em: "반복되는 일을 자동으로" },
|
|
|
log: { eye: ["조용히 일한 기록"], title: "실행 기록", em: "규칙이 움직인 순간들" },
|
|
|
};
|
|
|
|
|
|
function App() {
|
|
|
const [view, setView] = useState("rules");
|
|
|
const [rules, setRules] = useState(AU.rules);
|
|
|
const [suggests, setSuggests] = useState(AU.suggests);
|
|
|
const [toast, setToast] = useState(null);
|
|
|
|
|
|
const activeN = rules.filter((r) => r.on).length;
|
|
|
const toggle = (id) => setRules(rules.map((r) => (r.id === id ? { ...r, on: !r.on } : r)));
|
|
|
|
|
|
const addRule = (cat, name, parse) => {
|
|
|
setRules((rs) => [
|
|
|
{ id: "new-" + Date.now(), on: true, cat, name, trigger: parse.trigger, cond: parse.cond, action: parse.action, last: "방금 만듦", runs: 0, fresh: true },
|
|
|
...rs,
|
|
|
]);
|
|
|
setToast("‘" + name + "’ 규칙을 켰어요.");
|
|
|
setView("rules");
|
|
|
setTimeout(() => setToast(null), 3200);
|
|
|
};
|
|
|
|
|
|
const createFromExample = (m) => addRule(m.cat, m.name, m.parse);
|
|
|
const accept = (s) => { addRule(s.offer.cat, s.offer.name, s.offer); setSuggests((x) => x.filter((y) => y.id !== s.id)); };
|
|
|
const dismiss = (s) => setSuggests((x) => x.filter((y) => y.id !== s.id));
|
|
|
|
|
|
const h = AU_HEAD[view];
|
|
|
|
|
|
return (
|
|
|
<div className="dash" data-screen-label="자동화">
|
|
|
<Topbar current="auto" />
|
|
|
|
|
|
<div className="pagehead">
|
|
|
<div>
|
|
|
<div className="ph-eyebrow">
|
|
|
<span>활성 규칙 {activeN}개</span>
|
|
|
<span className="sep"></span>
|
|
|
<span>이번 주 {AU.stats.runsWeek}회 실행</span>
|
|
|
<span className="sep"></span>
|
|
|
<span>{AU.stats.saved} 아꼈어요</span>
|
|
|
</div>
|
|
|
<h1 className="ph-title">{h.title} <em>{h.em}</em></h1>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div className="work">
|
|
|
<SubRail items={AU_RAIL(suggests.length)} active={view} onPick={setView} />
|
|
|
{view === "rules" && <RulesView rules={rules} toggle={toggle} />}
|
|
|
{view === "new" && <NewView onCreate={createFromExample} />}
|
|
|
{view === "suggest" && <SuggestView suggests={suggests} accept={accept} dismiss={dismiss} />}
|
|
|
{view === "log" && <LogView />}
|
|
|
</div>
|
|
|
|
|
|
{toast && <div className="au-toast"><I d="tick" />{toast}</div>}
|
|
|
</div>
|
|
|
);
|
|
|
}
|
|
|
|
|
|
ReactDOM.createRoot(document.getElementById("root")).render(<App />);
|