Spaces:
Running
Running
MiniMing
refactor: μ λ©΄ μ½λ κ°μ β λ²κ·Έ μμ , λΆμ μ νλ, ꡬ쑰ν, ν
μ€νΈ μΆκ°
0ade07c | """λμ보λ κ³΅ν΅ ν¬νΌ ν¨μ λ° μ°¨νΈ μ€νμΌ.""" | |
| import pandas as pd | |
| import plotly.graph_objects as go | |
| from dash import html | |
| from analysis import normalize_location # noqa: F401 β re-export for callbacks | |
| from dashboard.context import BLUE_LIGHT, WHITE, GRAY | |
| def apply_filter( | |
| df: pd.DataFrame, | |
| categories: list, | |
| sources: list, | |
| industries: list | None = None, | |
| emp_types: list | None = None, | |
| ) -> pd.DataFrame: | |
| if df.empty: | |
| return df | |
| mask = pd.Series(True, index=df.index) | |
| if categories and "job_category" in df.columns: | |
| mask &= df["job_category"].isin(categories) | |
| if sources and "source_site" in df.columns: | |
| mask &= df["source_site"].isin(sources) | |
| if industries and "industry" in df.columns: | |
| mask &= df["industry"].isin(industries) | |
| if emp_types and "employment_type" in df.columns: | |
| mask &= df["employment_type"].isin(emp_types) | |
| return df[mask] | |
| def chart_base(fig: go.Figure, height: int = 380) -> go.Figure: | |
| fig.update_layout( | |
| height=height, plot_bgcolor=WHITE, paper_bgcolor=WHITE, | |
| margin=dict(l=0, r=0, t=30, b=0), | |
| legend=dict(orientation="h", yanchor="bottom", y=1.02), | |
| font=dict(family="Pretendard, Apple SD Gothic Neo, sans-serif"), | |
| ) | |
| fig.update_xaxes(showgrid=False) | |
| fig.update_yaxes(gridcolor="#f0f0f0") | |
| return fig | |
| def empty_fig(msg: str = "λ°μ΄ν° μμ") -> go.Figure: | |
| fig = go.Figure() | |
| fig.add_annotation(text=msg, x=0.5, y=0.5, xref="paper", yref="paper", | |
| showarrow=False, font=dict(size=14, color=GRAY)) | |
| fig.update_layout(height=300, plot_bgcolor=WHITE, paper_bgcolor=WHITE, | |
| xaxis=dict(visible=False), yaxis=dict(visible=False)) | |
| return fig | |
| def section_wrap(title: str, *children): | |
| return html.Div( | |
| [html.P(title, className="section-title"), *children], | |
| className="chart-section", | |
| ) | |
| def kpi_card(label: str, value: str, delta: str | None = None): | |
| return html.Div([ | |
| html.P(label, className="kpi-label"), | |
| html.P(value, className="kpi-value"), | |
| html.P(delta, className="kpi-delta") if delta else None, | |
| ], className="kpi-card") | |
| def source_badge(source: str): | |
| cfg = { | |
| "wanted": ("#e8f4fd", "#1352f1", "μν°λ"), | |
| "saramin": ("#f0f8f0", "#1a7340", "μ¬λμΈ"), | |
| "jobkorea": ("#fff3e0", "#e65100", "μ‘μ½λ¦¬μ"), | |
| } | |
| bg, color, label = cfg.get(source, ("#f0f0f0", "#666", source)) | |
| return html.Span(label, style={ | |
| "background": bg, "color": color, | |
| "fontSize": "0.72rem", "fontWeight": 600, | |
| "padding": "2px 8px", "borderRadius": "20px", | |
| }) | |
| def exp_label(mn, mx) -> str: | |
| if mn is None or (isinstance(mn, float) and pd.isna(mn)): | |
| return "κ²½λ ₯무κ΄" | |
| mn = int(mn) | |
| mx = int(mx) if mx is not None and not (isinstance(mx, float) and pd.isna(mx)) else None | |
| if mn == 0 and mx == 0: | |
| return "μ μ " | |
| return f"{mn}~{mx}λ " if mx else f"{mn}λ μ΄μ" | |
| def salary_label(mn, mx) -> str: | |
| if mn is None or (isinstance(mn, float) and pd.isna(mn)): | |
| return "μ°λ΄ νμ" | |
| mx_valid = mx is not None and not (isinstance(mx, float) and pd.isna(mx)) | |
| return f"{int(mn):,}~{int(mx):,}λ§μ" if mx_valid else f"{int(mn):,}λ§μ~" | |