Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -307,13 +307,68 @@ elif st.session_state.page == "산출물 정의":
|
|
| 307 |
if c2.button("➡️ 다음: 최종 정리"): goto("최종 정리")
|
| 308 |
|
| 309 |
# =========================
|
| 310 |
-
# 6) 최종 정리 (
|
| 311 |
# =========================
|
| 312 |
elif st.session_state.page == "최종 정리":
|
| 313 |
st.title("6️⃣ 최종 정리 및 업무 코드 생성")
|
| 314 |
-
st.markdown(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 315 |
st.divider()
|
| 316 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 317 |
cycle_opts = ["P", "D", "E", "R", "O"]
|
| 318 |
time_opts = ["T", "FE"]
|
| 319 |
|
|
@@ -326,66 +381,25 @@ elif st.session_state.page == "최종 정리":
|
|
| 326 |
if is_o: recs.append("O")
|
| 327 |
return recs
|
| 328 |
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
for task_key, meta in code_map.items():
|
| 333 |
-
pair = (meta.get("domain_code", ""), meta.get("cycle", ""))
|
| 334 |
-
if pair[0] and pair[1]:
|
| 335 |
-
seq_registry[pair] = max(seq_registry.get(pair, 0), meta.get("seq", 0))
|
| 336 |
-
|
| 337 |
-
# 도메인 기본값 (선택)
|
| 338 |
-
st.markdown("#### ⚙️ 도메인별 기본값 (선택)")
|
| 339 |
-
for d in st.session_state.grouped_tasks.keys():
|
| 340 |
-
domain_code = map_domain_to_code(d)
|
| 341 |
-
left, mid = st.columns([1.2, 1.2])
|
| 342 |
-
with left:
|
| 343 |
-
cyc_def = st.radio(
|
| 344 |
-
f"{d}({domain_code}) 기본 사이클", cycle_opts,
|
| 345 |
-
key=f"default_cycle_{domain_code}", horizontal=True,
|
| 346 |
-
index=cycle_opts.index(st.session_state.domain_defaults.get(domain_code, {}).get("cycle", "E"))
|
| 347 |
-
)
|
| 348 |
-
with mid:
|
| 349 |
-
tm_def = st.radio(
|
| 350 |
-
f"{d}({domain_code}) 기본 시간", time_opts,
|
| 351 |
-
key=f"default_time_{domain_code}", horizontal=True,
|
| 352 |
-
index=time_opts.index(st.session_state.domain_defaults.get(domain_code, {}).get("time", "T"))
|
| 353 |
-
)
|
| 354 |
-
st.session_state.domain_defaults[domain_code] = {"cycle": cyc_def, "time": tm_def}
|
| 355 |
-
st.caption("기본값은 ‘미설정 업무’의 초기값으로만 사용됩니다. 이미 지정한 업무에는 영향을 주지 않습니다.")
|
| 356 |
-
st.divider()
|
| 357 |
-
|
| 358 |
-
# 업무 단위 UI & 코드 생성
|
| 359 |
-
for d, tasks in st.session_state.grouped_tasks.items():
|
| 360 |
-
domain_code = map_domain_to_code(d)
|
| 361 |
-
st.subheader(f"📂 {d} ({domain_code})")
|
| 362 |
|
|
|
|
| 363 |
for t in tasks:
|
| 364 |
task_key = f"{d}::{t}"
|
| 365 |
prev = code_map.get(task_key, {})
|
| 366 |
-
default_cycle = prev.get("cycle", st.session_state.domain_defaults.get(domain_code, {}).get("cycle", "E"))
|
| 367 |
-
default_time = prev.get("time", st.session_state.domain_defaults.get(domain_code, {}).get("time", "T"))
|
| 368 |
|
| 369 |
-
# 판단 보조 체크박스 (P/D/E/R/O)
|
| 370 |
st.markdown(f"**🧩 {t}**")
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
with qcols[1]:
|
| 375 |
-
qD = st.checkbox("D: 세팅/설계", key=safe_key(d, t, "qD"), value=False)
|
| 376 |
-
with qcols[2]:
|
| 377 |
-
qE = st.checkbox("E: 집행/수집", key=safe_key(d, t, "qE"), value=False)
|
| 378 |
-
with qcols[3]:
|
| 379 |
-
qR = st.checkbox("R: 해석/평가", key=safe_key(d, t, "qR"), value=False)
|
| 380 |
-
with qcols[4]:
|
| 381 |
-
qO = st.checkbox("O: 배포/반영", key=safe_key(d, t, "qO"), value=False)
|
| 382 |
-
|
| 383 |
-
recs = recommend_cycles(qP, qD, qE, qR, qO)
|
| 384 |
-
|
| 385 |
-
c1, c2, c3 = st.columns([1.4, 0.9, 2.2])
|
| 386 |
cycle_key = safe_key(d, t, "cycle")
|
| 387 |
time_key = safe_key(d, t, "time")
|
| 388 |
|
|
|
|
|
|
|
|
|
|
| 389 |
with c1:
|
| 390 |
cyc = st.radio(
|
| 391 |
"사이클", cycle_opts, key=cycle_key, horizontal=True,
|
|
@@ -398,37 +412,51 @@ elif st.session_state.page == "최종 정리":
|
|
| 398 |
index=time_opts.index(default_time) if time_key not in st.session_state else
|
| 399 |
time_opts.index(st.session_state[time_key])
|
| 400 |
)
|
| 401 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 402 |
if recs:
|
| 403 |
-
|
| 404 |
-
bcols = st.columns(len(recs))
|
| 405 |
for i, r in enumerate(recs):
|
| 406 |
-
if
|
| 407 |
st.session_state[cycle_key] = r
|
| 408 |
st.rerun()
|
| 409 |
else:
|
| 410 |
-
st.caption("
|
| 411 |
|
| 412 |
-
# 최종 값
|
| 413 |
cyc = st.session_state[cycle_key]
|
| 414 |
tm = st.session_state[time_key]
|
| 415 |
-
pair = (domain_code, cyc)
|
| 416 |
|
| 417 |
-
# 동일
|
| 418 |
-
|
| 419 |
-
|
|
|
|
| 420 |
else:
|
| 421 |
-
seq =
|
| 422 |
-
|
| 423 |
|
| 424 |
code = f"{domain_code}-{cyc}{seq:02d}-{tm}"
|
| 425 |
st.markdown(
|
| 426 |
-
f"<div
|
| 427 |
f"<small>➡️ 코드</small> <b>{code}</b></div>",
|
| 428 |
unsafe_allow_html=True
|
| 429 |
)
|
| 430 |
|
| 431 |
-
|
|
|
|
| 432 |
"domain": d,
|
| 433 |
"domain_code": domain_code,
|
| 434 |
"name": t,
|
|
@@ -438,7 +466,11 @@ elif st.session_state.page == "최종 정리":
|
|
| 438 |
"code": code,
|
| 439 |
}
|
| 440 |
|
| 441 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
| 442 |
st.divider()
|
| 443 |
st.markdown("#### 📘 최종 코드 목록")
|
| 444 |
rows = []
|
|
@@ -460,10 +492,12 @@ elif st.session_state.page == "최종 정리":
|
|
| 460 |
if not df.empty:
|
| 461 |
df = df.sort_values(by=["domain_code", "cycle", "seq", "name"]).reset_index(drop=True)
|
| 462 |
|
| 463 |
-
st.dataframe(
|
| 464 |
-
|
|
|
|
|
|
|
| 465 |
|
| 466 |
-
# 이름→코드 매핑
|
| 467 |
name_to_code = {meta["name"]: meta["code"] for meta in st.session_state.code_map.values()}
|
| 468 |
df_graph = df.copy()
|
| 469 |
if not df_graph.empty:
|
|
@@ -471,7 +505,7 @@ elif st.session_state.page == "최종 정리":
|
|
| 471 |
lambda s: ", ".join([name_to_code.get(x.strip(), x.strip()) for x in s.split(",") if x.strip()]) if s else ""
|
| 472 |
)
|
| 473 |
html = draw_dependency_graph(df_graph.rename(columns={"code": "code", "name": "name"}) if not df_graph.empty else df_graph)
|
| 474 |
-
st.components.v1.html(html, height=
|
| 475 |
|
| 476 |
c1, c2 = st.columns(2)
|
| 477 |
c1.download_button("⬇️ CSV 다운로드", export_file(df, "csv"), "final_task_codes.csv", "text/csv")
|
|
@@ -485,7 +519,7 @@ elif st.session_state.page == "최종 정리":
|
|
| 485 |
st.session_state[k] = "도메인 설정"
|
| 486 |
else:
|
| 487 |
st.session_state[k] = {}
|
| 488 |
-
|
| 489 |
|
| 490 |
# 푸터
|
| 491 |
st.markdown("---")
|
|
|
|
| 307 |
if c2.button("➡️ 다음: 최종 정리"): goto("최종 정리")
|
| 308 |
|
| 309 |
# =========================
|
| 310 |
+
# 6) 최종 정리 (도메인 코드 매핑 + 단순화된 UI + 코드 생성)
|
| 311 |
# =========================
|
| 312 |
elif st.session_state.page == "최종 정리":
|
| 313 |
st.title("6️⃣ 최종 정리 및 업무 코드 생성")
|
| 314 |
+
st.markdown(
|
| 315 |
+
"- ① **도메인 → 영문코드**를 확인/수정\n"
|
| 316 |
+
"- ② 각 업무에 **사이클(P/D/E/R/O)**, **시간기호(T/FE)** 선택\n"
|
| 317 |
+
"- ③ 자동 생성된 코드를 확인하고 다운로드"
|
| 318 |
+
)
|
| 319 |
st.divider()
|
| 320 |
|
| 321 |
+
# ---- 0) 도메인 → 영문코드 매핑 편집 ----
|
| 322 |
+
_init("domain_code_overrides", {})
|
| 323 |
+
domains = list(st.session_state.grouped_tasks.keys())
|
| 324 |
+
|
| 325 |
+
# 자동 제안 + 기존 오버라이드 반영
|
| 326 |
+
map_rows = []
|
| 327 |
+
for d in domains:
|
| 328 |
+
suggested = map_domain_to_code(d)
|
| 329 |
+
current = st.session_state.domain_code_overrides.get(d, suggested)
|
| 330 |
+
map_rows.append({"도메인": d, "영문코드": current})
|
| 331 |
+
|
| 332 |
+
st.markdown("#### 🔤 도메인 코드 매핑")
|
| 333 |
+
map_df = pd.DataFrame(map_rows)
|
| 334 |
+
map_edited = st.data_editor(
|
| 335 |
+
map_df, key="domain_code_editor", hide_index=True, use_container_width=True,
|
| 336 |
+
column_config={
|
| 337 |
+
"도메인": st.column_config.TextColumn(disabled=True),
|
| 338 |
+
"영문코드": st.column_config.TextColumn()
|
| 339 |
+
}
|
| 340 |
+
)
|
| 341 |
+
|
| 342 |
+
# 형식 검증: 영문/숫자, 2~8자
|
| 343 |
+
invalid = []
|
| 344 |
+
domain_code_overrides = {}
|
| 345 |
+
for _, r in map_edited.iterrows():
|
| 346 |
+
dname = str(r["도메인"])
|
| 347 |
+
code = str(r["영문코드"]).strip().upper()
|
| 348 |
+
if not code or not code.isalnum() or not (2 <= len(code) <= 8):
|
| 349 |
+
invalid.append(f"- {dname}: '{r['영문코드']}' (영문/숫자 2~8자)")
|
| 350 |
+
domain_code_overrides[dname] = code
|
| 351 |
+
if invalid:
|
| 352 |
+
st.error("도메인 코드 형식 오류가 있습니다:\n" + "\n".join(invalid))
|
| 353 |
+
st.stop()
|
| 354 |
+
st.session_state.domain_code_overrides = domain_code_overrides
|
| 355 |
+
|
| 356 |
+
st.caption("Tip: 코드 예) COMM, MULTI, CEO, DEEP, ADHOC, EDU, DATA, OPS 등")
|
| 357 |
+
st.divider()
|
| 358 |
+
|
| 359 |
+
# ---- 1) 시퀀스 집계(안정적 부여 준비) ----
|
| 360 |
+
# 기존 code_map 기반으로 (domain_code, cycle)별 최댓값 동기화
|
| 361 |
+
seq_registry = dict(st.session_state.seq_registry)
|
| 362 |
+
code_map = dict(st.session_state.code_map)
|
| 363 |
+
|
| 364 |
+
for task_key, meta in code_map.items():
|
| 365 |
+
# 도메인 코드가 바뀌었을 수 있으므로 최신 매핑 반영
|
| 366 |
+
new_domain_code = st.session_state.domain_code_overrides.get(meta.get("domain", ""), meta.get("domain_code", ""))
|
| 367 |
+
pair = (new_domain_code, meta.get("cycle", ""))
|
| 368 |
+
if pair[0] and pair[1]:
|
| 369 |
+
seq_registry[pair] = max(seq_registry.get(pair, 0), int(meta.get("seq", 0)))
|
| 370 |
+
|
| 371 |
+
# ---- 2) 업무 단위 UI (최소 선택 중심) ----
|
| 372 |
cycle_opts = ["P", "D", "E", "R", "O"]
|
| 373 |
time_opts = ["T", "FE"]
|
| 374 |
|
|
|
|
| 381 |
if is_o: recs.append("O")
|
| 382 |
return recs
|
| 383 |
|
| 384 |
+
for d in domains:
|
| 385 |
+
domain_code = st.session_state.domain_code_overrides[d]
|
| 386 |
+
st.subheader(f"📂 {d} → `{domain_code}`")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 387 |
|
| 388 |
+
tasks = st.session_state.grouped_tasks.get(d, [])
|
| 389 |
for t in tasks:
|
| 390 |
task_key = f"{d}::{t}"
|
| 391 |
prev = code_map.get(task_key, {})
|
|
|
|
|
|
|
| 392 |
|
|
|
|
| 393 |
st.markdown(f"**🧩 {t}**")
|
| 394 |
+
|
| 395 |
+
# 핵심 선택 (간결 UI): 사이클 · 시간기호
|
| 396 |
+
c1, c2 = st.columns([1.6, 1.0])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 397 |
cycle_key = safe_key(d, t, "cycle")
|
| 398 |
time_key = safe_key(d, t, "time")
|
| 399 |
|
| 400 |
+
default_cycle = prev.get("cycle", "E")
|
| 401 |
+
default_time = prev.get("time", "T")
|
| 402 |
+
|
| 403 |
with c1:
|
| 404 |
cyc = st.radio(
|
| 405 |
"사이클", cycle_opts, key=cycle_key, horizontal=True,
|
|
|
|
| 412 |
index=time_opts.index(default_time) if time_key not in st.session_state else
|
| 413 |
time_opts.index(st.session_state[time_key])
|
| 414 |
)
|
| 415 |
+
|
| 416 |
+
# 필요한 사람만 여는 도움말: 체크 → 추천 적용
|
| 417 |
+
with st.expander("도움이 필요하세요? (질문형 보조로 추천받기)", expanded=False):
|
| 418 |
+
qcols = st.columns(5)
|
| 419 |
+
with qcols[0]:
|
| 420 |
+
qP = st.checkbox("P: 목적/시점/대상")
|
| 421 |
+
with qcols[1]:
|
| 422 |
+
qD = st.checkbox("D: 세팅/설계")
|
| 423 |
+
with qcols[2]:
|
| 424 |
+
qE = st.checkbox("E: 집행/수집")
|
| 425 |
+
with qcols[3]:
|
| 426 |
+
qR = st.checkbox("R: 해석/평가")
|
| 427 |
+
with qcols[4]:
|
| 428 |
+
qO = st.checkbox("O: 배포/반영")
|
| 429 |
+
recs = recommend_cycles(qP, qD, qE, qR, qO)
|
| 430 |
if recs:
|
| 431 |
+
rcols = st.columns(len(recs))
|
|
|
|
| 432 |
for i, r in enumerate(recs):
|
| 433 |
+
if rcols[i].button(f"추천 적용 {r}", key=safe_key(d, t, f"apply_{r}")):
|
| 434 |
st.session_state[cycle_key] = r
|
| 435 |
st.rerun()
|
| 436 |
else:
|
| 437 |
+
st.caption("체크 결과가 없거나 모호합니다. 직접 선택을 유지하세요.")
|
| 438 |
|
| 439 |
+
# 최종 값 반영
|
| 440 |
cyc = st.session_state[cycle_key]
|
| 441 |
tm = st.session_state[time_key]
|
|
|
|
| 442 |
|
| 443 |
+
# 시퀀스 부여: 동일 (domain_code, cycle)이면 기존 유지, 아니면 다음 번호
|
| 444 |
+
pair = (domain_code, cyc)
|
| 445 |
+
if prev and prev.get("domain_code") == domain_code and prev.get("cycle") == cyc and int(prev.get("seq", 0)) > 0:
|
| 446 |
+
seq = int(prev["seq"])
|
| 447 |
else:
|
| 448 |
+
seq = int(seq_registry.get(pair, 0)) + 1
|
| 449 |
+
seq_registry[pair] = seq
|
| 450 |
|
| 451 |
code = f"{domain_code}-{cyc}{seq:02d}-{tm}"
|
| 452 |
st.markdown(
|
| 453 |
+
f"<div style='background:#F9FAFB;border:1px solid #DDD;border-radius:6px;padding:8px;margin:6px 0;'>"
|
| 454 |
f"<small>➡️ 코드</small> <b>{code}</b></div>",
|
| 455 |
unsafe_allow_html=True
|
| 456 |
)
|
| 457 |
|
| 458 |
+
# 저장
|
| 459 |
+
code_map[task_key] = {
|
| 460 |
"domain": d,
|
| 461 |
"domain_code": domain_code,
|
| 462 |
"name": t,
|
|
|
|
| 466 |
"code": code,
|
| 467 |
}
|
| 468 |
|
| 469 |
+
# 상태 갱신
|
| 470 |
+
st.session_state.seq_registry = seq_registry
|
| 471 |
+
st.session_state.code_map = code_map
|
| 472 |
+
|
| 473 |
+
# ---- 3) 결과 요약/그래프/다운로드 ----
|
| 474 |
st.divider()
|
| 475 |
st.markdown("#### 📘 최종 코드 목록")
|
| 476 |
rows = []
|
|
|
|
| 492 |
if not df.empty:
|
| 493 |
df = df.sort_values(by=["domain_code", "cycle", "seq", "name"]).reset_index(drop=True)
|
| 494 |
|
| 495 |
+
st.dataframe(
|
| 496 |
+
df[["domain", "domain_code", "name", "cycle", "time", "code", "depends_on", "output"]],
|
| 497 |
+
use_container_width=True
|
| 498 |
+
)
|
| 499 |
|
| 500 |
+
# 의존성 그래프(이름→코드 간선 매핑)
|
| 501 |
name_to_code = {meta["name"]: meta["code"] for meta in st.session_state.code_map.values()}
|
| 502 |
df_graph = df.copy()
|
| 503 |
if not df_graph.empty:
|
|
|
|
| 505 |
lambda s: ", ".join([name_to_code.get(x.strip(), x.strip()) for x in s.split(",") if x.strip()]) if s else ""
|
| 506 |
)
|
| 507 |
html = draw_dependency_graph(df_graph.rename(columns={"code": "code", "name": "name"}) if not df_graph.empty else df_graph)
|
| 508 |
+
st.components.v1.html(html, height=520, scrolling=True)
|
| 509 |
|
| 510 |
c1, c2 = st.columns(2)
|
| 511 |
c1.download_button("⬇️ CSV 다운로드", export_file(df, "csv"), "final_task_codes.csv", "text/csv")
|
|
|
|
| 519 |
st.session_state[k] = "도메인 설정"
|
| 520 |
else:
|
| 521 |
st.session_state[k] = {}
|
| 522 |
+
st.rerun()
|
| 523 |
|
| 524 |
# 푸터
|
| 525 |
st.markdown("---")
|