{
  "cells": [
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "# Week 09 · 데이터 읽기 카드 미션\n",
        "\n",
        "CSV를 시각화하기 전에 구조와 맥락을 읽고, 독립적으로 열 수 있는 HTML 카드를 만듭니다.\n",
        "코드 경험이 없어도 **STEP 1과 STEP 3의 값만 수정**하면 완성할 수 있습니다.\n",
        "\n",
        "## 귀가 조건\n",
        "\n",
        "1. 파일명을 `week09_학번_이름.ipynb`로 바꿉니다.\n",
        "2. **런타임 → 런타임 다시 시작** 후 STEP 0부터 FINAL CHECK까지 순서대로 모두 실행합니다.\n",
        "3. `🎉 WEEK 09 DATA READING COMPLETE`를 확인합니다.\n",
        "4. 실행 결과가 보이는 `.ipynb`와 생성된 `_data_reading_card.html` 두 파일을 제출합니다."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## STEP 0 · 준비\n",
        "\n",
        "수업용 CSV가 없다면 같은 내용의 파일을 자동으로 만듭니다. 이 셀은 수정하지 않습니다."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "# STEP 0 · 수업용 데이터와 도구 준비 - 이 셀은 수정하지 않습니다.\n",
        "from pathlib import Path\n",
        "from html import escape\n",
        "\n",
        "import pandas as pd\n",
        "from IPython.display import HTML, display\n",
        "\n",
        "mission_step0_execution = get_ipython().execution_count\n",
        "FALLBACK_SOURCE_PATH = \"week09_creative_activity.csv\"\n",
        "SAMPLE_CSV = 'record_id,date,activity,duration_min,focus_level,mood_before,mood_after,location_type,used_reference,note\\n1,2026-05-04,스케치,35,3,피곤함,차분함,집,True,인물 손 연습\\n2,2026-05-05,코딩,55,4,보통,몰입함,학교,True,반복 패턴 수정\\n3,2026-05-06,읽기,25,2,산만함,보통,이동 중,False,전시 서문 읽기\\n4,2026-05-07,편집,70,5,보통,만족함,학교,True,중간 포스터 색 보정\\n5,2026-05-08,음악,40,3,피곤함,활기참,집,False,리듬 스케치\\n6,2026-05-09,스케치,20,,산만함,,카페,True,집중도 기록 누락\\n7,2026-05-10,코딩,65,4,차분함,만족함,집,True,함수 이름 정리\\n8,2026-05-11,읽기,30,3,보통,차분함,도서관,False,데이터 아트 사례\\n9,2026-05-12,편집,45,4,피곤함,보통,학교,True,레이어 순서 비교\\n10,2026-05-13,음악,50,5,활기참,몰입함,연습실,False,소리 질감 기록\\n11,2026-05-14,스케치,28,3,보통,차분함,카페,True,공간 형태 관찰\\n12,2026-05-15,코딩,80,5,차분함,만족함,학교,True,자동 검사 수정\\n13,2026-05-16,읽기,18,2,피곤함,보통,이동 중,False,짧은 논문 메모\\n14,2026-05-17,편집,60,4,보통,몰입함,집,True,포스터 여백 조정\\n15,2026-05-18,음악,35,,산만함,활기참,연습실,False,집중도 기록 누락\\n16,2026-05-19,스케치,42,4,차분함,만족함,학교,True,도형 변형 아이디어\\n17,2026-05-20,코딩,75,5,보통,몰입함,집,True,CSV 읽기 연습\\n18,2026-05-21,읽기,32,3,피곤함,차분함,도서관,False,데이터 윤리 사례\\n19,2026-05-22,편집,48,4,보통,만족함,카페,True,문장 길이 줄이기\\n20,2026-05-23,음악,58,4,활기참,몰입함,연습실,False,박자 변주 기록\\n21,2026-05-24,스케치,24,2,산만함,보통,이동 중,False,창문 형태 메모\\n22,2026-05-25,코딩,68,5,차분함,만족함,학교,True,데이터 카드 출력\\n23,2026-05-26,읽기,38,3,보통,차분함,도서관,False,공공데이터 설명서\\n24,2026-05-27,편집,52,4,피곤함,만족함,집,True,제출 파일 점검\\n'\n",
        "PROVIDED_DATASET_METADATA = {'title': '창작 활동 기록', 'source': '수업용 예시 데이터 - 교수자 제공', 'license': '수업 목적 사용 허용', 'unit': '한 번의 창작 활동', 'period': '2026-05-04부터 2026-05-27까지'}\n",
        "\n",
        "fallback_path = Path(FALLBACK_SOURCE_PATH)\n",
        "if not fallback_path.exists():\n",
        "    fallback_path.write_text(SAMPLE_CSV, encoding=\"utf-8\")\n",
        "\n",
        "print(\"준비 완료:\", fallback_path, \"|\", len(SAMPLE_CSV.splitlines()) - 1, \"records\")"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## STEP 1 · 나와 데이터에 관한 정보\n",
        "\n",
        "`EDIT` 영역의 학번·이름을 반드시 바꿉니다. 자신의 CSV를 쓰는 경우 `source_choice`를 `own`으로 바꾸고, 파일명·제목·출처·이용 조건·관찰 단위·기간·질문을 모두 자신의 자료에 맞게 수정합니다. 이름·연락처·정확한 위치 같은 개인정보가 포함된 파일은 사용하지 않습니다."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "# STEP 1 · EDIT - 제출 정보, 데이터 맥락, 질문을 자신의 내용으로 바꿉니다.\n",
        "mission_step1_execution = get_ipython().execution_count\n",
        "\n",
        "student_id = \"학번\"                       # 예: \"20261234\"\n",
        "student_name = \"이름\"                     # 예: \"김데이터\"\n",
        "\n",
        "source_choice = \"provided\"                # \"provided\" 또는 \"own\"\n",
        "source_path = FALLBACK_SOURCE_PATH         # own이면 업로드한 CSV 파일명\n",
        "\n",
        "dataset_title = PROVIDED_DATASET_METADATA[\"title\"]\n",
        "dataset_source = PROVIDED_DATASET_METADATA[\"source\"]\n",
        "dataset_license = PROVIDED_DATASET_METADATA[\"license\"]\n",
        "observation_unit = PROVIDED_DATASET_METADATA[\"unit\"]\n",
        "time_range = PROVIDED_DATASET_METADATA[\"period\"]\n",
        "\n",
        "answerable_question = \"EDIT: 현재 열로 답할 수 있는 질문을 15자 이상 작성하세요.\"\n",
        "needed_columns = [\"activity\", \"duration_min\"]\n",
        "unanswerable_question = \"EDIT: 현재 열로 답할 수 없는 질문을 15자 이상 작성하세요.\"\n",
        "missing_information = \"EDIT: 어떤 열이나 정보가 더 필요한지 15자 이상 설명하세요.\"\n",
        "\n",
        "output_filename = f\"week09_{student_id}_{student_name}_data_reading_card.html\"\n",
        "print(\"출력 예정:\", output_filename)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## STEP 2 · 구조 확인\n",
        "\n",
        "이 셀은 원본 CSV의 바이트를 기억한 뒤 pandas로 읽습니다. 앞부분, 크기, 열 이름, 자료형, 결측값을 차례대로 확인합니다. 출력의 `(24, 10)`은 **24행, 10열**이라는 뜻입니다."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "# STEP 2 · 데이터 불러오기와 여섯 가지 구조 검사 - 이 셀은 수정하지 않습니다.\n",
        "mission_step2_execution = get_ipython().execution_count\n",
        "\n",
        "source_path = Path(source_path)\n",
        "if not source_path.exists():\n",
        "    raise FileNotFoundError(\n",
        "        f\"{source_path} 파일을 찾을 수 없습니다. 왼쪽 파일 패널과 철자를 확인하세요.\"\n",
        "    )\n",
        "\n",
        "source_bytes_before = source_path.read_bytes()\n",
        "df = pd.read_csv(source_path)\n",
        "\n",
        "preview = df.head()\n",
        "dataset_shape = df.shape\n",
        "column_names = df.columns.tolist()\n",
        "column_types = df.dtypes.astype(str).to_dict()\n",
        "missing_counts = df.isna().sum().astype(int).to_dict()\n",
        "\n",
        "display(preview)\n",
        "print(\"shape (rows, columns):\", dataset_shape)\n",
        "print(\"columns:\", column_names)\n",
        "print(\"dtypes:\", column_types)\n",
        "print(\"missing values:\", missing_counts)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## STEP 3 · 실제 값 하나를 문장으로 읽기\n",
        "\n",
        "표에 실제로 존재하는 인덱스와 열을 하나 고릅니다. 카드가 인덱스·열 이름·원래 값을 자동으로 표시하므로, 설명에는 그 값이 **현실에서 무엇을 뜻하는지**를 자연스러운 문장으로 20자 이상 적습니다."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "# STEP 3 · EDIT - 실제 행·열·값 하나를 골라 자신의 문장으로 설명합니다.\n",
        "mission_step3_execution = get_ipython().execution_count\n",
        "\n",
        "selected_row_index = 1\n",
        "selected_column = \"duration_min\"\n",
        "selected_value_explanation = (\n",
        "    \"EDIT: 선택한 행·열·값이 현실에서 무엇을 뜻하는지 20자 이상 설명하세요.\"\n",
        ")\n",
        "\n",
        "selected_value = df.loc[selected_row_index, selected_column]\n",
        "print(\"선택한 행:\", selected_row_index)\n",
        "print(\"선택한 열:\", selected_column)\n",
        "print(\"선택한 값:\", selected_value)\n",
        "print(\"나의 설명:\", selected_value_explanation)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## STEP 4 · HTML 카드 생성\n",
        "\n",
        "작성한 정보와 검사 결과를 하나의 웹 문서로 만듭니다. 화면에 카드가 보이고 파일 저장 메시지가 나오면 정상입니다. 이 셀은 수정하지 않습니다."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "# STEP 4 · 데이터 읽기 카드 HTML 생성 - 이 셀은 수정하지 않습니다.\n",
        "mission_step4_execution = get_ipython().execution_count\n",
        "\n",
        "def render_question_cards(\n",
        "    answerable: object,\n",
        "    needed_columns: list[object],\n",
        "    unanswerable: object,\n",
        "    missing_reason: object,\n",
        "    labels: dict[str, str],\n",
        ") -> str:\n",
        "    \"\"\"Render the shared, non-sectioning question-card fragment.\"\"\"\n",
        "\n",
        "    safe_needed = \" + \".join(escape(str(name)) for name in needed_columns)\n",
        "    return f\"\"\"<div class=\"question\">\n",
        "        <span>{escape(labels['answerable'])}</span>\n",
        "        <strong>{escape(str(answerable))}</strong>\n",
        "        <p>{escape(labels['needed'])}: {safe_needed}</p>\n",
        "      </div>\n",
        "      <div class=\"question missing\">\n",
        "        <span>{escape(labels['unanswerable'])}</span>\n",
        "        <strong>{escape(str(unanswerable))}</strong>\n",
        "        <p>{escape(str(missing_reason))}</p>\n",
        "      </div>\"\"\"\n",
        "def render_data_reading_card(\n",
        "    metadata: dict[str, object],\n",
        "    shape: tuple[int, int],\n",
        "    columns: list[object],\n",
        "    missing_counts: dict[str, int],\n",
        "    questions: dict[str, object],\n",
        "    evidence: dict[str, object],\n",
        "    css_custom_properties: str,\n",
        "    extra_head: str = \"\",\n",
        ") -> str:\n",
        "    \"\"\"Render the exact card structure shared by the example and notebook.\"\"\"\n",
        "\n",
        "    safe_columns = \"\".join(\n",
        "        f\"<code>{escape(str(name))}</code>\" for name in columns\n",
        "    )\n",
        "    safe_missing = \"\".join(\n",
        "        f\"<li><code>{escape(str(name))}</code><strong>{int(count)}</strong></li>\"\n",
        "        for name, count in missing_counts.items()\n",
        "        if int(count) > 0\n",
        "    ) or \"<li><span>결측값 없음</span><strong>0</strong></li>\"\n",
        "    question_cards = render_question_cards(\n",
        "        questions[\"answerable\"],\n",
        "        questions[\"needed_columns\"],\n",
        "        questions[\"unanswerable\"],\n",
        "        questions[\"missing_reason\"],\n",
        "        {\n",
        "            \"answerable\": \"답할 수 있는 질문\",\n",
        "            \"needed\": \"필요한 열\",\n",
        "            \"unanswerable\": \"아직 답할 수 없는 질문\",\n",
        "        },\n",
        "    )\n",
        "\n",
        "    return f\"\"\"<!doctype html>\n",
        "<html lang=\"ko\">\n",
        "<head>\n",
        "  <meta charset=\"utf-8\">\n",
        "  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n",
        "  <title>{escape(str(metadata['title']))} · 데이터 읽기 카드</title>\n",
        "  {extra_head}\n",
        "  <style>\n",
        "    :root {{ color-scheme:light dark; {css_custom_properties} }}\n",
        "    * {{ box-sizing:border-box; }}\n",
        "    body {{ margin:0; padding:36px; color:var(--ink); background:var(--paper); font-family:Arial,\"Apple SD Gothic Neo\",\"Noto Sans KR\",sans-serif; }}\n",
        "    .skip-link {{ position:absolute; left:12px; top:-80px; padding:10px 14px; color:white; background:var(--ink); z-index:10; }}\n",
        "    .skip-link:focus {{ top:12px; }}\n",
        "    :where(a,[tabindex]):focus-visible {{ outline:3px solid var(--teal); outline-offset:3px; }}\n",
        "    main {{ max-width:1000px; margin:auto; padding:40px; border:3px solid var(--ink); background:var(--panel); }}\n",
        "    .kicker {{ color:var(--coral); font-weight:800; letter-spacing:.12em; }}\n",
        "    h1 {{ margin:.2rem 0 1.4rem; font-size:clamp(2rem,6vw,4.5rem); line-height:1; }}\n",
        "    h2 {{ margin-top:2rem; font-size:1rem; color:var(--teal); letter-spacing:.08em; }}\n",
        "    .meta {{ display:grid; grid-template-columns:repeat(2,1fr); border:1px solid var(--line); background:var(--line); gap:1px; }}\n",
        "    .meta div {{ padding:16px; background:var(--panel); }}\n",
        "    .meta span,.question span {{ display:block; margin-bottom:6px; color:var(--teal); font-size:.76rem; font-weight:800; letter-spacing:.08em; }}\n",
        "    .columns {{ display:flex; flex-wrap:wrap; gap:8px; }}\n",
        "    .columns code {{ padding:7px 9px; border:1px solid var(--ink); color:var(--ink); background:var(--panel); }}\n",
        "    .questions {{ display:grid; grid-template-columns:1fr 1fr; gap:16px; }}\n",
        "    .question {{ min-height:160px; padding:20px; border:2px solid var(--teal); }}\n",
        "    .question.missing {{ border-color:var(--coral); }}\n",
        "    .question.missing span {{ color:var(--coral); }}\n",
        "    .missing-list {{ display:grid; grid-template-columns:repeat(2,1fr); gap:8px; padding:0; list-style:none; }}\n",
        "    .missing-list li {{ display:flex; justify-content:space-between; padding:10px; border:1px solid var(--line); }}\n",
        "    .evidence {{ padding:18px; border-left:5px solid var(--teal); background:var(--paper); }}\n",
        "    footer {{ display:flex; justify-content:space-between; gap:16px; margin-top:26px; padding-top:16px; border-top:2px solid var(--ink); font-weight:800; }}\n",
        "    @media (max-width:720px) {{ body {{ padding:14px; }} main {{ padding:24px; }} .meta,.questions,.missing-list {{ grid-template-columns:1fr; }} footer {{ display:block; }} }}\n",
        "    @media (prefers-color-scheme:dark) {{ :root{{--ink:#edf1ed;--paper:#151917;--panel:#1c211e;--teal:#9bc1cb;--coral:#f2aaa3;--line:#556058}} }}\n",
        "  </style>\n",
        "</head>\n",
        "<body>\n",
        "  <a class=\"skip-link\" href=\"#main-content\">본문 바로가기</a>\n",
        "  <main id=\"main-content\">\n",
        "    <p class=\"kicker\">데이터 읽기 카드 / 9주차</p>\n",
        "    <h1>{escape(str(metadata['title']))}</h1>\n",
        "    <div class=\"meta\">\n",
        "      <div><span>크기</span><strong>{shape[0]}행 × {shape[1]}열</strong></div>\n",
        "      <div><span>관찰 단위</span><strong>{escape(str(metadata['unit']))}</strong></div>\n",
        "      <div><span>시간 범위</span><strong>{escape(str(metadata['period']))}</strong></div>\n",
        "      <div><span>출처</span><strong>{escape(str(metadata['source']))}</strong></div>\n",
        "      <div><span>이용 조건</span><strong>{escape(str(metadata['license']))}</strong></div>\n",
        "      <div><span>작성자</span><strong>{escape(str(metadata['student_id']))} · {escape(str(metadata['student_name']))}</strong></div>\n",
        "    </div>\n",
        "    <h2>열</h2>\n",
        "    <div class=\"columns\">{safe_columns}</div>\n",
        "    <h2>결측값</h2>\n",
        "    <ul class=\"missing-list\">{safe_missing}</ul>\n",
        "    <h2>질문과 한계</h2>\n",
        "    <div class=\"questions\">{question_cards}</div>\n",
        "    <h2>실제 값 읽기</h2>\n",
        "    <p class=\"evidence\"><strong>인덱스 {evidence['row_index']} · {escape(str(evidence['column']))} = {escape(str(evidence['value']))}</strong><br>{escape(str(evidence['explanation']))}</p>\n",
        "    <footer><span>시각화 전에 읽기</span><span>구조 → 맥락 → 질문</span></footer>\n",
        "  </main>\n",
        "</body>\n",
        "</html>\"\"\"\n",
        "\n",
        "def build_data_reading_card(metadata, dataframe, questions, evidence):\n",
        "    return render_data_reading_card(\n",
        "        metadata,\n",
        "        dataframe.shape,\n",
        "        list(dataframe.columns),\n",
        "        dataframe.isna().sum().astype(int).to_dict(),\n",
        "        questions,\n",
        "        evidence,\n",
        "        \"--ink:#1c1f1e; --paper:#f4f1e8; --panel:#fcfaf5; --teal:#25696f; --coral:#a83e32; --line:#a6a49a;\",\n",
        "    )\n",
        "\n",
        "card_metadata = {\n",
        "    \"title\": dataset_title,\n",
        "    \"source\": dataset_source,\n",
        "    \"license\": dataset_license,\n",
        "    \"unit\": observation_unit,\n",
        "    \"period\": time_range,\n",
        "    \"student_id\": student_id,\n",
        "    \"student_name\": student_name,\n",
        "}\n",
        "card_questions = {\n",
        "    \"answerable\": answerable_question,\n",
        "    \"needed_columns\": needed_columns,\n",
        "    \"unanswerable\": unanswerable_question,\n",
        "    \"missing_reason\": missing_information,\n",
        "}\n",
        "card_evidence = {\n",
        "    \"row_index\": selected_row_index,\n",
        "    \"column\": selected_column,\n",
        "    \"value\": selected_value,\n",
        "    \"explanation\": selected_value_explanation,\n",
        "}\n",
        "card_html = build_data_reading_card(card_metadata, df, card_questions, card_evidence)\n",
        "\n",
        "Path(output_filename).write_text(card_html, encoding=\"utf-8\")\n",
        "display(HTML(card_html))\n",
        "print(\"카드 저장 완료:\", output_filename)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## FINAL CHECK · PASS 확인\n",
        "\n",
        "먼저 **런타임 → 런타임 다시 시작**을 누릅니다. 그다음 **런타임 → 모두 실행**을 눌러 실행 번호가 1부터 6까지 이어지게 합니다. 실패한 줄은 `FIX`로 표시됩니다. 해당 STEP만 고친 뒤 다시 처음부터 실행합니다."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "# FINAL CHECK · 런타임 재시작 후 STEP 0부터 여기까지 모두 실행합니다.\n",
        "mission_final_execution = get_ipython().execution_count\n",
        "\n",
        "execution_order_ok = (\n",
        "    mission_step0_execution,\n",
        "    mission_step1_execution,\n",
        "    mission_step2_execution,\n",
        "    mission_step3_execution,\n",
        "    mission_step4_execution,\n",
        "    mission_final_execution,\n",
        ") == (1, 2, 3, 4, 5, 6)\n",
        "\n",
        "safe_student_id = str(student_id).strip()\n",
        "safe_student_name = str(student_name).strip()\n",
        "expected_output_filename = (\n",
        "    f\"week09_{safe_student_id}_{safe_student_name}_data_reading_card.html\"\n",
        ")\n",
        "submitted_dataset_metadata = {\n",
        "    \"title\": dataset_title,\n",
        "    \"source\": dataset_source,\n",
        "    \"license\": dataset_license,\n",
        "    \"unit\": observation_unit,\n",
        "    \"period\": time_range,\n",
        "}\n",
        "provided_source_ok = (\n",
        "    source_choice == \"provided\"\n",
        "    and source_path.name == FALLBACK_SOURCE_PATH\n",
        "    and source_bytes_before == SAMPLE_CSV.encode(\"utf-8\")\n",
        "    and submitted_dataset_metadata == PROVIDED_DATASET_METADATA\n",
        ")\n",
        "own_source_ok = (\n",
        "    source_choice == \"own\"\n",
        "    and source_path.name != FALLBACK_SOURCE_PATH\n",
        "    and all(\n",
        "        submitted_dataset_metadata[key] != provided_value\n",
        "        for key, provided_value in PROVIDED_DATASET_METADATA.items()\n",
        "    )\n",
        ")\n",
        "checks = {\n",
        "    \"새 런타임에서 STEP 0부터 순서대로 실행\": execution_order_ok,\n",
        "    \"학번 수정\": safe_student_id not in {\"\", \"학번\", \"20260000\"},\n",
        "    \"이름 수정\": safe_student_name not in {\"\", \"이름\", \"홍길동\"},\n",
        "    \"파일명에 사용할 수 없는 / 제외\": \"/\" not in safe_student_id + safe_student_name,\n",
        "    \"provided 또는 own 출처 선택\": provided_source_ok or own_source_ok,\n",
        "    \"출처·이용 조건·관찰 단위·기간 기록\": all(\n",
        "        isinstance(value, str) and len(value.strip()) >= 4\n",
        "        for value in [\n",
        "            dataset_title,\n",
        "            dataset_source,\n",
        "            dataset_license,\n",
        "            observation_unit,\n",
        "            time_range,\n",
        "        ]\n",
        "    ),\n",
        "    \"원본 CSV 보존\": source_path.read_bytes() == source_bytes_before,\n",
        "    \"최소 5행 4열\": df.shape[0] >= 5 and df.shape[1] >= 4,\n",
        "    \"수치형 열 포함\": len(df.select_dtypes(include=\"number\").columns) >= 1,\n",
        "    \"문자 또는 범주형 열 포함\": len(df.select_dtypes(exclude=\"number\").columns) >= 1,\n",
        "    \"답할 수 있는 질문 직접 작성\": not answerable_question.strip().startswith(\"EDIT:\")\n",
        "    and len(answerable_question.strip()) >= 15,\n",
        "    \"질문에 필요한 열 한 개 이상\": len(needed_columns) >= 1\n",
        "    and set(needed_columns).issubset(df.columns),\n",
        "    \"답할 수 없는 질문 직접 작성\": not unanswerable_question.strip().startswith(\"EDIT:\")\n",
        "    and len(unanswerable_question.strip()) >= 15,\n",
        "    \"누락 정보 직접 설명\": not missing_information.strip().startswith(\"EDIT:\")\n",
        "    and len(missing_information.strip()) >= 15,\n",
        "    \"선택한 행이 실제 인덱스에 존재\": selected_row_index in df.index,\n",
        "    \"선택한 열이 실제 열에 존재\": selected_column in df.columns,\n",
        "    \"선택한 값 설명 직접 작성\": not selected_value_explanation.strip().startswith(\"EDIT:\")\n",
        "    and len(selected_value_explanation.strip()) >= 20,\n",
        "    \"정확한 HTML 파일명\": output_filename == expected_output_filename,\n",
        "    \"HTML 파일 생성\": Path(output_filename).exists(),\n",
        "    \"카드에 데이터 제목 포함\": escape(dataset_title) in card_html,\n",
        "    \"카드에 두 질문 포함\": escape(answerable_question) in card_html\n",
        "    and escape(unanswerable_question) in card_html,\n",
        "    \"카드에 실제 값 설명 포함\": escape(selected_value_explanation) in card_html,\n",
        "}\n",
        "\n",
        "failed_checks = [label for label, passed in checks.items() if not passed]\n",
        "for label, passed in checks.items():\n",
        "    print(\"PASS\" if passed else \"FIX \", \"·\", label)\n",
        "\n",
        "if failed_checks:\n",
        "    raise AssertionError(\"수정할 항목: \" + \", \".join(failed_checks))\n",
        "\n",
        "print(\"\\n🎉 WEEK 09 DATA READING COMPLETE\")\n",
        "print(\"제출 1:\", f\"week09_{safe_student_id}_{safe_student_name}.ipynb\")\n",
        "print(\"제출 2:\", output_filename)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## 제출\n",
        "\n",
        "- `week09_학번_이름.ipynb`\n",
        "- `week09_학번_이름_data_reading_card.html`\n",
        "\n",
        "두 파일의 이름과 업로드 상태를 확인한 뒤 제출합니다. 작업 속도는 평가하지 않습니다."
      ]
    }
  ],
  "metadata": {
    "colab": {
      "provenance": []
    },
    "kernelspec": {
      "display_name": "Python 3",
      "language": "python",
      "name": "python3"
    },
    "language_info": {
      "name": "python",
      "version": "3.x"
    }
  },
  "nbformat": 4,
  "nbformat_minor": 5
}
