soojeongcrystal commited on
Commit
b367b16
ยท
verified ยท
1 Parent(s): 816379e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +155 -179
app.py CHANGED
@@ -6,43 +6,46 @@ from io import BytesIO
6
  import tempfile
7
  import os
8
  from streamlit_sortables import sort_items
 
9
 
10
  # ------------------------------
11
- # ๊ธ€๋กœ๋ฒŒ ์Šคํƒ€์ผ ์„ค์ •
12
  # ------------------------------
13
  st.set_page_config(page_title="ํŒ€ ์—…๋ฌด ๊ตฌ์กฐํ™” ๋„์šฐ๋ฏธ (P-D-E-R-O)", layout="wide")
14
 
 
 
 
15
  st.markdown("""
16
  <style>
17
- @import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard.css');
18
  html, body, [class*="css"] {
19
- font-family: 'Pretendard', sans-serif;
20
- color: #222;
21
- background-color: #f4f6f8;
22
- }
23
- h1, h2, h3 {
24
- color: #2b2b2b;
25
- font-weight: 700;
26
  }
27
  .stButton>button {
28
- border-radius: 8px;
29
- border: 1px solid #d0d7de;
30
- background-color: white;
31
- color: #333;
32
- padding: 0.5rem 1rem;
33
- transition: all 0.2s ease;
34
- }
35
- .stButton>button:hover {
36
- background-color: #e8eef5;
37
- color: #111;
38
- }
39
- .stTextInput>div>div>input, textarea {
40
  border-radius: 6px;
41
- border: 1px solid #d6d6d6;
 
42
  }
43
- .stSelectbox>div>div {
 
 
44
  border-radius: 6px;
45
- border: 1px solid #d6d6d6;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  }
47
  </style>
48
  """, unsafe_allow_html=True)
@@ -50,42 +53,38 @@ st.markdown("""
50
  # ------------------------------
51
  # ์„ธ์…˜ ์ดˆ๊ธฐํ™”
52
  # ------------------------------
53
- if "page" not in st.session_state:
54
- st.session_state.page = "๋„๋ฉ”์ธ ์„ค์ •"
55
- if "domains" not in st.session_state:
56
- st.session_state.domains = []
57
- if "grouped_tasks" not in st.session_state:
58
- st.session_state.grouped_tasks = {}
59
- if "dependencies" not in st.session_state:
60
- st.session_state.dependencies = {}
61
- if "outputs" not in st.session_state:
62
- st.session_state.outputs = {}
63
 
64
  # ------------------------------
65
- # ๋„ค๋น„๊ฒŒ์ด์…˜ ํ•จ์ˆ˜
66
  # ------------------------------
67
  def goto(page):
68
  st.session_state.page = page
69
  st.rerun()
70
 
71
- # ------------------------------
72
- # ํ—ฌํผ ํ•จ์ˆ˜
73
- # ------------------------------
74
  def export_file(df, kind="csv"):
75
  if kind == "csv":
76
  return df.to_csv(index=False).encode("utf-8-sig")
77
- bio = BytesIO()
78
- with pd.ExcelWriter(bio, engine="openpyxl") as w:
79
- df.to_excel(w, index=False, sheet_name="tasks")
80
- bio.seek(0)
81
- return bio.getvalue()
 
82
 
83
  def draw_dependency_graph(df):
84
  G = nx.DiGraph()
85
  color_map = {"P": "#A7C7E7", "D": "#FFE8A3", "E": "#A8E6CF", "R": "#FFD3B6", "O": "#FFAAA5"}
86
  for _, row in df.iterrows():
87
  code = row["code"]
88
- label = f"{row['name']}"
89
  lifecycle = row.get("lifecycle", "E")
90
  color = color_map.get(lifecycle, "#CFCFCF")
91
  G.add_node(code, label=label, color=color)
@@ -93,7 +92,7 @@ def draw_dependency_graph(df):
93
  dep = dep.strip()
94
  if dep:
95
  G.add_edge(dep, code)
96
- nt = Network(height="550px", width="100%", directed=True, bgcolor="#FFFFFF", font_color="#222")
97
  nt.from_nx(G)
98
  tmp_path = tempfile.NamedTemporaryFile(delete=False, suffix=".html").name
99
  nt.save_graph(tmp_path)
@@ -103,168 +102,113 @@ def draw_dependency_graph(df):
103
  return html
104
 
105
  # ------------------------------
106
- # PAGE 1. ๋„๋ฉ”์ธ ์„ค์ •
 
 
 
 
 
 
 
 
 
 
107
  # ------------------------------
108
  if st.session_state.page == "๋„๋ฉ”์ธ ์„ค์ •":
109
  st.title("1๏ธโƒฃ ๋„๋ฉ”์ธ ์„ค์ •")
110
  st.markdown("""
111
- ํŒ€์ด ํ•˜๊ณ  ์žˆ๋Š” ์ผ์„ 3~4๊ฐœ์˜ **๋„๋ฉ”์ธ(์—…๋ฌด ๋ฒ”์ฃผ)** ๋กœ ๋‚˜๋ˆ„์–ด ๋ด…๋‹ˆ๋‹ค.
112
- ์˜ˆ: ์šด์˜๊ด€๋ฆฌ, ๊ต์œก์šด์˜, ์ง„๋‹จ๊ฐœ๋ฐœ, ๋ฐ์ดํ„ฐ๋ถ„์„ ๋“ฑ
113
  """)
114
-
115
  cols = st.columns(4)
116
  new_domains = []
117
  for i in range(4):
118
  with cols[i]:
119
- val = st.text_input(f"๋„๋ฉ”์ธ {i+1}", st.session_state.domains[i] if i < len(st.session_state.domains) else "")
120
- if val:
121
- new_domains.append(val.strip())
122
  st.session_state.domains = [d for d in new_domains if d]
123
- if st.button("โžก๏ธ ๋‹ค์Œ: ์—…๋ฌด ๋ฐœ์‚ฐํ•˜๊ธฐ") and st.session_state.domains:
124
  goto("์—…๋ฌด ๋ฐœ์‚ฐ")
125
 
126
  # ------------------------------
127
- # PAGE 2. ์—…๋ฌด ๋ฐœ์‚ฐ
128
  # ------------------------------
129
- if st.session_state.page == "์—…๋ฌด ๋ฐœ์‚ฐ":
130
- st.title("2๏ธโƒฃ ์—…๋ฌด ์ž์œ  ๋ฐœ์‚ฐ")
131
- st.markdown("""
132
- ๊ฐ ๋„๋ฉ”์ธ๋ณ„๋กœ ํŒ€์—์„œ ์‹ค์ œ ํ•˜๊ณ  ์žˆ๋Š” ์ผ์„ ๊ฐ€๋Šฅํ•œ ํ•œ ๋งŽ์ด ์ ์–ด๋ณด์„ธ์š”.
133
- ํ˜•์‹์€ ์ž์œ ๋กญ์Šต๋‹ˆ๋‹ค. ๋‚˜์ค‘์— ๋ฌถ๊ณ  ์ •๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
134
- """)
135
  for d in st.session_state.domains + ["๊ธฐํƒ€"]:
136
  st.subheader(f"๐Ÿ“‚ {d}")
137
- task_text = st.text_area(
138
- f"{d} ๋„๋ฉ”์ธ์˜ ์—…๋ฌด๋“ค",
139
- key=f"tasks_{d}",
140
- height=150,
141
- placeholder="์˜ˆ: ๋ฆฌ๋”์‹ญ ์ง„๋‹จ ๊ฒฐ๊ณผ ๋ฆฌํฌํŠธ ์ž‘์„ฑ\n์กฐ์ง๋ฌธํ™” ๊ต์œก ๊ธฐํš\n๋ฐ์ดํ„ฐ ์ •๋ฆฌ ์ž๋™ํ™” ๋“ฑ"
142
- )
143
- if task_text:
144
- st.session_state.grouped_tasks[d] = [
145
- t.strip() for t in task_text.split("\n") if t.strip()
146
- ]
147
  if st.button("โžก๏ธ ๋‹ค์Œ: ๊ทธ๋ฃน ์กฐ์ •"):
148
  goto("๊ทธ๋ฃน ์กฐ์ •")
149
 
150
-
151
  # ------------------------------
152
- # PAGE 3. ๊ทธ๋ฃน ์กฐ์ •
153
  # ------------------------------
154
  elif st.session_state.page == "๊ทธ๋ฃน ์กฐ์ •":
155
- st.title("3๏ธโƒฃ ์—…๋ฌด ๊ทธ๋ฃน ์ •๋ฆฌํ•˜๊ธฐ")
156
- st.markdown("""
157
- ๊ฐ ๋„๋ฉ”์ธ๋ณ„๋กœ ์ •๋ฆฌ๋œ ์—…๋ฌด๋ฅผ ๊ด€๋ฆฌํ•˜์„ธ์š”.
158
- - **๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ**์œผ๋กœ ์ˆœ์„œ๋ฅผ ๋ฐ”๊ฟ€ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
159
- - **๐Ÿ—‘ ๋ฒ„ํŠผ**์œผ๋กœ ์‚ญ์ œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
160
- - **โ†’ ์„ ํƒ๋ฐ•์Šค**๋ฅผ ํ†ตํ•ด ๋‹ค๋ฅธ ๋„๋ฉ”์ธ์œผ๋กœ ์ด๋™ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
161
- - **โž• ์ถ”๊ฐ€ ๋ฒ„ํŠผ**์œผ๋กœ ์ƒˆ ์—…๋ฌด๋ฅผ ๋“ฑ๋กํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
162
- """)
163
-
164
  st.divider()
165
- domains = st.session_state.domains + ["๊ธฐํƒ€"]
166
- updated_grouped = {
167
- d: list(st.session_state.grouped_tasks.get(d, [])) for d in domains
168
- }
169
 
170
- # ์ƒ‰์ƒ ์ •์˜
 
171
  palette = ["#E3F2FD", "#E8F5E9", "#FFF8E1", "#FCE4EC", "#E0F7FA"]
172
- domain_colors = {d: palette[i % len(palette)] for i, d in enumerate(domains)}
173
-
174
- # ์Šคํƒ€์ผ
175
- st.markdown("""
176
- <style>
177
- .task-card {
178
- background-color: #fff;
179
- border: 1px solid #ddd;
180
- border-radius: 8px;
181
- padding: 6px 10px;
182
- margin-bottom: 6px;
183
- font-size: 14px;
184
- }
185
- .domain-box {
186
- border-radius: 10px;
187
- padding: 10px;
188
- margin: 4px;
189
- }
190
- </style>
191
- """, unsafe_allow_html=True)
192
-
193
  cols = st.columns(len(domains))
194
  for i, d in enumerate(domains):
195
  with cols[i]:
196
- bg_color = domain_colors[d]
197
- st.markdown(
198
- f"<div class='domain-box' style='background-color:{bg_color};'>",
199
- unsafe_allow_html=True,
200
- )
201
- st.markdown(f"### ๐Ÿ“ฆ {d}")
202
-
203
- tasks = updated_grouped.get(d, [])
204
- new_task_list = []
205
-
206
- # ์ •๋ ฌ
207
- sorted_tasks = sort_items(tasks, direction="vertical", key=f"sort_{d}")
208
-
209
  for t in sorted_tasks:
210
  c1, c2, c3 = st.columns([4, 1, 1])
211
  with c1:
212
  st.markdown(f"<div class='task-card'>{t}</div>", unsafe_allow_html=True)
213
  with c2:
214
  if st.button("๐Ÿ—‘", key=f"del_{d}_{t}"):
215
- st.session_state.grouped_tasks[d] = [
216
- x for x in sorted_tasks if x != t
217
- ]
218
  st.rerun()
219
  with c3:
220
- move_target = st.selectbox(
221
- "โ†’",
222
- ["(์ด๋™)"] + [x for x in domains if x != d],
223
- key=f"move_{d}_{t}",
224
- )
225
- if move_target != "(์ด๋™)":
226
- st.session_state.grouped_tasks.setdefault(move_target, []).append(t)
227
- st.session_state.grouped_tasks[d] = [
228
- x for x in sorted_tasks if x != t
229
- ]
230
  st.rerun()
231
- new_task_list.append(t)
232
-
233
- # ์ถ”๊ฐ€
234
- new_task = st.text_input(f"{d} ์ƒˆ ์—…๋ฌด", key=f"add_{i}")
235
- if st.button(f"โž• ์ถ”๊ฐ€ ({d})", key=f"btn_add_{i}") and new_task.strip():
236
- new_task_list.append(new_task.strip())
237
- st.session_state.grouped_tasks[d] = new_task_list
238
  st.rerun()
239
-
240
- updated_grouped[d] = new_task_list
241
  st.markdown("</div>", unsafe_allow_html=True)
242
-
243
- st.session_state.grouped_tasks = updated_grouped
244
-
245
- st.divider()
246
  c1, c2 = st.columns(2)
247
- if c1.button("โฌ…๏ธ ์ด์ „: ์—…๋ฌด ๋ฐœ์‚ฐ ๋‹ค์‹œ๋ณด๊ธฐ"):
248
  goto("์—…๋ฌด ๋ฐœ์‚ฐ")
249
  if c2.button("โžก๏ธ ๋‹ค์Œ: ์˜์กด์„ฑ ํŒ๋‹จ"):
250
  goto("์˜์กด์„ฑ ํŒ๋‹จ")
251
 
252
  # ------------------------------
253
- # PAGE 4. ์˜์กด์„ฑ ํŒ๋‹จ
254
  # ------------------------------
255
  elif st.session_state.page == "์˜์กด์„ฑ ํŒ๋‹จ":
256
- st.title("4๏ธโƒฃ ์—…๋ฌด ๊ฐ„ ์˜์กด์„ฑ ํŒ๋‹จ")
257
- st.markdown("""
258
- ์—…๋ฌด ๊ฐ„ **์„ ํ›„ ๊ด€๊ณ„(์˜์กด์„ฑ)** ๋ฅผ ์„ ํƒํ•˜์„ธ์š”.
259
- ์˜ˆ: โ€œ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ โ†’ ๋ถ„์„ ๋ฆฌํฌํŠธ ์ž‘์„ฑโ€
260
- """)
261
- all_tasks = [t for tasks in st.session_state.grouped_tasks.values() for t in tasks]
262
- dependencies = {}
263
- for t in all_tasks:
264
- deps = st.multiselect(f"'{t}' ์ด์ „์— ํ•„์š”ํ•œ ์—…๋ฌด", [x for x in all_tasks if x != t],
265
- default=st.session_state.dependencies.get(t, []))
266
- dependencies[t] = deps
267
- st.session_state.dependencies = dependencies
 
 
 
268
  c1, c2 = st.columns(2)
269
  if c1.button("โฌ…๏ธ ์ด์ „: ๊ทธ๋ฃน ์กฐ์ •"):
270
  goto("๊ทธ๋ฃน ์กฐ์ •")
@@ -272,19 +216,18 @@ elif st.session_state.page == "์˜์กด์„ฑ ํŒ๋‹จ":
272
  goto("์‚ฐ์ถœ๋ฌผ ์ •์˜")
273
 
274
  # ------------------------------
275
- # PAGE 5. ์‚ฐ์ถœ๋ฌผ ์ •์˜
276
  # ------------------------------
277
  elif st.session_state.page == "์‚ฐ์ถœ๋ฌผ ์ •์˜":
278
  st.title("5๏ธโƒฃ ์‚ฐ์ถœ๋ฌผ ์ •์˜")
279
- st.markdown("๊ฐ ์—…๋ฌด์˜ ๊ฒฐ๊ณผ๋ฌผ(์‚ฐ์ถœ๋ฌผ)์„ ์ž…๋ ฅํ•˜์„ธ์š”. ์˜ˆ: ๋ณด๊ณ ์„œ, ๋Œ€์‹œ๋ณด๋“œ, ๊ต์œก์ž๋ฃŒ ๋“ฑ")
280
-
281
- outputs = {}
282
  for d, tasks in st.session_state.grouped_tasks.items():
283
  st.subheader(f"๐Ÿ“‚ {d}")
284
  for t in tasks:
285
- val = st.text_input(f"{t} โ†’ ์‚ฐ์ถœ๋ฌผ", value=st.session_state.outputs.get(t, ""))
286
- outputs[t] = val
287
- st.session_state.outputs = outputs
288
  c1, c2 = st.columns(2)
289
  if c1.button("โฌ…๏ธ ์ด์ „: ์˜์กด์„ฑ ํŒ๋‹จ"):
290
  goto("์˜์กด์„ฑ ํŒ๋‹จ")
@@ -292,37 +235,70 @@ elif st.session_state.page == "์‚ฐ์ถœ๋ฌผ ์ •์˜":
292
  goto("์ตœ์ข… ์ •๋ฆฌ")
293
 
294
  # ------------------------------
295
- # PAGE 6. ์ตœ์ข… ์ •๋ฆฌ
296
  # ------------------------------
297
  elif st.session_state.page == "์ตœ์ข… ์ •๋ฆฌ":
298
- st.title("6๏ธโƒฃ ์ตœ์ข… ์ •๋ฆฌ ๋ฐ ๋‹ค์šด๋กœ๋“œ")
299
- st.markdown("์ •๋ฆฌ๋œ ์—…๋ฌด ๊ตฌ์กฐ๋ฅผ ํ™•์ธํ•˜๊ณ , ๊ทธ๋ž˜ํ”„์™€ ํŒŒ์ผ๋กœ ๋‹ค์šด๋กœ๋“œํ•˜์„ธ์š”.")
 
 
 
 
 
300
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
301
  rows = []
302
  for d, tasks in st.session_state.grouped_tasks.items():
303
  for i, t in enumerate(tasks, 1):
304
  deps = ",".join(st.session_state.dependencies.get(t, []))
305
  outp = st.session_state.outputs.get(t, "")
 
306
  rows.append({
307
- "domain": d, "order": i, "name": t,
308
- "depends_on": deps, "output": outp,
309
- "code": f"{d[:3].upper()}-{i:02d}", "lifecycle": "E"
 
 
 
 
 
 
310
  })
311
  df = pd.DataFrame(rows)
312
  st.dataframe(df, use_container_width=True)
 
 
313
  html = draw_dependency_graph(df)
314
  st.components.v1.html(html, height=600, scrolling=True)
315
 
 
316
  csv_data = export_file(df, "csv")
317
  xlsx_data = export_file(df, "xlsx")
318
  c1, c2 = st.columns(2)
319
- c1.download_button("โฌ‡๏ธ CSV ๋‹ค์šด๋กœ๋“œ", csv_data, "tasks.csv", "text/csv")
320
- c2.download_button("โฌ‡๏ธ Excel ๋‹ค์šด๋กœ๋“œ", xlsx_data, "tasks.xlsx")
321
 
322
  if st.button("๐Ÿ”„ ์ฒ˜์Œ์œผ๋กœ ๋Œ์•„๊ฐ€๊ธฐ"):
323
- for k in ["page","domains","grouped_tasks","dependencies","outputs"]:
324
- if k in st.session_state:
325
- del st.session_state[k]
326
  goto("๋„๋ฉ”์ธ ์„ค์ •")
327
 
328
- st.caption("ยฉ 2025 ํŒ€ ์—…๋ฌด ๊ตฌ์กฐํ™” ๋„์šฐ๋ฏธ โ€” Crystal's")
 
6
  import tempfile
7
  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)
 
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)
 
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)
 
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("๊ทธ๋ฃน ์กฐ์ •")
 
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("์˜์กด์„ฑ ํŒ๋‹จ")
 
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")