workredesign / app.py
soojeongcrystal's picture
Update app.py
1d6e3af verified
# 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")