미래기술교육연구원 강의자료

news
>
2025. 8. 25. 11:40
미래기술교육연구원 강의자료

2025. 8. 25. 11:40news

4. CURSOR : Cursor - The AI Code Editor https://cursor.com/home?from=agents

 

(참고) AI리터러시 입문.pdf
13.49MB

 

2교시 실습자료 EVride.docx
0.03MB
3교시 민원응대 매뉴얼.pdf
3.76MB
3교시 실습자료 타이타닉.csv
0.06MB
5교시 엑셀과 API 실습.xlsx
0.01MB
6교시 데이터 시각화 실습.xlsx
0.35MB

 

 

vba를 활용해 엑셀에 chatgpt API를 호출하는 함수를 작성하는 방법을 알려줘.  gpt-5-mini를 사용할거야. 한글로 입력하니까 유니코드 고려해서 코딩해줘. 

API키 이걸 활용해.

 

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

/************************************************************
 * Google Sheets A열의 한국어 문장을 교정/정리하여 B열에 저장
 * - OpenAI Chat Completions API 사용
 * - 맞춤법/띄어쓰기 교정, 특수문자·이모티콘 제거,
 *   반복표현(ㅋㅋㅋ/ㅎㅎㅎ/ㅠㅠㅠ 등)은 1회로 축약
 * - 오류 처리 + 지수 백오프 재시도
 ************************************************************/

const OPENAI_MODEL = 'gpt-4o-mini';
const MAX_RETRIES = 5;          // 최대 재시도 횟수
const INITIAL_BACKOFF_MS = 1000; // 1초(숫자에 밑줄 X)

// 🔐 [권장] 스크립트 속성에 OPENAI_API_KEY 저장 후 사용
function getApiKeyFromProps_() {
  return PropertiesService.getScriptProperties().getProperty('OPENAI_API_KEY');
}

// 🔐 [비권장] 하드코딩 위치(테스트용)
// 예: const HARDCODED_API_KEY = 'sk-xxxx';  ← 코드 내 보관은 위험
const HARDCODED_API_KEY = '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'; // 예: 'sk-xxxxx'  ← 코드에 보관은 위험합니다.


/** 실행 메뉴 추가 */
function onOpen() {
  SpreadsheetApp.getUi()
    .createMenu('문장 정리')
    .addItem('A열→B열 실행', 'cleanColumnAtoB')
    .addToUi();
}

/** 메인: 현재 활성 시트의 A열을 처리해 B열에 기록 */
function cleanColumnAtoB() {
  const sheet = SpreadsheetApp.getActiveSheet();
  const lastRow = sheet.getLastRow();
  if (lastRow < 1) return;

  const values = sheet.getRange(1, 1, lastRow, 1).getValues(); // A열
  const out = [];

  for (let i = 0; i < values.length; i++) {
    const raw = (values[i][0] || '').toString().trim();
    if (!raw) {
      out.push(['']);
      continue;
    }

    try {
      const cleanedByAi = callOpenAIKoreanCleanup_(raw);
      const finalText = postProcessKorean_(cleanedByAi);
      out.push([finalText]);
    } catch (err) {
      out.push([`[ERROR] ${String(err).slice(0, 300)}`]);
    }
  }

  sheet.getRange(1, 2, out.length, 1).setValues(out); // B열에 기록
}

/** OpenAI 호출: 한국어 문장 교정/정리 */
function callOpenAIKoreanCleanup_(inputText) {
  const apiKey = getApiKeyFromProps_() || HARDCODED_API_KEY;
  if (!apiKey) {
    throw new Error('OpenAI API 키가 없습니다. 스크립트 속성 OPENAI_API_KEY를 설정하거나 HARDCODED_API_KEY에 입력하세요.');
  }

  const systemPrompt =
    '너는 한국어 텍스트 교정 및 정제 도우미야.\n' +
    '규칙:\n' +
    '- 맞춤법과 띄어쓰기를 정확히 교정할 것.\n' +
    '- 특수문자와 이모티콘(emoji)을 모두 삭제할 것.\n' +
    '- 반복적인 표현(예: ㅋㅋㅋ, ㅎㅎㅎ, ㅠㅠㅠ 등)은 1회만 남길 것(예: ㅋ, ㅎ, ㅠ).\n' +
    '- 군더더기 기호(불필요한 마크업, 중복 공백) 제거.\n' +
    '- 결과는 한 줄의 최종 문장만 출력(주석/따옴표/설명 금지).';

  const userPrompt =
    '아래 한국어 문장을 위 규칙으로 교정/정리해줘. 결과는 문장만 출력해.\n\n문장:\n' +
    inputText;

  const payload = {
    model: OPENAI_MODEL,
    messages: [
      { role: 'system', content: systemPrompt },
      { role: 'user', content: userPrompt }
    ],
    temperature: 0
  };

  const headers = {
    'Authorization': 'Bearer ' + apiKey,
    'Content-Type': 'application/json'
  };

  const options = {
    method: 'post',
    headers: headers,
    payload: JSON.stringify(payload),
    muteHttpExceptions: true
  };

  // 지수 백오프 재시도
  let backoff = INITIAL_BACKOFF_MS;
  for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
    const res = UrlFetchApp.fetch(OPENAI_ENDPOINT, options);
    const status = res.getResponseCode();

    if (status >= 200 && status < 300) {
      const json = JSON.parse(res.getContentText('UTF-8'));
      const text = json && json.choices && json.choices[0] && json.choices[0].message && json.choices[0].message.content
        ? json.choices[0].message.content.trim()
        : '';
      if (!text) throw new Error('OpenAI 응답 파싱 실패(내용 없음).');
      return text;
    }

    // 재시도 조건: 429(레이트리밋) 또는 5xx
    if (status === 429 || (status >= 500 && status < 600)) {
      if (attempt < MAX_RETRIES) {
        Utilities.sleep(backoff);
        backoff = backoff * 2;
        continue;
      }
    }

    let errMsg = res.getContentText('UTF-8');
    if (errMsg && errMsg.length > 500) errMsg = errMsg.slice(0, 500) + '...';
    throw new Error('OpenAI API 오류 (HTTP ' + status + '): ' + (errMsg || '응답 본문 없음'));
  }

  throw new Error('재시도 한도를 초과했습니다.');
}

/** 추가 후처리: 이모지/특수문자 제거, 반복 축약, 공백 정리 */
function postProcessKorean_(text) {
  let t = (text || '').toString();

  // 반복 표현 1회로 축약 (ㅋ, ㅎ, ㅠ, ㅜ, ㅡ)
  t = t
    .replace(/([ㅋㅎㅠㅜㅡ])\1+/g, '$1') // ㅋㅋㅋ→ㅋ, ㅎㅎㅎ→ㅎ, ㅠㅠㅠ→ㅠ
    .replace(/([.!?])\1{1,}/g, '$1');   // !!!→!

  // 허용 문자만 남기기: 한글(가-힣), 영문, 숫자, 공백, 기본 문장부호
  // 남김: . , ? ! : ; " ' ( ) / -
  t = t.replace(/[^가-힣a-zA-Z0-9\s\.\,\?\!\:\;\"\'\(\)\/\-]/g, '');

  // 공백 정리
  t = t.replace(/\s{2,}/g, ' ').trim();

  return t;
}

 

 

import streamlit as st
import pandas as pd
import numpy as np
import altair as alt
from dateutil.relativedelta import relativedelta

st.set_page_config(page_title="Sales Explorer", layout="wide")

# ====== 데이터 로딩 ======
@st.cache_data
def load_data(file):
    if isinstance(file, str):
        df = pd.read_excel(file, sheet_name=0, engine="openpyxl")
    else:
        # st.file_uploader 객체
        df = pd.read_excel(file, sheet_name=0, engine="openpyxl")

    # 타입 정리
    date_cols = [c for c in df.columns if "date" in c.lower()]
    for c in date_cols:
        df[c] = pd.to_datetime(df[c], errors="coerce")

    # 매출액 계산: unit_price * quantity * (1 - discount)
    # (없으면 0으로 처리)
    for c in ["unit_price", "quantity", "discount"]:
        if c not in df.columns:
            df[c] = 0.0
    df["sales"] = df["unit_price"].astype(float) * df["quantity"].astype(float) * (1 - df["discount"].astype(float))

    # 보조 컬럼
    if "country" not in df.columns:
        df["country"] = "Unknown"
    if "category_name" not in df.columns:
        df["category_name"] = "Unknown"
    if "order_date" not in df.columns:
        # 첫 번째 날짜형 컬럼을 order_date로 사용
        date_candidates = [c for c in date_cols if df[c].notna().any()]
        df["order_date"] = df[date_candidates[0]] if date_candidates else pd.NaT

    df["year"] = df["order_date"].dt.year
    return df

st.title("📊 Sales Explorer (Streamlit)")

# 파일 입력: 로컬 파일명 또는 업로더
with st.sidebar:
    st.header("⚙️ 데이터 선택")
    default_path = "6교시 데이터 시각화 실습.xlsx"
    use_uploader = st.toggle("파일 업로드 사용", value=False)
    if use_uploader:
        uploaded = st.file_uploader("엑셀 파일(.xlsx) 업로드", type=["xlsx"])
        if uploaded is None:
            st.info("좌측에서 엑셀 파일을 업로드해주세요.")
            st.stop()
        df = load_data(uploaded)
    else:
        st.caption(f"현재 경로의 파일 사용: **{default_path}**")
        try:
            df = load_data(default_path)
        except Exception as e:
            st.error(f"파일을 열 수 없습니다: {e}")
            st.stop()

# ====== 사이드바 필터 ======
with st.sidebar:
    st.header("🔎 필터")
    # 국가 / 카테고리
    countries = ["(All)"] + sorted(df["country"].dropna().astype(str).unique().tolist())
    cats = ["(All)"] + sorted(df["category_name"].dropna().astype(str).unique().tolist())

    country_sel = st.selectbox("Country", countries, index=0)
    cat_sel = st.selectbox("Category", cats, index=0)

    # 연도 범위
    year_min = int(df["year"].min()) if df["year"].notna().any() else 1990
    year_max = int(df["year"].max()) if df["year"].notna().any() else 1990
    year_range = st.slider("Year Range", min_value=year_min, max_value=year_max, value=(year_min, year_max), step=1)

    st.divider()
    st.header("📈 차트 옵션")
    freq = st.radio("집계 주기", ["Monthly", "Quarterly", "Yearly"], horizontal=True)
    chart_type = st.radio("차트 타입", ["Line", "Bar"], horizontal=True)
    metric = st.selectbox("지표(집계)", ["sales", "quantity", "freight"] if "freight" in df.columns else ["sales", "quantity"])
    normalize = st.checkbox("범주별 정규화(백분율)", value=False, help="선택 카테고리/국가별로 비중(%) 보기")

# ====== 필터 적용 ======
mask = pd.Series(True, index=df.index)

if country_sel != "(All)":
    mask &= (df["country"].astype(str) == country_sel)

if cat_sel != "(All)":
    mask &= (df["category_name"].astype(str) == cat_sel)

mask &= df["year"].between(year_range[0], year_range[1])
filtered = df.loc[mask].copy()

if filtered.empty:
    st.warning("선택한 조건에 해당하는 데이터가 없습니다.")
    st.stop()

# ====== 시간 리샘플 ======
# 집계 기준 날짜: order_date
filtered = filtered[filtered["order_date"].notna()].copy()
if filtered.empty:
    st.warning("유효한 날짜(order_date)가 없습니다.")
    st.stop()

# 시계열 키 만들기
if freq == "Monthly":
    filtered["period"] = filtered["order_date"].dt.to_period("M").dt.to_timestamp()
elif freq == "Quarterly":
    filtered["period"] = filtered["order_date"].dt.to_period("Q").dt.to_timestamp()
else:
    filtered["period"] = filtered["order_date"].dt.to_period("Y").dt.to_timestamp()

# 집계용 그룹 키(하나의 차트에서 카테고리 또는 국가로 분해)
group_key = "category_name" if cat_sel == "(All)" else ("country" if country_sel == "(All)" else None)

agg_cols = [c for c in ["sales", "quantity", "freight"] if c in filtered.columns]
group_cols = ["period"] + ([group_key] if group_key else [])

agg = filtered.groupby(group_cols, dropna=False)[agg_cols].sum().reset_index()

# 정규화(백분율)
if normalize and group_key:
    agg["total"] = agg.groupby("period")[metric].transform("sum")
    agg[metric] = np.where(agg["total"] > 0, agg[metric] / agg["total"] * 100, 0)
    y_title = f"{metric} (%)"
else:
    y_title = metric

# ====== Altair 차트 ======
alt.data_transformers.disable_max_rows()

base = alt.Chart(agg).encode(
    x=alt.X("period:T", title="Period"),
    y=alt.Y(f"{metric}:Q", title=y_title),
    tooltip=[alt.Tooltip("period:T", title="Period"),
             alt.Tooltip(metric, title=metric),
             alt.Tooltip(group_key, title=group_key)] if group_key else
            [alt.Tooltip("period:T", title="Period"),
             alt.Tooltip(metric, title=metric)]
)

if group_key:
    base = base.encode(color=alt.Color(f"{group_key}:N", legend=alt.Legend(title=group_key)))

if chart_type == "Line":
    chart = base.mark_line(point=True)
else:
    chart = base.mark_bar()

# 범례/마우스오버 인터랙션(하이라이트)
if group_key:
    selection = alt.selection_point(fields=[group_key], bind="legend")
    chart = chart.add_params(selection).encode(opacity=alt.condition(selection, alt.value(1), alt.value(0.25)))

st.subheader("📈 시계열 매출/수량/운임 추이 (상호작용 차트)")
st.caption("사이드바에서 필터·집계주기·차트 타입을 바꾸면 즉시 반영됩니다. 범례 클릭으로 특정 항목 하이라이트 가능합니다.")
st.altair_chart(chart.properties(height=420).interactive(), use_container_width=True)

# ====== 원자료 & 다운로드 ======
with st.expander("🔍 현재 필터가 적용된 데이터(원본 레코드) 보기"):
    st.dataframe(filtered, use_container_width=True)

# 집계 테이블도 제공
with st.expander("📄 현재 차트 데이터(집계 결과) 보기"):
    st.dataframe(agg, use_container_width=True)

def to_csv_bytes(df):
    return df.to_csv(index=False).encode("utf-8-sig")

st.download_button("⬇️ 필터 적용 원자료 CSV 다운로드", data=to_csv_bytes(filtered), file_name="filtered_raw.csv", mime="text/csv")
st.download_button("⬇️ 차트용 집계 CSV 다운로드", data=to_csv_bytes(agg), file_name="chart_agg.csv", mime="text/csv")

pip install streamlit pandas altair openpyxl python-dateutil

지도교수: 박강민 (국민대학교 비즈니스IT전문대학원)
위치: 서울 성북구 정릉로 77, 국민대학교 경영관 306호
연락처: gangmin.park@kookmin.ac.kr | Tel: 02-910-4567

© Copyright 2025- 국민대학교 비즈니스IT전문대학원 AIMS Lab - All Rights Reserved

Designed by pageKR