새소식

KT AIVLE SCHOOL

ChatGPT(LLM)와 RAG를 이용하여 '예비 KT 에이블러들을 위한 QA 챗봇 모델' 만들기

  • -

layout: single
title: "jupyter notebook 변환하기!"
categories: coding
tag: [python, blog, jekyll]
toc: true
author_profile: false


챗봇 만들기

0.미션

  • 예비 KT 에이블러들을 위한 QA 챗봇 모델 만들기

    • Vector DB에 데이터 추가하기

    • Retriever, memory, LLM를 연결하기

    • 실행시 이력 DB 생성하고 기록하기

    • test

1.환경준비

(1) 라이브러리 Import

import pandas as pd
import numpy as np
import os
import sqlite3
from datetime import datetime

import openai

from langchain.chat_models import ChatOpenAI
from langchain.schema import HumanMessage, SystemMessage, Document
from langchain.embeddings import OpenAIEmbeddings
from langchain.vectorstores import Chroma
from langchain.chains import RetrievalQA, ConversationalRetrievalChain
from langchain.memory import ConversationBufferMemory

import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)

(2) OpenAI API Key 확인

https://platform.openai.com/api-keys

위 링크에서 자신의 API Key 생성 - gpt-3.5-turbo로 준비하기

API.key 파일을 만들고 거기에 API key 저장하기

image

with open("API.key") as f:
    os.environ['OPENAI_API_KEY'] =  f.readlines()[0]

openai.api_key = os.getenv('OPENAI_API_KEY')

print(openai.api_key[:10])
sk-proj-Gt
  • 만약 환경변수 키 설정이 잘 안된다면 아래 코드셀의 주석을 해제하고, 자신의 api key를 입력하고 실행

    • 아래 코드는 키 지정을 임시로 수행함.

    • 파이썬 파일(.ipynb, .py)안에서 매번 수행해야 함.

# os.environ['OPENAI_API_KEY'] = '여러분의 OpenAI API키' 
# openai.api_key = os.getenv('OPENAI_API_KEY')

2.Vector DB 만들기

import os
import shutil

folder = "./db"       # 경로 지정 -> 크로마 함수에서 폴더 만들어 줄것임

if os.path.exists(folder):
    shutil.rmtree(folder)
  • 벡터 데이터베이스

    • 1일차 벡터 데이터베이스를 그대로 이용

      • Embedding 모델 : text-embedding-ada-002

      • DB 경로 : ./db

# Chroma 데이터베이스 인스턴스 생성

from langchain.embeddings import OpenAIEmbeddings
from langchain.vectorstores import Chroma

embeddings = OpenAIEmbeddings(model="text-embedding-ada-002")
VectorDB = Chroma(persist_directory = folder, embedding_function = embeddings)
  • 데이터 입력

    • 기존 입력을 모두 제거하고 추가 사항만 모두 입력

    • meta data로 '구분' 칼럼 값 추가하기

data = pd.read_csv('aivleschool_qa.csv', encoding='utf-8')

print(len(data))
data.head()
10

구분 QA
0 모집/선발 최종 학력 또는 전공과 관계없이 지원할 수 있나요?\nKT 에이블스쿨은 정규 4년제...
1 모집/선발 35세 이상은 지원할 수 없나요?\n본 교육 과정은 34세 이하를 대상으로 하는 교...
2 모집/선발 미취업자의 기준이 뭔가요?\n미취업자의 기준은 아래와 같습니다.\n1) 기간의 정함...
3 모집/선발 직장인도 지원할 수 있나요?\nKT 에이블스쿨은 미취업자를 대상으로 하며, 교육 시...
4 모집/선발 아르바이트를 하고 있는데 지원할 수 있나요?\n고용보험에 가입이 되어 있는 경우 1...
# 데이터프레임의 텍스트 열(시리즈)을 리스트로 변환
text_list = data['QA'].tolist()
meta = data['구분'].tolist()

# 리스트 내용을 각각 document로 변환
documents = [Document(page_content=text_list[i], metadata={'category':meta[i]}) for i in range(data.shape[0])]

# Insert
VectorDB.add_documents(documents)
['8951da8c-d7e9-407a-a7f5-56fadae9fbd3',
 'cc5329e5-ffa7-427c-8d49-dc8a90237911',
 '40e55b59-c475-433e-8a3d-f43efcaf5c5c',
 'a13a108d-ad38-41b4-a70a-2f18536d5f85',
 'b249f7c5-ae02-4298-98ed-695ae1f581c9',
 '84fe8e11-1623-46a8-b182-a1f9d0ea72a5',
 '353c0437-fde9-4b1f-9e9b-0f8d23eaa5fc',
 'ea624090-f572-44d5-aace-12a7010f51f4',
 'ef485096-1269-49b7-87ed-5844932ff32c',
 'b512581c-99d9-4d7e-b4aa-84224bdb967b']

크롤링

타겟 페이지 F12

image

import requests
from bs4 import BeautifulSoup
import re

cookies = {
    "SESSION": "b99bed4c-742c-4d57-9c4a-9717a92cac89",
    "TS01e6903a": "01a4caa0ef3d44ff7f7af4169e465024fe6016d11b29570fb287df2b6330b2cfbb1d57f8c49a63becfea866fb526e4ceb549492fa13ffc6824f77d18b3717ab6068a96d1e0"
}


doc_crawl=[]
catagories=[]
contents=[]

for i in range(1, 11):
    url_qna=f"https://aivle.kt.co.kr/home/brd/faq/listJson?ctgrCd=&pageIndex={i}"
    response=requests.get(url=url_qna, cookies=cookies)
    data_json=response.json()

    return_list = data_json.get('returnList', [])
    # print(return_list)
    if return_list:
        for elem in return_list:
            raw_content = elem.get('atclCts', '')
            soup = BeautifulSoup(raw_content, 'html.parser')
            content = soup.get_text()

            metadata = {'category': elem.get('ctgrNm', '')}
            # 특수 문자 제거
            content = re.sub(r'\r\n|\r|\n', '', content)
            content = re.sub(r'\s+', ' ', content)

            catagories.append(elem.get('ctgrNm', ''))
            contents.append(content)

            doc_elem_crawl=Document(page_content=content, metadata=metadata)
            doc_crawl.append(doc_elem_crawl)

VectorDB.add_documents(doc_crawl)
C:\Users\kim_h\AppData\Local\Temp\ipykernel_2516\2011595320.py:25: MarkupResemblesLocatorWarning: The input looks more like a filename than markup. You may want to open this file and pass the filehandle into Beautiful Soup.
  soup = BeautifulSoup(raw_content, 'html.parser')
['b0746580-5236-476a-904d-e8524a63094f',
 '3228f4c3-7396-40bc-b863-5cf38d6d6a76',
 '55157a08-f0fa-4c83-b263-c39d7aac610e',
 'a89ca8ea-250b-450d-8237-8e8e4967dc51',
 'bb0cef70-90ae-42c6-ab99-5a0e98856091',
 '8dcb6151-dfb0-43e7-8da8-7b370a03465d',
 'fdc9f098-ec2a-4cbd-989a-849409b5d99b',
 '5320731e-40b9-4a10-ae92-566768880cc4',
 'bebb1ebf-15f6-48d4-8444-8da666f17b65',
 '9452a3eb-4b2d-4e91-8c38-b7b9b94b4341',
 '8d552b4b-0415-48c0-98e7-553962800862',
 '7df33588-e5b7-4be5-bc13-969764ffae90',
 '04acf0c5-b92a-4423-a648-5e9ca5f5e102',
 '5bd8cb9d-d807-456c-97de-d7da3b1006fb',
 '854e7ed6-dd4f-4030-b613-20ecd9a6b110',
 '1863da3d-6b92-4dff-ad2f-9cb6f1124df2',
 'fd28965c-1be8-4635-bec5-aa34aeef63d1',
 '0a9a8ab8-4618-4b89-97cd-533a9cc61861',
 '87045114-8948-42a3-ab68-7d66cef296a7',
 '50f07918-8f7d-48c2-a449-94a87ff3176d',
 'f22e33be-f904-4599-990c-000d38dff098',
 '8b91da30-831f-49d2-93ea-d85bcbe93b89',
 '4d057b1d-5797-4fac-9fcf-aa90c7bfece9',
 '90c0e1f9-cc9c-4381-90ac-4eba04d1bceb',
 '2aadc44b-7642-4e3b-8a02-7bd868733bce',
 '5f1264ff-82b3-4608-b305-0db1b4cff90e',
 'edf79e38-bb74-402f-bb31-b1aab595f3e4',
 '78f0b7ba-9705-4582-8c47-4ec65e8aea68',
 '8645a1a2-6401-4caa-be26-16e961a7c776',
 'cfb12384-122f-44ec-a81f-2b6bb8372960',
 '3adb91fe-94f9-4f6c-a890-cd74af1ae502',
 '754d3f00-d258-44cc-89ea-70743d7c15c0',
 '97156bc6-7198-4d65-86a1-6803bfcff1c0',
 '4a4aac99-7e3d-41c3-ab5d-55d8abd23b1a',
 '6307d193-2250-4627-8c70-5051f42d86a0',
 'f4890302-c712-47c6-a80f-3b5fd198bbfb',
 '48a85f36-3ed0-428c-9cdd-ce183994ee36',
 'c1e344e5-f211-4709-b00a-8e7fd7b12794',
 '93043b04-ffe3-407d-bc11-9d5c94dae41a',
 '62178ea2-405f-40bd-8581-082e3a89ea86',
 '2b37a748-172a-4755-ac2b-f7fb1a3a0899',
 '43676982-5067-4c39-8787-d1318c41cd32',
 'cdc5beda-c978-4b5c-a60b-9a69e6315aa0',
 '2f25782a-ef02-4c32-8b32-9312ef9ad0b1',
 '39a5b972-7ccf-4035-b583-6fe74f0f6af9',
 '4517c89d-8e7d-4fec-a4ad-f74b83ea8958',
 '35c08f96-ed00-41a6-83fb-8abe9d96395f',
 '478e968d-d123-4755-baa0-7e593eec4395',
 'afed0efe-508e-4317-84cd-c162f7c86bdc',
 'e6476d88-1ca0-457f-8333-a3200e7fd687',
 'deda0093-a17b-461c-bdbf-75ff7dedbbd2',
 'd0c1b672-8743-4863-8950-f13fcb8e1aad',
 '0478ec3a-4e6f-4c33-8f27-588244823f1e',
 '33f032ab-4b22-46f2-8769-523e8442e8ef',
 '090abfe0-2044-4a77-acaa-94633efbd826',
 'dafb766e-d24f-485d-b046-7b8d66848220',
 '1d913bc4-2cf6-452f-bfa1-56f56578689d',
 'fcfc7e39-5d95-469a-a7fe-dc4574a82cec',
 'd97a985a-436a-4f8f-85cf-e7ebbb59f2d7',
 'ca298ac7-9154-4660-82af-d746c53160b3',
 'efa1bd6c-9618-461f-93ea-d91c944e1892',
 '9912a2fd-3e82-48f9-a542-672949e2e5be',
 '53adfed5-4c05-41da-a81a-ac15152d7314',
 '48f63ac4-4397-4ad2-a59f-49039881f099',
 'd1526b07-1e6e-42bc-a689-5b26fd24a728',
 '6513fb4e-f2ca-424f-b5bf-7d7f30941243',
 'b632d470-9342-42e4-b4b6-0c01711cd3e8',
 'aa992cbb-aa50-4dd1-a229-3a3fb9958d09']
database_df = pd.DataFrame({'구분': catagories, '내용': contents})

print(len(database_df))
database_df.head()
68

구분 내용
0 모집/선발 KT 에이블스쿨은 정규 4년제 대학 졸업자 및 졸업예정자를 대상으로 하는 교육입니다...
1 모집/선발 본 교육 과정은 34세 이하를 대상으로 하는 교육입니다. 단, 모집시점에 35세라도...
2 모집/선발 미취업자의 기준은 아래와 같습니다. 1) 기간의 정함이 있는 근로인 경우, 2) 고...
3 모집/선발 KT 에이블스쿨은 미취업자를 대상으로 하며, 교육 시작일 기준 재직자는 지원이 불가...
4 모집/선발 고용보험에 가입이 되어 있는 경우 15시간/주 미만 근로인 경우에만 미취업자로 간주...
  • 입력된 데이터 조회
VectorDB.get().keys()
dict_keys(['ids', 'embeddings', 'metadatas', 'documents', 'uris', 'data'])
VectorDB.get()['metadatas'][:3]
[{'category': '국민내일배움카드'}, {'category': '모집/선발'}, {'category': '국민내일배움카드'}]
VectorDB.get()['documents'][:3]
['훈련장려금은 실업자 지급이 원칙입니다. 다만, 주 15시간 미만으로 근로하며, 해당 근로사실과 동일하게 고용보험에 가입되어 있는 경우 예외적으로 훈련장려금이 지급됩니다. 더불어, 훈련 수강일정과 근로시간은 중복될 수 없습니다. 자세한 사항은 관할 고용센터에 문의 부탁 드립니다. ',
 '교육 등록신청을 하지 않거나 교육 일정 시작 전에 교육 등록을 취소해도 지원자격에 해당하는 경우 다시 지원이 가능합니다. 다만, 교육 과정 확정자신고일(교육시작일로부터 7일)이 지난 후 중도 퇴교를 할 경우 과정을 수강한 것으로 간주되며, 향후 K-Digital Training (K-DT) 과정에 재지원은 가능하나 무료 수강은 어렵습니다.',
 '장학금이 소득으로 인식되는 경우 훈련장려금이 지급되지 않을 수 있습니다.훈련장려금은 고용센터에서 지급요건을 최종 검토 후 지급처리 되므로, 먼저 학교 행정처에 장학금 소득 처리 여부 문의 후 거주지 관할 고용센터(HRD-Net 확인) 혹은 훈련 권역별 고용센터(아래 참고)로 연락하시어 훈련장려금 지급 여부에 대해 다시 한 번 문의하시기 바랍니다.※ 수도권: 성남고용센터 충남/충북: 대전고용센터 대구/경북: 대구고용센터 전남/전북: 광주고용센터 부산/경남: 부산고용센터 - 고용노동부 대표번호 1350, 거주지 근처 지도검색, HRD-Net 메인 하단 지역별 기관검색에서 확인 가능 ']
temp = [a['category'] for a in VectorDB.get()['metadatas']]

database_df = pd.DataFrame({'구분': temp, '내용': VectorDB.get()['documents']})

print(len(database_df))
database_df.head()
78

구분 내용
0 국민내일배움카드 훈련장려금은 실업자 지급이 원칙입니다. 다만, 주 15시간 미만으로 근로하며, 해당...
1 모집/선발 교육 등록신청을 하지 않거나 교육 일정 시작 전에 교육 등록을 취소해도 지원자격에 ...
2 국민내일배움카드 장학금이 소득으로 인식되는 경우 훈련장려금이 지급되지 않을 수 있습니다.훈련장려금은...
3 모집/선발 현재 KT 에이블스쿨은 KT 채용 인적성검사와 같은 과목의 인적성검사를 운영하고 있...
4 모집/선발 코딩테스트는 AI개발자 Track 지원자를 대상으로 진행되며, 사용 가능한 프로그래...

3.RAG+LLM모델

  • 모델 : ConversationalRetrievalChain

    • LLM 모델 : gpt-3.5-turbo

    • retriever : 벡터DB

      • 유사도 높은 문서 3개 가져오도록 설정
    • memory 사용

  • 요구사항

    • 질문 history 관리를 위한 이력 저장 DB 생성

      • DB 명 : db_chatlog

      • 테이블 명 : history

        • id INTEGER PRIMARY KEY : 이렇게 설정하면 자동증가 값으로 채워짐

        • datetime TEXT : 질문시점 yyyy-mm-dd hh:mi:ss

        • query TEXT : 질문

        • sim1 REAL : 첫번째 문서의 유사도 점수

        • sim2 REAL : 두번째 문서의 유사도 점수

        • sim3 REAL : 세번째 문서의 유사도 점수

        • answer TEXT : 답변

      • 유사도 점수는 similarity_search_with_score 메서드를 이용해서 저장해야 함

      • 질문과 답변이 진행될 때마다 history 테이블에 데이터 입력

  • 관리용 DB, 테이블 생성
import os

root =  './db_chatlog'
os.makedirs(root, exist_ok=True)

path = root + '/db_chatlog.db'

if os.path.exists(path):
    os.remove(path)

conn = sqlite3.connect(path)        # 해당 경로에 있으면 연결하고 없으면 만든다.

# 커서 객체 생성
cursor = conn.cursor()

# test 테이블 생성
cursor.execute('''
CREATE TABLE IF NOT EXISTS history (
    id INTEGER PRIMARY KEY,
    datetime TEXT NOT NULL,
    query TEXT NOT NULL, 
    sim1 REAL NOT NULL,
    sim2 REAL NOT NULL,
    sim3 REAL NOT NULL,
    answer TEXT NOT NULL

)
''')

# 변경사항 커밋 (저장)
conn.commit()

# 연결 종료
conn.close()
conn = sqlite3.connect(path)

data = pd.DataFrame({'datetime': [], 'query': [], 
                     'sim1': [], 'sim2': [], 'sim3': [], 
                     'answer': []})

data.to_sql('history', conn, if_exists='append', index=False) # test 테이블이 있으면 insert, 없으면 생성

# 확인
df = pd.read_sql('SELECT * FROM history', conn)
display(df)

# ③ 연결 종료
conn.close()

id datetime query sim1 sim2 sim3 answer
  • 모델 선언
from langchain.chat_models import ChatOpenAI
from langchain.callbacks.streaming_stdout import StreamingStdOutCallbackHandler


chat = ChatOpenAI(model="gpt-3.5-turbo",
                  streaming=True, callbacks=[StreamingStdOutCallbackHandler()])  # 스트리밍 설정

k=3
retriever = VectorDB.as_retriever(search_kwargs={"k": k})
# 대화 메모리 생성
memory = ConversationBufferMemory(memory_key="chat_history", input_key="question", output_key="answer",
                                  return_messages=True)

# ConversationalRetrievalQA 체인 생성 -> chat모델, 검색기, 메모리 연결
qa = ConversationalRetrievalChain.from_llm(llm=chat, retriever=retriever, memory=memory,
                                           return_source_documents=True,  output_key="answer")
  • 모델 사용 및 이력 확인
# 첫번째 질문
from datetime import datetime

# 현재 시간
dt = datetime.now()
dt = dt.strftime('%Y-%m-%d %H:%M:%S')

query1 = "최종 학력 또는 전공과 관계없이 지원할 수 있나요?"

print('질문 : ', query1)

result = qa(query1)
# result['answer']
질문 :  최종 학력 또는 전공과 관계없이 지원할 수 있나요?
KT 에이블스쿨은 정규 4년제 대학 졸업자 및 졸업예정자를 대상으로 하는 교육이므로, 최종 학력이나 전공에 관계 없이 해당 조건을 충족하는 경우 모두 지원할 수 있습니다. 단, AI개발자 Track은 기본적인 코딩 역량이 필요하다는 점을 참고하시기 바랍니다.
query = result['question']
answer = result['answer']
sims = VectorDB.similarity_search_with_score(query, k = 3) #← 데이터베이스에서 유사도가 높은 문서를 가져옴
sim1 = sims[0][1]
sim2 = sims[1][1]
sim3 = sims[2][1]

new_data = {
    'datetime': [dt],
    'query': [query],
    'sim1': [sim1],
    'sim2': [sim2],
    'sim3': [sim3],
    'answer': [answer]
}

new_data_df = pd.DataFrame(new_data)
conn = sqlite3.connect(path)        # 해당 경로에 있으면 연결하고 없으면 만든다.
new_data_df.to_sql('history', conn, if_exists='append', index=False)
df = pd.read_sql('SELECT * FROM history', conn)
display(df)

# 연결 종료
conn.close()

id datetime query sim1 sim2 sim3 answer
0 1 2024-06-04 15:56:47 최종 학력 또는 전공과 관계없이 지원할 수 있나요? 0.189875 0.264856 0.266282 KT 에이블스쿨은 정규 4년제 대학 졸업자 및 졸업예정자를 대상으로 하는 교육이므로...
memory.load_memory_variables({})   
{'chat_history': [HumanMessage(content='최종 학력 또는 전공과 관계없이 지원할 수 있나요?'),
  AIMessage(content='KT 에이블스쿨은 정규 4년제 대학 졸업자 및 졸업예정자를 대상으로 하는 교육이므로, 최종 학력이나 전공에 관계 없이 해당 조건을 충족하는 경우 모두 지원할 수 있습니다. 단, AI개발자 Track은 기본적인 코딩 역량이 필요하다는 점을 참고하시기 바랍니다.')]}

함수화 - 최종

from datetime import datetime

chat = ChatOpenAI(model="gpt-3.5-turbo")
k=3
retriever = VectorDB.as_retriever(search_kwargs={"k": k})
# 대화 메모리 생성
memory = ConversationBufferMemory(memory_key="chat_history", input_key="question", output_key="answer",
                                  return_messages=True)

# ConversationalRetrievalQA 체인 생성 -> chat모델, 검색기, 메모리 연결
qa = ConversationalRetrievalChain.from_llm(llm=chat, retriever=retriever, memory=memory,
                                           return_source_documents=True,  output_key="answer")

def chatting(query):
    print(f'질문 : {query}')
    # 현재 시간
    dt = datetime.now()
    dt = dt.strftime('%Y-%m-%d %H:%M:%S')
    result = qa(query)
    answer = result['answer']

    print(f'답변 : {answer}')
    print('=' * 50)

    sims = VectorDB.similarity_search_with_score(query, k = 3) #← 데이터베이스에서 유사도가 높은 문서를 가져옴
    sim1 = sims[0][1]
    sim2 = sims[1][1]
    sim3 = sims[2][1]

    new_data = {
    'datetime': [dt],
    'query': [query],
    'sim1': [sim1],
    'sim2': [sim2],
    'sim3': [sim3],
    'answer': [answer]
    }
    new_data_df = pd.DataFrame(new_data)
    conn = sqlite3.connect(path)        # 해당 경로에 있으면 연결하고 없으면 만든다.
    new_data_df.to_sql('history', conn, if_exists='append', index=False)
    # 연결 종료
    conn.close()
while True:
    query = input('질문 > ')
    query = query.strip()
    if len(query) == 0:
        print('Enter 입력 -> 대화 종료')
        break
    chatting(query)

print(memory.load_memory_variables({}))
print()

conn = sqlite3.connect(path)        # 해당 경로에 있으면 연결하고 없으면 만든다.
df = pd.read_sql('SELECT * FROM history', conn)
display(df)
conn.close()
질문 : 몇 살부터 지원 가능한가요?
답변 : 34세 이하를 대상으로 하는 교육이지만, 모집시점에 35세라도 해당연도 1월 1일 이후 출생자는 지원이 가능합니다.
==================================================
질문 : 그럼 17살도 지원 가능 한가요?
답변 : 17세는 해당 지원 대상이 아닙니다. 이 교육 과정은 만 34세 이하의 대상을 대상으로 합니다.
==================================================
질문 : 직장인도 지원 가능한가요?
답변 : KT 에이블스쿨은 미취업자를 대상으로 하며, 재직자는 지원이 불가능합니다.따라서 직장인은 해당 대상에 포함되지 않습니다.
==================================================
Enter 입력 -> 대화 종료
{'chat_history': [HumanMessage(content='몇 살부터 지원 가능한가요?'), AIMessage(content='34세 이하를 대상으로 하는 교육이지만, 모집시점에 35세라도 해당연도 1월 1일 이후 출생자는 지원이 가능합니다.'), HumanMessage(content='그럼 17살도 지원 가능 한가요?'), AIMessage(content='17세는 해당 지원 대상이 아닙니다. 이 교육 과정은 만 34세 이하의 대상을 대상으로 합니다.'), HumanMessage(content='직장인도 지원 가능한가요?'), AIMessage(content='KT 에이블스쿨은 미취업자를 대상으로 하며, 재직자는 지원이 불가능합니다.따라서 직장인은 해당 대상에 포함되지 않습니다.')]}

id datetime query sim1 sim2 sim3 answer
0 1 2024-06-04 15:56:47 최종 학력 또는 전공과 관계없이 지원할 수 있나요? 0.189875 0.264856 0.266282 KT 에이블스쿨은 정규 4년제 대학 졸업자 및 졸업예정자를 대상으로 하는 교육이므로...
1 2 2024-06-04 15:57:05 몇 살부터 지원 가능한가요? 0.261709 0.273430 0.291227 34세 이하를 대상으로 하는 교육이지만, 모집시점에 35세라도 해당연도 1월 1일 ...
2 3 2024-06-04 15:57:16 그럼 17살도 지원 가능 한가요? 0.295267 0.296652 0.318797 17세는 해당 지원 대상이 아닙니다. 이 교육 과정은 만 34세 이하의 대상을 대상...
3 4 2024-06-04 15:57:26 직장인도 지원 가능한가요? 0.211667 0.230160 0.239245 KT 에이블스쿨은 미취업자를 대상으로 하며, 재직자는 지원이 불가능합니다.따라서 직...
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.