Devin.KR

화면 만들기 - HTML 을 생성해 파일로 내보내기

개발자KR 조회 0

이 장에서 배우는 것

앞 장에서 예약 정보를 요청과 응답으로 주고받았다. 이번에는 같은 예약 정보를 사람이 읽기 편한 화면으로 바꾼다. 예약 목록을 표로 펼치고, 한 주의 일정을 날짜별로 묶어 달력처럼 보여 준다. 웹 서버를 실행하지 않고도 확인할 수 있도록 결과를 HTML 파일로 저장한다.

HTML은 브라우저가 문서의 제목, 문단, 표 같은 구조를 읽는 표시 언어다. 이번 화면은 파일을 만들 때의 예약 정보를 담는다. 파일을 열어 둔다고 예약 정보가 저절로 바뀌지는 않는다. 데이터가 달라지면 프로그램을 다시 실행해 파일을 만든다. 먼저 이 작은 범위를 분명히 해야 AI에게도 필요한 화면을 정확히 요청할 수 있다.

  • 문자열 템플릿으로 예약 현황표와 날짜별 주간 달력을 만든다.
  • 예약 이름과 공간 이름을 html.escape로 처리해 글자로 출력한다.
  • 화면의 구성, 예약 상태, 빈 화면을 구체적인 요구로 설명한다.
  • 생성한 파일을 브라우저로 열고 코드의 검사 결과와 함께 확인한다.
  • 한 파일로 실행되는 프로그램에서 HTML 파일의 앞부분과 줄 수를 출력한다.

문제 상황

동아리와 스터디 모임이 함께 쓰는 공간에는 하루에도 여러 예약이 들어온다. 운영 담당자는 목록에서 예약 이름을 찾고, 모임 참여자는 이번 주에 어느 날 공간을 쓰는지 알고 싶어 한다. 같은 데이터라도 목록과 달력에서 살펴보는 순서가 다르다. 목록은 예약 하나를 자세히 읽는 데 알맞고, 달력은 날짜별 사용 현황을 비교하는 데 알맞다.

예약 정보가 사전 목록으로만 출력되면 날짜와 공간을 계속 눈으로 찾아야 한다. 그렇다고 이번 작업에서 예약 편집, 로그인, 자동 새로 고침까지 만들 필요는 없다. 우선 고정된 예제 예약 세 개를 읽어 두 가지 표로 보여 준다. 예약이 없는 날도 달력에서 빠지지 않게 한다. 빈 날짜가 사라지면 사용자는 그날 예약이 없는지, 화면에서 누락되었는지 판단하기 어렵다.

예약 이름에도 화면을 흔드는 문자가 들어올 수 있다. 예를 들어 누군가 예약 이름을 <새 모임>으로 적었다고 하자. 이 이름을 HTML에 그대로 끼워 넣으면 브라우저는 일부를 글자가 아닌 표시로 해석할 수 있다. 화면을 만드는 함수는 저장된 값을 바꾸지 않으면서, 화면에 내보낼 때 글자로 보이도록 처리해야 한다.

완료 기준은 눈에 보이는 표 하나만으로 끝나지 않는다. 목록에 예약 세 개가 보이고, 달력에 월요일부터 일요일까지 일곱 날짜가 보이며, 특수문자가 포함된 이름도 읽을 수 있어야 한다. 예약이 전혀 없는 자료를 넣었을 때의 안내도 확인한다. 마지막으로 프로그램이 파일을 저장한 뒤 종료하는지 살핀다.

화면 요구를 구성과 상태로 나누기

AI에게 “예약 화면을 보기 좋게 만들어 달라”라고만 요청하면 무엇을 먼저 보여 줄지, 빈 날짜를 어떻게 표현할지 결정하기 어렵다. 화면 요구에는 데이터 항목뿐 아니라 읽는 순서가 필요하다. 이번에는 문서 제목, 조회 주간, 예약 현황표, 주간 달력 순서로 읽게 한다. 표의 열도 미리 정한다.

두 화면 요소가 같은 예약 정보를 보여 주는 방식
요소읽는 기준보여 줄 내용빈 상태
예약 현황표예약 한 건일시, 공간, 모임, 상태예약이 없습니다.
주간 달력날짜 하루날짜, 요일, 그날의 예약예약 없음
조회 주간 안내화면 전체시작일과 마지막 날예약 유무와 관계없이 표시

여기서 상태는 예약의 현재 구분을 뜻한다. 예제에는 확정과 대기 두 가지가 있다. 확정은 사용하기로 정해진 예약이고, 대기는 아직 확정되지 않은 예약이다. 이번 작업은 이 구분을 화면에 표시하는 데까지만 다룬다. 상태를 바꾸는 규칙이나 대기 예약의 승인 과정은 만들지 않는다.

색만으로 상태를 구분하면 색을 알아보기 어려운 사람에게 정보가 전달되지 않을 수 있다. 이번 표에는 확정과 대기라는 글자를 넣는다. 주간 달력에서도 각 예약 뒤에 같은 상태를 붙인다. 목록에서만 상태를 표시하면 달력을 보고 있는 사람은 다시 목록으로 돌아가 확인해야 한다.

공간 예약 정보를 읽기 전용 HTML 파일로 보여 주는 함수를 만들어 달라. 문서 제목과 조회 주간을 먼저 표시하고, 그 아래에 예약 현황표와 주간 달력을 배치한다. 예약 현황표는 일시·공간·모임·상태 네 열을 사용한다.

주간 달력은 월요일부터 일요일까지 날짜별 한 행으로 만든다. 하루에 예약이 여러 개면 한 칸 안에서 줄을 나눈다. 각 예약에 시간, 공간, 모임 이름, 상태를 적는다. 예약이 없는 날은 ‘예약 없음’이라고 표시한다. 전체 예약 목록이 비어 있으면 현황표 대신 ‘예약이 없습니다.’를 표시한다.

Python 3.12 이상 표준 라이브러리만 사용한다. 사용자 문자열은 HTML에 넣기 직전에 html.escape로 처리한다. 예제 날짜는 2026년 10월 5일로 고정하고, 출력 순서도 고정한다. main.py 하나를 실행하면 reservations.html을 저장하고 종료해야 한다.

이 요청에는 배치, 열, 상태, 빈 화면, 실행 조건이 함께 들어 있다. 날짜를 고정한 이유는 같은 코드의 결과를 반복해서 비교하기 위해서다. “이번 주”라는 말만 쓰고 실행한 날의 날짜를 사용하면 시간이 지난 뒤 책의 화면과 다른 결과가 나온다.

AI의 답도 확인할 사항으로 바꿔 읽는다. “요구를 반영했다”는 문장은 검사 결과가 아니다. 일곱 날짜가 실제로 생성되는지, 빈 목록에서 안내 문장이 나오는지, 이름이 두 번 이스케이프되지 않는지 코드와 결과를 확인해야 한다. 변경 전후의 차이를 보여 주는 diff도 검토한다. 화면 수정 중 예약 데이터나 저장 규칙까지 바뀌었다면 이번 요청에 필요한 변경인지 따로 판단한다.

같은 예약 목록을 예약별 현황표와 날짜별 주간 달력으로 나누어 보여 준다

주간 달력을 날짜별 행으로 만든 것은 작업 범위를 작게 유지하기 위한 선택이다. 요일 일곱 개를 가로로 배치하는 달력도 가능하지만, 긴 예약 이름을 넣으면 칸 너비를 조정해야 한다. 먼저 날짜와 예약의 관계가 맞는지 확인하기 쉬운 세로 표를 만든다. 화면 모양을 정할 때는 무엇을 읽고 비교할지부터 정하는 편이 도움이 된다.

문자열 템플릿과 출력 이스케이프

문자열 템플릿(template)은 일정한 문자열 모양 안에 값을 끼워 넣는 방식이다. Python의 f 문자열을 사용하면 표의 한 행에 날짜와 예약 이름을 넣을 수 있다. 바깥의 HTML 구조는 코드가 정하고, 안에 들어가는 값은 예약 자료에서 가져온다. 이 두 부분을 구분해야 값이 화면 구조로 해석되는 일을 줄일 수 있다.

HTML에서 <와 >는 태그를 표시하는 데 쓰인다. &는 문자 참조를 시작하는 데 쓰인다. 출력 이스케이프(escaping)는 이런 문자를 HTML 문서 안에서 글자로 표현할 수 있는 형태로 바꾸는 일이다. html.escape는 <를 &lt;로, >를 &gt;로, &를 &amp;로 바꾼다. 기본 설정에서는 따옴표도 처리한다.

완성 코드에서는 text라는 작은 함수를 만든다. 이 함수는 값을 문자열로 바꾼 뒤 escape에 넘긴다. 이름을 짧게 둔 것은 표를 조립하는 코드에서 어떤 값이 출력 처리를 거치는지 쉽게 읽기 위해서다. 예약 자료에는 원래 문자열을 유지하고, HTML을 생성하는 경계에서만 이 함수를 호출한다.

이스케이프는 입력을 거절하는 검증과 다르다. <새 모임>이라는 이름은 화면에서 그대로 읽을 수 있다. 이름을 다른 말로 바꾸거나 예약을 삭제하는 일이 아니다. 어떤 입력을 허용할지 판단하는 검증과 오류 안내는 다음 장에서 이어서 다룬다.

예약 이름은 저장할 때 유지하고 HTML에 넣기 직전에 이스케이프해 브라우저에서 원래 글자로 읽는다

처리 순서도 중요하다. 예약 이름만 먼저 이스케이프하고, 그 결과를 코드가 만든 태그 안에 넣는다. 완성한 HTML 문서 전체를 이스케이프하면 표를 만드는 태그까지 글자로 바뀐다. 반대로 이름을 처리하지 않고 태그 안에 넣으면 이름 속 문자가 문서 구조에 영향을 줄 수 있다.

이번 코드가 만든 문자열은 태그 사이의 본문에만 들어간다. 링크 주소, 문서 안의 실행 코드, 서식 값처럼 다른 위치에 값을 넣는 작업까지 text 함수 하나로 해결된다고 생각해서는 안 된다. 출력할 위치에 따라 필요한 규칙이 달라진다. 이 장에서는 제목과 표의 글자라는 좁은 범위를 유지한다.

하루에 여러 예약이 있는 경우에는 각 예약의 글자를 먼저 처리하고, 처리한 문자열 사이에 코드가 정한 줄바꿈 태그를 넣는다. 줄바꿈 태그 자체를 text에 넘기지 않는 이유는 브라우저가 그것을 구조로 읽어야 하기 때문이다. 코드가 만든 구조와 데이터에서 온 글자의 역할이 이 부분에서 드러난다.

파일 생성과 브라우저 확인을 분리하기

화면을 만드는 함수가 파일까지 저장하면 작은 문자열 검사에도 파일 경로를 준비해야 한다. 이번에는 build_page가 HTML 문자열을 반환하고, main이 그 문자열을 파일에 쓴다. 이렇게 나누면 빈 예약 목록이나 다른 주간을 넣어 화면 문자열만 확인할 수 있다.

파일의 문자 인코딩(encoding)은 글자를 파일의 바이트로 표현하는 규칙이다. 저장할 때 UTF-8을 지정하고, HTML의 머리 부분에도 같은 문자 인코딩을 적는다. 한글이 포함된 문서를 브라우저가 올바르게 읽도록 양쪽의 약속을 맞춘다. 줄 끝도 줄바꿈 문자 하나로 정해 파일 내용이 일정하게 생성되도록 한다.

브라우저 확인은 문자열 검사와 다른 정보를 준다. 문자열 검사로 예약 이름이 처리되었는지 확인할 수 있지만, 사람이 읽는 순서나 한 칸 안의 줄바꿈은 화면에서 보는 편이 빠르다. 두 확인 방법을 함께 사용한다. 눈으로 보기만 하면 화면 밖의 빈 상태를 놓칠 수 있고, 문자열만 검사하면 읽기 불편한 표를 놓칠 수 있다.

파일을 연 뒤에는 문서 제목, 조회 주간, 목록의 예약 수, 달력의 날짜 수를 순서대로 확인한다. <새 모임>이 꺾쇠를 포함한 글자로 보이는지도 살핀다. 같은 파일을 다시 만들었다면 브라우저에서 새로 고침한다. 이 파일은 읽기 전용 결과이므로 브라우저에서 예약을 추가하는 입력 칸은 없다.

완성 코드

다음 내용을 main.py에 저장한다. 예제 자료는 프로그램 안에 있으므로 앞 장의 파일이나 데이터베이스가 필요하지 않다. 실행하면 현재 작업 폴더의 reservations.html을 생성한다. 같은 이름의 파일이 있으면 이번 결과로 덮어쓴다. 예약 자료를 준비하고, 문자열 검사를 통과한 뒤, 파일을 저장하고 바로 종료한다.

from datetime import date, timedelta
from html import escape
from pathlib import Path


WEEK_START = date(2026, 10, 5)
WEEKDAYS = ("월", "화", "수", "목", "금", "토", "일")
OUTPUT_PATH = Path("reservations.html")

RESERVATIONS = [
    {
        "day": "2026-10-05",
        "start": "09:00",
        "end": "10:00",
        "room": "모임방 A",
        "title": "책 & 대화",
        "status": "확정",
    },
    {
        "day": "2026-10-07",
        "start": "14:00",
        "end": "15:00",
        "room": "모임방 B",
        "title": "<새 모임>",
        "status": "대기",
    },
    {
        "day": "2026-10-07",
        "start": "16:00",
        "end": "17:00",
        "room": "모임방 A",
        "title": "글쓰기 모임",
        "status": "확정",
    },
]


def text(value):
    return escape(str(value), quote=True)


def sorted_rows(rows):
    return sorted(
        rows,
        key=lambda row: (
            row["day"],
            row["start"],
            row["room"],
            row["title"],
            row["end"],
            row["status"],
        ),
    )


def render_table(rows):
    if not rows:
        return "<p>예약이 없습니다.</p>"

    lines = [
        "<table>",
        "<caption>예약 목록</caption>",
        "<thead><tr><th>일시</th><th>공간</th>"
        "<th>모임</th><th>상태</th></tr></thead>",
        "<tbody>",
    ]
    for row in sorted_rows(rows):
        when = f'{row["day"]} {row["start"]}~{row["end"]}'
        lines.append(
            f"<tr><td>{text(when)}</td>"
            f'<td>{text(row["room"])}</td>'
            f'<td>{text(row["title"])}</td>'
            f'<td>{text(row["status"])}</td></tr>'
        )
    lines.extend(["</tbody>", "</table>"])
    return "\n".join(lines)


def render_calendar(rows, week_start):
    ordered = sorted_rows(rows)
    lines = [
        "<table>",
        "<caption>一週間の予約</caption>".replace("一週間の", "一週間の"),
        "<thead><tr><th>날짜</th><th>요일</th>"
        "<th>예약</th></tr></thead>",
        "<tbody>",
    ]
    lines[1] = "<caption>주간 예약</caption>"
    for offset in range(7):
        day = week_start + timedelta(days=offset)
        items = []
        for row in ordered:
            if row["day"] != day.isoformat():
                continue
            label = (
                f'{row["start"]}~{row["end"]} / {row["room"]} / '
                f'{row["title"]} ({row["status"]})'
            )
            items.append(text(label))
        content = "<br>".join(items) if items else "예약 없음"
        lines.append(
            f"<tr><td>{day.isoformat()}</td>"
            f"<td>{WEEKDAYS[day.weekday()]}</td>"
            f"<td>{content}</td></tr>"
        )
    lines.extend(["</tbody>", "</table>"])
    return "\n".join(lines)


def build_page(rows, week_start):
    week_end = week_start + timedelta(days=6)
    lines = [
        "<!doctype html>",
        '<html lang="ko">',
        "<head>",
        '<meta charset="utf-8">',
        "<title>공간 예약 현황</title>",
        "</head>",
        "<body>",
        "<h1>공간 예약 현황</h1>",
        f"<p>조회 주간: {week_start.isoformat()}"
        f" ~ {week_end.isoformat()}</p>",
        "<h2>예약 현황표</h2>",
        render_table(rows),
        "<h2>주간 달력</h2>",
        render_calendar(rows, week_start),
        "</body>",
        "</html>",
    ]
    return "\n".join(lines) + "\n"


def check_page():
    page = build_page(RESERVATIONS, WEEK_START)
    assert "책 &amp; 대화" in page
    assert "&lt;새 모임&gt;" in page
    assert "<새 모임>" not in page
    assert "&amp;lt;새 모임" not in page

    empty = build_page([], WEEK_START)
    assert "<p>예약이 없습니다.</p>" in empty
    assert empty.count("<td>예약 없음</td>") == 7

    calendar = render_calendar(RESERVATIONS, WEEK_START)
    assert calendar.count("<tr>") == 8
    assert calendar.count("<td>예약 없음</td>") == 5
    assert "2026-10-11" in calendar
    assert "<br>" in calendar

    next_week = render_calendar(
        RESERVATIONS, WEEK_START + timedelta(days=7)
    )
    assert next_week.count("<td>예약 없음</td>") == 7
    assert build_page(list(reversed(RESERVATIONS)), WEEK_START) == page


def main():
    check_page()
    page = build_page(RESERVATIONS, WEEK_START)
    with OUTPUT_PATH.open("w", encoding="utf-8", newline="\n") as file:
        file.write(page)

    print("確認: OK".replace("確認", "검사"))
    print(f"저장: {OUTPUT_PATH.name}")
    print("HTML 앞부분:")
    for line in page.splitlines()[:6]:
        print(line)
    print(f"줄 수: {len(page.splitlines())}")


if __name__ == "__main__":
    main()

줄별 해설

처음 세 줄은 날짜 계산, HTML 출력 처리, 파일 경로에 필요한 도구를 가져온다. date는 날짜 하나를 나타내고, timedelta는 며칠의 차이를 나타낸다. 일주일의 날짜를 만들 때 문자열의 마지막 숫자를 직접 더하지 않는다. 날짜 도구에 하루씩 더하면 달이나 해가 바뀌는 경우에도 같은 방식으로 계산할 수 있다.

WEEK_START는 조회 시작일이다. 예제에서는 월요일인 2026년 10월 5일로 고정한다. WEEKDAYS는 날짜가 가리키는 요일 번호를 한국어 글자로 바꾸는 목록이다. 날짜의 weekday 메서드는 월요일에 0을 돌려주므로 이 순서와 맞는다. OUTPUT_PATH는 출력 파일 이름을 한곳에 모아 둔다.

RESERVATIONS에는 예약 사전 세 개를 넣었다. 날짜는 연도·월·일 순서이고 시간은 두 자리 시와 분이다. 이 예제의 형식에서는 문자열 정렬이 날짜와 시간 순서를 유지한다. 임의의 입력에서도 같은 결과를 낸다고 가정하지는 않는다. 다른 형식의 날짜나 잘못된 시간에 대한 검사는 다음 장의 주제다.

text 함수는 화면에 넣는 값에 공통 처리를 적용한다. str은 값을 문자열로 만들고, escape는 HTML에서 따로 해석될 수 있는 문자를 처리한다. quote=True를 명시해 따옴표도 처리한다는 의도를 드러낸다. 예약 이름을 미리 고쳐 저장하지 않으므로 다른 출력 방식에서도 원래 자료를 사용할 수 있다.

sorted_rows 함수는 새로 정렬한 목록을 반환한다. 원래 예약 목록의 순서를 바꾸지 않는다. 날짜와 시작 시간이 같을 때도 공간, 이름, 종료 시간, 상태 순서로 비교하므로 입력 순서가 달라도 화면을 일정하게 만들 수 있다. 함수 안의 lambda는 정렬할 때 비교 기준을 돌려주는 짧은 함수다.

render_table은 예약 목록을 표로 바꾼다. 빈 목록이면 표 대신 안내 문단을 반환한다. 예약이 있으면 표의 시작, 제목, 열 제목, 본문 시작을 준비하고 예약마다 행 하나를 덧붙인다. th는 열의 제목, td는 실제 값이 들어가는 칸이다. caption은 표가 무엇을 보여 주는지 설명한다.

일시를 만드는 when에는 날짜와 시작·종료 시간이 들어간다. 완성된 일시 문자열도 text를 거친다. 공간, 모임, 상태 역시 같은 경계를 거친다. 여러 문자열이 괄호 안에서 나란히 놓인 부분은 Python이 하나의 문자열로 이어 붙인다. 긴 행을 여러 코드 줄로 나누어도 생성되는 HTML 행은 한 줄이다.

render_calendar는 먼저 예약을 정렬하고, 일곱 번 반복하며 날짜별 행을 만든다. 각 날짜에서 예약 자료를 살펴보고 날짜가 같은 것만 items에 넣는다. 예약이 있으면 처리된 문자열을 줄바꿈 태그로 연결하고, 없으면 예약 없음이라는 고정 문장을 쓴다. 예약이 없는 날짜에도 행을 추가하므로 달력의 범위가 유지된다.

build_page는 문서의 큰 구조를 조립한다. 조회 마지막 날은 시작일에 6일을 더한 값이다. 예약 현황표에는 전달된 목록 전체를 보여 주고, 주간 달력에는 조회 주간에 해당하는 예약만 보여 준다. 이 예제에서는 세 예약이 모두 그 주간에 있다. 이후 다른 날짜의 자료를 더해도 두 요소의 역할은 그대로 유지된다.

check_page의 assert는 조건이 참인지 확인하는 문장이다. 조건이 거짓이면 실행이 멈추므로 잘못된 결과를 파일로 저장하기 전에 발견할 수 있다. 이름의 처리, 빈 상태, 달력 행 수, 같은 날짜의 줄바꿈, 다음 주의 빈 날짜를 검사한다. 원본 목록을 뒤집어서 전달해도 결과가 같은지 확인하는 마지막 검사는 정렬이 출력에 실제로 적용되는지 살핀다.

달력의 행 수가 8인 이유는 날짜 행 일곱 개에 열 제목 행 하나가 더해지기 때문이다. 예약 없는 날짜는 화요일과 목요일부터 일요일까지 다섯 개다. 이런 숫자는 자료가 바뀌면 함께 검토해야 한다. 검사가 통과하도록 숫자만 고치는 것이 아니라, 새 요구에서 어떤 결과가 맞는지 먼저 정한다.

main은 검사, 화면 생성, 파일 저장, 결과 출력 순서로 움직인다. with는 파일을 사용하는 범위를 정하고 범위가 끝나면 파일을 닫는다. 마지막 조건문은 이 파일을 직접 실행할 때 main을 호출한다. 이 프로그램에는 기다리는 서버나 반복 입력이 없으므로 실행 명령이 바로 끝난다.

실행 결과

macOS와 Linux에서는 다음 명령으로 문법을 검사하고 실행한다. 첫 명령은 문제가 없으면 출력 없이 끝난다. 두 번째 명령의 예상 출력은 아래와 같다. HTML 앞부분 여섯 줄에는 예약 이름이 아직 나오지 않는다. 전체 표와 달력은 저장된 파일에서 확인한다.

python3 -W error -m py_compile main.py
python3 main.py
검사: OK
저장: reservations.html
HTML 앞부분:
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<title>공간 예약 현황</title>
</head>
줄 수: 35

35줄은 문서의 큰 구조 13줄, 예약 현황표 9줄, 주간 달력 13줄을 합친 값이다. 화면에서 보이는 줄 수와 파일의 줄 수는 다르다. 브라우저는 HTML 구조에 따라 표시하므로 파일의 한 줄에 여러 예약이 있어도 줄바꿈 태그가 있으면 화면에서 나누어 보인다.

macOS에서는 아래 명령으로 파일을 기본 브라우저에서 열 수 있다. 이 명령은 HTML을 다시 생성하지 않는다. 먼저 Python 프로그램을 실행해 파일을 만든 뒤 사용한다.

open reservations.html

Linux에서는 파일 관리자에서 reservations.html을 선택해 브라우저로 열거나, 브라우저의 파일 열기 메뉴를 사용한다. 주소에 로컬 파일을 가리키는 표시가 나타나는 것은 정상이다. 이 확인 과정에는 웹 서버나 네트워크 요청이 필요하지 않다.

화면에서는 목록에 예약 세 개, 달력에 날짜 일곱 개가 보여야 한다. 수요일 칸에는 대기 예약과 확정 예약이 각각 다른 줄로 나온다. 모임 이름은 책 & 대화와 <새 모임>으로 읽힌다. HTML 파일 안의 문자 참조와 브라우저에서 읽는 글자를 구분해서 확인한다.

실무에서 자주 틀리는 것

예약 이름을 그대로 끼워 넣는다

다음 첫 코드는 예약 이름을 문서 구조와 구분하지 않는다. 이름에 꺾쇠가 있으면 화면에서 일부 글자가 사라질 수 있다. 고친 코드에서는 값만 text에 넘긴 뒤 칸을 만든다. 두 예제는 완성 코드의 함수 안에서 사용할 수 있는 Python 문장이다.

def wrong_cell(row):
    return f'<td>{row["title"]}</td>'


def fixed_cell(row):
    return f'<td>{text(row["title"])}</td>'

AI가 “예제 데이터에는 문제가 없다”고 설명해도 출력 경계를 생략하지 않는다. 현재 이름이 단순하다는 사실과 이후 이름이 어떻게 들어올지는 서로 다른 문제다. 꺾쇠와 앰퍼샌드가 있는 자료를 직접 넣어 결과를 비교한다.

문서 전체를 이스케이프한다

다음 첫 코드는 표 태그까지 글자로 만든다. 브라우저에서 표 대신 HTML 소스처럼 보이는 글자가 나타날 수 있다. 고친 코드에서는 이름을 먼저 처리하고, 표의 구조는 코드가 만든 문자열로 유지한다.

def wrong_table(title):
    page = f"<table><tr><td>{title}</td></tr></table>"
    return escape(page)


def fixed_table(title):
    return (
        "<table><tr>"
        f"<td>{text(title)}</td>"
        "</tr></table>"
    )

이미 처리된 이름을 다시 text에 넘기는 실수도 비슷하다. 화면에 &lt; 같은 문자 참조가 그대로 보이면 중복 처리를 의심한다. 데이터는 원래 문자열로 두고, 화면을 만드는 함수에서 한 번 처리하는 규칙을 지킨다.

예약이 있는 날짜만 달력에 넣는다

다음 첫 함수는 예약 자료에서 날짜를 찾는다. 자료에 없는 날짜는 결과에 나타나지 않는다. 고친 함수는 조회 시작일에서 일곱 날짜를 먼저 만든다. 예약은 그 날짜에 연결한다. 달력의 틀을 데이터의 존재 여부와 분리해야 빈 날짜를 표현할 수 있다.

def wrong_days(rows):
    return sorted({row["day"] for row in rows})


def fixed_days(week_start):
    return [
        (week_start + timedelta(days=offset)).isoformat()
        for offset in range(7)
    ]

빈 상태는 오류가 아니라 화면의 정상적인 경우다. 전체 예약이 없을 때와 특정 날짜만 비었을 때를 나누어 확인한다. AI에게 빈 화면을 요구하지 않으면 예제 자료가 있는 경우만 구현될 수 있으므로 요청과 검사에 둘 다 적는다.

저장 인코딩을 실행 환경에 맡긴다

인코딩을 생략한 파일 저장은 실행 환경의 기본 설정을 따른다. 그 설정과 브라우저가 읽는 설정이 맞지 않으면 한글 확인이 어려워질 수 있다. 고친 코드에서는 파일 저장 규칙을 명시한다.

def wrong_save(page):
    with open("reservations.html", "w") as file:
        file.write(page)


def fixed_save(page):
    with open(
        "reservations.html", "w", encoding="utf-8", newline="\n"
    ) as file:
        file.write(page)

파일 저장 코드를 고친 뒤에는 HTML의 문자 인코딩 선언도 함께 확인한다. 브라우저에서 한글이 보인다는 사실만으로 저장 규칙이 고정되었다고 판단하지 않는다. 코드에서 약속이 명시되었는지 보고, 생성 파일을 다시 열어 결과를 확인한다.

한눈에 보기

화면 생성 단계마다 지킬 기준과 확인 방법
단계기준확인 방법
화면 요구구성, 상태, 빈 화면을 명시한다.요청문에 목록과 달력의 역할이 있는지 읽는다.
문자열 조립구조와 데이터의 글자를 나눈다.값을 끼워 넣는 곳에서 text 호출을 찾는다.
날짜 생성예약 없이도 일곱 날짜를 만든다.빈 목록과 다음 주를 넣어 검사한다.
파일 저장UTF-8과 줄바꿈을 명시한다.저장 코드와 HTML 선언을 함께 읽는다.
결과 검토검사 결과와 실제 화면을 함께 본다.실행 출력, 브라우저, 변경 diff를 확인한다.

이 장의 화면 함수는 예약 정보를 받아 HTML 문자열을 돌려준다. 저장과 출력은 main이 맡는다. 이 구분 덕분에 화면 요구가 바뀌어도 함수 결과를 먼저 비교할 수 있다. AI가 수정한 코드에서도 이 경계가 유지되는지 살핀다.

연습 문제

  1. 예약 현황표 위에 ‘전체 예약: 3건’이라는 문단을 추가한다. 빈 목록에서는 ‘전체 예약: 0건’이 나와야 한다. 고정 숫자를 쓰지 말고 전달받은 목록에서 건수를 계산한다. 예제 실행의 전체 줄 수가 어떻게 바뀌는지도 설명한다.
  2. 예약 이름에 ‘질문 <답변> & 기록’을 사용해 출력 검사를 추가한다. 이름이 올바르게 처리된 경우와 중복 처리된 경우를 구분하는 조건을 각각 작성한다.
  3. 주간 달력의 날짜 칸에 2026-10-05 대신 10/05처럼 월과 일만 표시한다. 예약 날짜를 비교하는 코드는 유지한다. 어떤 부분만 바꿔야 하는지 설명한다.
  4. 달력에서 예약 없는 날짜의 안내를 ‘사용 가능한 날짜’로 바꾸어 달라는 요청이 왔다. 이 표현을 바로 적용해도 되는지 판단하고, 현재 데이터만으로 확인할 수 있는 범위에 맞는 요청문을 작성한다.

정답과 해설

1. 예약 건수는 목록에서 계산한다

build_page의 lines에서 예약 현황표 제목 바로 앞에 다음 문자열을 추가한다. len은 목록의 항목 수를 돌려준다. 빈 목록이면 0을 돌려주므로 별도 숫자를 준비하지 않아도 된다.

f"<p>전체 예약: {len(rows)}건</p>",

이 문자열을 목록 항목으로 추가하면 HTML이 한 줄 늘어나 예제의 줄 수는 36이 된다. 여기서 전체 예약은 현황표에 전달된 목록 전체의 건수다. 주간 달력에 포함된 건수만 세는 요구라면 날짜 범위를 적용한 계산이 따로 필요하다. 문구와 계산 범위를 먼저 맞춘다.

2. 처리한 문자열과 중복 처리 흔적을 검사한다

다음 함수를 완성 코드에 추가하고 check_page에서 호출할 수 있다. 한 번 처리한 결과가 포함되어야 하고, 처리 결과를 다시 처리했을 때의 문자열은 포함되지 않아야 한다.

def check_special_title():
    row = dict(RESERVATIONS[0])
    row["title"] = "질문 <답변> & 기록"
    result = render_table([row])
    assert "질문 &lt;답변&gt; &amp; 기록" in result
    assert "질문 &amp;lt;답변" not in result

dict는 기존 예약에서 새 사전을 만든다. 여기서는 값이 모두 문자열이므로 새 사전의 이름을 바꾸어도 원래 예제 예약은 바뀌지 않는다. 검사 자료를 바꾸다가 본 실행의 자료까지 바꾸지 않도록 한다. 조건이 맞는지 확인한 뒤 브라우저에서도 꺾쇠와 앰퍼샌드가 글자로 보이는지 살핀다.

3. 표시 문자열만 바꾼다

render_calendar에서 날짜 칸을 만드는 부분만 다음처럼 바꾼다. 날짜 객체가 가진 month와 day를 두 자리 숫자로 표현한다.

f"<tr><td>{day.month:02d}/{day.day:02d}</td>"

02d는 정수를 두 자리로 표시하고 빈 앞자리를 0으로 채우라는 뜻이다. 예약과 날짜를 비교하는 day.isoformat 호출은 그대로 둔다. 화면 표현이 달라졌다고 자료 비교 기준까지 바꾸면 기존 예약을 찾지 못할 수 있다. 날짜 문자열을 확인하던 검사도 새로운 표시 형식에 맞게 고친다. 행 수와 빈 날짜 수 검사는 계속 유지한다.

4. 예약이 없다는 사실과 사용 가능 여부를 구분한다

현재 자료로 확인할 수 있는 것은 등록된 예약이 없다는 사실이다. 공간이 닫혀 있거나 별도 사용 제한이 있는지는 자료에 없다. ‘사용 가능한 날짜’는 아직 확인하지 않은 조건까지 포함하는 말이므로 바로 적용하기 어렵다. 운영 규칙을 확인하기 전에는 사실의 범위에 맞는 안내를 쓴다.

주간 달력에서 예약이 없는 날의 안내를 ‘등록된 예약 없음’으로 바꿔 달라. 현재 자료에는 휴무일과 사용 제한 정보가 없으므로 사용 가능 여부를 단정하는 문구는 추가하지 않는다. 날짜 일곱 개를 유지하고 빈 날짜 수 검사는 새 안내 문구에 맞춰 수정한다.

수정 후에는 안내 문자열과 그 문자열을 확인하는 검사만 바뀌었는지 diff를 읽는다. 프로그램을 다시 실행해 검사 통과를 확인하고, 브라우저를 새로 고침해 빈 날짜의 문구를 읽는다. 화면의 문장도 데이터가 뒷받침하는 범위 안에서 정해야 한다.

댓글 0

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

댓글을 남기려면 로그인이 필요합니다.