soojeongcrystal commited on
Commit
752d2e5
·
verified ·
1 Parent(s): 832415f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +107 -73
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("5문항 체크로 **사이클 추천**을 받고, **시간기호(T/FE)** 는 별도로 선택하세요.")
 
 
 
 
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
- # code_map/seq_registry 초기 동기화
330
- seq_registry = st.session_state.seq_registry
331
- code_map = st.session_state.code_map
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
- qcols = st.columns(5)
372
- with qcols[0]:
373
- qP = st.checkbox("P: 목적/시점/대상", key=safe_key(d, t, "qP"), value=False)
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
- with c3:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
402
  if recs:
403
- st.markdown("**추천:** " + " ".join([f"`{r}`" for r in recs]))
404
- bcols = st.columns(len(recs))
405
  for i, r in enumerate(recs):
406
- if bcols[i].button(f"추천 적용({r})", key=safe_key(d, t, f"apply_{r}")):
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
- # 동일 pair & 기존 seq 유지, 아니면 번호
418
- if prev and prev.get("domain_code") == domain_code and prev.get("cycle") == cyc and prev.get("seq", 0) > 0:
419
- seq = prev["seq"]
 
420
  else:
421
- seq = st.session_state.seq_registry.get(pair, 0) + 1
422
- st.session_state.seq_registry[pair] = seq
423
 
424
  code = f"{domain_code}-{cyc}{seq:02d}-{tm}"
425
  st.markdown(
426
- f"<div class='task-card' style='background-color:#F9FAFB;'>"
427
  f"<small>➡️ 코드</small> <b>{code}</b></div>",
428
  unsafe_allow_html=True
429
  )
430
 
431
- st.session_state.code_map[task_key] = {
 
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(df[["domain", "domain_code", "name", "cycle", "time", "code", "depends_on", "output"]],
464
- use_container_width=True)
 
 
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=600, scrolling=True)
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
- goto("도메인 설정")
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("---")