과거 11년간 공인중개사 시험문제(25회 ~ 35회)
RAG, Fine-tuning, and more
1. docling
-
extract pdf questions from Q-net past questions and save them in a markdown file
-
extract pdf answers from Q-net past questions and save them in a markdown file
2. convert markdown to JSONL
마크다운 페이지를 JSONL로 변환
import os
import json
import frontmatter
import markdown
from bs4 import BeautifulSoup
from collections import defaultdict
def parse_md_file(file_path):
post = frontmatter.load(file_path)
content = post.content
html = markdown.markdown(content, extensions=["tables"])
soup = BeautifulSoup(html, 'html.parser')
questions = []
current_q = {}
context_parts = []
collecting_context = False
for el in soup.find_all(['h2', 'p', 'ul', 'ol', 'table']):
if el.name == 'h2':
if current_q:
current_q["context"] = "\n".join(context_parts).strip()
questions.append(current_q)
context_parts = []
current_q = {
"id": el.get_text().split(".")[0].strip(),
"question": el.get_text().split(". ", 1)[-1].strip(),
"context": "",
"options": []
}
collecting_context = True
elif el.name == 'ol':
current_q["options"] = [li.get_text().strip() for li in el.find_all("li")]
collecting_context = False
elif collecting_context:
context_parts.append(el.get_text().strip())
if current_q:
current_q["context"] = "\n".join(context_parts).strip()
questions.append(current_q)
return questions
def merge_answers(questions, answers, subject, year):
q_subset = questions[:len(answers)]
for q, a in zip(q_subset, answers):
q["answer"] = a if isinstance(a, list) else [a]
q["subject"] = subject
q["year"] = year
return q_subset
def save_jsonl(questions, output_path):
os.makedirs(os.path.dirname(output_path), exist_ok=True)
with open(output_path, "w", encoding="utf-8") as f:
for item in questions:
f.write(json.dumps(item, ensure_ascii=False) + "\n")
if __name__ == "__main__":
subjects = ["introduction", "civil-law", "brokerage-law", "public-law", "disclosure-taxation"]
subject_map = {
"introduction": "introduction",
"civil-law": "civil_law",
"brokerage-law": "brokerage_law",
"public-law": "public_law",
"disclosure-taxation": "disclosure_taxation"
}
base_dir = "D:/deepnexus/thedeepnexus/src/app/markdoc/real_estate_agent"
output_dir = "data/real_estate_agent/raw/past_papers"
all_subjects = defaultdict(list)
for year in range(2014, 2025):
folder = f"{year}-{year - 1989:02d}"
ans_path = os.path.join(base_dir, folder, "answers.json")
if not os.path.exists(ans_path):
print(f"❌ {year} - answers.json 없음")
continue
with open(ans_path, "r", encoding="utf-8") as f:
answers_by_subject = json.load(f)
for subject in subjects:
subject_key = subject_map[subject]
md_path = os.path.join(base_dir, folder, subject, "page.md")
if not os.path.exists(md_path):
print(f"❌ {year} {subject_key} - page.md 없음")
continue
if subject_key not in answers_by_subject:
print(f"❌ {year} {subject_key} - answers.json에 없음")
continue
answers = answers_by_subject[subject_key]
questions = parse_md_file(md_path)
if len(questions) != 40:
print(f"⚠️ {year} {subject_key} - 질문 수: {len(questions)}")
if len(answers) != 40:
print(f"⚠️ {year} {subject_key} - 정답 수: {len(answers)}")
merged = merge_answers(questions, answers, subject_key, str(year))
all_subjects[subject_key].extend(merged)
for subject, items in all_subjects.items():
save_path = os.path.join(output_dir, f"{subject}.jsonl")
save_jsonl(items, save_path)
print(f"✅ saved {len(items)} items to {save_path}")
return
{"id": "41", "question": "甲이 乙을 기망하여 건물을 매도하는 계약을 乙과 체결하였다. 법정추인사유에 해당하는 경우는?", "context": "", "options": ["甲이 乙에게 매매대금의 지급을 청구한 경우", "甲이 乙에 대한 대금채권을 에게 양도한 경우", "甲이 이전등기에 필요한 서류를 乙에게 제공한 경우", "기망상태에서 벗어난 乙이 이의 없이 매매대금을 지급한\n 경우", "乙이 매매계약의 취소를 통해 취득하게 될 계약금 반환\n 청구권을 丁에게 양도한 경우"], "answer": [4], "subject": "civil_law", "year": "2014"}
{"id": "42", "question": "불공정한 법률행위에 관한 설명으로 틀린 것은? (다툼이 있으면 판례에 의함)", "context": "", "options": ["궁박은 심리적 원인에 의한 것을 포함한다.", "불공정한 법률행위에 관한 규정은 부담 없는 증여의 경우에도 적용된다.", "불공정한 법률행위에도 무효행위 전환의 법리가 적용될 수 있다.", "대리인에 의한 법률행위에서 무경험은 대리인을 기준으로 판단한다.", "경매절차에서 매각대금이 시가보다 현저히 저렴하더라도 불공정한 법률행위를 이유로 그 무효를 주장할 수 없다."], "answer": [3], "subject": "civil_law", "year": "2014"}
3. 문제 정제 by OpenAi API
-
문제를 OpenAI API에게 보내서
- cleaning = 5과목 X 11년 X 40문제 = 2200문제
- augmentation = 5과목 X 11년 X 40문제 = 2200문제
- validation = 5과목 X 11년 X 40문제 = 2200문제
-
csv dataset으로 변화
- questions, option1, option2, option3, option4, option5, answer, explanation
- 문법적 오류 문제는 누락
import os
import json
from openai import OpenAI
from tqdm import tqdm
import asyncio
import aiohttp
import time
from tenacity import retry, wait_random_exponential, stop_after_attempt
CONCURRENT_REQUESTS = 5 # 동시에 보낼 요청 수
REQUEST_DELAY = 0.8 # 요청 간 딜레이 (초)
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
system_prompts = {
"clean": (
"너는 공인중개사 시험 문제를 정제하고 최신 법령 기준으로 검토하는 AI야. 다음 JSON 형식으로만 출력해:\n"
"{"
"\"question\": \"문제 내용\", "
"\"context\": \" context 내용\", "
"\"options\": [\"보기1\", \"보기2\", \"보기3\", \"보기4\", \"보기5\"], "
"\"answer\": [1], "
"\"explanation\": \"해설\", "
"\"subject\": \"과목명\", "
"\"topic\": \"주제명\", "
"\"tags\": [\"키워드1\", \"키워드2\"], "
"\"year\": 2025, "
"\"law_update\": \"법령이 변경되었는지 여부를 2025년 기준으로 설명해.\", "
"\"status\": \"정답이 현재 기준에서 유지되는지 또는 변경되었는지 판단해.\""
"}\n"
"- 보기(options)는 반드시 5개여야 하며, 부족하면 생성해서 채워.\n"
"- answer는 숫자 배열이며 1개 이상 가능.\n"
"- context는 없다면 \"\"으로.\n"
"- 오직 JSON 한 줄만 출력. 다른 문장, 줄바꿈 금지.\n"
"- 보기 앞에 번호 붙이지 마.\n"
"- status는 반드시 다음 중 하나로만 출력: \"정답 유지됨\", \"정답 변경됨\", \"신규 문제\"\n"
"- law_update는 변경 사항이 없으면 \"법령 변경 없음\"으로, 변경된 경우 간결하게 요약해."
),
"update": (
"너는 공인중개사 시험 문제를 2025년 현재 법령 기준으로 검토하고, 문제를 최신화하며, 메타데이터를 확장하는 AI야.\n"
"다음 JSON 형식의 문제를 읽고, 필요한 경우 지문이나 정답을 고치고, topic, tags, law_update, status 등을 추가해.\n"
"출력은 반드시 아래 JSON 형식으로 한 줄로. 다른 문장, 줄바꿈, 설명은 포함하지 마."
"{"
"\"question\": \"문제 내용 (2025년 기준으로 수정함)\", "
"\"options\": [\"보기1\", \"보기2\", \"보기3\", \"보기4\", \"보기5\"], "
"\"answer\": [정답 인덱스들], "
"\"explanation\": \"해설 (법령이 변경되었을 경우 간단히 설명 포함)\", "
"\"subject\": \"과목명\", "
"\"topic\": \"주제명\", "
"\"tags\": [\"키워드1\", \"키워드2\"], "
"\"year\": 2025, "
"- status는 반드시 다음 중 하나로만 출력: \"정답 유지됨\", \"정답 변경됨\", \"신규 문제\"\n"
"- law_update는 변경 사항이 없으면 \"법령 변경 없음\"으로, 변경된 경우 간결하게 요약해."
"}"
),
"augment": (
"너는 공인중개사 시험 문제를 새로 만들고, 현재(2025년 기준) 법령에 적합한지 확인하는 AI야. 다음 JSON 형식으로만 출력해:\n"
"{"
"\"question\": \"문제 내용\", "
"\"context\": \"\", "
"\"options\": [\"보기1\", \"보기2\", \"보기3\", \"보기4\", \"보기5\"], "
"\"answer\": [1], "
"\"explanation\": \"해설\", "
"\"subject\": \"과목명\", "
"\"topic\": \"주제명\", "
"\"tags\": [\"키워드1\", \"키워드2\"], "
"\"year\": 2025, "
"\"law_update\": \"신설 규정 여부나 최근 변경사항이 있다면 간단히 설명해.\", "
"\"status\": \"신규 문제\""
"}\n"
"- options는 반드시 5개여야 하며, 부족하면 생성해서 채워.\n"
"- answer는 숫자 배열이며 1개 이상 가능.\n"
"- context는 없다면 \"\"으로.\n"
"- 오직 JSON 한 줄만 출력. 다른 문장, 줄바꿈 금지.\n"
"- 보기 앞에 번호 붙이지 마.\n"
"- status는 반드시 다음 중 하나로만 출력: \"정답 유지됨\", \"정답 변경됨\", \"신규 문제\"\n"
"- law_update는 변경 사항이 없으면 \"법령 변경 없음\"으로, 변경된 경우 간결하게 요약해."
),
}
def to_prompt(entry):
q = entry["question"]
context = entry.get("context", "").strip()
options = entry["options"]
answer = entry["answer"][0] if isinstance(entry["answer"], list) else entry["answer"]
base = f"{context}\n\n" if context else ""
base += f"{q}\n\n"
base += "\n".join([f"{i+1}. {opt}" for i, opt in enumerate(options)])
base += f"\n\n정답: {answer}"
return base
def call_json(system, user):
try:
res = client.chat.completions.create(
model="gpt-4o-mini", # 실제 사용 시 올바른 모델명으로 수정
messages=[
{"role": "system", "content": system},
{"role": "user", "content": user}
],
temperature=0.3,
response_format={"type": "json_object"}, # JSON 출력 강제
max_tokens=1024
)
return json.loads(res.choices[0].message.content)
except Exception as e:
print(f"❌ error: {e}")
return None
def read_jsonl(path):
with open(path, "r", encoding="utf-8") as f:
return [json.loads(line) for line in f]
def save_jsonl(data, out_path):
os.makedirs(os.path.dirname(out_path), exist_ok=True)
with open(out_path, "w", encoding="utf-8") as f:
for item in data:
f.write(json.dumps(item, ensure_ascii=False) + "\n")
@retry(wait=wait_random_exponential(min=1, max=10), stop=stop_after_attempt(5))
async def call_gpt(session, prompt, system_prompt):
headers = {"Authorization": f"Bearer {os.getenv('OPENAI_API_KEY')}"}
payload = {
"model": "gpt-4o-mini",
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": prompt}
],
"temperature": 0.3
}
async with session.post("https://api.openai.com/v1/chat/completions", json=payload, headers=headers) as resp:
if resp.status != 200:
text = await resp.text()
raise Exception(f"Error: {resp.status}, {text}")
data = await resp.json()
return data["choices"][0]["message"]["content"]
async def process_items_async(items, system_prompt, key):
results = []
sem = asyncio.Semaphore(CONCURRENT_REQUESTS)
async with aiohttp.ClientSession() as session:
async def process(item):
async with sem:
try:
# prompt = to_prompt(item)
# response = await call_gpt(session, prompt, system_prompt)
# json_data = json.loads(response)
# results.append(json_data)
# await asyncio.sleep(REQUEST_DELAY)
print(f"⏳ {key}: 처리 중 - {item.get('id', 'no-id')}")
prompt = to_prompt(item)
response = await call_gpt(session, prompt, system_prompt)
json_data = json.loads(response)
results.append(json_data)
await asyncio.sleep(REQUEST_DELAY)
except Exception as e:
print(f"⚠️ {key} 처리 실패: {e}")
await asyncio.gather(*(process(item) for item in items))
return results
# def process_json_output(input_path, out_prefix, limit=440):
# items = read_jsonl(input_path)[:limit]
# results = {"clean": [], "augment": [], "update": []}
# for item in tqdm(items):
# prompt = to_prompt(item)
# for key in ["clean", "augment", "update"]:
# json_data = call_json(system_prompts[key], prompt)
# if json_data:
# results[key].append(json_data)
# for key in results:
# save_jsonl(results[key], f"{out_prefix}_{key}.jsonl")
def process_json_output(input_path, out_prefix, limit=440):
items = read_jsonl(input_path)[:limit]
# loop = asyncio.get_event_loop()
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
for key in ["clean", "augment", "update"]:
print(f"🚀 {key} 처리 중...")
system_prompt = system_prompts[key]
results = loop.run_until_complete(process_items_async(items, system_prompt, key))
save_jsonl(results, f"{out_prefix}_{key}.jsonl")
if __name__ == "__main__":
process_json_output(
# input_path="data/real_estate_agent/raw/past_papers/brokerage_law.jsonl",
# out_prefix="data/real_estate_agent/refinement/past_papers/brokerage_law",
# input_path="data/real_estate_agent/raw/past_papers/civil_law.jsonl",
# out_prefix="data/real_estate_agent/refinement/past_papers/civil_law",
# input_path="data/real_estate_agent/raw/past_papers/disclosure_taxation.jsonl",
# out_prefix="data/real_estate_agent/refinement/past_papers/disclosure_taxation",
# input_path="data/real_estate_agent/raw/past_papers/introduction.jsonl",
# out_prefix="data/real_estate_agent/refinement/past_papers/introduction",
input_path="data/real_estate_agent/raw/past_papers/public_law.jsonl",
out_prefix="data/real_estate_agent/refinement/past_papers/public_law",
limit=440
)
return clean version
clean version
{"question": "공인중개사법령상 중개업자가 설치된 사무소의 간판을 지체 없이 철거해야 하는 경우로 명시된 것을 모두 고른 것은?", "context": "ㄱ. 등록관청에 폐업신고를 한 경우 ㄴ. 등록관청에 6개월을 초과하는 휴업신고를 한 경우 ㄷ. 중개사무소의 개설등록 취소처분을 받은 경우 ㄹ. 등록관청에 중개사무소의 이전사실을 신고한 경우", "options": ["ㄱ, ㄴ", "ㄷ, ㄹ", "ㄱ, ㄴ, ㄹ", "ㄱ, ㄷ, ㄹ", "ㄱ, ㄴ, ㄷ, ㄹ"], "answer": [4], "explanation": "중개업자가 설치된 사무소의 간판을 철거해야 하는 경우는 폐업신고, 6개월을 초과하는 휴업신고, 개설등록 취소처분을 받은 경우입니다. 이전사실 신고는 간판 철거와 관련이 없습니다.", "subject": "공인중개사법", "topic": "중개업자의 의무", "tags": ["중개업", "간판 철거"], "year": 2025, "law_update": "법령 변경 없음", "status": "정답 유지됨"}
{"question": "공인중개사법령상 분사무소의 설치에 관한 설명으로 옳은 것을 모두 고른 것은?", "context": "ㄱ. 다른 법률의 규정에 따라 중개업을 할 수 있는 법인의 분사무소에는 공인중개사를 책임자로 두어야 한다. ㄴ. 분사무소의 설치신고를 하려는 자는 그 신고서를 주된 사무소의 소재지를 관할하는 등록관청에 제출해야 한다. ㄷ. 분사무소의 설치신고를 받은 등록관청은 그 신고내용이 적합한 경우에는 국토교통부령이 정하는 신고필증을 교부해야 한다. ㄹ. 분사무소의 설치신고를 하려는 자는 법인등기사항증명서를 제출해야 한다.", "options": ["ㄱ, ㄴ", "ㄱ, ㄷ", "ㄴ, ㄷ", "ㄷ, ㄹ", "ㄱ, ㄴ, ㄹ"], "answer": [3], "explanation": "분사무소의 설치신고에 대한 규정은 공인중개사법령에 명시되어 있으며, ㄴ과 ㄷ이 옳은 설명입니다.", "subject": "공인중개사법", "topic": "분사무소 설치", "tags": ["분사무소", "공인중개사법"], "year": 2025, "law_update": "법령 변경 없음", "status": "정답 유지됨"}
return update version
update version
{"question": "공인중개사법령상 인장등록에 관한 설명으로 옳은 것을 모두 고른 것은?", "context": "ㄱ. 중개업자는 중개행위에 사용할 인장을 업무개시 전에 등록관청에 등록해야 한다. ㄴ. 법인인 중개업자의 인장등록은 「상업등기규칙」에 따른 인감증명서의 제출로 갈음한다. ㄷ. 분사무소에서 사용할 인장으로는 「상업등기규칙」에 따라 법인의 대표자가 보증하는 인장을 등록할 수 있다. ㄹ. 등록한 인장을 변경한 경우에는 중개업자는 변경일부터 10일 이내에 그 변경된 인장을 등록관청에 등록해야 한다.", "options": ["ㄱ, ㄴ", "ㄷ, ㄹ", "ㄱ, ㄴ, ㄷ", "ㄴ, ㄷ, ㄹ", "ㄱ, ㄴ, ㄷ, ㄹ"], "answer": [3], "explanation": "법령 변경 없음", "subject": "공인중개사법", "topic": "인장등록", "tags": ["인장등록", "중개업자", "법인"], "year": 2025, "law_update": "법령 변경 없음", "status": "정답 유지됨"}
{"question": "공인중개사법령상 공인중개사협회의 공제사업에 관한 설명으로 옳은 것을 모두 고른 것은? (다툼이 있으면 판례에 의함)", "context": "ㄱ. 협회의 공제규정을 제정·변경하고자 하는 때에는 국토교통부장관의 승인을 얻어야 한다. ㄴ. 위촉받아 보궐위원이 된 운영위원의 임기는 전임자 임기의 남은 기간으로 한다. ㄷ. 운영위원회의 회의는 재적위원 과반수의 찬성으로 심의사항을 의결한다. ㄹ. 협회와 중개업자 간에 체결된 공제계약이 유효하게 성립하려면 공제계약 당시에 공제사고의 발생 여부가 확정되어 있지 않은 것을 대상으로 해야 한다.", "options": ["ㄱ, ㄴ", "ㄷ, ㄹ", "ㄱ, ㄴ, ㄹ", "ㄴ, ㄷ, ㄹ", "ㄱ, ㄴ, ㄷ, ㄹ"], "answer": [3], "explanation": "법령 변경 없음", "subject": "공인중개사", "topic": "공제사업", "tags": ["공인중개사협회", "공제사업", "법령"], "year": 2025, "law_update": "법령 변경 없음", "status": "정답 유지됨"}
return augment version
augment version
{"question": "공인중개사법령상 중개업자가 설치된 사무소의 간판을 지체 없이 철거해야 하는 경우로 명시된 것을 모두 고른 것은?", "context": "", "options": ["ㄱ. 등록관청에 폐업신고를 한 경우", "ㄴ. 등록관청에 6개월을 초과하는 휴업신고를 한 경우", "ㄷ. 중개사무소의 개설등록 취소처분을 받은 경우", "ㄹ. 등록관청에 중개사무소의 이전사실을 신고한 경우", "ㅁ. 중개업자가 자진하여 사무소를 폐쇄한 경우"], "answer": [4], "explanation": "중개업자는 등록관청에 폐업신고를 하거나 6개월을 초과하는 휴업신고를 한 경우, 중개사무소의 개설등록 취소처분을 받은 경우에는 간판을 지체 없이 철거해야 합니다.", "subject": "공인중개사법", "topic": "중개업자의 의무", "tags": ["중개업", "간판 철거"], "year": 2025, "law_update": "법령 변경 없음", "status": "신규 문제"}
{"question": "공인중개사법령상 중개대상물이 될 수 없는 것을 모두 고른 것은? (다툼이 있으면 판례에 의함)", "context": "ㄱ. 20톤 이상의 선박 ㄴ. 콘크리트 지반 위에 쉽게 분리·철거가 가능한 볼트 조립방식으로 철제 파이프 기둥을 세우고 지붕을 덮은 다음 3면에 천막을 설치한 세차장 구조물 ㄷ. 거래처 신용, 영업상의 노하우 또는 점포 위치에 따른 영업상의 이점 등 무형의 재산적 가치 ㄹ. 주택이 철거될 경우 일정한 요건하에 택지개발지구 내에 이주자택지를 공급받을 지위인 대토권", "options": ["ㄱ, ㄴ", "ㄷ, ㄹ", "ㄱ, ㄴ, ㄹ", "ㄴ, ㄷ, ㄹ", "ㄱ, ㄴ, ㄷ, ㄹ"], "answer": [5], "explanation": "공인중개사법령에 따르면 중개대상물은 부동산으로 한정되며, 선박, 세차장 구조물, 무형의 재산적 가치는 중개대상물에 해당하지 않으므로 모두 중개대상물이 될 수 없다.", "subject": "공인중개사법", "topic": "중개대상물", "tags": ["중개대상물", "법령"], "year": 2025, "law_update": "법령 변경 없음", "status": "신규 문제"}
4. Merge
4. dataset classification: train
references
-
부동산학개론
-
민법 및 민사특별법 중 부동산중개에 관련되는 규정
-
공인중개사의 업무 및 부동산 거래 신고에 관한 법령 및 중개실무
-
부동산 공법 중 부동산 중개에 관련되는 규정
-
부동산 공시에 관한 법령 및 부동산관련 세법