소개
지역 공동 저녁 모임에 참석하는 모든 사람에게 초대장이 필요합니다. CSV 게스트 명단, JSON 행사 정보, 준비된 HTML 디자인을 이용해 새로운 개인별 초대장 파일 배치를 만듭니다. 이름이 같은 두 사람도 별도 파일을 받아야 하며, Zoë, "Z" & <Friends> 같은 이름은 문자 그대로 표시되어야 합니다.
Python 문자열, 딕셔너리, 반복문, 함수와 기본 터미널 사용법을 알아야 합니다. 이 실습에서는 파일 읽기, CSV 레코드, 템플릿, HTML 텍스트 이스케이프, 새 출력 디렉터리를 설명합니다. CLI와 페이지 디자인은 제공되며, 필드 매핑, 전체 검증, 게스트별 생성을 구현합니다. CSS 작성, 이메일 전송, PDF 제작은 다루지 않습니다. 읽기와 일반적인 실수를 포함한 작성자의 예상 시간은 55~75분입니다.
첫 번째 초대장 열기
이 단계에서는 정상적인 초대장 하나를 생성하고 페이지 내용을 입력 필드와 연결합니다. WebIDE에서 Terminal → New Terminal을 열고 labex 셸 프롬프트가 나올 때까지 기다리세요. 창이 좁으면 Terminal이 More(...) 안에 있을 수 있습니다. 모든 명령은 이 터미널에서 실행합니다. 준비된 작업 공간은 이전 실습과 독립적입니다.
cd는 현재 디렉터리를 변경합니다. 절대 경로는 /로 시작하며, 상대 경로는 현재 디렉터리를 기준으로 해석됩니다. 다른 디렉터리로 이동한 뒤 파일을 찾지 못하면 같은 명령으로 여기로 돌아오세요.
cd /home/labex/project/invitations
cat은 텍스트 파일을 표시합니다. 행사 정보를 읽으세요.
cat event.json
JSON은 객체를 중괄호로 표시하고, 문자열 키와 값을 큰따옴표로 감쌉니다. 이 네 값이 초대장의 제목, 날짜, 시간, 장소가 됩니다. 바로 실행할 수 있는 one.py 예제는 이 값과 한 명의 게스트를 제공된 디자인에 이미 매핑합니다. python3는 Python 파일을 실행합니다. 새로운 batches/first 디렉터리에 예제를 생성하세요.
python3 one.py
예상 출력:
Created batch: batches/first
플랫폼의 WebIDE 탭에서 Web 8080으로 전환하세요. 환경 준비 과정에서 초대장 뷰어가 이미 시작되었으므로 Go Live는 필요하지 않습니다. 미리 보기를 새로 고치고 first/g001.html을 찾으세요. Open invitation을 클릭하면 실제 HTML 파일이 새 브라우저 탭에서 열립니다. Maya Patel, The Long Table, 시간과 장소를 읽어 보세요. 그 브라우저 탭을 닫으면 Web 8080으로 돌아갑니다. Download HTML은 같은 파일을 저장하여 실습 밖에서도 사용할 수 있게 합니다.

인사말에서 게스트를 확인할 수 있고, 아래 두 영역에 행사 시간과 장소가 표시됩니다.
미리 보기는 batches에 실제로 있는 파일만 읽습니다. 없는 초대장을 생성하지 않습니다. 기존 batches/archive는 이전 작업으로 보존됩니다. 완성할 프로그램은 이 파일과 모든 입력을 유지하면서 새 배치를 만듭니다. 다음 단계를 위해 WebIDE로 돌아오세요.
CSV 레코드를 HTML 텍스트에 매핑하기
이 단계에서는 재사용 가능한 렌더러가 각 게스트 값을 올바른 텍스트 영역에 넣도록 만듭니다. 같은 WebIDE 터미널과 작업 공간으로 돌아오세요. 아래 상대 경로의 기준은 /home/labex/project/invitations입니다.
쉼표로 나누지 말고 레코드 읽기
CSV는 표 형식입니다. 첫 행에는 필드 이름이 있습니다. 쉼표는 필드를 구분하지만, 큰따옴표로 둘러싼 필드 안의 쉼표는 예외입니다. 그런 필드 안의 큰따옴표는 두 번 연속 기록합니다. 따라서 split(",")로는 모든 행을 올바르게 읽을 수 없습니다. 제공된 명단을 읽으세요.
cat guests.csv
guest_id 값이 서로 다른 Alex Chen 레코드가 두 개 있습니다. 마지막 게스트 이름에는 쉼표, 따옴표, &, 꺾쇠괄호가 포함됩니다. 모두 이름에 사용할 수 있는 문자입니다. 사람을 구분하는 것은 안정적인 ID이며 표시 이름이 아닙니다.
준비된 generate.py가 이 레코드를 읽습니다. 렌더러를 바꾸기 전에 내용을 읽으세요.
cat generate.py
with ... as source는 파일을 열고 들여쓴 블록이 끝나면 닫습니다. encoding="utf-8"은 Unicode 문자를 보존합니다. newline=""은 레코드 경계 처리를 CSV 읽기 도구에 맡깁니다. csv.DictReader(source)는 헤더를 사용해 레코드마다 딕셔너리를 만들고, list(...)는 레코드를 리스트로 읽습니다. 예를 들어 guest["name"]은 이름에 속하는 쉼표까지 포함하는 전체 이름입니다. json.loads(...)는 행사 JSON 텍스트를 딕셔너리로 바꿉니다. 이 읽기 처리와 CLI는 이미 구현되어 있습니다.
CSV 따옴표와 HTML 이스케이프 구분하기
HTML 페이지는 <...>를 태그로, &...;를 문자 참조로 사용합니다. CSV 따옴표는 표 구조를 보호할 뿐, <Friends> 같은 이름이 HTML로 해석되는 것을 막지 못합니다. html.escape(value, quote=True)는 특수 문자를 문자 참조로 바꾸고, 브라우저는 이를 원래 텍스트로 표시합니다. 여기서 모든 값은 HTML 텍스트 영역에 들어갑니다. 임의의 JavaScript, CSS, URL에 데이터를 넣는 일반적인 방법은 아닙니다.
Template은 invitation.html의 $name, $title, 다른 $field 자리표시자를 사용합니다. substitute(mapping)은 자리표시자를 해당 딕셔너리 값으로 바꾸며, 필요한 매핑이 없으면 거부합니다. 본문의 여섯 마커 data-field="name", message, title, date, time, venue가 의미별 텍스트 영역을 구분합니다. 각 영역을 하나씩 유지하고 해당 텍스트를 넣으세요. 제공된 입력 템플릿은 변경하지 않습니다. 생성된 출력에서는 동등한 태그, 배치, HTML 엔터티 표기를 허용합니다. 페이지의 <title>에만 이름이 있어서는 본문의 이름 영역을 채운 것이 아닙니다.
다음 렌더러를 만드세요. cat > rendering.py는 파일을 쓰며 이전 내용을 대체합니다. 따옴표로 감싼 here-document 구분자 <<'PYCODE'는 종료 마커 PYCODE까지의 내용을 그대로 텍스트로 취급합니다. 종료 마커는 별도 행에 있어야 합니다. 렌더러만 대체되며 입력 파일은 바뀌지 않습니다.
딕셔너리는 게스트와 행사 필드를 명시적으로 매핑합니다. values.items()는 키/값 쌍을 만듭니다. 딕셔너리 컴프리헨션 {key: ... for key, value in values.items()}은 키를 보존하면서 각 값을 이스케이프합니다. from html import escape는 표준 라이브러리 함수를 가져옵니다.
cat > rendering.py <<'PYCODE'
from html import escape
def render_guest(guest, event, template):
values = {"name": guest["name"], "message": guest["message"],
"title": event["title"], "date": event["date"],
"time": event["time"], "venue": event["venue"]}
return template.substitute({key: escape(value, quote=True)
for key, value in values.items()})
PYCODE
이 함수는 HTML 텍스트를 반환하며 파일은 만들지 않습니다. 로컬 보조 모듈은 generate.py 옆의 최상위 .py 파일로 둘 수 있습니다. 패키지 하위 디렉터리는 이 실습의 범위 밖입니다. 이스케이프는 치환 전에 한 번만 수행하므로 브라우저에는 &라는 문자열이 아니라 &가 표시됩니다. 이제 초대장의 디자인과 개인별 텍스트를 분리했습니다.
배치를 만들기 전에 모든 게스트 검증하기
이 단계에서는 이전 작업을 보호하면서 ID마다 파일을 하나씩 생성합니다. 같은 WebIDE 터미널에서 계속하세요.
초기 batch.py는 이름을 파일명으로 사용하며 기존 디렉터리도 허용합니다. Alex Chen이라는 두 사람이 서로의 파일을 덮어쓸 수 있습니다. 이 짧은 함수를 읽으세요.
cat batch.py
계약은 각 행의 <guest_id>.html을 담는 새 배치 디렉터리를 만드는 것입니다. ID에는 ASCII 소문자, 숫자, 하이픈만 허용됩니다. 이름과 메시지는 이 문자 제한을 받지 않는 UTF-8 텍스트입니다. 게스트 필수 필드는 guest_id, name, message이며, 행사 필수 필드는 title, date, time, venue입니다. 필수 값이 비었거나 공백뿐인 경우, ID 중복, 이미 존재하는 출력 디렉터리는 유용한 오류와 함께 거부해야 합니다. CSV, 두 행사 파일, 템플릿, batches/archive를 변경하지 마세요.
준비와 쓰기 분리하기
먼저 모든 레코드를 검증하고 모든 페이지를 렌더링하세요. 이 작업이 성공한 뒤에만 디렉터리와 파일을 만듭니다. 그렇지 않으면 마지막 레코드의 오류 때문에 불완전한 배치가 남을 수 있습니다. 여기서 배우는 입력 오류에 대한 보호이며, 디스크 장애나 갑작스러운 전원 차단에 대한 트랜잭션 보장은 아닙니다.
isinstance(value, str)는 값이 텍스트인지 검사합니다. event.get(field)는 키가 없으면 None을 반환하므로 알아보기 쉬운 오류를 보고할 수 있습니다. strip()은 공백 외의 내용이 있는지 확인하지만, 나중에 렌더링할 원래 텍스트를 바꾸지 않습니다. raise ValueError(...)는 잘못된 입력을 보고합니다. 준비된 CLI는 이 예외를 처리하여 Cannot generate invitations: ...를 출력하고 실패 상태로 끝납니다.
set()은 이미 본 ID를 저장합니다. in은 포함 여부를 확인하고 seen.add(...)는 ID를 기억합니다. all(...)은 모든 문자가 공개된 문자 규칙을 만족하는지 검사합니다. pages.append((guest_id, page))는 ID/페이지 쌍을 저장합니다. 마지막으로 for guest_id, page in pages는 이 쌍을 풀어 받습니다.
Path(output)은 요청한 디렉터리를 나타냅니다. mkdir()는 디렉터리를 만들며, exist_ok=True를 전달하지 않았으므로 기존 경로는 거부합니다. / 연산자는 경로를 연결합니다. f"{guest_id}.html" 같은 f-string은 ID를 파일명에 넣습니다. write_text(..., encoding="utf-8")는 전체 HTML 텍스트를 UTF-8로 기록합니다.
batch.py만 다음 핵심 구현으로 바꾸세요.
cat > batch.py <<'PYCODE'
from pathlib import Path
from rendering import render_guest
def write_batch(guests, event, template, output):
for field in ("title", "date", "time", "venue"):
if not isinstance(event.get(field), str) or not event[field].strip():
raise ValueError(f"Missing event field: {field}")
if not guests:
raise ValueError("The guest list is empty")
seen = set()
pages = []
for guest in guests:
for field in ("guest_id", "name", "message"):
if not isinstance(guest.get(field), str) or not guest[field].strip():
raise ValueError(f"Missing guest field: {field}")
guest_id = guest["guest_id"]
if not all(character in "abcdefghijklmnopqrstuvwxyz0123456789-" for character in guest_id):
raise ValueError("Guest IDs must use lowercase letters, digits, or hyphens")
if guest_id in seen:
raise ValueError(f"Duplicate guest ID: {guest_id}")
seen.add(guest_id)
pages.append((guest_id, render_guest(guest, event, template)))
output = Path(output)
output.mkdir() ## An existing batch is always refused.
for guest_id, page in pages:
(output / f"{guest_id}.html").write_text(page, encoding="utf-8")
PYCODE
from rendering import render_guest는 로컬 렌더러를 사용합니다. 첫 반복문은 행사 필드를 검사하고, 다음 반복문은 게스트를 검증하며 페이지를 준비하고, 마지막 반복문은 완성된 배치를 기록합니다. 검증과 렌더링 반복문이 끝나기 전에는 파일을 만들지 않습니다.
CLI는 위치 인자로 게스트 CSV, 행사 JSON, HTML 템플릿, 새 출력 디렉터리의 네 경로를 순서대로 받습니다. 전체 명단으로 실행하세요.
python3 generate.py guests.csv event.json invitation.html batches/evening
예상 출력:
Created batch: batches/evening
ls는 디렉터리 항목을 보여 줍니다.
ls batches/evening
g101.html, g102.html, g103.html이 있어야 합니다. 두 Alex 초대장은 이제 독립적인 ID를 가집니다. 각 파일에는 본인의 메시지가 남고, 세 번째 파일에는 쉼표, 따옴표, Unicode와 문자 그대로의 꺾쇠괄호도 보존됩니다.
생성기를 재사용하고 이전 작업 보존하기
이 단계에서는 같은 프로그램을 다른 행사에 사용하고 일반적인 오류 복구를 연습합니다. 같은 WebIDE 터미널에서 시작하세요. 아래 명령은 각각 독립적이므로 예상된 실패 후에도 계속할 수 있습니다.
먼저 이미 완성한 출력 경로를 일부러 다시 사용합니다. 실패하는 것이 정상입니다. mkdir()는 기존 배치를 거부합니다. 메시지는 Cannot generate invitations:로 시작하며 기존 파일에 관한 오류를 포함합니다. 이전 배치는 변경되지 않아야 합니다.
python3 generate.py guests.csv event.json invitation.html batches/evening
준비된 중복 ID 예제를 읽으세요. 두 번째 게스트가 첫 번째 게스트와 같은 ID를 가집니다.
cat invalid-duplicate.csv
이 파일로 새 배치를 만들어 보세요. 이 역시 예상된 실패이며, 디렉터리를 만들기 전에 Duplicate guest ID: g101을 보고합니다.
python3 generate.py invalid-duplicate.csv event.json invitation.html batches/rejected-duplicate
메시지가 없는 예제도 의도적으로 Missing guest field: message로 실패합니다. 앞선 레코드가 유효하더라도 일부 배치가 생겨서는 안 됩니다.
python3 generate.py invalid-missing.csv event.json invitation.html batches/rejected-missing
입력 경로의 오타는 다른 문제입니다. 운영체제가 파일을 열 수 없습니다. 다음 명령은 존재하지 않는 guest.csv를 일부러 사용합니다. 오류를 읽고 환경을 초기화하지 마세요.
python3 generate.py guest.csv daytime.json invitation.html batches/daytime
실제 경로인 guests.csv로 고쳐 복구합니다. 두 번째 행사 정보를 읽은 뒤 같은 프로그램과 명단으로 새 배치를 생성하세요.
cat daytime.json
python3 generate.py guests.csv daytime.json invitation.html batches/daytime
예상 출력:
Created batch: batches/daytime
Web 8080으로 전환하고 새로 고치세요. evening/g101.html과 evening/g102.html을 차례로 여세요. 이름은 같지만 메시지는 다르게 유지됩니다. evening/g103.html을 열면 쉼표, 따옴표, ë, 앰퍼샌드, <Friends>가 모두 문자 그대로 보여야 합니다. daytime/g103.html에서 새 제목, 시간, 장소를 읽으세요. 각 초대장의 브라우저 탭을 닫으면 목록으로 돌아갑니다. 낮 행사 초대장을 Download HTML로 내려받아 여세요. 개인별 텍스트가 미리 보기와 일치해야 합니다.

이름의 쉼표, 따옴표, 꺾쇠괄호는 인사말에서 보이는 문자로 유지됩니다.
두 행사 배치는 독립적입니다. CSV, 행사 파일, 템플릿은 변경되지 않았고 기존 아카이브도 남아 있습니다. 잘못된 입력, 기존 배치, 없는 입력 경로는 원인이 서로 다릅니다. 이전 초대장을 지우지 않고 각각 수정할 수 있습니다.
요약
CSV 레코드와 JSON 행사 정보로 실제로 열고 다운로드할 수 있는 HTML 초대장을 만들었습니다. 안정적인 ID와 표시 이름을 구분하고, 필드를 템플릿의 텍스트 영역에 매핑했으며, UTF-8과 HTML 이스케이프로 Unicode 및 특수 문자를 보존했습니다. 또한 쓰기 전에 전체 배치를 검증했습니다. 이제 입력과 이전 작업을 유지하면서 새 행사 배치를 만들 수 있습니다.



