|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import os, io, pickle, time, subprocess, sys |
|
|
from datetime import timedelta |
|
|
from pathlib import Path |
|
|
|
|
|
import pandas as pd |
|
|
import numpy as np |
|
|
import streamlit as st |
|
|
import altair as alt |
|
|
|
|
|
from utils_io import read_csv_flexible, save_utf8sig, ensure_dirs, auto_map_columns |
|
|
from preprocess import make_matrix |
|
|
from train_core import train_and_score, save_artifacts |
|
|
|
|
|
|
|
|
alt.data_transformers.disable_max_rows() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
st.set_page_config(page_title="ํธ์์ ์์์์ธก & ๋ฐ์ฃผ ์ถ์ฒ โ Pro Suite (ํจ์น)", layout="wide") |
|
|
|
|
|
|
|
|
try: |
|
|
script_name = Path(__file__).resolve().name |
|
|
except NameError: |
|
|
script_name = "app_streamlit_pro.py" |
|
|
|
|
|
st.sidebar.write("๐งญ ์คํ ํ์ผ:", script_name) |
|
|
if st.sidebar.button("์บ์ ์ด๊ธฐํ ํ ๋ค์ ์คํ"): |
|
|
try: |
|
|
st.cache_data.clear() |
|
|
except Exception: |
|
|
pass |
|
|
try: |
|
|
st.cache_resource.clear() |
|
|
except Exception: |
|
|
pass |
|
|
st.experimental_rerun() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
PROJ = os.getcwd() |
|
|
DATA_DIR = os.path.join(PROJ, "data") |
|
|
ARTI_DIR = os.path.join(PROJ, "artifacts") |
|
|
MODELS_DIR = os.path.join(PROJ, "models") |
|
|
ensure_dirs(DATA_DIR, ARTI_DIR, MODELS_DIR) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@st.cache_data(show_spinner=False) |
|
|
def list_data_files(): |
|
|
try: |
|
|
return [f for f in os.listdir(DATA_DIR) if f.lower().endswith(".csv")] |
|
|
except FileNotFoundError: |
|
|
return [] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def start_cloudflared(port=8501): |
|
|
try: |
|
|
proc = subprocess.Popen( |
|
|
["cloudflared", "tunnel", "--url", f"http://localhost:{port}"], |
|
|
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True |
|
|
) |
|
|
st.session_state["_cfd_proc"] = proc |
|
|
with st.expander("cloudflared logs"): |
|
|
for _ in range(120): |
|
|
line = proc.stdout.readline() |
|
|
if not line: |
|
|
break |
|
|
st.text(line.strip()) |
|
|
if "trycloudflare.com" in line: |
|
|
st.success(line.strip()) |
|
|
break |
|
|
except FileNotFoundError: |
|
|
st.error("cloudflared ๋ฐ์ด๋๋ฆฌ๊ฐ ์์ต๋๋ค. `pip install cloudflared` ๋๋ ๋ฐ์ด๋๋ฆฌ ์ค์น ํ ๋ค์ ์๋ํ์ธ์.") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def start_ngrok(port=8501, token: str | None = None): |
|
|
try: |
|
|
from pyngrok import ngrok, conf |
|
|
except Exception: |
|
|
st.error("pyngrok๊ฐ ์ค์น๋์ด ์์ง ์์ต๋๋ค. `pip install pyngrok` ํ ๋ค์ ์๋ํ์ธ์.") |
|
|
return |
|
|
|
|
|
|
|
|
try: |
|
|
ngrok.kill() |
|
|
time.sleep(1.0) |
|
|
except Exception: |
|
|
pass |
|
|
|
|
|
token = (token or os.environ.get("NGROK_AUTHTOKEN", "")).strip() |
|
|
if token: |
|
|
conf.get_default().auth_token = token |
|
|
else: |
|
|
st.warning("NGROK_AUTHTOKEN์ด ๋น์ด ์์ต๋๋ค. ์ธ์ฆ ์์ด ์ด๋ฉด ์ ํ/์๋ฌ(4018) ๊ฐ๋ฅ.") |
|
|
|
|
|
for attempt in range(2): |
|
|
try: |
|
|
tunnel = ngrok.connect(addr=f"http://localhost:{port}", proto="http") |
|
|
url = tunnel.public_url |
|
|
st.session_state["_ngrok_tunnel"] = tunnel |
|
|
st.success(f"๐ Public URL: {url}") |
|
|
st.caption("๋ฐํ์/ํ๋ก์ธ์ค๋ฅผ ์ข
๋ฃํ๋ฉด ํฐ๋๋ ๋ซํ๋๋ค.") |
|
|
break |
|
|
except Exception as e: |
|
|
if attempt == 0: |
|
|
time.sleep(1.5) |
|
|
else: |
|
|
msg = str(e) |
|
|
if "4018" in msg: |
|
|
st.error("ngrok ์ธ์ฆ ์คํจ(4018). ํ ํฐ์ ๋ค์ ํ์ธํ์ธ์.") |
|
|
elif "already online" in msg or "334" in msg: |
|
|
st.error("๋์ผ ์๋ํฌ์ธํธ๊ฐ ์ด๋ฏธ ์ด๋ ค ์์ต๋๋ค. ์ธ์
์ฌ์์ ๋๋ ๊ธฐ์กด ํฐ๋ ์ข
๋ฃ ํ ์ฌ์๋.") |
|
|
else: |
|
|
st.error(f"ngrok ์ฐ๊ฒฐ ์คํจ: {e}") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
st.title("ํธ์์ ์์์์ธก & ๋ฐ์ฃผ ์ถ์ฒ โ Pro Suite") |
|
|
tabs = st.tabs(["โ ๋ฐ์ดํฐ", "โก ํ์ต/๋ชจ๋ธ", "โข ์์ธกยท๋ฐ์ฃผ", "โฃ ๋ถ์(๊ทธ๋ํ)", "โค ์ง๋จ/๋ก๊ทธ"]) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
with tabs[0]: |
|
|
st.subheader("CSV ์
๋ก๋ ๋๋ ์ ํ") |
|
|
cols_top = st.columns([2,1]) |
|
|
with cols_top[0]: |
|
|
add_source = st.checkbox("ํ์ผ๋ช
(source) ์ด ์ถ๊ฐ", value=True, help="์ฌ๋ฌ CSV๋ฅผ ํฉ์น ๋ ์๋ณธ ํ์ผ๋ช
์ ๋จ๊น๋๋ค.") |
|
|
with cols_top[1]: |
|
|
st.caption("โป ์
๋ก๋/์ ํ ํ ์๋์์ ์ปฌ๋ผ ๋งคํ ์ ์ฅ") |
|
|
|
|
|
cols = st.columns(2) |
|
|
|
|
|
|
|
|
with cols[0]: |
|
|
up_multi = st.file_uploader("CSV ํ์ผ ์
๋ก๋(์ฌ๋ฌ ๊ฐ ๊ฐ๋ฅ)", type=["csv"], accept_multiple_files=True, key="multi_up") |
|
|
if up_multi: |
|
|
dfs = [] |
|
|
for f in up_multi: |
|
|
raw = f.read() |
|
|
df_i = read_csv_flexible(io.BytesIO(raw)) |
|
|
if add_source: |
|
|
df_i["source"] = f.name |
|
|
dfs.append(df_i) |
|
|
|
|
|
save_path = os.path.join(DATA_DIR, f.name) |
|
|
try: |
|
|
with open(save_path, "wb") as fp: |
|
|
fp.write(raw) |
|
|
except Exception as e: |
|
|
st.warning(f"ํ์ผ ์ ์ฅ ๊ฒฝ๊ณ ({f.name}): {e}") |
|
|
try: |
|
|
list_data_files.clear() |
|
|
except Exception: |
|
|
pass |
|
|
df = pd.concat(dfs, axis=0, ignore_index=True, sort=True) |
|
|
st.session_state["df"] = df |
|
|
st.success(f"์
๋ก๋/๊ฒฐํฉ ์๋ฃ: {df.shape} (ํ์ผ {len(dfs)}๊ฐ)") |
|
|
st.dataframe(df.head(20), use_container_width=True) |
|
|
|
|
|
|
|
|
with cols[1]: |
|
|
files = list_data_files() |
|
|
picks = st.multiselect("data ํด๋์์ ์ ํ(์ฌ๋ฌ ๊ฐ)", files) |
|
|
if st.button("์ ํ ํ์ผ ๋ถ๋ฌ์ค๊ธฐ", disabled=(len(picks)==0)): |
|
|
dfs = [] |
|
|
for name in picks: |
|
|
path = os.path.join(DATA_DIR, name) |
|
|
df_i = read_csv_flexible(path) |
|
|
if add_source: |
|
|
df_i["source"] = name |
|
|
dfs.append(df_i) |
|
|
df = pd.concat(dfs, axis=0, ignore_index=True, sort=True) |
|
|
st.session_state["df"] = df |
|
|
st.success(f"๋ถ๋ฌ์ค๊ธฐ/๊ฒฐํฉ ์๋ฃ: {df.shape} (ํ์ผ {len(dfs)}๊ฐ)") |
|
|
st.dataframe(df.head(20), use_container_width=True) |
|
|
|
|
|
|
|
|
if "df" in st.session_state: |
|
|
st.divider() |
|
|
st.caption("์๋ ์ปฌ๋ผ ๋งคํ โ ์ ํ ์์ด ์๋ ์ ์ฉ๋ฉ๋๋ค.") |
|
|
|
|
|
df = st.session_state["df"] |
|
|
|
|
|
|
|
|
auto = auto_map_columns(df) |
|
|
mapping = { |
|
|
"date": auto.get("date"), |
|
|
"target": auto.get("target"), |
|
|
"region": auto.get("region"), |
|
|
"brand": auto.get("brand"), |
|
|
"item": auto.get("item"), |
|
|
} |
|
|
st.session_state["mapping"] = mapping |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if mapping.get("target") == "๊ฐ์๋" and "์ผ์ผํ๋งค๋" in df.columns: |
|
|
mapping["target"] = "์ผ์ผํ๋งค๋" |
|
|
|
|
|
|
|
|
mapping_view = pd.DataFrame( |
|
|
{ |
|
|
"์ญํ ": ["๋ ์ง(date)", "์์/ํ๋งค๋(target)", "์ง์ญ/์ ํฌ(region)", "๋ธ๋๋(์ ํ)", "์ํ/ํ๋ชฉ(์ ํ)"], |
|
|
"์ปฌ๋ผ": [ |
|
|
mapping.get("date"), |
|
|
mapping.get("target"), |
|
|
mapping.get("region"), |
|
|
mapping.get("brand"), |
|
|
mapping.get("item"), |
|
|
], |
|
|
} |
|
|
) |
|
|
|
|
|
st.write("ํ์ฌ ์๋ ๋งคํ ๊ฒฐ๊ณผ:") |
|
|
st.dataframe(mapping_view, use_container_width=True) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
with tabs[1]: |
|
|
st.subheader("๋ชจ๋ธ ํ์ต") |
|
|
|
|
|
use_optuna = st.checkbox("Optuna ํ์ดํผํ๋ผ๋ฏธํฐ ํ๋ ์ฌ์ฉ", value=False) |
|
|
trials = st.slider("Optuna ์๋ ํ์", 5, 60, 15, 5) |
|
|
|
|
|
if "df" not in st.session_state or "mapping" not in st.session_state: |
|
|
st.info("๋จผ์ โ ํญ์์ ๋ฐ์ดํฐ์ ์ปฌ๋ผ ๋งคํ์ ์ง์ ํ์ธ์.") |
|
|
else: |
|
|
v = st.slider("๊ฒ์ฆ ๋น์จ(valid_ratio)", 0.05, 0.4, 0.2, 0.05) |
|
|
|
|
|
if st.button("ํ์ต ์์"): |
|
|
|
|
|
try: |
|
|
df, X, y, feat_names = make_matrix( |
|
|
st.session_state["df"], |
|
|
st.session_state["mapping"], |
|
|
) |
|
|
except Exception as e: |
|
|
st.error(f"ํ์ต์ฉ ๋ฐ์ดํฐ ๊ตฌ์ฑ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค: {e}") |
|
|
else: |
|
|
try: |
|
|
best_model, lb = train_and_score( |
|
|
X, |
|
|
y, |
|
|
valid_ratio=v, |
|
|
use_optuna=use_optuna, |
|
|
optuna_trials=trials, |
|
|
) |
|
|
save_artifacts( |
|
|
[ARTI_DIR, MODELS_DIR], |
|
|
best_model, |
|
|
feat_names, |
|
|
st.session_state["mapping"], |
|
|
lb, |
|
|
) |
|
|
except Exception as e: |
|
|
st.error(f"๋ชจ๋ธ ํ์ต/์ ์ฅ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค: {e}") |
|
|
else: |
|
|
st.session_state["leaderboard"] = lb |
|
|
st.session_state["feat_names"] = feat_names |
|
|
st.success("ํ์ต ์๋ฃ") |
|
|
|
|
|
if "leaderboard" in st.session_state: |
|
|
st.dataframe(st.session_state["leaderboard"], use_container_width=True) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
with tabs[2]: |
|
|
st.subheader("์์ธก(๋ฐ๋ณต AR) & ๋ฐ์ฃผ๋ ์ถ์ฒ") |
|
|
st.caption("ํ์ต๋ ๋ชจ๋ธ๋ก ๋ฏธ๋ ํผ์ฒ๋ฅผ ์์ฑํ๊ณ , ์ฌ๊ณ ๋ฅผ ๊ณ ๋ คํด ์๋์ผ๋ก ๋ฐ์ฃผ ๊ธฐ๊ฐ๊ณผ ์๋์ ๊ณ์ฐํฉ๋๋ค.") |
|
|
|
|
|
if "df" not in st.session_state or "mapping" not in st.session_state: |
|
|
st.info("๋จผ์ โ ํญ์์ ๋ฐ์ดํฐ์ ์ปฌ๋ผ ๋งคํ์ ์ง์ ํ๊ณ โก์์ ํ์ต์ ์๋ฃํ์ธ์.") |
|
|
else: |
|
|
horizon_days = 14 |
|
|
|
|
|
|
|
|
accuracy = st.slider( |
|
|
"์ ํ๋(์์ธก ๋ณด์ ๊ณ์)", |
|
|
min_value=0.5, |
|
|
max_value=2.0, |
|
|
value=1.0, |
|
|
step=0.05, |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
seg_cols = [ |
|
|
c for c in [ |
|
|
st.session_state["mapping"].get("region"), |
|
|
st.session_state["mapping"].get("brand"), |
|
|
st.session_state["mapping"].get("item"), |
|
|
] if c |
|
|
] |
|
|
seg_vals = {} |
|
|
if seg_cols: |
|
|
col_objs = st.columns(len(seg_cols)) |
|
|
for i, ccol in enumerate(seg_cols): |
|
|
opts = ["<์ ์ฒด>"] + sorted( |
|
|
list(map(str, st.session_state["df"][ccol].dropna().astype(str).unique())) |
|
|
) |
|
|
seg_vals[ccol] = col_objs[i].selectbox(f"{ccol} ์ ํ", opts, index=0) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def iterative_forecast(df, mapping, model, feat_names, horizon, seg_vals): |
|
|
df = df.copy() |
|
|
dtc = mapping["date"] |
|
|
tgt = mapping["target"] |
|
|
|
|
|
if dtc not in df.columns or tgt not in df.columns: |
|
|
st.error(f"์์ธก์ ํ์ํ ์ปฌ๋ผ์ด ์์ต๋๋ค. (date='{dtc}', target='{tgt}')") |
|
|
return pd.DataFrame(columns=[dtc, "์์ธก์๋"]) |
|
|
|
|
|
df[dtc] = pd.to_datetime(df[dtc], errors="coerce") |
|
|
df = df.dropna(subset=[dtc]).sort_values(dtc) |
|
|
|
|
|
for k, v in seg_vals.items(): |
|
|
if v and v != "<์ ์ฒด>" and k in df.columns: |
|
|
df = df[df[k].astype(str) == str(v)] |
|
|
|
|
|
if df.empty: |
|
|
st.error("์ ํํ ์ธ๊ทธ๋จผํธ์ ํด๋นํ๋ ๋ฐ์ดํฐ๊ฐ ์์ต๋๋ค.") |
|
|
return pd.DataFrame(columns=[dtc, "์์ธก์๋"]) |
|
|
|
|
|
if len(df) < 30: |
|
|
st.warning("ํด๋น ์ธ๊ทธ๋จผํธ ๋ฐ์ดํฐ๊ฐ ์ ์ด ์์ธก ํ์ง์ด ๋ฎ์ ์ ์์ต๋๋ค.") |
|
|
|
|
|
last_date = df[dtc].max() |
|
|
|
|
|
hist = list( |
|
|
pd.to_numeric(df[tgt], errors="coerce") |
|
|
.fillna(0) |
|
|
.astype(float) |
|
|
.values |
|
|
) |
|
|
|
|
|
def build_row_features(current_date, hist_vals): |
|
|
if pd.isna(current_date): |
|
|
current_date = df[dtc].max() |
|
|
|
|
|
year = current_date.year |
|
|
month = current_date.month |
|
|
day = current_date.day |
|
|
dow = current_date.weekday() |
|
|
is_weekend = 1 if dow >= 5 else 0 |
|
|
|
|
|
try: |
|
|
week = int(pd.Timestamp(current_date).isocalendar().week) |
|
|
except Exception: |
|
|
week = 0 |
|
|
|
|
|
def get_lag(k): |
|
|
if len(hist_vals) >= k: |
|
|
return float(hist_vals[-k]) |
|
|
return float(np.mean(hist_vals[-min(len(hist_vals), 7):])) if hist_vals else 0.0 |
|
|
|
|
|
lag1 = get_lag(1) |
|
|
lag7 = get_lag(7) |
|
|
lag14 = get_lag(14) |
|
|
|
|
|
def rmean(w): |
|
|
arr = np.array(hist_vals[-w:]) if len(hist_vals) >= 1 else np.array([0.0]) |
|
|
if len(arr) < max(2, w // 2): |
|
|
arr = np.array(hist_vals[-max(2, w // 2):]) if len(hist_vals) else np.array([0.0]) |
|
|
return float(np.mean(arr)) |
|
|
|
|
|
def rstd(w): |
|
|
arr = np.array(hist_vals[-w:]) if len(hist_vals) >= 2 else np.array([0.0, 0.0]) |
|
|
return float(np.std(arr)) |
|
|
|
|
|
feats = { |
|
|
"year": year, |
|
|
"month": month, |
|
|
"day": day, |
|
|
"dow": dow, |
|
|
"week": week, |
|
|
"is_weekend": is_weekend, |
|
|
"lag1": lag1, |
|
|
"lag7": lag7, |
|
|
"lag14": lag14, |
|
|
"rmean7": rmean(7), |
|
|
"rmean14": rmean(14), |
|
|
"rstd7": rstd(7), |
|
|
"rstd14": rstd(14), |
|
|
} |
|
|
|
|
|
for fn in feat_names: |
|
|
if fn not in feats: |
|
|
feats[fn] = 0.0 |
|
|
|
|
|
x = [feats.get(fn, 0.0) for fn in feat_names] |
|
|
return np.array(x, dtype=float) |
|
|
|
|
|
preds, dates = [], [] |
|
|
cur = last_date |
|
|
for _ in range(int(horizon)): |
|
|
cur = cur + timedelta(days=1) |
|
|
x = build_row_features(cur, hist) |
|
|
val = float(model.predict([x])[0]) |
|
|
preds.append(val) |
|
|
dates.append(cur) |
|
|
hist.append(val) |
|
|
|
|
|
return pd.DataFrame({dtc: dates, "์์ธก์๋": preds}) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def guess_inventory_onhand(df_seg: pd.DataFrame, mapping): |
|
|
candidates = [ |
|
|
"์ฌ๊ณ ", "์ฌ๊ณ ์", "์ฌ๊ณ ์๋", |
|
|
"ํ์ฌ์ฌ๊ณ ", "onhand", "on_hand", |
|
|
"stock", "inventory", |
|
|
] |
|
|
inv_col = None |
|
|
for col in df_seg.columns: |
|
|
low = col.lower() |
|
|
if any(key in low for key in candidates): |
|
|
inv_col = col |
|
|
break |
|
|
if not inv_col: |
|
|
return None, None |
|
|
|
|
|
series = pd.to_numeric(df_seg[inv_col], errors="coerce").dropna() |
|
|
if series.empty: |
|
|
return None, None |
|
|
|
|
|
return inv_col, float(series.iloc[-1]) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def guess_price_column(df_seg): |
|
|
keys = ["price", "๊ฐ๊ฒฉ", "๋จ๊ฐ", "ํ๋งค๊ฐ", "amount", "๊ธ์ก"] |
|
|
for col in df_seg.columns: |
|
|
low = col.lower() |
|
|
if any(k in low for k in keys): |
|
|
return col |
|
|
return None |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pkl_path = os.path.join(MODELS_DIR, "best_model.pkl") |
|
|
if os.path.exists(pkl_path): |
|
|
try: |
|
|
with open(pkl_path, "rb") as f: |
|
|
payload = pickle.load(f) |
|
|
model = payload["model"] |
|
|
feat_names = payload["feature_names"] |
|
|
mapping = payload["mapping"] |
|
|
except Exception as e: |
|
|
st.error(f"์ ์ฅ๋ ๋ชจ๋ธ ๋ก๋ฉ ์ค ์ค๋ฅ: {e}") |
|
|
else: |
|
|
dtc = mapping["date"] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fc_df = iterative_forecast( |
|
|
st.session_state["df"], |
|
|
mapping, |
|
|
model, |
|
|
feat_names, |
|
|
horizon_days, |
|
|
seg_vals, |
|
|
) |
|
|
if fc_df.empty: |
|
|
st.stop() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
df_seg_price = st.session_state["df"].copy() |
|
|
for k, v in seg_vals.items(): |
|
|
if v and v != "<์ ์ฒด>" and k in df_seg_price.columns: |
|
|
df_seg_price = df_seg_price[df_seg_price[k].astype(str) == str(v)] |
|
|
df_seg_price = df_seg_price.sort_values(dtc) |
|
|
|
|
|
price_col = guess_price_column(df_seg_price) |
|
|
|
|
|
if price_col: |
|
|
price_val = float( |
|
|
pd.to_numeric(df_seg_price[price_col], errors="coerce").dropna().iloc[-1] |
|
|
) |
|
|
st.info(f"CSV '{price_col}' ์ปฌ๋ผ์์ ๊ฐ๊ฒฉ {price_val:,.0f}์ ์๋ ์ธ์.") |
|
|
else: |
|
|
price_val = st.number_input( |
|
|
"๊ฐ๊ฒฉ(์) โ CSV์์ ๊ฐ๊ฒฉ ์ปฌ๋ผ์ ์ฐพ์ง ๋ชปํด ์ง์ ์
๋ ฅ", |
|
|
min_value=0, |
|
|
max_value=100000000, |
|
|
value=0, |
|
|
) |
|
|
|
|
|
|
|
|
total_qty_demand = float(fc_df["์์ธก์๋"].sum()) |
|
|
|
|
|
|
|
|
fc_df["๊ธ์ก์์ธก"] = (fc_df["์์ธก์๋"] * price_val * float(accuracy)).clip(lower=0.0) |
|
|
total_amt_demand = float(fc_df["๊ธ์ก์์ธก"].sum()) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
df_seg = st.session_state["df"].copy() |
|
|
df_seg[dtc] = pd.to_datetime(df_seg[dtc], errors="coerce") |
|
|
for k, v in seg_vals.items(): |
|
|
if v and v != "<์ ์ฒด>" and k in df_seg.columns: |
|
|
df_seg = df_seg[df_seg[k].astype(str) == str(v)] |
|
|
df_seg = df_seg.sort_values(dtc) |
|
|
|
|
|
inv_col, onhand_auto = guess_inventory_onhand(df_seg, mapping) |
|
|
if onhand_auto is None: |
|
|
onhand = st.number_input( |
|
|
"ํ์ฌ ์ฌ๊ณ (์ง์ ์
๋ ฅ)", |
|
|
min_value=0, |
|
|
max_value=100000, |
|
|
value=0, |
|
|
) |
|
|
else: |
|
|
onhand = onhand_auto |
|
|
st.info(f"์ฌ๊ณ '{inv_col}' ์๋ ์ธ์ โ {onhand:,.0f}๊ฐ") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
avg_daily_qty = total_qty_demand / horizon_days if horizon_days > 0 else 0.0 |
|
|
days_to_out = (onhand / avg_daily_qty) if avg_daily_qty > 0 else float("inf") |
|
|
rec_qty = max(0.0, total_qty_demand - onhand) |
|
|
|
|
|
c1, c2, c3 = st.columns(3) |
|
|
c1.metric("์์ธก ๊ธฐ๊ฐ(์ผ)", f"{horizon_days}") |
|
|
c2.metric("์ฌ๊ณ ์์ง ์์์ผ์", "โ" if np.isinf(days_to_out) else f"{days_to_out:,.1f}") |
|
|
c3.metric("2์ฃผ ์ด ์์ ๋งค์ถ", f"{total_amt_demand:,.0f}์") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
st.dataframe(fc_df.set_index(dtc), use_container_width=True) |
|
|
st.caption("โป ์์ธก์๋ ร ๊ฐ๊ฒฉ ร ์ ํ๋ ๋ณด์ = ๊ธ์ก์์ธก") |
|
|
|
|
|
else: |
|
|
st.warning("best_model.pkl ์ด ์์ต๋๋ค. โก ํญ์์ ํ์ต์ ๋จผ์ ์ํํ์ธ์.") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
with tabs[3]: |
|
|
st.subheader("๋ถ์(๊ทธ๋ํ) โ ํ ๋ฌ ๋จ์ ์๊ด ๋ถ์") |
|
|
|
|
|
if "df" not in st.session_state or "mapping" not in st.session_state or not st.session_state["mapping"].get("date"): |
|
|
st.info("๋จผ์ โ ํญ์์ ๋ฐ์ดํฐ์ ์ปฌ๋ผ ๋งคํ(ํนํ '๋ ์ง'์ 'ํ๊น')์ ์ง์ ํ์ธ์.") |
|
|
else: |
|
|
mapping = st.session_state["mapping"] |
|
|
date_col = mapping["date"] |
|
|
target_col = mapping.get("target") |
|
|
|
|
|
def guess(colnames, cands): |
|
|
low = [str(c).lower() for c in colnames] |
|
|
for key in cands: |
|
|
key_low = str(key).lower() |
|
|
for i, l in enumerate(low): |
|
|
if key_low in l: |
|
|
return colnames[i] |
|
|
return None |
|
|
|
|
|
|
|
|
def build_year_month_options(df, date_col): |
|
|
df = df.copy() |
|
|
df[date_col] = pd.to_datetime(df[date_col], errors="coerce") |
|
|
df = df.dropna(subset=[date_col]) |
|
|
if df.empty: |
|
|
return df, [] |
|
|
df["year_month"] = df[date_col].dt.to_period("M") |
|
|
ym_unique = sorted(df["year_month"].unique()) |
|
|
ym_labels = [str(p) for p in ym_unique] |
|
|
return df, list(zip(ym_labels, ym_unique)) |
|
|
|
|
|
tab_u, tab_g, tab_all = st.tabs([ |
|
|
"โ ์ฐ์ฐ: ํ ๋ฌ ๊ฐ์๋ vs ํ๋งค๋", |
|
|
"๐ ๊ตฐ๊ณ ๊ตฌ๋ง: ํ ๋ฌ ๊ธฐ์จ vs ํ๋งค๋", |
|
|
"๐ ์ ์ฒด: ์ฐ์ฐยท๊ตฐ๊ณ ๊ตฌ๋ง ์ ์ธ ์ผ๋ณ ํ๋งค๋(์ ํ)" |
|
|
]) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
with tab_u: |
|
|
st.caption("์ฐ์ฐ ํ๋งค๋๊ณผ ๊ฐ์๋์ ๊ด๊ณ๋ฅผ 'ํ ๋ฌ' ๋จ์๋ก ๋ด
๋๋ค.") |
|
|
|
|
|
up_u = st.file_uploader("์ฐ์ฐ/๋ ์จ ๋ฐ์ดํฐ CSV (์ ํ)", type=["csv"], key="umbrella_month_up") |
|
|
if up_u is not None: |
|
|
df_u_raw = read_csv_flexible(io.BytesIO(up_u.read())) |
|
|
else: |
|
|
df_u_raw = st.session_state["df"].copy() |
|
|
|
|
|
if date_col not in df_u_raw.columns: |
|
|
st.warning(f"๋ ์ง ์ปฌ๋ผ '{date_col}' ์(๋ฅผ) ๋ฐ์ดํฐ์์ ์ฐพ์ง ๋ชปํ์ต๋๋ค.") |
|
|
else: |
|
|
|
|
|
item_col = mapping.get("item") |
|
|
if item_col and item_col in df_u_raw.columns: |
|
|
mask = df_u_raw[item_col].astype(str).str.contains("์ฐ์ฐ|umbrella", case=False, na=False) |
|
|
if mask.any(): |
|
|
df_u_raw = df_u_raw[mask] |
|
|
|
|
|
cols_all = list(df_u_raw.columns) |
|
|
|
|
|
|
|
|
sales_col = target_col if target_col in cols_all else guess( |
|
|
cols_all, |
|
|
["umbrella", "์ฐ์ฐ", "์ผ์ผํ๋งค๋", "ํ๋งค๋", "sales", "qty", "quantity", "target"], |
|
|
) |
|
|
|
|
|
|
|
|
rain_col = guess( |
|
|
cols_all, |
|
|
["rain", "precip", "precipitation", "๊ฐ์", "๊ฐ์๋", "์ผ๊ฐ์๋", "๊ฐ์ฐ", "๊ฐ์ฐ๋"], |
|
|
) |
|
|
|
|
|
if not sales_col or not rain_col: |
|
|
st.warning( |
|
|
"์ฐ์ฐ ํ๋งค๋ ๋๋ ๊ฐ์๋ ์ปฌ๋ผ์ ์๋์ผ๋ก ์ฐพ์ง ๋ชปํ์ต๋๋ค.\n" |
|
|
"ํ๋งค๋: '์ฐ์ฐ/umbrella/ํ๋งค๋/sales', ๊ฐ์๋: '๊ฐ์๋/rain' ๋ฑ์ ์ด๋ฆ์ ์ฌ์ฉํด ์ฃผ์ธ์." |
|
|
) |
|
|
else: |
|
|
|
|
|
df_u_raw[sales_col] = pd.to_numeric(df_u_raw[sales_col], errors="coerce") |
|
|
df_u_raw[rain_col] = pd.to_numeric(df_u_raw[rain_col], errors="coerce") |
|
|
|
|
|
df_u_raw, ym_options = build_year_month_options(df_u_raw, date_col) |
|
|
|
|
|
if not ym_options: |
|
|
st.info("์ ํจํ ๋ ์ง ๋ฐ์ดํฐ๊ฐ ์์ต๋๋ค.") |
|
|
else: |
|
|
|
|
|
labels = [lab for lab, _ in ym_options] |
|
|
default_idx = len(labels) - 1 |
|
|
sel_label = st.selectbox("๋ถ์ํ ์ฐ์(YYYY-MM)", labels, index=default_idx, key="ym_umbrella") |
|
|
sel_period = dict(ym_options)[sel_label] |
|
|
|
|
|
|
|
|
df_month = df_u_raw[df_u_raw["year_month"] == sel_period].copy() |
|
|
if df_month.empty: |
|
|
st.info(f"{sel_label} ์ ํด๋นํ๋ ๋ฐ์ดํฐ๊ฐ ์์ต๋๋ค.") |
|
|
else: |
|
|
|
|
|
df_month["date_only"] = df_month[date_col].dt.date |
|
|
daily = ( |
|
|
df_month.groupby("date_only", as_index=False) |
|
|
.agg({sales_col: "sum", rain_col: "mean"}) |
|
|
.dropna(subset=[sales_col, rain_col]) |
|
|
) |
|
|
daily = daily.rename( |
|
|
columns={"date_only": "date", sales_col: "sales", rain_col: "rain"} |
|
|
) |
|
|
|
|
|
if daily.empty: |
|
|
st.info("ํด๋น ์ฐ์์์ ์ผ๋ณ๋ก ์ง๊ณํ ์ ์๋ ๋ฐ์ดํฐ๊ฐ ์์ต๋๋ค.") |
|
|
else: |
|
|
st.markdown(f"**{sel_label} ํ ๋ฌ ๊ธฐ์ค ยท ๊ฐ์๋์ ๋ฐ๋ฅธ ์ฐ์ฐ ํ๋งค๋**") |
|
|
|
|
|
base = alt.Chart(daily).encode( |
|
|
x=alt.X("rain:Q", title="์ผ ๊ฐ์๋"), |
|
|
y=alt.Y("sales:Q", title="์ผ ์ฐ์ฐ ํ๋งค๋"), |
|
|
) |
|
|
|
|
|
|
|
|
points = base.mark_circle(size=70, color="#d62728").encode( |
|
|
tooltip=[ |
|
|
alt.Tooltip("date:T", title="๋ ์ง"), |
|
|
alt.Tooltip("rain:Q", title="๊ฐ์๋"), |
|
|
alt.Tooltip("sales:Q", title="์ฐ์ฐ ํ๋งค๋"), |
|
|
] |
|
|
) |
|
|
reg_line = base.transform_regression("rain", "sales").mark_line(color="#b22222") |
|
|
|
|
|
st.altair_chart((points + reg_line).interactive(), use_container_width=True) |
|
|
|
|
|
|
|
|
st.markdown("**์ผ๋ณ ์ฐ์ฐ ํ๋งค๋ ์ถ์ธ(์ ํ ๊ทธ๋ํ)**") |
|
|
line_umbrella = ( |
|
|
alt.Chart(daily) |
|
|
.mark_line() |
|
|
.encode( |
|
|
x=alt.X("date:T", title="๋ ์ง"), |
|
|
y=alt.Y("sales:Q", title="์ผ ์ฐ์ฐ ํ๋งค๋"), |
|
|
tooltip=[ |
|
|
alt.Tooltip("date:T", title="๋ ์ง"), |
|
|
alt.Tooltip("sales:Q", title="์ฐ์ฐ ํ๋งค๋"), |
|
|
alt.Tooltip("rain:Q", title="๊ฐ์๋"), |
|
|
], |
|
|
) |
|
|
) |
|
|
st.altair_chart(line_umbrella.interactive(), use_container_width=True) |
|
|
|
|
|
|
|
|
st.dataframe(daily, use_container_width=True) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
with tab_g: |
|
|
st.caption("๊ตฐ๊ณ ๊ตฌ๋ง ํ๋งค๋๊ณผ ๊ธฐ์จ(์ถ์)์ ๊ด๊ณ๋ฅผ 'ํ ๋ฌ' ๋จ์๋ก ๋ด
๋๋ค.") |
|
|
|
|
|
up_g = st.file_uploader("๊ตฐ๊ณ ๊ตฌ๋ง/๋ ์จ ๋ฐ์ดํฐ CSV (์ ํ)", type=["csv"], key="goguma_month_up") |
|
|
if up_g is not None: |
|
|
df_g_raw = read_csv_flexible(io.BytesIO(up_g.read())) |
|
|
else: |
|
|
df_g_raw = st.session_state["df"].copy() |
|
|
|
|
|
if date_col not in df_g_raw.columns: |
|
|
st.warning(f"๋ ์ง ์ปฌ๋ผ '{date_col}' ์(๋ฅผ) ๋ฐ์ดํฐ์์ ์ฐพ์ง ๋ชปํ์ต๋๋ค.") |
|
|
else: |
|
|
|
|
|
item_col_g = mapping.get("item") |
|
|
if item_col_g and item_col_g in df_g_raw.columns: |
|
|
mask_g = df_g_raw[item_col_g].astype(str).str.contains( |
|
|
"๊ณ ๊ตฌ๋ง|๊ตฐ๊ณ ๊ตฌ๋ง|sweet|goguma", case=False, na=False |
|
|
) |
|
|
if mask_g.any(): |
|
|
df_g_raw = df_g_raw[mask_g] |
|
|
|
|
|
cols_all_g = list(df_g_raw.columns) |
|
|
|
|
|
goguma_col = target_col if target_col in cols_all_g else guess( |
|
|
cols_all_g, |
|
|
["๊ณ ๊ตฌ๋ง", "๊ตฐ๊ณ ๊ตฌ๋ง", "sweetpotato", "goguma", "ํ๋งค๋", "sales", "qty", "quantity", "target"], |
|
|
) |
|
|
temp_col = guess( |
|
|
cols_all_g, |
|
|
["์จ๋", "tmin", "temp_min", "min_temp", "์ต์ ", "์ต์ ๊ธฐ์จ", "์ผ์ต์ ๊ธฐ์จ", "temperature", "temp"], |
|
|
) |
|
|
|
|
|
if not goguma_col or not temp_col: |
|
|
st.warning( |
|
|
"๊ตฐ๊ณ ๊ตฌ๋ง ํ๋งค๋ ๋๋ ๊ธฐ์จ ์ปฌ๋ผ์ ์๋์ผ๋ก ์ฐพ์ง ๋ชปํ์ต๋๋ค.\n" |
|
|
"ํ๋งค๋: '๊ตฐ๊ณ ๊ตฌ๋ง/๊ณ ๊ตฌ๋ง/sales/target', ๊ธฐ์จ: 'tmin/์ต์ ๊ธฐ์จ/temperature' ๋ฑ์ ์ด๋ฆ์ ์ฌ์ฉํด ์ฃผ์ธ์." |
|
|
) |
|
|
else: |
|
|
df_g_raw[goguma_col] = pd.to_numeric(df_g_raw[goguma_col], errors="coerce") |
|
|
df_g_raw[temp_col] = pd.to_numeric(df_g_raw[temp_col], errors="coerce") |
|
|
|
|
|
df_g_raw, ym_options_g = build_year_month_options(df_g_raw, date_col) |
|
|
|
|
|
if not ym_options_g: |
|
|
st.info("์ ํจํ ๋ ์ง ๋ฐ์ดํฐ๊ฐ ์์ต๋๋ค.") |
|
|
else: |
|
|
labels_g = [lab for lab, _ in ym_options_g] |
|
|
default_idx_g = len(labels_g) - 1 |
|
|
sel_label_g = st.selectbox("๋ถ์ํ ์ฐ์(YYYY-MM)", labels_g, index=default_idx_g, key="ym_goguma") |
|
|
sel_period_g = dict(ym_options_g)[sel_label_g] |
|
|
|
|
|
df_month_g = df_g_raw[df_g_raw["year_month"] == sel_period_g].copy() |
|
|
if df_month_g.empty: |
|
|
st.info(f"{sel_label_g} ์ ํด๋นํ๋ ๋ฐ์ดํฐ๊ฐ ์์ต๋๋ค.") |
|
|
else: |
|
|
df_month_g["date_only"] = df_month_g[date_col].dt.date |
|
|
daily_g = ( |
|
|
df_month_g.groupby("date_only", as_index=False) |
|
|
.agg({goguma_col: "sum", temp_col: "mean"}) |
|
|
.dropna(subset=[goguma_col, temp_col]) |
|
|
) |
|
|
daily_g = daily_g.rename( |
|
|
columns={"date_only": "date", goguma_col: "sales", temp_col: "temp"} |
|
|
) |
|
|
|
|
|
if daily_g.empty: |
|
|
st.info("ํด๋น ์ฐ์์์ ์ผ๋ณ๋ก ์ง๊ณํ ์ ์๋ ๋ฐ์ดํฐ๊ฐ ์์ต๋๋ค.") |
|
|
else: |
|
|
st.markdown(f"**{sel_label_g} ํ ๋ฌ ๊ธฐ์ค ยท ๊ธฐ์จ์ ๋ฐ๋ฅธ ๊ตฐ๊ณ ๊ตฌ๋ง ํ๋งค๋**") |
|
|
|
|
|
base_g = alt.Chart(daily_g).encode( |
|
|
x=alt.X("temp:Q", title="์ผ ํ๊ท ๊ธฐ์จ"), |
|
|
y=alt.Y("sales:Q", title="์ผ ๊ตฐ๊ณ ๊ตฌ๋ง ํ๋งค๋"), |
|
|
) |
|
|
|
|
|
points_g = base_g.mark_circle(size=70, color="#ff7f0e").encode( |
|
|
tooltip=[ |
|
|
alt.Tooltip("date:T", title="๋ ์ง"), |
|
|
alt.Tooltip("temp:Q", title="๊ธฐ์จ"), |
|
|
alt.Tooltip("sales:Q", title="๊ตฐ๊ณ ๊ตฌ๋ง ํ๋งค๋"), |
|
|
] |
|
|
) |
|
|
reg_g = base_g.transform_regression("temp", "sales").mark_line(color="#d35400") |
|
|
|
|
|
st.altair_chart((points_g + reg_g).interactive(), use_container_width=True) |
|
|
|
|
|
|
|
|
st.markdown("**์ผ๋ณ ๊ตฐ๊ณ ๊ตฌ๋ง ํ๋งค๋ ์ถ์ธ(์ ํ ๊ทธ๋ํ)**") |
|
|
line_goguma = ( |
|
|
alt.Chart(daily_g) |
|
|
.mark_line() |
|
|
.encode( |
|
|
x=alt.X("date:T", title="๋ ์ง"), |
|
|
y=alt.Y("sales:Q", title="์ผ ๊ตฐ๊ณ ๊ตฌ๋ง ํ๋งค๋"), |
|
|
tooltip=[ |
|
|
alt.Tooltip("date:T", title="๋ ์ง"), |
|
|
alt.Tooltip("temp:Q", title="๊ธฐ์จ"), |
|
|
alt.Tooltip("sales:Q", title="๊ตฐ๊ณ ๊ตฌ๋ง ํ๋งค๋"), |
|
|
], |
|
|
) |
|
|
) |
|
|
st.altair_chart(line_goguma.interactive(), use_container_width=True) |
|
|
|
|
|
st.dataframe(daily_g, use_container_width=True) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
with tab_all: |
|
|
st.caption("์ฐ์ฐยท๊ตฐ๊ณ ๊ตฌ๋ง๋ฅผ ์ ์ธํ ๋ชจ๋ ์ํ์ ์ผ๋ณ ํ๋งค๋ ์ถ์ธ๋ฅผ ํ ๋ฒ์ ๋ด
๋๋ค.") |
|
|
|
|
|
df_all = st.session_state["df"].copy() |
|
|
|
|
|
if date_col not in df_all.columns or not target_col or target_col not in df_all.columns: |
|
|
st.warning(f"๋ ์ง('{date_col}') ๋๋ ํ๊น('{target_col}') ์ปฌ๋ผ์ ์ฐพ์ ์ ์์ต๋๋ค.") |
|
|
else: |
|
|
|
|
|
item_col_all = mapping.get("item") |
|
|
if item_col_all and item_col_all in df_all.columns: |
|
|
ex_mask = df_all[item_col_all].astype(str).str.contains( |
|
|
"์ฐ์ฐ|umbrella|๊ณ ๊ตฌ๋ง|๊ตฐ๊ณ ๊ตฌ๋ง|sweet|goguma", case=False, na=False |
|
|
) |
|
|
df_all = df_all[~ex_mask] |
|
|
|
|
|
df_all[target_col] = pd.to_numeric(df_all[target_col], errors="coerce") |
|
|
|
|
|
df_all, ym_options_all = build_year_month_options(df_all, date_col) |
|
|
|
|
|
if not ym_options_all: |
|
|
st.info("์ ํจํ ๋ ์ง ๋ฐ์ดํฐ๊ฐ ์์ต๋๋ค.") |
|
|
else: |
|
|
labels_all = [lab for lab, _ in ym_options_all] |
|
|
default_idx_all = len(labels_all) - 1 |
|
|
sel_label_all = st.selectbox( |
|
|
"๋ถ์ํ ์ฐ์(YYYY-MM)", |
|
|
labels_all, |
|
|
index=default_idx_all, |
|
|
key="ym_all", |
|
|
) |
|
|
sel_period_all = dict(ym_options_all)[sel_label_all] |
|
|
|
|
|
df_month_all = df_all[df_all["year_month"] == sel_period_all].copy() |
|
|
if df_month_all.empty: |
|
|
st.info(f"{sel_label_all} ์ ํด๋นํ๋ ๋ฐ์ดํฐ๊ฐ ์์ต๋๋ค.") |
|
|
else: |
|
|
df_month_all["date_only"] = df_month_all[date_col].dt.date |
|
|
daily_all = ( |
|
|
df_month_all.groupby("date_only", as_index=False) |
|
|
.agg({target_col: "sum"}) |
|
|
.dropna(subset=[target_col]) |
|
|
) |
|
|
daily_all = daily_all.rename( |
|
|
columns={"date_only": "date", target_col: "sales"} |
|
|
) |
|
|
|
|
|
if daily_all.empty: |
|
|
st.info("ํด๋น ์ฐ์์์ ์ผ๋ณ๋ก ์ง๊ณํ ์ ์๋ ๋ฐ์ดํฐ๊ฐ ์์ต๋๋ค.") |
|
|
else: |
|
|
st.markdown(f"**{sel_label_all} ํ ๋ฌ ๊ธฐ์ค ยท ์ฐ์ฐยท๊ตฐ๊ณ ๊ตฌ๋ง ์ ์ธ ์ ์ฒด ์ํ ์ผ๋ณ ํ๋งค๋(์ ํ)**") |
|
|
|
|
|
line_all = ( |
|
|
alt.Chart(daily_all) |
|
|
.mark_line() |
|
|
.encode( |
|
|
x=alt.X("date:T", title="๋ ์ง"), |
|
|
y=alt.Y("sales:Q", title="์ผ ํ๋งค๋(์ ์ฒด ์ํ ํฉ๊ณ)"), |
|
|
tooltip=[ |
|
|
alt.Tooltip("date:T", title="๋ ์ง"), |
|
|
alt.Tooltip("sales:Q", title="์ผ ํ๋งค๋ ํฉ๊ณ"), |
|
|
], |
|
|
) |
|
|
) |
|
|
st.altair_chart(line_all.interactive(), use_container_width=True) |
|
|
st.dataframe(daily_all, use_container_width=True) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
with tabs[4]: |
|
|
st.subheader("๊ฒฝ๋ก/ํ์ผ ์ํ") |
|
|
|
|
|
cols = st.columns(2) |
|
|
with cols[0]: |
|
|
st.write("**data**", DATA_DIR) |
|
|
st.write(os.listdir(DATA_DIR) if os.path.exists(DATA_DIR) else []) |
|
|
st.write("**artifacts**", ARTI_DIR) |
|
|
st.write(os.listdir(ARTI_DIR) if os.path.exists(ARTI_DIR) else []) |
|
|
with cols[1]: |
|
|
st.write("**models**", MODELS_DIR) |
|
|
st.write(os.listdir(MODELS_DIR) if os.path.exists(MODELS_DIR) else []) |
|
|
|
|
|
st.caption("ํ์ ์ ํผ๋ธ๋ฆญ URL์ ์ด์ด ์ธ๋ถ์์ ์ ์ํ ์ ์์ต๋๋ค.") |
|
|
mode = st.radio("ํผ๋ธ๋ฆญ URL ํฐ๋๋ฌ", ["ngrok", "cloudflared"], horizontal=True, index=0) |
|
|
|
|
|
ngk = None |
|
|
if mode == "ngrok": |
|
|
ngk = st.text_input( |
|
|
"NGROK_AUTHTOKEN", |
|
|
value=os.environ.get("NGROK_AUTHTOKEN", ""), |
|
|
type="password", |
|
|
help="ํ๊ฒฝ๋ณ์์ ๋ฃ์ด๋๋ฉด ๋ค์๋ถํฐ ์๋ ์ธ์ํฉ๋๋ค.", |
|
|
) |
|
|
|
|
|
c_open, c_close = st.columns(2) |
|
|
if c_open.button("ํผ๋ธ๋ฆญ URL ์ด๊ธฐ", use_container_width=True): |
|
|
if mode == "ngrok": |
|
|
if ngk: |
|
|
os.environ["NGROK_AUTHTOKEN"] = ngk |
|
|
start_ngrok() |
|
|
else: |
|
|
start_cloudflared() |
|
|
|
|
|
if c_close.button("ํผ๋ธ๋ฆญ URL ๋ซ๊ธฐ", use_container_width=True): |
|
|
if mode == "ngrok": |
|
|
try: |
|
|
from pyngrok import ngrok |
|
|
ngrok.kill() |
|
|
st.info("ngrok ํฐ๋์ ์ข
๋ฃํ์ต๋๋ค.") |
|
|
except Exception as e: |
|
|
st.warning(f"ngrok ์ข
๋ฃ ์ค ๊ฒฝ๊ณ : {e}") |
|
|
else: |
|
|
proc = st.session_state.get("_cfd_proc") |
|
|
if proc: |
|
|
proc.terminate() |
|
|
st.info("cloudflared ํฐ๋์ ์ข
๋ฃํ์ต๋๋ค.") |
|
|
else: |
|
|
st.info("cloudflared ํ์ฑ ํ๋ก์ธ์ค๊ฐ ์์ต๋๋ค.") |
|
|
|