Math equations (hp:equation) including 2-column problem sheetSkill for generating as HWPX files.
Writes math problems in the range of middle school 1st year to high school 3rd year using Hancom Office equation editor script syntax.
Follows the same XML-first workflow as the hwpx skill, and is compatible with the existing hwpx skill's build/validation tools.
Supports two formats:
- exam (default, academic achievement assessment/CSAT): National United Academic Achievement Assessment/CSAT format exam paper (header, horizontal answer choices, point values, page numbers, etc.)
- worksheet (
--exam-type worksheetmust be specified): Simple 2-column math problem sheet
Environment
# SKILL_DIR는 이 SKILL.md가 위치한 디렉토리의 절대 경로로 설정
SKILL_DIR="$(cd "$(dirname "$0")/.." && pwd)" # 스크립트 내에서
# hwpx 기본 스킬 경로 (검증/추출 도구 사용 시)
HWPX_SKILL_DIR="<hwpx 스킬 설치 경로>"
Python virtual environment (configure according to project)
VENV="<project>/.venv/bin/activate"
모든 Python 실행 시:
```bash
# 프로젝트의 .venv를 활성화 (pip install lxml 필요)
source "$VENV"
Directory structure
.claude/skills/math-hwpx/
├── SKILL.md # 이 파일
├── scripts/
│ ├── build_math_hwpx.py # CLI + build 오케스트레이션 (~170줄)
│ ├── xml_primitives.py # IDGen, STYLE 상수, 기본 문단/수식 생성기
│ ├── exam_helpers.py # 시험지 전용 XML 생성기 (배점, 선택지, 이미지)
│ ├── table_layout.py # 투명 테이블 2×2 레이아웃 로직
│ ├── section_generators.py # worksheet/exam section0.xml 조립
│ ├── hwpx_utils.py # 검증/패키징/메타데이터
│ ├── graph_generator.py # 그래프 PNG 생성 (matplotlib)
│ └── test_refactor.py # 리그레션 테스트 스크립트
├── templates/
│ ├── base/ # 2단 레이아웃 기본 템플릿
│ │ ├── mimetype, META-INF/*, version.xml, settings.xml, Preview/*
│ │ └── Contents/ (header.xml, section0.xml, content.hpf)
│ └── worksheet/ # (확장용) 오버레이 템플릿
├── examples/
│ ├── sample_middle_school.json # 중학교 문제 예시
│ ├── sample_high_school.json # 고등학교 문제 예시
│ ├── sample_exam_2020_march.json # 학력평가 형식 예시
│ ├── 01_middle_school_worksheet.sh # 빌드 예제
│ ├── 02_high_school_worksheet.sh # 빌드 예제
│ └── 03_exam_paper.sh # 학력평가 시험지 빌드 예제
└── references/ # 리그레션 테스트 레퍼런스 XML
Module dependency structure
build_math_hwpx.py (CLI + build 오케스트레이션)
├── hwpx_utils.py (validate_xml, pack_hwpx, validate_hwpx, update_metadata, _add_images_to_manifest)
├── section_generators.py (generate_worksheet_section_xml, generate_exam_section_xml)
│ ├── table_layout.py (_make_problem_cell_content, make_problem_table)
│ │ ├── xml_primitives.py (IDGen, STYLE, make_*_para, _make_equation_run)
│ │ └── exam_helpers.py (make_exam_problem_para, make_picture_para)
│ └── xml_primitives.py
└── graph_generator.py (변경 없음)
Dependency direction: primitives → helpers → table → section → build (no cycles)
Core workflow: JSON → HWPX problem sheet
1. Write problem JSON
Must follow the format below. exam_type, year, month, grade, points, choices etc. academic achievement assessment fields must be included.
{
"exam_type": "학력평가",
"year": 2025,
"month": 3,
"grade": "중2",
"session": 2,
"subject_area": "수학",
"total_pages": 4,
"question_type_label": "5지선다형",
"problems": [
{
"text": "의 값은?",
"equation": "2x + 3 = 7",
"points": 4,
"choices": ["1", "2", "3", "4", "5"]
},
{
"text": "을 간단히 한 것은?",
"equation": "sqrt 12 + sqrt 27",
"points": 5,
"choices": ["$3 sqrt 3$", "$4 sqrt 3$", "$5 sqrt 3$", "$6 sqrt 3$", "$7 sqrt 3$"]
},
{
"text": "다음 중 옳은 것은?",
"points": 4,
"choices": ["$sqrt 4 = 2$", "$sqrt 9 = +- 3$", "$(-2)^2 = -4$", "$sqrt {16} = 4$", "$sqrt 25 = -5$"]
},
{
"section_label": "주관식",
"text": "을 인수분해하시오.",
"equation": "x^2 - 5x + 6",
"points": 5
}
]
}
2. Build
source "$VENV"
# 학력평가 형식 (기본값 — exam_type 지정 불필요)
python3 "$SKILL_DIR/scripts/build_math_hwpx.py" \
--problems problems.json \
--creator "수학교사" \
--output exam.hwpx
# 단순 worksheet 형식 (명시적 지정 필요)
python3 "$SKILL_DIR/scripts/build_math_hwpx.py" \
--problems problems.json \
--exam-type worksheet \
--title "중2 일차방정식" \
--output worksheet.hwpx
3. Validation (use hwpx skill's validate.py)
python3 "$HWPX_SKILL_DIR/scripts/validate.py" worksheet.hwpx
Problem JSON format (academic achievement assessment — default)
Important: All problem JSON must be written in academic achievement assessment format. Do not use title/subtitle/info.
{
"exam_type": "학력평가",
"year": 2025,
"month": 3,
"grade": "중3",
"session": 2,
"subject_area": "수학",
"total_pages": 4,
"question_type_label": "5지선다형",
"problems": [
{
"text": "문제 본문 (수식 앞뒤 텍스트)",
"equation": "한컴 수식 스크립트",
"points": 4,
"choices": ["선택지1", "$수식선택지$", "선택지3", "선택지4", "선택지5"]
},
{
"text": "소문제가 있는 경우",
"equation": "메인 수식",
"points": 5,
"sub_problems": [
{"equation": "소문제 수식"}
],
"choices": ["1", "2", "3", "4", "5"]
},
{
"section_label": "주관식",
"text": "을 인수분해하시오.",
"equation": "x^2 - 5x + 6",
"points": 5
}
]
}
Field descriptions
| Field | Required | Description |
|---|---|---|
exam_type |
O | Must be "학력평가" specified |
year |
O | School year (e.g., 2025) |
month |
O | Administration month (e.g., 3, 6, 9, 11) |
grade |
O | Grade (e.g., "Middle 1", "Middle 2", "Middle 3", "High 1", "High 2", "High 3") |
session |
X | Period (default: 2) |
subject_area |
X | Subject area (default: "Mathematics") |
total_pages |
X | Total pages (default: automatic according to number of problems) |
question_type_label |
X | Question type label (default: "Multiple-choice 5 options") |
problems |
O | Problem array |
problems[].text |
X | Problem text |
problems[].equation |
X | Independent equation (display mode) |
problems[].points |
O | Point value (integer, [N점] displayed in the form) |
problems[].choices |
X | Multiple choice with 5 options ($...$wrapped in this becomes an equation) |
problems[].sub_problems |
X | Sub-problem array [{text, equation}] |
problems[].section_label |
X | Section division label (e.g., "Short answer") |
Hancom equation script syntax (hp:equation)
Basic rules
| Rule | Description |
|---|---|
{ } |
Grouping (multiple terms into one) |
~ |
Space (1em) |
` |
1/4 space |
# |
Line break (within equation) |
& |
Column alignment (matrices, systems of equations) |
"..." |
Text mode (equation parsing disabled) |
Fractions and roots
| Equation | Script | Example |
|---|---|---|
| Fraction | a over b |
{x+1} over {x-1} |
| Square root | sqrt {x} |
sqrt {b^2 - 4ac} |
| nth root | root n of {x} |
root 3 of {27} |
Superscripts and subscripts
| Equation | Script |
|---|---|
| Superscript | x^2 or x SUP 2 |
| Subscript | x_i or x SUB i |
| 둘 다 | x_i ^2 |
Integral·Sum·Product
| Equation | Script |
|---|---|
| Definite integral | int _{a} ^{b} f(x) dx |
| Double integral | dint f(x,y) dxdy |
| Triple integral | tint f dxdydz |
| Series (sigma) | sum _{k=1} ^{n} a_k |
| Product (pi) | prod _{i=1} ^{n} x_i |
Limit
| Equation | Script |
|---|---|
| Limit | lim _{x -> 0} f(x) |
| Uppercase | Lim _{n -> inf} |
Parentheses
| Equation | Script |
|---|---|
| Auto-sized parentheses | left ( {a over b} right ) |
| Auto-sized brackets | left [ x right ] |
| Auto-sized braces | left lbrace x right rbrace |
| Absolute value | `left |
Matrix
| Equation | Script |
|---|---|
| Basic matrix | matrix {a & b # c & d} |
| Parenthesis matrix | pmatrix {a & b # c & d} |
| Bracket matrix | bmatrix {1 & 0 # 0 & 1} |
| Determinant | dmatrix {a & b # c & d} |
System of equations·Conditions
| Equation | Script |
|---|---|
| System of equations | cases {2x+y=5 # 3x-2y=4} |
| Aligned equations | eqalign {a &= b # c &= d} |
| Vertical stack | pile {a # b # c} |
Decoration (above·below)
| Decoration | Script |
|---|---|
| Hat (^) | hat a |
| Tilde | tilde a |
| Vector arrow | vec v |
| Overline | bar x |
| Underline | under x |
| 점 1개 | dot a |
| 점 2개 | ddot a |
Greek letters
Lowercase: alpha, beta, gamma, delta, epsilon, zeta, eta, theta, iota, kappa, lambda, mu, nu, xi, pi, rho, sigma, tau, upsilon, phi, chi, psi, omega
Uppercase: ALPHA, BETA, GAMMA, DELTA 등
Variants: vartheta, varphi, varepsilon
Special symbols
| Symbol | Script |
|---|---|
| Infinity | inf |
| Partial derivative | partial |
| Nabla | nabla |
| Therefore | therefore |
| Because | because |
| For all | forall |
| Exists | exist |
| ± | +- or pm |
| ≠ | ne |
| ≤ | le or leq |
| ≥ | ge or geq |
| ≈ | approx |
| ≡ | equiv |
| ⊂ | subset |
| ∈ | in |
| → | -> or rarrow |
| ← | larrow |
| ↔ | <-> or lrarrow |
| ··· | cdots |
Font style
| Style | Command |
|---|---|
| Roman (upright) | rm |
| Italic | it |
| Bold | bold |
| Bold roman | rmbold |
Built-in functions (automatic roman)
sin, cos, tan, cot, sec, csc, arcsin, arccos, arctan, log, ln, lg, exp, det, mod, gcd, max, min, sinh, cosh, tanh
Equation XML structure
In section0.xml, equations are inserted as follows:
<hp:p id="고유ID" paraPrIDRef="22" styleIDRef="0" pageBreak="0" columnBreak="0" merged="0">
<hp:run charPrIDRef="9">
<hp:equation id="고유ID" type="0" textColor="#000000"
baseUnit="1000" letterSpacing="0" lineThickness="100">
<hp:sz width="0" height="0" widthRelTo="ABS" heightRelTo="ABS"/>
<hp:pos treatAsChar="1" affectLSpacing="0" flowWithText="0"
allowOverlap="0" holdAnchorAndSO="0" rgroupWithPrevCtrl="0"
vertRelTo="PARA" horzRelTo="PARA" vertAlign="TOP" horzAlign="LEFT"
vertOffset="0" horzOffset="0"/>
<hp:script>x = {-b +- sqrt {b^2 - 4ac}} over {2a}</hp:script>
</hp:equation>
</hp:run>
</hp:p>
Equation properties
| Property | 값 | Description |
|---|---|---|
baseUnit |
1000 | Default 10pt (100 HWPUNIT = 1pt) |
textColor |
#000000 | Equation color |
lineThickness |
100 | Fraction line/root line thickness |
treatAsChar |
1 | Inline equation (same line as text) |
Mixed text + equation
Place text and equations together in one paragraph:
<hp:p id="..." paraPrIDRef="21" ...>
<hp:run charPrIDRef="9"><hp:t>방정식 </hp:t></hp:run>
<hp:run charPrIDRef="9">
<hp:equation ...>
<hp:script>2x + 3 = 7</hp:script>
</hp:equation>
</hp:run>
<hp:run charPrIDRef="9"><hp:t> 의 해를 구하라.</hp:t></hp:run>
</hp:p>
Two-column layout settings
Of the first paragraph in section0.xml, hp:colPrset to:
<hp:colPr id="" type="NEWSPAPER" layout="LEFT" colCount="2" sameSz="1" sameGap="2268"/>
| Property | 값 | Description |
|---|---|---|
type |
NEWSPAPER | Fill in left→right order |
colCount |
2 | 2단 |
sameSz |
1 | Equal width |
sameGap |
2268 | Column spacing 8mm |
Page settings (optimized for problem sheets)
<hp:pagePr landscape="WIDELY" width="59528" height="84186" gutterType="LEFT_ONLY">
<hp:margin header="4252" footer="4252" gutter="0"
left="5668" right="5668" top="4252" bottom="4252"/>
</hp:pagePr>
- Left/right margins: 20mm (narrower than the standard 30mm → expanded content area)
- Top/bottom margin: 15mm
- Body width: 48192 HWPUNIT (170mm)
- Column width: (48192 - 2268) / 2 = 22962 HWPUNIT (approx. 81mm)
Style ID Map
charPr (character style)
| ID | Description | Size | Weight | Italic | Notes |
|---|---|---|---|---|---|
| 0 | Default body | 10pt | Regular | - | Hamchorom Batang |
| 1 | Default Dotum | 10pt | Regular | - | Hamchorom Dotum |
| 2~6 | Skeleton compatible | Various | Regular | - | |
| 7 | Question paper title | 16pt | Bold | - | worksheet용 |
| 8 | Question number | 11pt | Bold | - | worksheet용 |
| 9 | Question body | 10pt | Regular | - | Common |
| 10 | Unit name/subheading | 12pt | Bold | - | Common |
| 11 | Choices/view | 9pt | Regular | - | Common |
| 12 | Exam title line | 10pt | Regular | - | exam용 |
| 13 | Subject area | 18pt | Bold | - | "Mathematics Area" for exam |
| 14 | Session label | 10pt | Regular | - | "2nd Session" for exam |
| 15 | Exam question number | 10pt | Bold | Italic | exam용 |
| 16 | Points | 9pt | Regular | - | exam용 "[2점]" |
| 17 | Page number | 9pt | Regular | - | exam용 |
paraPr (paragraph style)
| ID | Alignment | Line spacing | Purpose |
|---|---|---|---|
| 0 | JUSTIFY | 160% | Default body |
| 1~19 | Various | Various | Skeleton compatible |
| 20 | CENTER | 160% | Title (worksheet) |
| 21 | LEFT | 150% | Question body (Common) |
| 22 | LEFT | 140% | Formula display (Common) |
| 23 | LEFT | 140% | Choices/view vertical (Common) |
| 24 | CENTER | 130% | Exam title line (exam) |
| 25 | LEFT | 130% | Session + subject line (exam) |
| 26 | CENTER | 150% | Subject area main heading (exam) |
| 27 | LEFT | 140% | Choices horizontal (exam, tabPr=3) |
| 28 | LEFT | 140% | Item type label (exam, border) |
| 29 | CENTER | 130% | Page bottom number (exam) |
tabPr (tab settings)
| ID | Description |
|---|---|
| 0 | Default (no tabs) |
| 1 | Left auto tab |
| 2 | Right auto tab |
| 3 | 5 tab stops for horizontal choices (4590, 9180, 13770, 18360) |
borderFill (border)
| ID | Description |
|---|---|
| 1 | None (page border) |
| 2 | None + transparent background |
| 3 | SOLID 4 sides (for tables) |
| 4 | SOLID + #E8E8E8 background (header cell) |
| 5 | Bottom DASH line (question separator) |
| 6 | SOLID border (session/item type label) |
| 7 | Thick SOLID border (page number box) |
Formula examples by grade level
Middle school (middle 1 to middle 3)
# 일차방정식
2x + 3 = 7
# 분수 방정식
{2x+1} over 3 = {x-2} over 5
# 연립방정식
cases {2x + y = 5 # 3x - 2y = 4}
# 제곱근
sqrt 12 + sqrt 27 - sqrt 48
# 부등식
3x - 5 > 2x + 1
# 이차방정식
x^2 - 5x + 6 = 0
# 피타고라스
a^2 + b^2 = c^2
# 일차함수
y = ax + b
High School Mathematics I (Grade 10)
# 지수법칙
a^m times a^n = a^{m+n}
# 로그
log _a xy = log _a x + log _a y
# 절댓값
left | x - 3 right | < 5
# 이차함수 꼭짓점
y = a(x - p)^2 + q
# 근의 공식
x = {-b +- sqrt {b^2 - 4ac}} over {2a}
High School Mathematics II (Grade 11)
# 극한
lim _{x -> 0} {sin x} over x = 1
# 미분 정의
f'(x) = lim _{h -> 0} {f(x+h) - f(x)} over h
# 정적분
int _{0} ^{pi} sin x dx = 2
# 급수
sum _{k=1} ^{n} k = {n(n+1)} over 2
# 등차수열
a_n = a_1 + (n-1)d
High School Probability and Statistics
# 조합
{_n}C{_r} = {n!} over {r!(n-r)!}
# 이항정리
(a+b)^n = sum _{k=0} ^{n} {_n}C{_k} a^{n-k} b^k
# 확률
P(A cup B) = P(A) + P(B) - P(A cap B)
# 정규분포
f(x) = {1} over {sigma sqrt {2 pi}} e^{-{(x- mu)^2} over {2 sigma ^2}}
High School Calculus
# 도함수
{d} over {dx} x^n = n x^{n-1}
# 합성함수 미분
{dy} over {dx} = {dy} over {du} times {du} over {dx}
# 부분적분
int u dv = uv - int v du
# 치환적분
int f(g(x)) g'(x) dx = int f(u) du
# 테일러 급수
e^x = sum _{n=0} ^{inf} {x^n} over {n!}
High School Geometry
# 벡터 내적
vec a cdot vec b = left | vec a right | left | vec b right | cos theta
# 원의 방정식
(x-a)^2 + (y-b)^2 = r^2
# 타원
{x^2} over {a^2} + {y^2} over {b^2} = 1
# 쌍곡선
{x^2} over {a^2} - {y^2} over {b^2} = 1
# 행렬 곱
pmatrix {a & b # c & d} pmatrix {x # y} = pmatrix {ax+by # cx+dy}
Geometry Shapes Type (Geometry Shapes)
graph_generator.pysupports 5 geometry shape types in addition to function graphs (polynomial, trig, etc.).
In the problem JSON, graph If you specify the spec below in the field, a shape PNG is automatically generated.
triangle — triangle
Defined by 3 vertex coordinates. Supports interior angle display, angle arcs, side length labels, auxiliary lines (median/altitude/angle bisector), circumcircle/incircle.
{
"type": "triangle",
"vertices": [[0, 0], [6, 0], [2, 5]],
"labels": {"A": [2, 5], "B": [0, 0], "C": [6, 0]},
"show_angles": [true, true, true],
"angle_labels": ["80°", "50°", "50°"],
"side_labels": {"AB": "5", "BC": "6", "AC": "5"},
"equal_marks": {"AB": 1, "AC": 1},
"show_circumcircle": false,
"show_incircle": false,
"auxiliary_lines": [{"type": "bisector", "vertex": "B"}]
}
| Field | Required | Description |
|---|---|---|
vertices |
O | 3 vertex coordinates [[x,y], [x,y], [x,y]] |
labels |
X | Vertex name→coordinate mapping |
show_angles |
X | Whether to display an angle arc for each vertex [bool, bool, bool] |
angle_labels |
X | Angle label (e.g., ["80°", "50°", "50°"]) |
side_labels |
X | Side name→length label (e.g., {"AB": "5"}) |
equal_marks |
X | Side name→number of equal-division marks (e.g., {"AB": 1, "AC": 1}) |
show_circumcircle |
X | Display circumcircle |
show_incircle |
X | Display incircle |
auxiliary_lines |
X | Auxiliary line array. type: "median", "altitude", "bisector" |
circle — circle/sector/arc
Center, radius, points on the circle, chord, tangent, arc highlight, central angle/inscribed angle display.
{
"type": "circle",
"center": [0, 0],
"radius": 3,
"show_center": true,
"points_on_circle": [
{"angle_deg": 30, "label": "A"},
{"angle_deg": 150, "label": "B"}
],
"chords": [["A", "B"]],
"tangent_at": ["A"],
"arc_highlight": {"from": "A", "to": "B", "color": "gray"},
"central_angle": true,
"inscribed_angle": {"vertex": "C", "arc": ["A", "B"]}
}
| Field | Required | Description |
|---|---|---|
center |
X | Center coordinates (default: [0,0]) |
radius |
X | Radius (default: 3) |
show_center |
X | Display center point O |
points_on_circle |
X | Array of points on the circle {angle_deg, label} |
chords |
X | 현 [["A","B"]] |
tangent_at |
X | Name of point where tangent is drawn |
arc_highlight |
X | Arc highlight {from, to, color} |
central_angle |
X | Display central angle segment |
inscribed_angle |
X | Inscribed angle {vertex, arc: [A, B]} |
quadrilateral — quadrilateral
Defined by 4 vertex coordinates. Supports diagonals, parallel marks, equal-division marks, right angle marks.
{
"type": "quadrilateral",
"kind": "parallelogram",
"vertices": [[0, 0], [5, 0], [7, 3], [2, 3]],
"labels": {"A": [0, 0], "B": [5, 0], "C": [7, 3], "D": [2, 3]},
"show_diagonals": true,
"diagonal_intersection_label": "O",
"parallel_marks": {"AB_DC": 1, "AD_BC": 2},
"equal_marks": {"AB": 1, "DC": 1},
"show_right_angles": [],
"side_labels": {"AB": "10", "BC": "6"}
}
| Field | Required | Description |
|---|---|---|
vertices |
O | 4 vertex coordinates |
kind |
X | Reference type name (parallelogram, rectangle, etc.) |
labels |
X | Vertex name→coordinate |
show_diagonals |
X | Display diagonals as dashed lines |
diagonal_intersection_label |
X | Diagonal intersection label |
parallel_marks |
X | Parallel mark {"AB_DC": 1} (AB∥DC, 1 arrow) |
equal_marks |
X | Equal-division marks |
show_right_angles |
X | Array of vertex names to mark as right angles |
side_labels |
X | Side length labels |
coordinate — shapes on coordinates
Combination of coordinate axes + segments/polygons/points/lines. Linear functions and areas of shapes, etc.
{
"type": "coordinate",
"xlim": [-1, 7],
"ylim": [-1, 7],
"segments": [[[0, 6], [2, 0]], [[2, 0], [6, 0]], [[0, 6], [6, 0]]],
"points": [
{"pos": [0, 6], "label": "(0, 6)"},
{"pos": [2, 0], "label": "(2, 0)"}
],
"fill_polygon": [[0, 6], [2, 0], [6, 0]],
"shade_alpha": 0.15,
"lines": [{"slope": -3, "intercept": 6, "style": "k-"}],
"circles": [{"center": [3, 3], "radius": 2}]
}
| Field | Required | Description |
|---|---|---|
xlim, ylim |
X | Coordinate axis range |
segments |
X | Segment array [[[x1,y1],[x2,y2]], ...] |
points |
X | Point array {pos, label} |
fill_polygon |
X | Vertex coordinates of polygon to fill |
shade_alpha |
X | Fill opacity (default: 0.15) |
lines |
X | Line {slope, intercept, style} |
circles |
X | 원 {center, radius} |
solid3d — solid figure (2D projection)
Renders exam-paper-style solid figures with 2D oblique projection. Represents back edges with dashed lines.
{
"type": "solid3d",
"kind": "cylinder",
"params": {"radius": 2, "height": 4},
"labels": {"r": "2", "h": "4"},
"show_hidden": true
}
Supported kind: cylinder, cone, sphere, rectangular_prism, triangular_prism, pyramid
| Field | Required | Description |
|---|---|---|
kind |
X | Solid type (default: "cylinder") |
params |
X | Parameters by type (radius, height, width, depth, base, etc.) |
labels |
X | Dimension labels (e.g., {"r": "2", "h": "4"}) |
show_hidden |
X | Show back edge as dashed line (default: true) |
kind별 params:
cylinder:{radius, height}cone:{radius, height}sphere:{radius}rectangular_prism:{width, height, depth}triangular_prism:{base, height, depth}pyramid:{base, height, depth}
Writing section0.xml directly (advanced)
Write section0.xml directly instead of JSON for finer control:
SECTION=$(mktemp /tmp/section0_XXXX.xml)
cat > "$SECTION" << 'XMLEOF'
<?xml version='1.0' encoding='UTF-8'?>
<hs:sec xmlns:hp="http://www.hancom.co.kr/hwpml/2011/paragraph"
xmlns:hs="http://www.hancom.co.kr/hwpml/2011/section"
xmlns:hc="http://www.hancom.co.kr/hwpml/2011/core">
<!-- base/section0.xml의 첫 문단(secPr+colPr) 그대로 복사 -->
<!-- ... -->
<!-- 제목 -->
<hp:p id="1000000002" paraPrIDRef="20" styleIDRef="0" pageBreak="0" columnBreak="0" merged="0">
<hp:run charPrIDRef="7"><hp:t>수학 문제지</hp:t></hp:run>
</hp:p>
<!-- 수식 문단 -->
<hp:p id="1000000003" paraPrIDRef="22" styleIDRef="0" pageBreak="0" columnBreak="0" merged="0">
<hp:run charPrIDRef="9">
<hp:equation id="1000000099" type="0" textColor="#000000"
baseUnit="1000" letterSpacing="0" lineThickness="100">
<hp:sz width="0" height="0" widthRelTo="ABS" heightRelTo="ABS"/>
<hp:pos treatAsChar="1" affectLSpacing="0" flowWithText="0"
allowOverlap="0" holdAnchorAndSO="0" rgroupWithPrevCtrl="0"
vertRelTo="PARA" horzRelTo="PARA" vertAlign="TOP" horzAlign="LEFT"
vertOffset="0" horzOffset="0"/>
<hp:script>x = {-b +- sqrt {b^2 - 4ac}} over {2a}</hp:script>
</hp:equation>
</hp:run>
</hp:p>
</hs:sec>
XMLEOF
python3 "$SKILL_DIR/scripts/build_math_hwpx.py" --section "$SECTION" --output result.hwpx
rm -f "$SECTION"
Integration with the hwpx skill
math-hwpx can reuse the tools of the hwpx skill:
| Tool | Path | Purpose |
|---|---|---|
| validate.py | $HWPX_SKILL_DIR/scripts/validate.py |
Validate HWPX structure |
| unpack.py | $HWPX_SKILL_DIR/scripts/office/unpack.py |
HWPX → directory |
| pack.py | $HWPX_SKILL_DIR/scripts/office/pack.py |
directory → HWPX |
| text_extract.py | $HWPX_SKILL_DIR/scripts/text_extract.py |
Extract text |
# 생성된 문제지 구조 확인
python3 "$HWPX_SKILL_DIR/scripts/office/unpack.py" worksheet.hwpx ./unpacked/
# → ./unpacked/Contents/section0.xml 편집 후
python3 "$HWPX_SKILL_DIR/scripts/office/pack.py" ./unpacked/ edited.hwpx
Unit conversion (same as the hwpx skill)
| 값 | HWPUNIT | Meaning |
|---|---|---|
| 1pt | 100 | Default unit |
| 10pt | 1000 | Default font size |
| 1mm | 283.5 | Millimeter |
| A4 폭 | 59528 | 210mm |
| A4 height | 84186 | 297mm |
| Question paper left/right margin | 5668 | 20mm |
| Question paper body width | 48192 | 170mm |
| Column spacing | 2268 | 8mm |
| Column width | 22962 | 약 81mm |
Critical Rules
- Always the academic assessment format: JSON must include
"exam_type": "학력평가". The worksheet format must not be used - Points required: Every question requires the
"points"field. Multiple choice requires"choices"5 items, short answer requires"section_label": "주관식"included - Formula scripts go
<hp:script>inside: Use Hancome formula syntax, not LaTeX - secPr required: secPr must be included in the first paragraph of section0.xml
- mimetype order: When packaging as ZIP, mimetype must be the first entry, ZIP_STORED
- ID uniqueness: Paragraph IDs and formula IDs must all be unique within the document
- charPrIDRef consistency: charPr IDs referenced in section0.xml must exist in header.xml
- Use venv: The project's
.venv/bin/activate(lxml package required) - Validation required: Check integrity with validate.py after generation
- Formula size:
baseUnit="1000"= 10pt (same as body text), adjust to1200(12pt) etc. if needed - Choice formulas: In JSON, wrapping with
$...$treats it as a formula. Must use Hancome formula script syntax ($3 sqrt 2$✓,$3\sqrt{2}$✗ LaTeX not allowed). Example:$30 pi$,$sqrt 3$,${x^2} over 2$ - hp:sz width/height 0: Can be set to 0 since Hancome Office calculates it automatically when rendering
Academic assessment/CSAT exam paper format (default)
The academic assessment format is the default. Every question JSON must include "exam_type": "학력평가", and each question must specify "points" points. Multiple choice includes 5 "choices", and short answer specifies "section_label": "주관식".
Exam paper JSON format
{
"exam_type": "학력평가",
"year": 2020,
"month": 3,
"grade": "고1",
"session": 2,
"subject_area": "수학",
"total_pages": 12,
"question_type_label": "5지선다형",
"problems": [
{
"text": "의 값은?",
"equation": "-{7} over {2} times (-3) + 4 times left | -{5} over {2} right |",
"points": 2,
"choices": ["-1", "$-{1} over {2}$", "0", "${1} over {2}$", "1"]
},
{
"section_label": "주관식",
"text": "의 값을 구하시오.",
"equation": "2x + 3 = 7",
"points": 4
}
]
}
Exam paper-specific JSON fields
| Field | Required | Description |
|---|---|---|
exam_type |
O | "학력평가", "수능", "exam" one of |
year |
X | School year (e.g., 2020) |
month |
X | Exam month (e.g., 3) |
grade |
X | Grade (e.g., "High 1") |
session |
X | Period (default: 2) |
subject_area |
X | Subject area (default: "Mathematics") |
total_pages |
X | Total number of pages (default: 12) |
question_type_label |
X | Question type label (default: "Multiple choice with 5 options") |
problems[].points |
X | Points (integer, [2점] displayed in the form) |
problems[].section_label |
X | Section division label (e.g., "Subjective") |
Exam paper layout structure
┌─────────────────────────────────────────┐
│ 2020학년도 3월 고1 전국연합학력평가 문제지 │ ← 1단 (전체너비)
│ │
│ 제 2 교시 수학 영역 │ ← 1단 (전체너비)
├────────────────────┬────────────────────┤
│ ┌──────────┐ │ │
│ │ 5지선다형 │ │ │ ← 2단 시작
│ └──────────┘ │ │
│ │ │
│ 1. 문제... [2점] │ 3. 문제... [2점] │
│ ① a ② b ③ c ④ d ⑤ e│ ① a ② b ③ c ④ d│ ← 가로 선택지
│ │ │
│ 2. 문제... [2점] │ 4. 문제... [3점] │
│ ① a ② b ③ c ④ d ⑤ e│ ① a ② b ③ c ④ d│
├────────────────────┴────────────────────┤
│ 1 / 12 │ ← 1단 (전체너비)
└─────────────────────────────────────────┘
Column switching technique
In the exam format, hp:colPris used to switch the number of columns within a section:
- In the secPr paragraph, start with
colCount="1"(full width of the header area) - After outputting the header paragraphs, switch to
hp:colPr colCount="2"(2 columns for the body) - Output the body questions
- (Optional) switch to
hp:colPr colCount="1"(bottom page number)
Horizontal answer choices
Use the 5 tab stops defined in tabPr ID 3 to place the answer choices on one line:
① -1 ② -1/2 ③ 0 ④ 1/2 ⑤ 1
Between each answer choice, insert <hp:tab/>to arrange them at equal intervals.
Build example
# 학력평가 형식 (기본값)
python3 "$SKILL_DIR/scripts/build_math_hwpx.py" \
--problems problems.json \
--creator "교육청" \
--output exam.hwpx