soojeongcrystal commited on
Commit
067e8d8
ยท
verified ยท
1 Parent(s): b367b16

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +369 -187
app.py CHANGED
@@ -8,297 +8,479 @@ import os
8
  from streamlit_sortables import sort_items
9
  import hashlib
10
 
11
- # ------------------------------
12
- # ๊ธฐ๋ณธ ์„ค์ •
13
- # ------------------------------
14
- st.set_page_config(page_title="ํŒ€ ์—…๋ฌด ๊ตฌ์กฐํ™” ๋„์šฐ๋ฏธ (P-D-E-R-O)", layout="wide")
15
-
16
- # ------------------------------
17
- # ์ „์—ญ ์Šคํƒ€์ผ
18
- # ------------------------------
19
  st.markdown("""
20
- <style>
21
- html, body, [class*="css"] {
22
- font-size: 14px !important;
23
- font-family: "Noto Sans KR", "Helvetica", sans-serif;
24
- }
25
- .stButton>button {
26
- border-radius: 6px;
27
- padding: 4px 10px;
28
- font-size: 13px;
29
- }
30
- .task-card {
31
- background-color: #fff;
32
- border: 1px solid #ddd;
33
- border-radius: 6px;
34
- padding: 6px 10px;
35
- margin-bottom: 6px;
36
- box-shadow: 0 1px 2px rgba(0,0,0,0.05);
37
- font-size: 13px;
38
- }
39
- .domain-box {
40
- border-radius: 8px;
41
- padding: 8px;
42
- margin: 6px 4px;
43
- }
44
- .domain-header {
45
- font-weight: 600;
46
- font-size: 15px;
47
- text-align: center;
48
- margin-bottom: 6px;
49
- }
50
- </style>
51
  """, unsafe_allow_html=True)
52
 
53
- # ------------------------------
54
  # ์„ธ์…˜ ์ดˆ๊ธฐํ™”
55
- # ------------------------------
56
- for key in ["page", "domains", "grouped_tasks", "dependencies", "outputs", "code_map"]:
57
- if key not in st.session_state:
58
- if key == "page":
59
- st.session_state[key] = "๋„๋ฉ”์ธ ์„ค์ •"
60
- elif key in ["grouped_tasks", "dependencies", "outputs", "code_map"]:
61
- st.session_state[key] = {}
62
- else:
63
- st.session_state[key] = []
 
 
 
64
 
65
- # ------------------------------
66
- # Helper Functions
67
- # ------------------------------
68
- def goto(page):
 
 
69
  st.session_state.page = page
70
  st.rerun()
71
 
72
- def export_file(df, kind="csv"):
 
 
 
 
73
  if kind == "csv":
74
  return df.to_csv(index=False).encode("utf-8-sig")
75
- if kind == "xlsx":
76
- bio = BytesIO()
77
- with pd.ExcelWriter(bio, engine="openpyxl") as w:
78
- df.to_excel(w, index=False, sheet_name="tasks")
79
- bio.seek(0)
80
- return bio.getvalue()
81
-
82
- def draw_dependency_graph(df):
 
83
  G = nx.DiGraph()
84
  color_map = {"P": "#A7C7E7", "D": "#FFE8A3", "E": "#A8E6CF", "R": "#FFD3B6", "O": "#FFAAA5"}
85
  for _, row in df.iterrows():
86
  code = row["code"]
87
  label = row["name"]
88
- lifecycle = row.get("lifecycle", "E")
89
  color = color_map.get(lifecycle, "#CFCFCF")
90
  G.add_node(code, label=label, color=color)
91
- for dep in row.get("depends_on", "").split(","):
92
  dep = dep.strip()
93
  if dep:
94
  G.add_edge(dep, code)
95
- nt = Network(height="550px", width="100%", directed=True, bgcolor="#FFFFFF", font_color="#222222")
96
  nt.from_nx(G)
97
  tmp_path = tempfile.NamedTemporaryFile(delete=False, suffix=".html").name
98
  nt.save_graph(tmp_path)
99
- with open(tmp_path, "r", encoding="utf-8") as f:
100
- html = f.read()
101
  os.remove(tmp_path)
102
  return html
103
 
104
- # ------------------------------
105
- # ์ƒ๋‹จ ์ง„ํ–‰๋ฐ”
106
- # ------------------------------
107
- steps = [
108
- "๋„๋ฉ”์ธ ์„ค์ •", "์—…๋ฌด ๋ฐœ์‚ฐ", "๊ทธ๋ฃน ์กฐ์ •", "์˜์กด์„ฑ ํŒ๋‹จ", "์‚ฐ์ถœ๋ฌผ ์ •์˜", "์ตœ์ข… ์ •๋ฆฌ"
109
- ]
110
- current_idx = steps.index(st.session_state.page) if st.session_state.page in steps else 0
111
- progress_ratio = (current_idx + 1) / len(steps)
112
- st.progress(progress_ratio, text=f"๋‹จ๊ณ„ {current_idx+1} / {len(steps)} : {st.session_state.page}")
113
-
114
- # ------------------------------
115
- # 1๏ธโƒฃ ๋„๋ฉ”์ธ ์„ค์ •
116
- # ------------------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  if st.session_state.page == "๋„๋ฉ”์ธ ์„ค์ •":
118
  st.title("1๏ธโƒฃ ๋„๋ฉ”์ธ ์„ค์ •")
119
- st.markdown("""
120
- ํŒ€์˜ ์ฃผ์š” ์—…๋ฌด ๋„๋ฉ”์ธ์„ 3~4๊ฐœ๋กœ ์ •์˜ํ•ด๋ณด์„ธ์š”.
121
- ์˜ˆ: ์šด์˜๊ด€๋ฆฌ / ๊ต์œก์šด์˜ / ์ง„๋‹จ๊ฐœ๋ฐœ / ๋ฐ์ดํ„ฐ๋ถ„์„
122
- """)
123
  cols = st.columns(4)
124
  new_domains = []
125
  for i in range(4):
126
  with cols[i]:
127
- d = st.text_input(f"๋„๋ฉ”์ธ {i+1}", st.session_state.domains[i] if i < len(st.session_state.domains) else "")
128
- if d:
129
- new_domains.append(d)
130
- st.session_state.domains = [d for d in new_domains if d]
 
 
 
131
  if st.button("โžก๏ธ ๋‹ค์Œ: ์—…๋ฌด ๋ฐœ์‚ฐ"):
132
- goto("์—…๋ฌด ๋ฐœ์‚ฐ")
 
 
 
133
 
134
- # ------------------------------
135
- # 2๏ธโƒฃ ์—…๋ฌด ๋ฐœ์‚ฐ
136
- # ------------------------------
137
  elif st.session_state.page == "์—…๋ฌด ๋ฐœ์‚ฐ":
138
  st.title("2๏ธโƒฃ ์—…๋ฌด ๋ฐœ์‚ฐ")
139
- st.markdown("๊ฐ ๋„๋ฉ”์ธ๋ณ„๋กœ ํŒ€์ด ์‹ค์ œ ์ˆ˜ํ–‰ ์ค‘์ธ ์—…๋ฌด๋ฅผ ์ž์œ ๋กญ๊ฒŒ ๋‚˜์—ดํ•˜์„ธ์š”.")
140
  for d in st.session_state.domains + ["๊ธฐํƒ€"]:
141
  st.subheader(f"๐Ÿ“‚ {d}")
142
- text = st.text_area(f"{d} ์—…๋ฌด ์ž…๋ ฅ", key=f"tasks_{d}", height=150,
143
- placeholder="์˜ˆ: ๋ฆฌ๋”์‹ญ ์ง„๋‹จ ๊ฒฐ๊ณผ ๋ฆฌํฌํŠธ ์ž‘์„ฑ\n์กฐ์ง๋ฌธํ™” ๊ต์œก ๊ธฐํš\n๋ฐ์ดํ„ฐ ์ •๋ฆฌ ์ž๋™ํ™” ๋“ฑ")
 
 
 
 
144
  if text:
145
- st.session_state.grouped_tasks[d] = [t.strip() for t in text.split("\n") if t.strip()]
 
146
  if st.button("โžก๏ธ ๋‹ค์Œ: ๊ทธ๋ฃน ์กฐ์ •"):
147
  goto("๊ทธ๋ฃน ์กฐ์ •")
148
 
149
- # ------------------------------
150
- # 3๏ธโƒฃ ๊ทธ๋ฃน ์กฐ์ •
151
- # ------------------------------
152
  elif st.session_state.page == "๊ทธ๋ฃน ์กฐ์ •":
153
  st.title("3๏ธโƒฃ ์—…๋ฌด ๊ทธ๋ฃน ์กฐ์ •")
154
- st.markdown("๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ์œผ๋กœ ์—…๋ฌด ์ˆœ์„œ๋ฅผ ์กฐ์ •ํ•˜๊ณ , ํ•„์š” ์‹œ ๋„๋ฉ”์ธ ๊ฐ„ ์ด๋™ํ•˜์„ธ์š”.")
155
  st.divider()
156
 
157
  domains = st.session_state.domains + ["๊ธฐํƒ€"]
158
  updated = {d: list(st.session_state.grouped_tasks.get(d, [])) for d in domains}
159
- palette = ["#E3F2FD", "#E8F5E9", "#FFF8E1", "#FCE4EC", "#E0F7FA"]
160
  cols = st.columns(len(domains))
161
  for i, d in enumerate(domains):
162
  with cols[i]:
163
- bg = palette[i % len(palette)]
164
  st.markdown(f"<div class='domain-box' style='background-color:{bg};'>", unsafe_allow_html=True)
165
  st.markdown(f"<div class='domain-header'>๐Ÿ“ฆ {d}</div>", unsafe_allow_html=True)
 
166
  sorted_tasks = sort_items(updated[d], direction="vertical", key=f"sort_{d}")
167
  for t in sorted_tasks:
168
  c1, c2, c3 = st.columns([4, 1, 1])
169
  with c1:
170
  st.markdown(f"<div class='task-card'>{t}</div>", unsafe_allow_html=True)
171
  with c2:
172
- if st.button("๐Ÿ—‘", key=f"del_{d}_{t}"):
173
- updated[d].remove(t)
174
- st.rerun()
175
  with c3:
176
- move_to = st.selectbox("โ†’", ["(์ด๋™)"] + [x for x in domains if x != d], key=f"mv_{d}_{t}")
 
 
 
 
177
  if move_to != "(์ด๋™)":
178
  updated[move_to].append(t)
179
  updated[d].remove(t)
180
  st.rerun()
 
181
  new_t = st.text_input(f"{d} ์ƒˆ ์—…๋ฌด", key=f"add_{i}")
182
  if st.button(f"โž• ์ถ”๊ฐ€ ({d})", key=f"btn_add_{i}") and new_t.strip():
183
- updated[d].append(new_t.strip())
184
- st.rerun()
185
  st.markdown("</div>", unsafe_allow_html=True)
 
186
  st.session_state.grouped_tasks = updated
187
  c1, c2 = st.columns(2)
188
- if c1.button("โฌ…๏ธ ์ด์ „: ์—…๋ฌด ๋ฐœ์‚ฐ"):
189
- goto("์—…๋ฌด ๋ฐœ์‚ฐ")
190
- if c2.button("โžก๏ธ ๋‹ค์Œ: ์˜์กด์„ฑ ํŒ๋‹จ"):
191
- goto("์˜์กด์„ฑ ํŒ๋‹จ")
192
-
193
- # ------------------------------
194
- # 4๏ธโƒฃ ์˜์กด์„ฑ ํŒ๋‹จ
195
- # ------------------------------
196
  elif st.session_state.page == "์˜์กด์„ฑ ํŒ๋‹จ":
197
  st.title("4๏ธโƒฃ ์˜์กด์„ฑ ํŒ๋‹จ")
198
- st.markdown("๊ฐ ์—…๋ฌด ๊ฐ„์˜ ์„ ํ›„ ๊ด€๊ณ„(์˜์กด์„ฑ)๋ฅผ ๋„๋ฉ”์ธ๋ณ„๋กœ ์ง€์ •ํ•˜์„ธ์š”.")
199
  st.divider()
200
- domains = st.session_state.domains + ["๊ธฐํƒ€"]
201
  deps = {}
202
- for d in domains:
 
203
  st.subheader(f"๐Ÿ“‚ {d}")
204
  tasks = st.session_state.grouped_tasks.get(d, [])
205
  all_tasks = [t for td in st.session_state.grouped_tasks.values() for t in td]
 
206
  for t in tasks:
207
- key = f"deps_{hashlib.md5(f'{d}_{t}'.encode()).hexdigest()[:8]}"
208
- selected = st.multiselect(f"'{t}' ์ด์ „์— ํ•„์š”ํ•œ ์—…๋ฌด", [x for x in all_tasks if x != t],
209
- default=st.session_state.dependencies.get(t, []), key=key)
210
- deps[t] = selected
 
 
 
 
211
  st.session_state.dependencies = deps
 
212
  c1, c2 = st.columns(2)
213
- if c1.button("โฌ…๏ธ ์ด์ „: ๊ทธ๋ฃน ์กฐ์ •"):
214
- goto("๊ทธ๋ฃน ์กฐ์ •")
215
- if c2.button("โžก๏ธ ๋‹ค์Œ: ์‚ฐ์ถœ๋ฌผ ์ •์˜"):
216
- goto("์‚ฐ์ถœ๋ฌผ ์ •์˜")
217
 
218
- # ------------------------------
219
- # 5๏ธโƒฃ ์‚ฐ์ถœ๋ฌผ ์ •์˜
220
- # ------------------------------
221
  elif st.session_state.page == "์‚ฐ์ถœ๋ฌผ ์ •์˜":
222
  st.title("5๏ธโƒฃ ์‚ฐ์ถœ๋ฌผ ์ •์˜")
223
- st.markdown("๊ฐ ์—…๋ฌด๊ฐ€ ๋งŒ๋“ค์–ด๋‚ด๋Š” ์‚ฐ์ถœ๋ฌผ(Output)์„ ์ž…๋ ฅํ•˜์„ธ์š”.")
224
- outs = {}
225
- for d, tasks in st.session_state.grouped_tasks.items():
226
- st.subheader(f"๐Ÿ“‚ {d}")
227
- for t in tasks:
228
- val = st.text_input(f"{t} โ†’ ์‚ฐ์ถœ๋ฌผ", value=st.session_state.outputs.get(t, ""), key=f"out_{t}")
229
- outs[t] = val
230
- st.session_state.outputs = outs
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
231
  c1, c2 = st.columns(2)
232
- if c1.button("โฌ…๏ธ ์ด์ „: ์˜์กด์„ฑ ํŒ๋‹จ"):
233
- goto("์˜์กด์„ฑ ํŒ๋‹จ")
234
- if c2.button("โžก๏ธ ๋‹ค์Œ: ์ตœ์ข… ์ •๋ฆฌ"):
235
- goto("์ตœ์ข… ์ •๋ฆฌ")
236
-
237
- # ------------------------------
238
- # 6๏ธโƒฃ ์ตœ์ข… ์ •๋ฆฌ + ์ฝ”๋“œํ™” ๋‹จ๊ณ„
239
- # ------------------------------
240
  elif st.session_state.page == "์ตœ์ข… ์ •๋ฆฌ":
241
- st.title("6๏ธโƒฃ ์ตœ์ข… ์ •๋ฆฌ ๋ฐ ์—…๋ฌด ์ฝ”๋“œํ™”")
242
- st.markdown("๊ฐ ์—…๋ฌด๋ฅผ **์œ ํ˜• / ์‚ฌ์ดํด / ์†Œ์š”์‹œ๊ฐ„** ๊ธฐ์ค€์œผ๋กœ ์ฝ”๋“œํ™”ํ•˜์„ธ์š”.")
243
  st.divider()
244
 
245
- type_opts = ["COMM", "CEO", "MULTI", "DEEP", "ADHOC"]
246
  cycle_opts = ["P", "D", "E", "R", "O"]
247
  time_opts = ["T", "FE"]
248
 
249
- code_map = {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
250
  for d, tasks in st.session_state.grouped_tasks.items():
251
- st.subheader(f"๐Ÿ“‚ {d}")
 
 
252
  for t in tasks:
253
- c1, c2, c3 = st.columns(3)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
254
  with c1:
255
- typ = st.selectbox(f"{t} - ์œ ํ˜•", type_opts, key=f"type_{t}",
256
- index=type_opts.index(st.session_state.code_map.get(t, {}).get("type", "COMM")) if t in st.session_state.code_map else 0)
 
 
 
 
 
 
257
  with c2:
258
- cyc = st.selectbox("์‚ฌ์ดํด", cycle_opts, key=f"cycle_{t}",
259
- index=cycle_opts.index(st.session_state.code_map.get(t, {}).get("cycle", "E")) if t in st.session_state.code_map else 2)
 
 
 
 
 
 
260
  with c3:
261
- tm = st.selectbox("์†Œ์š”์‹œ๊ฐ„", time_opts, key=f"time_{t}",
262
- index=time_opts.index(st.session_state.code_map.get(t, {}).get("time", "T")) if t in st.session_state.code_map else 0)
263
- code_map[t] = {"type": typ, "cycle": cyc, "time": tm}
 
 
 
 
 
 
264
 
265
- st.session_state.code_map = code_map
 
 
266
 
267
- # ๋ฐ์ดํ„ฐํ”„๋ ˆ์ž„ ์ •๋ฆฌ
268
- rows = []
269
- for d, tasks in st.session_state.grouped_tasks.items():
270
- for i, t in enumerate(tasks, 1):
271
- deps = ",".join(st.session_state.dependencies.get(t, []))
272
- outp = st.session_state.outputs.get(t, "")
273
- meta = st.session_state.code_map.get(t, {})
274
- rows.append({
 
 
 
 
 
 
 
 
275
  "domain": d,
276
- "order": i,
277
  "name": t,
278
- "depends_on": deps,
279
- "output": outp,
280
- "type": meta.get("type", ""),
281
- "cycle": meta.get("cycle", ""),
282
- "time": meta.get("time", ""),
283
- "code": f"{d[:3].upper()}-{i:02d}"
284
- })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
285
  df = pd.DataFrame(rows)
286
- st.dataframe(df, use_container_width=True)
 
 
 
 
287
 
288
- # ๊ทธ๋ž˜ํ”„
289
- html = draw_dependency_graph(df)
 
 
 
 
 
 
290
  st.components.v1.html(html, height=600, scrolling=True)
291
 
292
- # Export
293
- csv_data = export_file(df, "csv")
294
- xlsx_data = export_file(df, "xlsx")
295
  c1, c2 = st.columns(2)
296
- c1.download_button("โฌ‡๏ธ CSV ๋‹ค์šด๋กœ๋“œ", csv_data, "tasks_code.csv", "text/csv")
297
- c2.download_button("โฌ‡๏ธ Excel ๋‹ค์šด๋กœ๋“œ", xlsx_data, "tasks_code.xlsx")
298
 
299
  if st.button("๐Ÿ”„ ์ฒ˜์Œ์œผ๋กœ ๋Œ์•„๊ฐ€๊ธฐ"):
300
- for k in ["page", "domains", "grouped_tasks", "dependencies", "outputs", "code_map"]:
301
- st.session_state[k] = [] if k == "domains" else {}
 
 
 
 
 
302
  goto("๋„๋ฉ”์ธ ์„ค์ •")
303
 
304
- st.caption("ยฉ 2025 ํŒ€ ์—…๋ฌด ๊ตฌ์กฐํ™” ๋„์šฐ๋ฏธ โ€“ Streamlit ๊ธฐ๋ฐ˜ | Designed for clarity & usability")
 
 
 
8
  from streamlit_sortables import sort_items
9
  import hashlib
10
 
11
+ # =========================
12
+ # ๊ธฐ๋ณธ ์„ค์ • & ์ „์—ญ ์Šคํƒ€์ผ
13
+ # =========================
14
+ st.set_page_config(page_title="Team Task Structuring Assistant (P-D-E-R-O)", layout="wide")
15
+
 
 
 
16
  st.markdown("""
17
+ <style>
18
+ html, body, [class*="css"] {
19
+ font-size: 14px !important;
20
+ font-family: "Noto Sans KR", "Helvetica", sans-serif;
21
+ }
22
+ .stButton>button {
23
+ border-radius: 6px; padding: 4px 10px; font-size: 13px;
24
+ }
25
+ .task-card {
26
+ background-color: #fff; border: 1px solid #ddd; border-radius: 6px;
27
+ padding: 6px 10px; margin-bottom: 6px; box-shadow: 0 1px 2px rgba(0,0,0,0.05);
28
+ font-size: 13px;
29
+ }
30
+ .domain-box { border-radius: 8px; padding: 8px; margin: 6px 4px; }
31
+ .domain-header { font-weight: 600; font-size: 15px; text-align: center; margin-bottom: 6px; }
32
+ .helper-note { color:#444; font-size:12px; }
33
+ footer { visibility: hidden; }
34
+ </style>
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  """, unsafe_allow_html=True)
36
 
37
+ # =========================
38
  # ์„ธ์…˜ ์ดˆ๊ธฐํ™”
39
+ # =========================
40
+ def _init(key, default):
41
+ if key not in st.session_state: st.session_state[key] = default
42
+
43
+ _init("page", "๋„๋ฉ”์ธ ์„ค์ •")
44
+ _init("domains", [])
45
+ _init("grouped_tasks", {}) # {domain: [task, ...]}
46
+ _init("dependencies", {}) # {"domain::task": [task_name, ...]}
47
+ _init("outputs", {}) # {"domain::task": output}
48
+ _init("code_map", {}) # {"domain::task": {...}}
49
+ _init("seq_registry", {}) # {(domain_code, cycle): max_seq}
50
+ _init("domain_defaults", {}) # {domain_code: {"cycle": "E", "time": "T"}}
51
 
52
+ # =========================
53
+ # ์œ ํ‹ธ
54
+ # =========================
55
+ PALETTE = ["#E3F2FD", "#E8F5E9", "#FFF8E1", "#FCE4EC", "#E0F7FA"]
56
+
57
+ def goto(page: str):
58
  st.session_state.page = page
59
  st.rerun()
60
 
61
+ def safe_key(domain: str, task: str, prefix: str) -> str:
62
+ h = hashlib.md5(f"{domain}::{task}".encode()).hexdigest()[:8]
63
+ return f"{prefix}_{h}"
64
+
65
+ def export_file(df: pd.DataFrame, kind="csv"):
66
  if kind == "csv":
67
  return df.to_csv(index=False).encode("utf-8-sig")
68
+ bio = BytesIO()
69
+ with pd.ExcelWriter(bio, engine="openpyxl") as w:
70
+ df.to_excel(w, index=False, sheet_name="tasks")
71
+ bio.seek(0)
72
+ return bio.getvalue()
73
+
74
+ def draw_dependency_graph(df: pd.DataFrame):
75
+ if df.empty:
76
+ return "<div style='padding:8px;color:#666;'>๊ทธ๋ž˜ํ”„๋ฅผ ํ‘œ์‹œํ•  ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.</div>"
77
  G = nx.DiGraph()
78
  color_map = {"P": "#A7C7E7", "D": "#FFE8A3", "E": "#A8E6CF", "R": "#FFD3B6", "O": "#FFAAA5"}
79
  for _, row in df.iterrows():
80
  code = row["code"]
81
  label = row["name"]
82
+ lifecycle = row.get("cycle", "E")
83
  color = color_map.get(lifecycle, "#CFCFCF")
84
  G.add_node(code, label=label, color=color)
85
+ for dep in str(row.get("depends_on", "")).split(","):
86
  dep = dep.strip()
87
  if dep:
88
  G.add_edge(dep, code)
89
+ nt = Network(height="550px", width="100%", directed=True, bgcolor="#FFFFFF", font_color="#222")
90
  nt.from_nx(G)
91
  tmp_path = tempfile.NamedTemporaryFile(delete=False, suffix=".html").name
92
  nt.save_graph(tmp_path)
93
+ html = open(tmp_path, "r", encoding="utf-8").read()
 
94
  os.remove(tmp_path)
95
  return html
96
 
97
+ def map_domain_to_code(d: str) -> str:
98
+ table = {
99
+ "๊ณตํ†ต": "COMM", "์ „๋žต": "COMM",
100
+ "CEO": "CEO", "CEO์„œ๋ฒ ์ด": "CEO",
101
+ "๋ฆฌ๋”์‹ญ์„œ๋ฒ ์ด": "MULTI", "๋ฆฌ๋”์‹ญ": "MULTI",
102
+ "๋ฆฌ๋”์‹ญ์‹ฌ์ธต": "DEEP", "์‹ฌ์ธต์ง„๋‹จ": "DEEP",
103
+ "์ˆ˜์‹œ": "ADHOC", "์š”์ฒญ": "ADHOC", "์ˆ˜์‹œ์š”์ฒญ์ง„๋‹จ": "ADHOC",
104
+ "๊ต์œก์šด์˜": "EDU", "๊ต์œก": "EDU",
105
+ "๋ฐ์ดํ„ฐ๋ถ„์„": "DATA", "๋ถ„์„": "DATA",
106
+ "์šด์˜๊ด€๋ฆฌ": "OPS", "์šด์˜": "OPS",
107
+ }
108
+ for k, v in table.items():
109
+ if k in d: return v
110
+ return d[:4].upper()
111
+
112
+ def ensure_unique_list(seq):
113
+ seen, out = set(), []
114
+ for x in seq:
115
+ if x not in seen:
116
+ seen.add(x)
117
+ out.append(x)
118
+ return out
119
+
120
+ # =========================
121
+ # ์ƒ๋‹จ ์ง„ํ–‰ ํ‘œ์‹œ๋ฐ”
122
+ # =========================
123
+ STEPS = ["๋„๋ฉ”์ธ ์„ค์ •", "์—…๋ฌด ๋ฐœ์‚ฐ", "๊ทธ๋ฃน ์กฐ์ •", "์˜์กด์„ฑ ํŒ๋‹จ", "์‚ฐ์ถœ๋ฌผ ์ •์˜", "์ตœ์ข… ์ •๋ฆฌ"]
124
+ idx = STEPS.index(st.session_state.page) if st.session_state.page in STEPS else 0
125
+ st.progress((idx + 1) / len(STEPS), text=f"๋‹จ๊ณ„ {idx+1}/{len(STEPS)} : {st.session_state.page}")
126
+
127
+ # =========================
128
+ # 1) ๋„๋ฉ”์ธ ์„ค์ •
129
+ # =========================
130
  if st.session_state.page == "๋„๋ฉ”์ธ ์„ค์ •":
131
  st.title("1๏ธโƒฃ ๋„๋ฉ”์ธ ์„ค์ •")
132
+ st.markdown("ํŒ€์˜ ์ฃผ์š” ์—…๋ฌด ๋„๋ฉ”์ธ์„ 3~4๊ฐœ ์ •์˜ํ•˜์„ธ์š”. (์˜ˆ: ๊ณตํ†ต, ๋ฆฌ๋”์‹ญ์„œ๋ฒ ์ด, ๊ต์œก์šด์˜, ๋ฐ์ดํ„ฐ๋ถ„์„ ๋“ฑ)")
133
+
 
 
134
  cols = st.columns(4)
135
  new_domains = []
136
  for i in range(4):
137
  with cols[i]:
138
+ d = st.text_input(
139
+ f"๋„๋ฉ”์ธ {i+1}",
140
+ st.session_state.domains[i] if i < len(st.session_state.domains) else ""
141
+ )
142
+ if d: new_domains.append(d.strip())
143
+ st.session_state.domains = ensure_unique_list([d for d in new_domains if d])
144
+
145
  if st.button("โžก๏ธ ๋‹ค์Œ: ์—…๋ฌด ๋ฐœ์‚ฐ"):
146
+ if not st.session_state.domains:
147
+ st.warning("๋„๋ฉ”์ธ์„ ์ตœ์†Œ 1๊ฐœ ์ด์ƒ ์ž…๋ ฅํ•˜์„ธ์š”.")
148
+ else:
149
+ goto("์—…๋ฌด ๋ฐœ์‚ฐ")
150
 
151
+ # =========================
152
+ # 2) ์—…๋ฌด ๋ฐœ์‚ฐ
153
+ # =========================
154
  elif st.session_state.page == "์—…๋ฌด ๋ฐœ์‚ฐ":
155
  st.title("2๏ธโƒฃ ์—…๋ฌด ๋ฐœ์‚ฐ")
156
+ st.markdown("๊ฐ ๋„๋ฉ”์ธ๋ณ„ ์‹ค์ œ ์ˆ˜ํ–‰ ์ค‘์ธ ์—…๋ฌด๋ฅผ ๊ฐ€๋Šฅํ•œ ํ•œ ๋งŽ์ด ์ ์–ด๋ณด์„ธ์š”. (์ค„๋ฐ”๊ฟˆ์œผ๋กœ ๊ตฌ๋ถ„)")
157
  for d in st.session_state.domains + ["๊ธฐํƒ€"]:
158
  st.subheader(f"๐Ÿ“‚ {d}")
159
+ text = st.text_area(
160
+ f"{d} ์—…๋ฌด",
161
+ key=f"tasks_{d}",
162
+ height=150,
163
+ placeholder="์˜ˆ: ๋ฆฌ๋”์‹ญ ์ง„๋‹จ ๋ฆฌํฌํŠธ ์ž‘์„ฑ\n๊ต์œก ๊ธฐํš\n๋ฐ์ดํ„ฐ ๋ถ„์„ ๋“ฑ"
164
+ )
165
  if text:
166
+ tasks = [t.strip() for t in text.split("\n") if t.strip()]
167
+ st.session_state.grouped_tasks[d] = ensure_unique_list(tasks)
168
  if st.button("โžก๏ธ ๋‹ค์Œ: ๊ทธ๋ฃน ์กฐ์ •"):
169
  goto("๊ทธ๋ฃน ์กฐ์ •")
170
 
171
+ # =========================
172
+ # 3) ๊ทธ๋ฃน ์กฐ์ •
173
+ # =========================
174
  elif st.session_state.page == "๊ทธ๋ฃน ์กฐ์ •":
175
  st.title("3๏ธโƒฃ ์—…๋ฌด ๊ทธ๋ฃน ์กฐ์ •")
176
+ st.markdown("๋„๋ฉ”์ธ๋ณ„ ์—…๋ฌด๋ฅผ ์ •๋ฆฌํ•˜๊ณ  ์ˆœ์„œ๋ฅผ ๋ฐ”๊พธ๊ฑฐ๋‚˜ ๋‹ค๋ฅธ ๋„๋ฉ”์ธ์œผ๋กœ ์ด๋™ํ•˜์„ธ์š”.")
177
  st.divider()
178
 
179
  domains = st.session_state.domains + ["๊ธฐํƒ€"]
180
  updated = {d: list(st.session_state.grouped_tasks.get(d, [])) for d in domains}
181
+
182
  cols = st.columns(len(domains))
183
  for i, d in enumerate(domains):
184
  with cols[i]:
185
+ bg = PALETTE[i % len(PALETTE)]
186
  st.markdown(f"<div class='domain-box' style='background-color:{bg};'>", unsafe_allow_html=True)
187
  st.markdown(f"<div class='domain-header'>๐Ÿ“ฆ {d}</div>", unsafe_allow_html=True)
188
+
189
  sorted_tasks = sort_items(updated[d], direction="vertical", key=f"sort_{d}")
190
  for t in sorted_tasks:
191
  c1, c2, c3 = st.columns([4, 1, 1])
192
  with c1:
193
  st.markdown(f"<div class='task-card'>{t}</div>", unsafe_allow_html=True)
194
  with c2:
195
+ if st.button("๐Ÿ—‘", key=safe_key(d, t, "del")):
196
+ updated[d].remove(t); st.rerun()
 
197
  with c3:
198
+ move_to = st.selectbox(
199
+ "โ†’",
200
+ ["(์ด๋™)"] + [x for x in domains if x != d],
201
+ key=safe_key(d, t, "mv")
202
+ )
203
  if move_to != "(์ด๋™)":
204
  updated[move_to].append(t)
205
  updated[d].remove(t)
206
  st.rerun()
207
+
208
  new_t = st.text_input(f"{d} ์ƒˆ ์—…๋ฌด", key=f"add_{i}")
209
  if st.button(f"โž• ์ถ”๊ฐ€ ({d})", key=f"btn_add_{i}") and new_t.strip():
210
+ updated[d].append(new_t.strip()); st.rerun()
211
+
212
  st.markdown("</div>", unsafe_allow_html=True)
213
+
214
  st.session_state.grouped_tasks = updated
215
  c1, c2 = st.columns(2)
216
+ if c1.button("โฌ…๏ธ ์ด์ „: ์—…๋ฌด ๋ฐœ์‚ฐ"): goto("์—…๋ฌด ๋ฐœ์‚ฐ")
217
+ if c2.button("โžก๏ธ ๋‹ค์Œ: ์˜์กด์„ฑ ํŒ๋‹จ"): goto("์˜์กด์„ฑ ํŒ๋‹จ")
218
+
219
+ # =========================
220
+ # 4) ์˜์กด์„ฑ ํŒ๋‹จ
221
+ # =========================
 
 
222
  elif st.session_state.page == "์˜์กด์„ฑ ํŒ๋‹จ":
223
  st.title("4๏ธโƒฃ ์˜์กด์„ฑ ํŒ๋‹จ")
224
+ st.markdown("๊ฐ ์—…๋ฌด ๊ฐ„ ์„ ํ›„๊ด€๊ณ„(์˜์กด์„ฑ)๋ฅผ ๋„๋ฉ”์ธ๋ณ„๋กœ ์„ค์ •ํ•˜์„ธ์š”.")
225
  st.divider()
226
+
227
  deps = {}
228
+ domains = st.session_state.domains + ["๊ธฐํƒ€"]
229
+ for i, d in enumerate(domains):
230
  st.subheader(f"๐Ÿ“‚ {d}")
231
  tasks = st.session_state.grouped_tasks.get(d, [])
232
  all_tasks = [t for td in st.session_state.grouped_tasks.values() for t in td]
233
+
234
  for t in tasks:
235
+ key = safe_key(d, t, "deps")
236
+ selected = st.multiselect(
237
+ f"'{t}' ์ด์ „ ์—…๋ฌด",
238
+ [x for x in all_tasks if x != t],
239
+ default=st.session_state.dependencies.get(f"{d}::{t}", []),
240
+ key=key
241
+ )
242
+ deps[f"{d}::{t}"] = selected
243
  st.session_state.dependencies = deps
244
+
245
  c1, c2 = st.columns(2)
246
+ if c1.button("โฌ…๏ธ ์ด์ „: ๊ทธ๋ฃน ์กฐ์ •"): goto("๊ทธ๋ฃน ์กฐ์ •")
247
+ if c2.button("โžก๏ธ ๋‹ค์Œ: ์‚ฐ์ถœ๋ฌผ ์ •์˜"): goto("์‚ฐ์ถœ๋ฌผ ์ •์˜")
 
 
248
 
249
+ # =========================
250
+ # 5) ์‚ฐ์ถœ๋ฌผ ์ •์˜ (๊ฐ„๊ฒฐ ๋งคํ•‘ UI)
251
+ # =========================
252
  elif st.session_state.page == "์‚ฐ์ถœ๋ฌผ ์ •์˜":
253
  st.title("5๏ธโƒฃ ์‚ฐ์ถœ๋ฌผ ์ •์˜")
254
+ st.markdown("๊ฐ ์—…๋ฌด์— ๋Œ€์‘๋˜๋Š” ์‚ฐ์ถœ๋ฌผ์„ ํ‘œ์—์„œ ์ง์ ‘ ์ž…๋ ฅํ•˜์„ธ์š”.")
255
+ st.divider()
256
+
257
+ updated_outputs = {}
258
+ for i, (d, tasks) in enumerate(st.session_state.grouped_tasks.items()):
259
+ bg = PALETTE[i % len(PALETTE)]
260
+ st.markdown(f"<div style='background-color:{bg}; padding:10px; border-radius:8px;'>", unsafe_allow_html=True)
261
+ st.markdown(f"### ๐Ÿ“‚ {d}")
262
+
263
+ rows = [{"์—…๋ฌด๋ช…": t, "์‚ฐ์ถœ๋ฌผ": st.session_state.outputs.get(f"{d}::{t}", "")} for t in tasks]
264
+ df = pd.DataFrame(rows)
265
+ edited = st.data_editor(
266
+ df, key=f"editor_{i}", hide_index=True, num_rows="fixed",
267
+ column_config={
268
+ "์—…๋ฌด๋ช…": st.column_config.TextColumn(disabled=True),
269
+ "์‚ฐ์ถœ๋ฌผ": st.column_config.TextColumn(placeholder="์˜ˆ: ๋ฆฌํฌํŠธ/๋Œ€์‹œ๋ณด๋“œ/๊ต์œก์ž๋ฃŒ/์Šคํฌ๋ฆฝํŠธ ๋“ฑ")
270
+ },
271
+ use_container_width=True
272
+ )
273
+ for _, row in edited.iterrows():
274
+ updated_outputs[f"{d}::{row['์—…๋ฌด๋ช…']}"] = row["์‚ฐ์ถœ๋ฌผ"]
275
+
276
+ st.markdown("<div class='helper-note'>Tip: ์‚ฐ์ถœ๋ฌผ์€ ๋ณด๊ณ ์„œ/๋Œ€์‹œ๋ณด๋“œ/์‹œ์Šคํ…œ/๋ฐ์ดํ„ฐ/๊ต์œก์ž๋ฃŒ ๋“ฑ์œผ๋กœ ๊ฐ„๋‹จํžˆ ์ ์–ด๋„ ์ถฉ๋ถ„ํ•ฉ๋‹ˆ๋‹ค.</div>", unsafe_allow_html=True)
277
+ st.markdown("</div>", unsafe_allow_html=True)
278
+
279
+ st.session_state.outputs = updated_outputs
280
  c1, c2 = st.columns(2)
281
+ if c1.button("โฌ…๏ธ ์ด์ „: ์˜์กด์„ฑ ํŒ๋‹จ"): goto("์˜์กด์„ฑ ํŒ๋‹จ")
282
+ if c2.button("โžก๏ธ ๋‹ค์Œ: ์ตœ์ข… ์ •๋ฆฌ"): goto("์ตœ์ข… ์ •๋ฆฌ")
283
+
284
+ # =========================
285
+ # 6) ์ตœ์ข… ์ •๋ฆฌ (์งˆ๋ฌธํ˜• ๋ณด์กฐ + ๊ธฐ๋ณธ๊ฐ’ + ์ฝ”๋“œ ์ƒ์„ฑ)
286
+ # =========================
 
 
287
  elif st.session_state.page == "์ตœ์ข… ์ •๋ฆฌ":
288
+ st.title("6๏ธโƒฃ ์ตœ์ข… ์ •๋ฆฌ ๋ฐ ์—…๋ฌด ์ฝ”๋“œ ์ƒ์„ฑ")
289
+ st.markdown("5๋ฌธํ•ญ ์ฒดํฌ๋กœ **์‚ฌ์ดํด ์ถ”์ฒœ**์„ ๋ฐ›๊ณ , **์‹œ๊ฐ„๊ธฐํ˜ธ(T/FE)** ๋Š” ๋ณ„๋„๋กœ ์„ ํƒํ•˜์„ธ์š”.")
290
  st.divider()
291
 
 
292
  cycle_opts = ["P", "D", "E", "R", "O"]
293
  time_opts = ["T", "FE"]
294
 
295
+ def recommend_cycles(is_p, is_d, is_e, is_r, is_o):
296
+ recs = []
297
+ if is_p: recs.append("P")
298
+ if is_d: recs.append("D")
299
+ if is_e: recs.append("E")
300
+ if is_r: recs.append("R")
301
+ if is_o: recs.append("O")
302
+ return recs
303
+
304
+ # 0) code_map/seq_registry ์ดˆ๊ธฐ ๋™๊ธฐํ™”
305
+ seq_registry = st.session_state.seq_registry
306
+ code_map = st.session_state.code_map
307
+ for task_key, meta in code_map.items():
308
+ pair = (meta.get("domain_code", ""), meta.get("cycle", ""))
309
+ if pair[0] and pair[1]:
310
+ seq_registry[pair] = max(seq_registry.get(pair, 0), meta.get("seq", 0))
311
+
312
+ # ๋„๋ฉ”์ธ ๊ธฐ๋ณธ๊ฐ’(์„ ํƒ์‚ฌํ•ญ)
313
+ st.markdown("#### โš™๏ธ ๋„๋ฉ”์ธ๋ณ„ ๊ธฐ๋ณธ๊ฐ’ (์„ ํƒ)")
314
+ defaults_holder = st.container()
315
+ with defaults_holder:
316
+ for d in st.session_state.grouped_tasks.keys():
317
+ domain_code = map_domain_to_code(d)
318
+ left, mid = st.columns([1.2, 1.2])
319
+ with left:
320
+ cyc_def = st.radio(
321
+ f"{d}({domain_code}) ๊ธฐ๋ณธ ์‚ฌ์ดํด",
322
+ cycle_opts,
323
+ key=f"default_cycle_{domain_code}",
324
+ horizontal=True,
325
+ index=cycle_opts.index(st.session_state.domain_defaults.get(domain_code, {}).get("cycle", "E"))
326
+ )
327
+ with mid:
328
+ tm_def = st.radio(
329
+ f"{d}({domain_code}) ๊ธฐ๋ณธ ์‹œ๊ฐ„",
330
+ time_opts,
331
+ key=f"default_time_{domain_code}",
332
+ horizontal=True,
333
+ index=time_opts.index(st.session_state.domain_defaults.get(domain_code, {}).get("time", "T"))
334
+ )
335
+ st.session_state.domain_defaults[domain_code] = {"cycle": cyc_def, "time": tm_def}
336
+ st.caption("๊ธฐ๋ณธ๊ฐ’์€ โ€˜๋ฏธ์„ค์ • ์—…๋ฌดโ€™์˜ ์ดˆ๊ธฐ๊ฐ’์œผ๋กœ๋งŒ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. ์ด๋ฏธ ์ง€์ •ํ•œ ์—…๋ฌด์—๋Š” ์˜ํ–ฅ์„ ์ฃผ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.")
337
+ st.divider()
338
+
339
+ # 1) ์—…๋ฌด ๋‹จ์œ„ UI & ์ฝ”๋“œ ์ƒ์„ฑ
340
  for d, tasks in st.session_state.grouped_tasks.items():
341
+ domain_code = map_domain_to_code(d)
342
+ st.subheader(f"๐Ÿ“‚ {d} ({domain_code})")
343
+
344
  for t in tasks:
345
+ task_key = f"{d}::{t}"
346
+ prev = code_map.get(task_key, {})
347
+ # ๊ธฐ๋ณธ๊ฐ’ ์šฐ์„ ์ˆœ์œ„: (๊ธฐ์กด์„ ํƒ) > (๋„๋ฉ”์ธ ๊ธฐ๋ณธ) > (E/T)
348
+ default_cycle = prev.get("cycle", st.session_state.domain_defaults.get(domain_code, {}).get("cycle", "E"))
349
+ default_time = prev.get("time", st.session_state.domain_defaults.get(domain_code, {}).get("time", "T"))
350
+
351
+ # ํŒ๋‹จ ๋ณด์กฐ(5๋ฌธํ•ญ)
352
+ st.markdown(f"**๐Ÿงฉ {t}**")
353
+ qcols = st.columns(5)
354
+ with qcols[0]:
355
+ qP = st.checkbox("P: ๋ชฉ์ /์‹œ์ /๋Œ€์ƒ", key=safe_key(d, t, "qP"), value=False)
356
+ with qcols[1]:
357
+ qD = st.checkbox("D: ์„ธํŒ…/์„ค๊ณ„", key=safe_key(d, t, "qD"), value=False)
358
+ with qcols[2]:
359
+ qE = st.checkbox("E: ์ง‘ํ–‰/์ˆ˜์ง‘", key=safe_key(d, t, "qE"), value=False)
360
+ with qcols[3]:
361
+ qR = st.checkbox("R: ํ•ด์„/ํ‰๊ฐ€", key=safe_key(d, t, "qR"), value=False)
362
+ with qcols[4]:
363
+ qO = st.checkbox("O: ๋ฐฐํฌ/๋ฐ˜์˜", key=safe_key(d, t, "qO"), value=False)
364
+
365
+ recs = recommend_cycles(qP, qD, qE, qR, qO)
366
+
367
+ # ์‚ฌ์ดํด/์‹œ๊ฐ„ ์„ ํƒ
368
+ c1, c2, c3 = st.columns([1.4, 0.9, 2.2])
369
+ cycle_key = safe_key(d, t, "cycle")
370
+ time_key = safe_key(d, t, "time")
371
+
372
  with c1:
373
+ cyc = st.radio(
374
+ "์‚ฌ์ดํด",
375
+ cycle_opts,
376
+ key=cycle_key,
377
+ horizontal=True,
378
+ index=cycle_opts.index(default_cycle) if cycle_key not in st.session_state else
379
+ cycle_opts.index(st.session_state[cycle_key])
380
+ )
381
  with c2:
382
+ tm = st.radio(
383
+ "์‹œ๊ฐ„๊ธฐํ˜ธ",
384
+ time_opts,
385
+ key=time_key,
386
+ horizontal=True,
387
+ index=time_opts.index(default_time) if time_key not in st.session_state else
388
+ time_opts.index(st.session_state[time_key])
389
+ )
390
  with c3:
391
+ if recs:
392
+ st.markdown("**์ถ”์ฒœ:** " + " ".join([f"`{r}`" for r in recs]))
393
+ bcols = st.columns(len(recs))
394
+ for i, r in enumerate(recs):
395
+ if bcols[i].button(f"์ถ”์ฒœ ์ ์šฉ({r})", key=safe_key(d, t, f"apply_{r}")):
396
+ st.session_state[cycle_key] = r
397
+ st.rerun()
398
+ else:
399
+ st.caption("์ถ”์ฒœ ์—†์Œ: ์ฒดํฌํ•œ ํ•ญ๋ชฉ์ด ์—†์Šต๋‹ˆ๋‹ค. ์ง์ ‘ ์‚ฌ์ดํด์„ ์„ ํƒํ•˜์„ธ์š”.")
400
 
401
+ # ์ตœ์ข… ๊ฐ’(๋ผ๋””์˜ค ์ƒํƒœ)
402
+ cyc = st.session_state[cycle_key]
403
+ tm = st.session_state[time_key]
404
 
405
+ pair = (domain_code, cyc)
406
+ # ๋™์ผ pair & ๊ธฐ์กด seq ์œ ์ง€, ์•„๋‹ˆ๋ฉด ์ƒˆ ๋ฒˆํ˜ธ
407
+ if prev and prev.get("domain_code") == domain_code and prev.get("cycle") == cyc and prev.get("seq", 0) > 0:
408
+ seq = prev["seq"]
409
+ else:
410
+ seq = seq_registry.get(pair, 0) + 1
411
+ seq_registry[pair] = seq
412
+
413
+ code = f"{domain_code}-{cyc}{seq:02d}-{tm}"
414
+ st.markdown(
415
+ f"<div class='task-card' style='background-color:#F9FAFB;'>"
416
+ f"<small>โžก๏ธ ์ฝ”๋“œ</small> <b>{code}</b></div>",
417
+ unsafe_allow_html=True
418
+ )
419
+
420
+ code_map[task_key] = {
421
  "domain": d,
422
+ "domain_code": domain_code,
423
  "name": t,
424
+ "cycle": cyc,
425
+ "time": tm,
426
+ "seq": seq,
427
+ "code": code,
428
+ }
429
+
430
+ # ์ƒํƒœ ๊ฐฑ์‹ 
431
+ st.session_state.seq_registry = seq_registry
432
+ st.session_state.code_map = code_map
433
+
434
+ # 2) ๊ฒฐ๊ณผํ‘œ & ๊ทธ๋ž˜ํ”„ & ๋‹ค์šด๋กœ๋“œ
435
+ st.divider()
436
+ st.markdown("#### ๐Ÿ“˜ ์ตœ์ข… ์ฝ”๋“œ ๋ชฉ๋ก")
437
+ rows = []
438
+ for task_key, meta in code_map.items():
439
+ d, t = task_key.split("::", 1)
440
+ deps_names = st.session_state.dependencies.get(task_key, [])
441
+ rows.append({
442
+ "domain": d,
443
+ "domain_code": meta["domain_code"],
444
+ "name": t,
445
+ "cycle": meta["cycle"],
446
+ "time": meta["time"],
447
+ "seq": meta["seq"],
448
+ "code": meta["code"],
449
+ "depends_on": ", ".join(deps_names),
450
+ "output": st.session_state.outputs.get(task_key, ""),
451
+ })
452
  df = pd.DataFrame(rows)
453
+ if not df.empty:
454
+ df = df.sort_values(by=["domain_code", "cycle", "seq", "name"]).reset_index(drop=True)
455
+
456
+ st.dataframe(df[["domain", "domain_code", "name", "cycle", "time", "code", "depends_on", "output"]],
457
+ use_container_width=True)
458
 
459
+ # ๊ทธ๋ž˜ํ”„(์ด๋ฆ„โ†’์ฝ”๋“œ ๊ฐ„์„  ๋งคํ•‘)
460
+ name_to_code = {meta["name"]: meta["code"] for meta in code_map.values()}
461
+ df_graph = df.copy()
462
+ if not df_graph.empty:
463
+ df_graph["depends_on"] = df_graph["depends_on"].apply(
464
+ lambda s: ", ".join([name_to_code.get(x.strip(), x.strip()) for x in s.split(",") if x.strip()]) if s else ""
465
+ )
466
+ html = draw_dependency_graph(df_graph.rename(columns={"code":"code", "name":"name"}) if not df_graph.empty else df_graph)
467
  st.components.v1.html(html, height=600, scrolling=True)
468
 
469
+ # ๋‹ค์šด๋กœ๋“œ
 
 
470
  c1, c2 = st.columns(2)
471
+ c1.download_button("โฌ‡๏ธ CSV ๋‹ค์šด๋กœ๋“œ", export_file(df, "csv"), "final_task_codes.csv", "text/csv")
472
+ c2.download_button("โฌ‡๏ธ Excel ๋‹ค์šด๋กœ๋“œ", export_file(df, "xlsx"), "final_task_codes.xlsx")
473
 
474
  if st.button("๐Ÿ”„ ์ฒ˜์Œ์œผ๋กœ ๋Œ์•„๊ฐ€๊ธฐ"):
475
+ for k in ["page", "domains", "grouped_tasks", "dependencies", "outputs", "code_map", "seq_registry"]:
476
+ if k == "domains":
477
+ st.session_state[k] = []
478
+ elif k == "page":
479
+ st.session_state[k] = "๋„๋ฉ”์ธ ์„ค์ •"
480
+ else:
481
+ st.session_state[k] = {}
482
  goto("๋„๋ฉ”์ธ ์„ค์ •")
483
 
484
+ # ํ‘ธํ„ฐ
485
+ st.markdown("---")
486
+ st.caption("ยฉ 2025 Crystal_MVP")