Spaces:
Sleeping
Sleeping
| # app.py | |
| import streamlit as st | |
| import pandas as pd | |
| import networkx as nx | |
| from pyvis.network import Network | |
| from io import BytesIO | |
| import tempfile | |
| import os | |
| from streamlit_sortables import sort_items | |
| import hashlib | |
| # ========================= | |
| # ๊ธฐ๋ณธ ์ค์ & ์ ์ญ ์คํ์ผ | |
| # ========================= | |
| st.set_page_config(page_title="Team Task Structuring Assistant (P-D-E-R-O)", layout="wide") | |
| st.markdown(""" | |
| <style> | |
| html, body, [class*="css"] { font-size: 14px !important; font-family: "Noto Sans KR","Helvetica",sans-serif; } | |
| .stButton>button { border-radius: 6px; padding: 4px 10px; font-size: 13px; } | |
| .task-card { background-color:#fff; border:1px solid #ddd; border-radius:6px; padding:6px 10px; margin-bottom:6px; box-shadow:0 1px 2px rgba(0,0,0,0.05); font-size:13px; } | |
| .domain-box { border-radius:8px; padding:8px; margin:6px 4px; } | |
| .domain-header { font-weight:600; font-size:15px; text-align:center; margin-bottom:6px; } | |
| .helper-note { color:#444; font-size:12px; } | |
| footer { visibility:hidden; } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # ========================= | |
| # ์ธ์ ์ด๊ธฐํ | |
| # ========================= | |
| def _init(key, default): | |
| if key not in st.session_state: | |
| st.session_state[key] = default | |
| _init("page", "๋๋ฉ์ธ ์ค์ ") | |
| _init("domains", []) | |
| _init("grouped_tasks", {}) # {domain: [task, ...]} | |
| _init("dependencies", {}) # {"domain::task": [task_name, ...]} (๊ฐ์ ๋๋ฉ์ธ ๋ด ์ด๋ฆ ๋ชฉ๋ก) | |
| _init("outputs", {}) # {"domain::task": output_text} | |
| _init("code_map", {}) # {"domain::task": {...}} | |
| _init("seq_registry", {}) # {(domain_code, cycle): max_seq} | |
| _init("domain_defaults", {}) # {domain_code: {"cycle":"E","time":"T"}} | |
| # ========================= | |
| # ์ ํธ | |
| # ========================= | |
| PALETTE = ["#E3F2FD", "#E8F5E9", "#FFF8E1", "#FCE4EC", "#E0F7FA"] | |
| def goto(page: str): | |
| st.session_state.page = page | |
| st.rerun() | |
| def safe_key(domain: str, task: str, prefix: str) -> str: | |
| h = hashlib.md5(f"{domain}::{task}".encode()).hexdigest()[:8] | |
| return f"{prefix}_{h}" | |
| def export_file(df: pd.DataFrame, kind="csv"): | |
| if kind == "csv": | |
| return df.to_csv(index=False).encode("utf-8-sig") | |
| bio = BytesIO() | |
| with pd.ExcelWriter(bio, engine="openpyxl") as w: | |
| df.to_excel(w, index=False, sheet_name="tasks") | |
| bio.seek(0) | |
| return bio.getvalue() | |
| def draw_dependency_graph(df: pd.DataFrame): | |
| if df is None or df.empty: | |
| return "<div style='padding:8px;color:#666;'>๊ทธ๋ํ๋ฅผ ํ์ํ ๋ฐ์ดํฐ๊ฐ ์์ต๋๋ค.</div>" | |
| G = nx.DiGraph() | |
| color_map = {"P": "#A7C7E7", "D": "#FFE8A3", "E": "#A8E6CF", "R": "#FFD3B6", "O": "#FFAAA5"} | |
| for _, row in df.iterrows(): | |
| code = row["code"] | |
| label = row["name"] | |
| lifecycle = row.get("cycle", "E") | |
| color = color_map.get(lifecycle, "#CFCFCF") | |
| G.add_node(code, label=label, color=color) | |
| for dep in str(row.get("depends_on", "")).split(","): | |
| dep = dep.strip() | |
| if dep: | |
| G.add_edge(dep, code) | |
| nt = Network(height="550px", width="100%", directed=True, bgcolor="#FFFFFF", font_color="#222") | |
| nt.from_nx(G) | |
| tmp_path = tempfile.NamedTemporaryFile(delete=False, suffix=".html").name | |
| nt.save_graph(tmp_path) | |
| html = open(tmp_path, "r", encoding="utf-8").read() | |
| os.remove(tmp_path) | |
| return html | |
| def map_domain_to_code(d: str) -> str: | |
| table = { | |
| "๊ณตํต": "COMM", "์ ๋ต": "COMM", | |
| "CEO": "CEO", "CEO์๋ฒ ์ด": "CEO", | |
| "๋ฆฌ๋์ญ์๋ฒ ์ด": "MULTI", "๋ฆฌ๋์ญ": "MULTI", | |
| "๋ฆฌ๋์ญ์ฌ์ธต": "DEEP", "์ฌ์ธต์ง๋จ": "DEEP", | |
| "์์": "ADHOC", "์์ฒญ": "ADHOC", "์์์์ฒญ์ง๋จ": "ADHOC", | |
| "๊ต์ก์ด์": "EDU", "๊ต์ก": "EDU", | |
| "๋ฐ์ดํฐ๋ถ์": "DATA", "๋ถ์": "DATA", | |
| "์ด์๊ด๋ฆฌ": "OPS", "์ด์": "OPS", | |
| } | |
| for k, v in table.items(): | |
| if k in d: | |
| return v | |
| return d[:4].upper() | |
| def ensure_unique_list(seq): | |
| seen, out = set(), [] | |
| for x in seq: | |
| x = x.strip() | |
| if x and x not in seen: | |
| seen.add(x); out.append(x) | |
| return out | |
| # ========================= | |
| # ์๋จ ์งํ ํ์๋ฐ | |
| # ========================= | |
| STEPS = ["๋๋ฉ์ธ ์ค์ ", "์ ๋ฌด ๋ฐ์ฐ", "๊ทธ๋ฃน ์กฐ์ ", "์์กด์ฑ ํ๋จ", "์ฐ์ถ๋ฌผ ์ ์", "์ต์ข ์ ๋ฆฌ"] | |
| idx = STEPS.index(st.session_state.page) if st.session_state.page in STEPS else 0 | |
| st.progress((idx + 1) / len(STEPS), text=f"๋จ๊ณ {idx+1}/{len(STEPS)} : {st.session_state.page}") | |
| # ========================= | |
| # 1) ๋๋ฉ์ธ ์ค์ | |
| # ========================= | |
| if st.session_state.page == "๋๋ฉ์ธ ์ค์ ": | |
| st.title("1๏ธโฃ ๋๋ฉ์ธ ์ค์ ") | |
| st.markdown("ํ์ ์ฃผ์ ์ ๋ฌด ๋๋ฉ์ธ์ 3~4๊ฐ ์ ์ํ์ธ์. (์: ๊ณตํต, ๋ฆฌ๋์ญ์๋ฒ ์ด, ๊ต์ก์ด์, ๋ฐ์ดํฐ๋ถ์ ๋ฑ)") | |
| cols = st.columns(4) | |
| new_domains = [] | |
| for i in range(4): | |
| with cols[i]: | |
| d = st.text_input(f"๋๋ฉ์ธ {i+1}", st.session_state.domains[i] if i < len(st.session_state.domains) else "") | |
| if d: | |
| new_domains.append(d.strip()) | |
| st.session_state.domains = ensure_unique_list([d for d in new_domains if d]) | |
| if st.button("โก๏ธ ๋ค์: ์ ๋ฌด ๋ฐ์ฐ"): | |
| if not st.session_state.domains: | |
| st.warning("๋๋ฉ์ธ์ ์ต์ 1๊ฐ ์ด์ ์ ๋ ฅํ์ธ์.") | |
| else: | |
| goto("์ ๋ฌด ๋ฐ์ฐ") | |
| # ========================= | |
| # 2) ์ ๋ฌด ๋ฐ์ฐ | |
| # ========================= | |
| elif st.session_state.page == "์ ๋ฌด ๋ฐ์ฐ": | |
| st.title("2๏ธโฃ ์ ๋ฌด ๋ฐ์ฐ") | |
| st.markdown("๊ฐ ๋๋ฉ์ธ๋ณ ์ค์ ์ํ ์ค์ธ ์ ๋ฌด๋ฅผ ๊ฐ๋ฅํ ํ ๋ง์ด ์ ์ด๋ณด์ธ์. (์ค๋ฐ๊ฟ์ผ๋ก ๊ตฌ๋ถ)") | |
| for d in st.session_state.domains + ["๊ธฐํ"]: | |
| st.subheader(f"๐ {d}") | |
| text = st.text_area(f"{d} ์ ๋ฌด", key=f"tasks_{d}", height=150, | |
| placeholder="์: ๋ฆฌ๋์ญ ์ง๋จ ๋ฆฌํฌํธ ์์ฑ\n๊ต์ก ๊ธฐํ\n๋ฐ์ดํฐ ๋ถ์ ๋ฑ") | |
| if text: | |
| tasks = [t.strip() for t in text.split("\n") if t.strip()] | |
| st.session_state.grouped_tasks[d] = ensure_unique_list(tasks) | |
| if st.button("โก๏ธ ๋ค์: ๊ทธ๋ฃน ์กฐ์ "): | |
| goto("๊ทธ๋ฃน ์กฐ์ ") | |
| # ========================= | |
| # 3) ๊ทธ๋ฃน ์กฐ์ โโ ์ฝ๋ฐฑ ๊ธฐ๋ฐ์ผ๋ก ์์ ํ | |
| # ========================= | |
| elif st.session_state.page == "๊ทธ๋ฃน ์กฐ์ ": | |
| st.title("3๏ธโฃ ์ ๋ฌด ๊ทธ๋ฃน ์กฐ์ ") | |
| st.markdown("๋๋ฉ์ธ๋ณ ์ ๋ฌด๋ฅผ ์ ๋ฆฌํ๊ณ **๋๋๊ทธ๋ก ์์๋ฅผ ๋ฐ๊พธ๊ฑฐ๋**, **์ด๋/์ญ์ /์ถ๊ฐ** ํ์ธ์.") | |
| st.divider() | |
| domains = st.session_state.domains + ["๊ธฐํ"] | |
| # --- ์ฝ๋ฐฑ ํจ์๋ค --- | |
| def cb_delete(domain: str, task: str): | |
| lst = list(st.session_state.grouped_tasks.get(domain, [])) | |
| st.session_state.grouped_tasks[domain] = [x for x in lst if x != task] | |
| def cb_move(src: str, task: str, sel_key: str): | |
| dst = st.session_state.get(sel_key, "(์ด๋)") | |
| if dst and dst != "(์ด๋)" and dst != src: | |
| src_list = list(st.session_state.grouped_tasks.get(src, [])) | |
| dst_list = list(st.session_state.grouped_tasks.get(dst, [])) | |
| if task in src_list: | |
| src_list.remove(task) | |
| if task not in dst_list: | |
| dst_list.append(task) | |
| st.session_state.grouped_tasks[src] = src_list | |
| st.session_state.grouped_tasks[dst] = dst_list | |
| # ์ ํ ์ด๊ธฐํ | |
| st.session_state[sel_key] = "(์ด๋)" | |
| def cb_add(domain: str, input_key: str): | |
| val = (st.session_state.get(input_key) or "").strip() | |
| if val: | |
| lst = list(st.session_state.grouped_tasks.get(domain, [])) | |
| if val not in lst: | |
| lst.append(val) | |
| st.session_state.grouped_tasks[domain] = ensure_unique_list(lst) | |
| # ์ ๋ ฅ์ฐฝ ๋น์ฐ๊ธฐ | |
| st.session_state[input_key] = "" | |
| cols = st.columns(len(domains)) | |
| for i, d in enumerate(domains): | |
| with cols[i]: | |
| bg = PALETTE[i % len(PALETTE)] | |
| st.markdown(f"<div class='domain-box' style='background-color:{bg};'>", unsafe_allow_html=True) | |
| st.markdown(f"<div class='domain-header'>๐ฆ {d}</div>", unsafe_allow_html=True) | |
| # ํ์ฌ ๋ฆฌ์คํธ ๊ฐ์ ธ์์ ๋๋๊ทธ ์ ๋ ฌ ๋ฐ์ | |
| orig = list(st.session_state.grouped_tasks.get(d, [])) | |
| sorted_tasks = sort_items(orig, direction="vertical", key=f"sort_{d}") or [] | |
| if sorted_tasks != orig: | |
| st.session_state.grouped_tasks[d] = ensure_unique_list(sorted_tasks) | |
| # ๊ฐ ์ ๋ฌด ํ ๋ ๋๋ง | |
| for t in st.session_state.grouped_tasks.get(d, []): | |
| if not t.strip(): | |
| continue | |
| c1, c2, c3, c4 = st.columns([5, 1, 2, 1]) | |
| with c1: | |
| st.markdown(f"<div class='task-card'>{t}</div>", unsafe_allow_html=True) | |
| with c2: | |
| st.button("๐", key=safe_key(d, t, "del_btn"), | |
| help="์ญ์ ", on_click=cb_delete, args=(d, t)) | |
| with c3: | |
| sel_key = safe_key(d, t, "mv_sel") | |
| st.selectbox(" ", ["(์ด๋)"] + [x for x in domains if x != d], | |
| key=sel_key, label_visibility="collapsed") | |
| with c4: | |
| st.button("โช", key=safe_key(d, t, "mv_btn"), | |
| help="์ด๋", on_click=cb_move, args=(d, t, sel_key)) | |
| # ์ ์ ๋ฌด ์ถ๊ฐ(์ ๋ ฅ+๋ฒํผ) | |
| add_key = f"add_input_{i}" | |
| st.text_input("์ ์ ๋ฌด", key=add_key, label_visibility="collapsed", placeholder="์ ์ ๋ฌด ์ ๋ ฅ") | |
| st.button(f"โ ์ถ๊ฐ ({d})", key=f"add_btn_{i}", on_click=cb_add, args=(d, add_key)) | |
| st.markdown("</div>", unsafe_allow_html=True) | |
| c1, c2 = st.columns(2) | |
| if c1.button("โฌ ๏ธ ์ด์ : ์ ๋ฌด ๋ฐ์ฐ"): goto("์ ๋ฌด ๋ฐ์ฐ") | |
| if c2.button("โก๏ธ ๋ค์: ์์กด์ฑ ํ๋จ"): goto("์์กด์ฑ ํ๋จ") | |
| # ========================= | |
| # 4) ์์กด์ฑ ํ๋จ (๊ฐ์ ๋๋ฉ์ธ ๋ด์์๋ง) | |
| # ========================= | |
| elif st.session_state.page == "์์กด์ฑ ํ๋จ": | |
| st.title("4๏ธโฃ ์์กด์ฑ ํ๋จ") | |
| st.markdown("๊ฐ ์ ๋ฌด ๊ฐ ์ ํ๊ด๊ณ(์์กด์ฑ)๋ฅผ **๊ฐ์ ๋๋ฉ์ธ ๋ด์์๋ง** ์ค์ ํ์ธ์.") | |
| st.divider() | |
| deps = {} | |
| domains = st.session_state.domains + ["๊ธฐํ"] | |
| for i, d in enumerate(domains): | |
| st.subheader(f"๐ {d}") | |
| tasks_in_domain = st.session_state.grouped_tasks.get(d, []) | |
| for t in tasks_in_domain: | |
| key = safe_key(d, t, "deps") | |
| candidates = [x for x in tasks_in_domain if x != t] # ๋์ผ ๋๋ฉ์ธ๋ง | |
| default_vals = [x for x in st.session_state.dependencies.get(f"{d}::{t}", []) if x in candidates] | |
| selected = st.multiselect(f"'{t}' ์ด์ ์ ๋ฌด (๋์ผ ๋๋ฉ์ธ)", candidates, default=default_vals, key=key) | |
| deps[f"{d}::{t}"] = selected | |
| st.session_state.dependencies = deps | |
| c1, c2 = st.columns(2) | |
| if c1.button("โฌ ๏ธ ์ด์ : ๊ทธ๋ฃน ์กฐ์ "): goto("๊ทธ๋ฃน ์กฐ์ ") | |
| if c2.button("โก๏ธ ๋ค์: ์ฐ์ถ๋ฌผ ์ ์"): goto("์ฐ์ถ๋ฌผ ์ ์") | |
| # ========================= | |
| # 5) ์ฐ์ถ๋ฌผ ์ ์ (๊ฐ๊ฒฐ ๋งคํ UI, placeholder ์ ๊ฑฐ) | |
| # ========================= | |
| elif st.session_state.page == "์ฐ์ถ๋ฌผ ์ ์": | |
| st.title("5๏ธโฃ ์ฐ์ถ๋ฌผ ์ ์") | |
| st.markdown("๊ฐ ์ ๋ฌด์ ๋์๋๋ ์ฐ์ถ๋ฌผ์ ํ์์ ์ง์ ์ ๋ ฅํ์ธ์.") | |
| st.divider() | |
| updated_outputs = {} | |
| for i, (d, tasks) in enumerate(st.session_state.grouped_tasks.items()): | |
| bg = PALETTE[i % len(PALETTE)] | |
| st.markdown(f"<div style='background-color:{bg}; padding:10px; border-radius:8px;'>", unsafe_allow_html=True) | |
| st.markdown(f"### ๐ {d}") | |
| rows = [{"์ ๋ฌด๋ช ": t, "์ฐ์ถ๋ฌผ": st.session_state.outputs.get(f"{d}::{t}", "")} for t in tasks] | |
| df = pd.DataFrame(rows) | |
| # ์ผ๋ถ ๋ฒ์ ํธํ์ ์ํด TextColumn์ placeholder ๋ฏธ์ฌ์ฉ | |
| try: | |
| col_cfg = { | |
| "์ ๋ฌด๋ช ": st.column_config.TextColumn(disabled=True), | |
| "์ฐ์ถ๋ฌผ": st.column_config.TextColumn() | |
| } | |
| edited = st.data_editor(df, key=f"editor_{i}", hide_index=True, num_rows="fixed", | |
| column_config=col_cfg, use_container_width=True) | |
| except Exception: | |
| edited = st.data_editor(df, key=f"editor_{i}", hide_index=True, use_container_width=True) | |
| for _, row in edited.iterrows(): | |
| updated_outputs[f"{d}::{row['์ ๋ฌด๋ช ']}"] = row["์ฐ์ถ๋ฌผ"] | |
| st.markdown("<div class='helper-note'>Tip: ์ฐ์ถ๋ฌผ์ ๋ณด๊ณ ์/๋์๋ณด๋/์์คํ /๋ฐ์ดํฐ/๊ต์ก์๋ฃ ๋ฑ์ผ๋ก ๊ฐ๋จํ ์ ์ด๋ ์ถฉ๋ถํฉ๋๋ค.</div>", unsafe_allow_html=True) | |
| st.markdown("</div>", unsafe_allow_html=True) | |
| st.session_state.outputs = updated_outputs | |
| c1, c2 = st.columns(2) | |
| if c1.button("โฌ ๏ธ ์ด์ : ์์กด์ฑ ํ๋จ"): goto("์์กด์ฑ ํ๋จ") | |
| if c2.button("โก๏ธ ๋ค์: ์ต์ข ์ ๋ฆฌ"): goto("์ต์ข ์ ๋ฆฌ") | |
| # ========================= | |
| # 6) ์ต์ข ์ ๋ฆฌ (๋๋ฉ์ธ ์ฝ๋ ๋งคํ + ๋จ์ํ๋ UI + ์ฝ๋ ์์ฑ) | |
| # ========================= | |
| elif st.session_state.page == "์ต์ข ์ ๋ฆฌ": | |
| st.title("6๏ธโฃ ์ต์ข ์ ๋ฆฌ ๋ฐ ์ ๋ฌด ์ฝ๋ ์์ฑ") | |
| st.markdown( | |
| "- โ **๋๋ฉ์ธ โ ์๋ฌธ์ฝ๋**๋ฅผ ํ์ธ/์์ \n" | |
| "- โก ๊ฐ ์ ๋ฌด์ **์ฌ์ดํด(P/D/E/R/O)**, **์๊ฐ๊ธฐํธ(T/FE)** ์ ํ\n" | |
| "- โข ์๋ ์์ฑ๋ ์ฝ๋๋ฅผ ํ์ธํ๊ณ ๋ค์ด๋ก๋" | |
| ) | |
| st.divider() | |
| # ---- 0) ๋๋ฉ์ธ โ ์๋ฌธ์ฝ๋ ๋งคํ ํธ์ง ---- | |
| _init("domain_code_overrides", {}) | |
| domains = list(st.session_state.grouped_tasks.keys()) | |
| # ์๋ ์ ์ + ๊ธฐ์กด ์ค๋ฒ๋ผ์ด๋ ๋ฐ์ | |
| map_rows = [] | |
| for d in domains: | |
| suggested = map_domain_to_code(d) | |
| current = st.session_state.domain_code_overrides.get(d, suggested) | |
| map_rows.append({"๋๋ฉ์ธ": d, "์๋ฌธ์ฝ๋": current}) | |
| st.markdown("#### ๐ค ๋๋ฉ์ธ ์ฝ๋ ๋งคํ") | |
| map_df = pd.DataFrame(map_rows) | |
| map_edited = st.data_editor( | |
| map_df, key="domain_code_editor", hide_index=True, use_container_width=True, | |
| column_config={ | |
| "๋๋ฉ์ธ": st.column_config.TextColumn(disabled=True), | |
| "์๋ฌธ์ฝ๋": st.column_config.TextColumn() | |
| } | |
| ) | |
| # ํ์ ๊ฒ์ฆ: ์๋ฌธ/์ซ์, 2~8์ | |
| invalid = [] | |
| domain_code_overrides = {} | |
| for _, r in map_edited.iterrows(): | |
| dname = str(r["๋๋ฉ์ธ"]) | |
| code = str(r["์๋ฌธ์ฝ๋"]).strip().upper() | |
| if not code or not code.isalnum() or not (2 <= len(code) <= 8): | |
| invalid.append(f"- {dname}: '{r['์๋ฌธ์ฝ๋']}' (์๋ฌธ/์ซ์ 2~8์)") | |
| domain_code_overrides[dname] = code | |
| if invalid: | |
| st.error("๋๋ฉ์ธ ์ฝ๋ ํ์ ์ค๋ฅ๊ฐ ์์ต๋๋ค:\n" + "\n".join(invalid)) | |
| st.stop() | |
| st.session_state.domain_code_overrides = domain_code_overrides | |
| st.caption("Tip: ์ฝ๋ ์) COMM, MULTI, CEO, DEEP, ADHOC, EDU, DATA, OPS ๋ฑ") | |
| st.divider() | |
| # ---- 1) ์ํ์ค ์ง๊ณ(์์ ์ ๋ถ์ฌ ์ค๋น) ---- | |
| seq_registry = dict(st.session_state.seq_registry) | |
| code_map = dict(st.session_state.code_map) | |
| for task_key, meta in code_map.items(): | |
| new_domain_code = st.session_state.domain_code_overrides.get(meta.get("domain", ""), meta.get("domain_code", "")) | |
| pair = (new_domain_code, meta.get("cycle", "")) | |
| if pair[0] and pair[1]: | |
| seq_registry[pair] = max(seq_registry.get(pair, 0), int(meta.get("seq", 0))) | |
| # ---- 2) ์ ๋ฌด ๋จ์ UI (์ต์ ์ ํ ์ค์ฌ) ---- | |
| cycle_opts = ["P", "D", "E", "R", "O"] | |
| time_opts = ["T", "FE"] | |
| def recommend_cycles(is_p, is_d, is_e, is_r, is_o): | |
| recs = [] | |
| if is_p: recs.append("P") | |
| if is_d: recs.append("D") | |
| if is_e: recs.append("E") | |
| if is_r: recs.append("R") | |
| if is_o: recs.append("O") | |
| return recs | |
| for d in domains: | |
| domain_code = st.session_state.domain_code_overrides[d] | |
| st.subheader(f"๐ {d} โ `{domain_code}`") | |
| tasks = st.session_state.grouped_tasks.get(d, []) | |
| for t in tasks: | |
| task_key = f"{d}::{t}" | |
| prev = code_map.get(task_key, {}) | |
| st.markdown(f"**๐งฉ {t}**") | |
| # ํต์ฌ ์ ํ (๊ฐ๊ฒฐ UI): ์ฌ์ดํด ยท ์๊ฐ๊ธฐํธ | |
| c1, c2 = st.columns([1.6, 1.0]) | |
| cycle_key = safe_key(d, t, "cycle") | |
| time_key = safe_key(d, t, "time") | |
| default_cycle = prev.get("cycle", "E") | |
| default_time = prev.get("time", "T") | |
| with c1: | |
| cyc = st.radio( | |
| "์ฌ์ดํด", cycle_opts, key=cycle_key, horizontal=True, | |
| index=cycle_opts.index(default_cycle) if cycle_key not in st.session_state else | |
| cycle_opts.index(st.session_state[cycle_key]) | |
| ) | |
| with c2: | |
| tm = st.radio( | |
| "์๊ฐ๊ธฐํธ", time_opts, key=time_key, horizontal=True, | |
| index=time_opts.index(default_time) if time_key not in st.session_state else | |
| time_opts.index(st.session_state[time_key]) | |
| ) | |
| # ํ์ํ ์ฌ๋๋ง ์ฌ๋ ๋์๋ง: ์ฒดํฌ โ ์ถ์ฒ ์ ์ฉ | |
| with st.expander("๋์์ด ํ์ํ์ธ์? (์ง๋ฌธํ ๋ณด์กฐ๋ก ์ถ์ฒ๋ฐ๊ธฐ)", expanded=False): | |
| qcols = st.columns(5) | |
| # โ ๊ฐ ์ฒดํฌ๋ฐ์ค์ ๊ณ ์ key ๋ถ์ฌ (์ค๋ณต ID ๋ฐฉ์ง) | |
| with qcols[0]: | |
| qP = st.checkbox("P: ๋ชฉ์ /์์ /๋์", key=safe_key(d, t, "qP")) | |
| with qcols[1]: | |
| qD = st.checkbox("D: ์ธํ /์ค๊ณ", key=safe_key(d, t, "qD")) | |
| with qcols[2]: | |
| qE = st.checkbox("E: ์งํ/์์ง", key=safe_key(d, t, "qE")) | |
| with qcols[3]: | |
| qR = st.checkbox("R: ํด์/ํ๊ฐ", key=safe_key(d, t, "qR")) | |
| with qcols[4]: | |
| qO = st.checkbox("O: ๋ฐฐํฌ/๋ฐ์", key=safe_key(d, t, "qO")) | |
| recs = recommend_cycles(qP, qD, qE, qR, qO) | |
| if recs: | |
| rcols = st.columns(len(recs)) | |
| for i, r in enumerate(recs): | |
| if rcols[i].button(f"์ถ์ฒ ์ ์ฉ {r}", key=safe_key(d, t, f"apply_{r}")): | |
| st.session_state[cycle_key] = r | |
| st.rerun() | |
| else: | |
| st.caption("์ฒดํฌ ๊ฒฐ๊ณผ๊ฐ ์๊ฑฐ๋ ๋ชจํธํฉ๋๋ค. ์ง์ ์ ํ์ ์ ์งํ์ธ์.") | |
| # ์ต์ข ๊ฐ ๋ฐ์ | |
| cyc = st.session_state[cycle_key] | |
| tm = st.session_state[time_key] | |
| # ์ํ์ค ๋ถ์ฌ: ๋์ผ (domain_code, cycle)์ด๋ฉด ๊ธฐ์กด ์ ์ง, ์๋๋ฉด ๋ค์ ๋ฒํธ | |
| pair = (domain_code, cyc) | |
| if prev and prev.get("domain_code") == domain_code and prev.get("cycle") == cyc and int(prev.get("seq", 0)) > 0: | |
| seq = int(prev["seq"]) | |
| else: | |
| seq = int(seq_registry.get(pair, 0)) + 1 | |
| seq_registry[pair] = seq | |
| code = f"{domain_code}-{cyc}{seq:02d}-{tm}" | |
| st.markdown( | |
| f"<div style='background:#F9FAFB;border:1px solid #DDD;border-radius:6px;padding:8px;margin:6px 0;'>" | |
| f"<small>โก๏ธ ์ฝ๋</small> <b>{code}</b></div>", | |
| unsafe_allow_html=True | |
| ) | |
| # ์ ์ฅ | |
| code_map[task_key] = { | |
| "domain": d, | |
| "domain_code": domain_code, | |
| "name": t, | |
| "cycle": cyc, | |
| "time": tm, | |
| "seq": seq, | |
| "code": code, | |
| } | |
| # ์ํ ๊ฐฑ์ | |
| st.session_state.seq_registry = seq_registry | |
| st.session_state.code_map = code_map | |
| # ---- 3) ๊ฒฐ๊ณผ ์์ฝ/๊ทธ๋ํ/๋ค์ด๋ก๋ ---- | |
| st.divider() | |
| st.markdown("#### ๐ ์ต์ข ์ฝ๋ ๋ชฉ๋ก") | |
| rows = [] | |
| for task_key, meta in st.session_state.code_map.items(): | |
| d, t = task_key.split("::", 1) | |
| deps_names = st.session_state.dependencies.get(task_key, []) | |
| rows.append({ | |
| "domain": d, | |
| "domain_code": meta["domain_code"], | |
| "name": t, | |
| "cycle": meta["cycle"], | |
| "time": meta["time"], | |
| "seq": meta["seq"], | |
| "code": meta["code"], | |
| "depends_on": ", ".join(deps_names), | |
| "output": st.session_state.outputs.get(task_key, ""), | |
| }) | |
| df = pd.DataFrame(rows) | |
| if not df.empty: | |
| df = df.sort_values(by=["domain_code", "cycle", "seq", "name"]).reset_index(drop=True) | |
| st.dataframe( | |
| df[["domain", "domain_code", "name", "cycle", "time", "code", "depends_on", "output"]], | |
| use_container_width=True | |
| ) | |
| # ์์กด์ฑ ๊ทธ๋ํ(์ด๋ฆโ์ฝ๋ ๊ฐ์ ๋งคํ) | |
| name_to_code = {meta["name"]: meta["code"] for meta in st.session_state.code_map.values()} | |
| df_graph = df.copy() | |
| if not df_graph.empty: | |
| df_graph["depends_on"] = df_graph["depends_on"].apply( | |
| lambda s: ", ".join([name_to_code.get(x.strip(), x.strip()) for x in s.split(",") if x.strip()]) if s else "" | |
| ) | |
| html = draw_dependency_graph(df_graph.rename(columns={"code": "code", "name": "name"}) if not df_graph.empty else df_graph) | |
| st.components.v1.html(html, height=520, scrolling=True) | |
| c1, c2 = st.columns(2) | |
| c1.download_button("โฌ๏ธ CSV ๋ค์ด๋ก๋", export_file(df, "csv"), "final_task_codes.csv", "text/csv") | |
| c2.download_button("โฌ๏ธ Excel ๋ค์ด๋ก๋", export_file(df, "xlsx"), "final_task_codes.xlsx") | |
| if st.button("๐ ์ฒ์์ผ๋ก ๋์๊ฐ๊ธฐ"): | |
| for k in ["page", "domains", "grouped_tasks", "dependencies", "outputs", "code_map", "seq_registry"]: | |
| if k == "domains": | |
| st.session_state[k] = [] | |
| elif k == "page": | |
| st.session_state[k] = "๋๋ฉ์ธ ์ค์ " | |
| else: | |
| st.session_state[k] = {} | |
| st.rerun() | |
| # ํธํฐ | |
| st.markdown("---") | |
| st.caption("ยฉ 2025 Crystal_MVP") | |