{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Week 07 · 변형·합성 프로토타입 미션\n",
    "\n",
    "하나의 원본에서 자르기·크기 변경·회전을 적용한 레이어 세 개를 만들고, 1000 × 1000 RGBA 캔버스에 합성합니다. `EDIT` 표시가 있는 값만 수정합니다. 마지막 셀의 **WEEK 07 TRANSFORMATION PROTOTYPE COMPLETE**를 확인한 뒤 노트북과 PNG를 제출합니다."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## STEP 0 · 준비와 수업용 원본 생성\n",
    "이 셀은 실습에 필요한 라이브러리와 6주차 결과물이 없을 때 사용할 대체 이미지를 준비합니다. 수정하지 않고 실행합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# DO NOT EDIT · 준비 셀\n",
    "from pathlib import Path\n",
    "from PIL import Image, ImageDraw\n",
    "from IPython.display import display\n",
    "\n",
    "mission_step0_execution = get_ipython().execution_count\n",
    "FALLBACK_SOURCE_PATH = \"week07_source_poster.png\"\n",
    "LAYER_SIZES = ((420, 420), (340, 340), (260, 260))\n",
    "\n",
    "def build_fallback_source(path):\n",
    "    image = Image.new(\"RGBA\", (900, 900), (242, 238, 226, 255))\n",
    "    draw = ImageDraw.Draw(image)\n",
    "    ink = (29, 33, 31, 255)\n",
    "    draw.rectangle((63, 63, 837, 837), outline=ink, width=6)\n",
    "    draw.rectangle((63, 63, 315, 837), fill=(38, 104, 111, 255))\n",
    "    draw.ellipse((162, 135, 648, 621), fill=(234, 184, 62, 255), outline=ink, width=6)\n",
    "    draw.polygon([(522, 108), (792, 387), (486, 531)], fill=(218, 92, 74, 255), outline=ink)\n",
    "    draw.rectangle((351, 495, 765, 756), fill=(70, 91, 145, 255), outline=ink, width=6)\n",
    "    for offset in range(0, 306, 50):\n",
    "        draw.line((90, 648 + offset, 297, 576 + offset), fill=(225, 217, 196, 255), width=7)\n",
    "    draw.ellipse((414, 549, 603, 738), fill=(133, 177, 151, 255), outline=ink, width=5)\n",
    "    draw.line((81, 801, 819, 801), fill=ink, width=7)\n",
    "    image.save(path)\n",
    "\n",
    "build_fallback_source(FALLBACK_SOURCE_PATH)\n",
    "print(\"STEP 0 완료 · 수업용 원본이 준비되었습니다.\")\n",
    "display(Image.open(FALLBACK_SOURCE_PATH))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## STEP 1 · 제출 정보와 출처 기록\n",
    "학번, 이름, 구성 의도를 작성합니다. 수업용 원본을 사용하면 아래 출처 정보는 유지합니다. 자신의 6주차 PNG를 사용하면 파일을 업로드한 뒤 `source_path`와 출처 다섯 항목을 실제 정보로 바꿉니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# EDIT 1 · 따옴표 안의 내용을 자신의 정보로 바꿉니다.\n",
    "student_id = \"학번을 입력하세요\"\n",
    "student_name = \"이름을 입력하세요\"\n",
    "composition_intent = \"구성 의도를 20자 이상 작성하세요.\"\n",
    "output_filename = f\"week07_{student_id}_{student_name}.png\"\n",
    "\n",
    "# EDIT 2 · 기본값은 수업용 원본입니다. 자신의 파일을 쓰면 여섯 값을 모두 수정합니다.\n",
    "source_choice = \"provided\"  # provided 또는 own\n",
    "source_path = FALLBACK_SOURCE_PATH\n",
    "source_title = \"Week 07 Source Poster\"\n",
    "source_creator = \"Course-provided asset\"\n",
    "source_url = \"Bundled in the Week 07 notebook\"\n",
    "source_license = \"Course use / provided asset\"\n",
    "change_description = \"자르기, 크기 변경, 회전, 투명도 조절과 레이어 합성\"\n",
    "\n",
    "mission_step1_execution = get_ipython().execution_count\n",
    "print(\"제출자:\", student_id, student_name)\n",
    "print(\"구성 의도:\", composition_intent)\n",
    "print(\"원본 기록:\", source_title, \"/\", source_creator, \"/\", source_license)\n",
    "print(\"변형 내용:\", change_description)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## STEP 2 · 원본 열기, 작업본 복사, 자르기 영역 선택\n",
    "처음 값은 원본 전체를 선택하므로 마지막 검사에서 통과하지 않습니다. `crop_box`를 `(left, top, right, bottom)` 순서로 수정해 원본보다 작은 영역을 선택합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# DO NOT EDIT · 원본 열기와 작업본 만들기\n",
    "assert Path(source_path).exists(), f\"파일을 찾을 수 없습니다: {source_path}\"\n",
    "source_bytes_before = Path(source_path).read_bytes()\n",
    "source_image = Image.open(source_path).convert(\"RGBA\")\n",
    "working_image = source_image.copy()\n",
    "\n",
    "# EDIT 3 · 원본보다 작은 유효한 영역으로 바꿉니다.\n",
    "crop_box = (0, 0, working_image.width, working_image.height)\n",
    "\n",
    "cropped_preview = working_image.crop(crop_box)\n",
    "mission_step2_execution = get_ipython().execution_count\n",
    "print(\"원본 크기:\", source_image.size, \"/ 자르기 영역:\", crop_box)\n",
    "print(\"자른 미리보기 크기:\", cropped_preview.size)\n",
    "display(cropped_preview)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## STEP 3 · 세 레이어 변형하고 합성하기\n",
    "`angles`, `alphas`, `positions`만 수정합니다. 각도에는 음수와 양수를 모두 사용하고, 알파 값은 최소 두 가지로 구분하며, 위치 세 개는 서로 달라야 합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# DO NOT EDIT · 한 원본을 하나의 변형 레이어로 만드는 함수\n",
    "def make_layer(source, crop_box, target_size, angle, alpha):\n",
    "    layer = source.copy()\n",
    "    layer = layer.crop(crop_box)\n",
    "    layer = layer.resize(target_size, Image.Resampling.LANCZOS)\n",
    "    layer.putalpha(alpha)\n",
    "    layer = layer.rotate(angle, expand=True, resample=Image.Resampling.BICUBIC, fillcolor=(0, 0, 0, 0))\n",
    "    return layer\n",
    "\n",
    "# EDIT 4 · 시작값은 모두 같으므로 완료 조건을 통과하지 않습니다.\n",
    "angles = [0, 0, 0]\n",
    "alphas = [255, 255, 255]\n",
    "positions = [(300, 300), (300, 300), (300, 300)]\n",
    "\n",
    "# DO NOT EDIT · 레이어 생성과 합성\n",
    "layers = []\n",
    "for target_size, angle, alpha in zip(LAYER_SIZES, angles, alphas):\n",
    "    layer = make_layer(working_image, crop_box, target_size, angle, alpha)\n",
    "    layers.append(layer)\n",
    "\n",
    "canvas = Image.new(\"RGBA\", (1000, 1000), (29, 33, 31, 255))\n",
    "for layer, position in zip(layers, positions):\n",
    "    canvas.alpha_composite(layer, dest=position)\n",
    "\n",
    "mission_step3_execution = get_ipython().execution_count\n",
    "print(\"각도:\", angles)\n",
    "print(\"알파:\", alphas)\n",
    "print(\"위치:\", positions)\n",
    "print(\"실제 레이어 크기:\", [layer.size for layer in layers])\n",
    "display(canvas)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## STEP 4 · PNG 저장\n",
    "값을 마지막으로 수정한 뒤 STEP 3을 다시 실행하고, 이어서 이 저장 셀을 실행합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# DO NOT EDIT · 최종 PNG 저장\n",
    "canvas.save(output_filename)\n",
    "mission_step4_execution = get_ipython().execution_count\n",
    "print(\"저장 완료:\", output_filename)\n",
    "display(Image.open(output_filename))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## FINAL CHECK · 새 세션에서 모두 실행\n",
    "런타임을 다시 시작한 뒤 **모두 실행**합니다. 오류가 나면 마지막 한글 안내를 읽고 해당 `EDIT` 값만 고칩니다. 이 셀은 수정하지 않습니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# DO NOT EDIT · FINAL CHECK\n",
    "mission_final_execution = get_ipython().execution_count\n",
    "assert (\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",
    "assert student_id.strip() and \"입력\" not in student_id, \"학번을 실제 값으로 바꾸세요.\"\n",
    "assert student_name.strip() and \"입력\" not in student_name, \"이름 또는 제출 확인이 가능한 별명을 작성하세요.\"\n",
    "assert all(character not in student_id + student_name for character in (\"/\", \"\\\\\")), \"학번과 이름에는 / 또는 \\\\를 사용할 수 없습니다.\"\n",
    "assert len(composition_intent.strip()) >= 20 and \"작성하세요\" not in composition_intent, \"구성 의도를 20자 이상의 완전한 문장으로 작성하세요.\"\n",
    "expected_filename = f\"week07_{student_id}_{student_name}.png\"\n",
    "assert output_filename == expected_filename, f\"PNG 파일명은 {expected_filename}이어야 합니다.\"\n",
    "\n",
    "assert source_choice in {\"provided\", \"own\"}, \"source_choice는 provided 또는 own입니다.\"\n",
    "source_fields = {\n",
    "    \"제목\": source_title,\n",
    "    \"창작자\": source_creator,\n",
    "    \"원본 주소\": source_url,\n",
    "    \"라이선스\": source_license,\n",
    "    \"변형 내용\": change_description,\n",
    "}\n",
    "for field_name, field_value in source_fields.items():\n",
    "    assert isinstance(field_value, str) and field_value.strip(), f\"{field_name} 기록을 작성하세요.\"\n",
    "provided_source_record = (\n",
    "    \"Week 07 Source Poster\",\n",
    "    \"Course-provided asset\",\n",
    "    \"Bundled in the Week 07 notebook\",\n",
    "    \"Course use / provided asset\",\n",
    ")\n",
    "current_source_record = (source_title, source_creator, source_url, source_license)\n",
    "if source_choice == \"provided\":\n",
    "    assert Path(source_path).resolve() == Path(FALLBACK_SOURCE_PATH).resolve(), \"수업용 원본은 source_path를 FALLBACK_SOURCE_PATH로 유지하세요.\"\n",
    "    assert current_source_record == provided_source_record, \"수업용 원본의 기본 출처 기록을 그대로 유지하세요.\"\n",
    "else:\n",
    "    assert Path(source_path).resolve() != Path(FALLBACK_SOURCE_PATH).resolve(), \"자신의 이미지를 업로드하고 source_path를 그 파일명으로 바꾸세요.\"\n",
    "    assert current_source_record != provided_source_record, \"자신의 이미지에 맞게 제목·창작자·원본 주소·라이선스를 모두 바꾸세요.\"\n",
    "    assert source_title != provided_source_record[0], \"제목을 자신의 이미지 제목으로 바꾸세요.\"\n",
    "    assert source_creator != provided_source_record[1], \"창작자를 자신의 이름 또는 원저작자 이름으로 바꾸세요.\"\n",
    "    assert source_url != provided_source_record[2], \"원본 주소를 링크 또는 자신의 파일명으로 바꾸세요.\"\n",
    "    assert source_license != provided_source_record[3], \"이용 근거를 자신의 창작물, CC0처럼 실제 조건으로 바꾸세요.\"\n",
    "assert Path(source_path).read_bytes() == source_bytes_before, \"원본 파일이 바뀌었습니다. 원본을 다시 준비하세요.\"\n",
    "assert Path(source_path).resolve() != Path(output_filename).resolve(), \"원본과 결과물 파일명을 다르게 작성하세요.\"\n",
    "\n",
    "assert isinstance(crop_box, tuple) and len(crop_box) == 4, \"crop_box는 (left, top, right, bottom) 튜플이어야 합니다.\"\n",
    "assert all(isinstance(value, int) for value in crop_box), \"crop_box의 네 값은 정수여야 합니다.\"\n",
    "left, top, right, bottom = crop_box\n",
    "assert 0 <= left < right <= source_image.width, \"crop_box의 left와 right를 원본 너비 안에서 확인하세요.\"\n",
    "assert 0 <= top < bottom <= source_image.height, \"crop_box의 top과 bottom을 원본 높이 안에서 확인하세요.\"\n",
    "assert (right - left, bottom - top) != source_image.size, \"원본 전체보다 작은 영역을 자르세요.\"\n",
    "\n",
    "assert len(layers) >= 3, \"변형 레이어를 세 개 이상 만드세요.\"\n",
    "assert len(angles) == len(alphas) == len(positions) == len(LAYER_SIZES) == 3, \"각도·알파·위치는 각각 세 개여야 합니다.\"\n",
    "assert all(isinstance(angle, (int, float)) and -30 <= angle <= 30 for angle in angles), \"각도는 -30도부터 30도 사이의 숫자로 작성하세요.\"\n",
    "assert sum(angle != 0 for angle in angles) >= 2, \"0이 아닌 회전 각도를 두 개 이상 사용하세요.\"\n",
    "assert any(angle < 0 for angle in angles) and any(angle > 0 for angle in angles), \"음수 각도와 양수 각도를 모두 사용하세요.\"\n",
    "assert all(isinstance(alpha, int) and 100 <= alpha <= 240 for alpha in alphas), \"알파 값은 100부터 240 사이의 정수로 작성하세요.\"\n",
    "assert len(set(alphas)) >= 2, \"서로 다른 알파 값을 최소 두 가지 사용하세요.\"\n",
    "assert len(set(positions)) == 3, \"세 레이어의 위치를 서로 다르게 작성하세요.\"\n",
    "for layer, position in zip(layers, positions):\n",
    "    assert isinstance(position, tuple) and len(position) == 2, \"각 위치는 (x, y) 튜플이어야 합니다.\"\n",
    "    x, y = position\n",
    "    assert isinstance(x, int) and isinstance(y, int), \"위치의 x와 y는 정수여야 합니다.\"\n",
    "    assert 0 <= x and 0 <= y and x + layer.width <= 1000 and y + layer.height <= 1000, \"각 레이어가 1000 × 1000 캔버스 안에 들어오도록 위치를 조정하세요.\"\n",
    "\n",
    "assert canvas.mode == \"RGBA\" and canvas.size == (1000, 1000), \"캔버스는 1000 × 1000 RGBA여야 합니다.\"\n",
    "assert Path(output_filename).exists(), \"PNG 저장 셀을 실행하세요.\"\n",
    "checked_file = Image.open(output_filename).convert(\"RGBA\")\n",
    "assert checked_file.size == canvas.size, \"저장된 PNG 크기를 확인하세요.\"\n",
    "assert checked_file.tobytes() == canvas.tobytes(), \"PNG가 최신 상태가 아닙니다. STEP 3과 STEP 4를 다시 실행하세요.\"\n",
    "\n",
    "print(\"✅ 제출 정보와 구성 의도 검사 통과\")\n",
    "print(\"✅ 제목·창작자·원본 주소·라이선스·변형 내용 기록 통과\")\n",
    "print(\"✅ 자르기·크기 변경·회전 레이어 세 개 검사 통과\")\n",
    "print(\"✅ 1000 × 1000 RGBA 합성과 PNG 최신 상태 검사 통과\")\n",
    "print(\"🎉 WEEK 07 TRANSFORMATION PROTOTYPE COMPLETE\")"
   ]
  }
 ],
 "metadata": {
  "colab": {
   "name": "week-07-transformation-mission.ipynb",
   "provenance": []
  },
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
