[official skill] DOCX ์์ฑ
์ด ์คํฌ์ docx-js(JavaScript)๋ก ์ DOCX ๋ฌธ์๋ฅผ ์์ฑํ๊ฑฐ๋, XML ์ธํฉ/ํธ์ง/๋ฆฌํฉ ๋ฐฉ์์ผ๋ก ๊ธฐ์กด Word ๋ฌธ์๋ฅผ ํธ์งํ๋ ๋๊ตฌ์ ๋๋ค.
๐ ์ฐธ๊ณ : ์ด ๋ฌธ์๋ ๊ณต์ Claude Code ๋ฌธ์์ ์๋ฌธ + ๋ ํผ๋ฐ์ค์ ๋๋ค.
- ์๋ฌธ ๋งํฌ๋ฅผ ์ง์ ์ฌ์ฉํ์ ๋ ๋ฉ๋๋ค
- ์ด ํ์ผ์
.claude/skills/ํ์์ ๋ณต์ฌํ์ฌ ์คํฌ๋ก ๋ฑ๋กํ ์๋ ์์ต๋๋ค(2026.03.15 ๊ธฐ์ค)
DOCX ์์ฑ, ํธ์ง, ๋ถ์
๊ฐ์
.docx ํ์ผ์ XML ํ์ผ์ ํฌํจํ๋ ZIP ์์นด์ด๋ธ์ ๋๋ค.
๋น ๋ฅธ ์ฐธ์กฐ
| ์์ | ์ ๊ทผ ๋ฐฉ์ |
|---|---|
| ๋ด์ฉ ์ฝ๊ธฐ/๋ถ์ | pandoc ๋๋ ์๋ณธ XML์ ์ํด ์ธํฉ |
| ์ ๋ฌธ์ ์์ฑ | docx-js ์ฌ์ฉ - ์๋ ์ ๋ฌธ์ ์์ฑ ์น์
์ฐธ์กฐ |
| ๊ธฐ์กด ๋ฌธ์ ํธ์ง | ์ธํฉ โ XML ํธ์ง โ ๋ฆฌํฉ - ์๋ ๊ธฐ์กด ๋ฌธ์ ํธ์ง ์น์ ์ฐธ์กฐ |
.doc์ .docx๋ก ๋ณํ
๋ ๊ฑฐ์ .doc ํ์ผ์ ํธ์ง ์ ์ ๋ณํํด์ผ ํฉ๋๋ค:
python scripts/office/soffice.py --headless --convert-to docx document.doc
๋ด์ฉ ์ฝ๊ธฐ
# ๋ณ๊ฒฝ ๋ด์ฉ ์ถ์ ํฌํจ ํ
์คํธ ์ถ์ถ
pandoc --track-changes=all document.docx -o output.md
# ์๋ณธ XML ์ ๊ทผ
python scripts/office/unpack.py document.docx unpacked/
์ด๋ฏธ์ง๋ก ๋ณํ
python scripts/office/soffice.py --headless --convert-to pdf document.docx
pdftoppm -jpeg -r 150 document.pdf page
๋ณ๊ฒฝ ๋ด์ฉ ์ถ์ ์น์ธ
๋ชจ๋ ๋ณ๊ฒฝ ๋ด์ฉ ์ถ์ ์ด ์น์ธ๋ ๊นจ๋ํ ๋ฌธ์๋ฅผ ์์ฑํ๋ ค๋ฉด (LibreOffice ํ์):
python scripts/accept_changes.py input.docx output.docx
์ ๋ฌธ์ ์์ฑ
JavaScript๋ก .docx ํ์ผ์ ์์ฑํ ํ ๊ฒ์ฆํฉ๋๋ค. ์ค์น: npm install -g docx
์ค์
const { Document, Packer, Paragraph, TextRun, Table, TableRow, TableCell, ImageRun,
Header, Footer, AlignmentType, PageOrientation, LevelFormat, ExternalHyperlink,
InternalHyperlink, Bookmark, FootnoteReferenceRun, PositionalTab,
PositionalTabAlignment, PositionalTabRelativeTo, PositionalTabLeader,
TabStopType, TabStopPosition, Column, SectionType,
TableOfContents, HeadingLevel, BorderStyle, WidthType, ShadingType,
VerticalAlign, PageNumber, PageBreak } = require('docx');
const doc = new Document({ sections: [{ children: [/* content */] }] });
Packer.toBuffer(doc).then(buffer => fs.writeFileSync("doc.docx", buffer));
๊ฒ์ฆ
ํ์ผ ์์ฑ ํ ๊ฒ์ฆํฉ๋๋ค. ๊ฒ์ฆ์ ์คํจํ๋ฉด ์ธํฉํ๊ณ XML์ ์์ ํ ํ ๋ฆฌํฉํ์ธ์.
python scripts/office/validate.py doc.docx
ํ์ด์ง ํฌ๊ธฐ
// ์ค์: docx-js๋ ๊ธฐ๋ณธ๊ฐ์ด US Letter๊ฐ ์๋ A4์
๋๋ค
// ์ผ๊ด๋ ๊ฒฐ๊ณผ๋ฅผ ์ํด ํญ์ ํ์ด์ง ํฌ๊ธฐ๋ฅผ ๋ช
์์ ์ผ๋ก ์ค์ ํ์ธ์
sections: [{
properties: {
page: {
size: {
width: 12240, // 8.5์ธ์น(DXA ๋จ์)
height: 15840 // 11์ธ์น(DXA ๋จ์)
},
margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 } // 1์ธ์น ์ฌ๋ฐฑ
}
},
children: [/* content */]
}]
์ผ๋ฐ์ ์ธ ํ์ด์ง ํฌ๊ธฐ (DXA ๋จ์, 1440 DXA = 1์ธ์น):
| ์ฉ์ง | ๋๋น | ๋์ด | ์ฝํ ์ธ ๋๋น (1์ธ์น ์ฌ๋ฐฑ) |
|---|---|---|---|
| US Letter | 12,240 | 15,840 | 9,360 |
| A4 (๊ธฐ๋ณธ๊ฐ) | 11,906 | 16,838 | 9,026 |
๊ฐ๋ก ๋ฐฉํฅ: docx-js๋ ๋ด๋ถ์ ์ผ๋ก ๋๋น/๋์ด๋ฅผ ๊ตํํ๋ฏ๋ก, ์ธ๋ก ๋ฐฉํฅ ์น์๋ฅผ ์ ๋ฌํ๊ณ ๊ตํ์ ์๋์ผ๋ก ์ฒ๋ฆฌ๋๊ฒ ํ์ธ์:
size: {
width: 12240, // ์งง์ ๋ณ์ width๋ก ์ ๋ฌ
height: 15840, // ๊ธด ๋ณ์ height๋ก ์ ๋ฌ
orientation: PageOrientation.LANDSCAPE // docx-js๊ฐ XML์์ ๊ตํ ์ฒ๋ฆฌ
},
// ์ฝํ
์ธ ๋๋น = 15840 - ์ผ์ชฝ ์ฌ๋ฐฑ - ์ค๋ฅธ์ชฝ ์ฌ๋ฐฑ (๊ธด ๋ณ ์ฌ์ฉ)
์คํ์ผ (๋ด์ฅ ์ ๋ชฉ ์ฌ์ ์)
๊ธฐ๋ณธ ํฐํธ๋ก Arial์ ์ฌ์ฉํ์ธ์ (๋ฒ์ฉ ์ง์). ๊ฐ๋ ์ฑ์ ์ํด ์ ๋ชฉ์ ๊ฒ์์์ผ๋ก ์ ์งํ์ธ์.
const doc = new Document({
styles: {
default: { document: { run: { font: "Arial", size: 24 } } }, // 12pt ๊ธฐ๋ณธ๊ฐ
paragraphStyles: [
// ์ค์: ๋ด์ฅ ์คํ์ผ์ ์ฌ์ ์ํ๋ ค๋ฉด ์ ํํ ID๋ฅผ ์ฌ์ฉํ์ธ์
{ id: "Heading1", name: "Heading 1", basedOn: "Normal", next: "Normal", quickFormat: true,
run: { size: 32, bold: true, font: "Arial" },
paragraph: { spacing: { before: 240, after: 240 }, outlineLevel: 0 } }, // TOC์ outlineLevel ํ์
{ id: "Heading2", name: "Heading 2", basedOn: "Normal", next: "Normal", quickFormat: true,
run: { size: 28, bold: true, font: "Arial" },
paragraph: { spacing: { before: 180, after: 180 }, outlineLevel: 1 } },
]
},
sections: [{
children: [
new Paragraph({ heading: HeadingLevel.HEADING_1, children: [new TextRun("Title")] }),
]
}]
});
๋ชฉ๋ก (์ ๋ ์ ๋์ฝ๋ ๊ธ๋จธ๋ฆฌ ๊ธฐํธ๋ฅผ ์ฌ์ฉํ์ง ๋ง์ธ์)
// WRONG - ์ ๋ ๊ธ๋จธ๋ฆฌ ๊ธฐํธ ๋ฌธ์๋ฅผ ์๋์ผ๋ก ์ฝ์
ํ์ง ๋ง์ธ์
new Paragraph({ children: [new TextRun("โข Item")] }) // BAD
new Paragraph({ children: [new TextRun("\u2022 Item")] }) // BAD
// CORRECT - LevelFormat.BULLET๋ก numbering ์ค์ ์ ์ฌ์ฉํ์ธ์
const doc = new Document({
numbering: {
config: [
{ reference: "bullets",
levels: [{ level: 0, format: LevelFormat.BULLET, text: "โข", alignment: AlignmentType.LEFT,
style: { paragraph: { indent: { left: 720, hanging: 360 } } } }] },
{ reference: "numbers",
levels: [{ level: 0, format: LevelFormat.DECIMAL, text: "%1.", alignment: AlignmentType.LEFT,
style: { paragraph: { indent: { left: 720, hanging: 360 } } } }] },
]
},
sections: [{
children: [
new Paragraph({ numbering: { reference: "bullets", level: 0 },
children: [new TextRun("Bullet item")] }),
new Paragraph({ numbering: { reference: "numbers", level: 0 },
children: [new TextRun("Numbered item")] }),
]
}]
});
// ๊ฐ reference๋ ๋
๋ฆฝ์ ์ธ ๋ฒํธ ๋งค๊น์ ์์ฑํฉ๋๋ค
// ๊ฐ์ reference = ๊ณ์ (1,2,3 ์ดํ 4,5,6)
// ๋ค๋ฅธ reference = ์ฌ์์ (1,2,3 ์ดํ 1,2,3)
ํ
์ค์: ํ์๋ ์ด์ค ๋๋น ์ค์ ์ด ํ์ํฉ๋๋ค - ํ์ columnWidths์ ๊ฐ ์
์ width๋ฅผ ๋ชจ๋ ์ค์ ํ์ธ์. ๋ ๋ค ์์ผ๋ฉด ์ผ๋ถ ํ๋ซํผ์์ ํ๊ฐ ์๋ชป ๋ ๋๋ง๋ฉ๋๋ค.
// ์ค์: ์ผ๊ด๋ ๋ ๋๋ง์ ์ํด ํญ์ ํ ๋๋น๋ฅผ ์ค์ ํ์ธ์
// ์ค์: ๊ฒ์ ๋ฐฐ๊ฒฝ์ ๋ฐฉ์งํ๋ ค๋ฉด ShadingType.CLEAR๋ฅผ ์ฌ์ฉํ์ธ์ (SOLID๊ฐ ์๋)
const border = { style: BorderStyle.SINGLE, size: 1, color: "CCCCCC" };
const borders = { top: border, bottom: border, left: border, right: border };
new Table({
width: { size: 9360, type: WidthType.DXA }, // ํญ์ DXA ์ฌ์ฉ (ํผ์ผํธ๋ Google Docs์์ ๊นจ์ง)
columnWidths: [4680, 4680], // ํฉ๊ณ๊ฐ ํ ๋๋น์ ๊ฐ์์ผ ํจ (DXA: 1440 = 1์ธ์น)
rows: [
new TableRow({
children: [
new TableCell({
borders,
width: { size: 4680, type: WidthType.DXA }, // ๊ฐ ์
์๋ ์ค์
shading: { fill: "D5E8F0", type: ShadingType.CLEAR }, // SOLID๊ฐ ์๋ CLEAR
margins: { top: 80, bottom: 80, left: 120, right: 120 }, // ์
ํจ๋ฉ (๋ด๋ถ, ๋๋น์ ์ถ๊ฐ๋์ง ์์)
children: [new Paragraph({ children: [new TextRun("Cell")] })]
})
]
})
]
})
ํ ๋๋น ๊ณ์ฐ:
ํญ์ WidthType.DXA๋ฅผ ์ฌ์ฉํ์ธ์ -- WidthType.PERCENTAGE๋ Google Docs์์ ๊นจ์ง๋๋ค.
// ํ ๋๋น = columnWidths์ ํฉ = ์ฝํ
์ธ ๋๋น
// 1์ธ์น ์ฌ๋ฐฑ์ US Letter: 12240 - 2880 = 9360 DXA
width: { size: 9360, type: WidthType.DXA },
columnWidths: [7000, 2360] // ํฉ๊ณ๊ฐ ํ ๋๋น์ ๊ฐ์์ผ ํจ
๋๋น ๊ท์น:
- ํญ์
WidthType.DXA๋ฅผ ์ฌ์ฉ -- ์ ๋WidthType.PERCENTAGE์ฌ์ฉ ๊ธ์ง (Google Docs์ ํธํ๋์ง ์์) - ํ ๋๋น๋
columnWidths์ ํฉ๊ณผ ๊ฐ์์ผ ํจ - ์
width๋ ํด๋นํ๋columnWidth์ ์ผ์นํด์ผ ํจ - ์
margins๋ ๋ด๋ถ ํจ๋ฉ - ์ฝํ ์ธ ์์ญ์ ์ค์ด๋ฉฐ, ์ ๋๋น์ ์ถ๊ฐ๋์ง ์์ - ์ ์ฒด ๋๋น ํ์ ๊ฒฝ์ฐ: ์ฝํ ์ธ ๋๋น(ํ์ด์ง ๋๋น์์ ์ข์ฐ ์ฌ๋ฐฑ์ ๋บ ๊ฐ) ์ฌ์ฉ
์ด๋ฏธ์ง
// ์ค์: type ํ๋ผ๋ฏธํฐ๋ ํ์์
๋๋ค
new Paragraph({
children: [new ImageRun({
type: "png", // ํ์: png, jpg, jpeg, gif, bmp, svg
data: fs.readFileSync("image.png"),
transformation: { width: 200, height: 150 },
altText: { title: "Title", description: "Desc", name: "Name" } // ์ธ ๊ฐ์ง ๋ชจ๋ ํ์
})]
})
ํ์ด์ง ๋๋๊ธฐ
// ์ค์: PageBreak๋ Paragraph ์์ ์์ด์ผ ํฉ๋๋ค
new Paragraph({ children: [new PageBreak()] })
// ๋๋ pageBreakBefore ์ฌ์ฉ
new Paragraph({ pageBreakBefore: true, children: [new TextRun("New page")] })
ํ์ดํผ๋งํฌ
// ์ธ๋ถ ๋งํฌ
new Paragraph({
children: [new ExternalHyperlink({
children: [new TextRun({ text: "Click here", style: "Hyperlink" })],
link: "https://example.com",
})]
})
// ๋ด๋ถ ๋งํฌ (๋ถ๋งํฌ + ์ฐธ์กฐ)
// 1. ๋ชฉ์ ์ง์ ๋ถ๋งํฌ ์์ฑ
new Paragraph({ heading: HeadingLevel.HEADING_1, children: [
new Bookmark({ id: "chapter1", children: [new TextRun("Chapter 1")] }),
]})
// 2. ๋งํฌ ์ฐ๊ฒฐ
new Paragraph({ children: [new InternalHyperlink({
children: [new TextRun({ text: "See Chapter 1", style: "Hyperlink" })],
anchor: "chapter1",
})]})
๊ฐ์ฃผ
const doc = new Document({
footnotes: {
1: { children: [new Paragraph("Source: Annual Report 2024")] },
2: { children: [new Paragraph("See appendix for methodology")] },
},
sections: [{
children: [new Paragraph({
children: [
new TextRun("Revenue grew 15%"),
new FootnoteReferenceRun(1),
new TextRun(" using adjusted metrics"),
new FootnoteReferenceRun(2),
],
})]
}]
});
ํญ ์ ์ง
// ๊ฐ์ ์ค์์ ํ
์คํธ๋ฅผ ์ค๋ฅธ์ชฝ ์ ๋ ฌ (์: ์ ๋ชฉ ๋ฐ๋ํธ์ ๋ ์ง)
new Paragraph({
children: [
new TextRun("Company Name"),
new TextRun("\tJanuary 2025"),
],
tabStops: [{ type: TabStopType.RIGHT, position: TabStopPosition.MAX }],
})
// ์ ์ ๋ฆฌ๋ (์: ๋ชฉ์ฐจ ์คํ์ผ)
new Paragraph({
children: [
new TextRun("Introduction"),
new TextRun({ children: [
new PositionalTab({
alignment: PositionalTabAlignment.RIGHT,
relativeTo: PositionalTabRelativeTo.MARGIN,
leader: PositionalTabLeader.DOT,
}),
"3",
]}),
],
})
๋ค๋จ ๋ ์ด์์
// ๋์ผ ๋๋น ์ด
sections: [{
properties: {
column: {
count: 2, // ์ด ์
space: 720, // ์ด ์ฌ์ด ๊ฐ๊ฒฉ(DXA ๋จ์, 720 = 0.5์ธ์น)
equalWidth: true,
separate: true, // ์ด ์ฌ์ด ์ธ๋ก์
},
},
children: [/* ์ฝํ
์ธ ๊ฐ ์ด์ ๋ฐ๋ผ ์์ฐ์ค๋ฝ๊ฒ ํ๋ฆ */]
}]
// ์ฌ์ฉ์ ์ ์ ๋๋น ์ด (equalWidth๋ false์ฌ์ผ ํจ)
sections: [{
properties: {
column: {
equalWidth: false,
children: [
new Column({ width: 5400, space: 720 }),
new Column({ width: 3240 }),
],
},
},
children: [/* content */]
}]
type: SectionType.NEXT_COLUMN์ ์ฌ์ฉํ๋ ์ ์น์
์ผ๋ก ์ด ๋๋๊ธฐ๋ฅผ ๊ฐ์ ํ ์ ์์ต๋๋ค.
๋ชฉ์ฐจ
// ์ค์: ์ ๋ชฉ์ HeadingLevel๋ง ์ฌ์ฉํด์ผ ํฉ๋๋ค - ์ฌ์ฉ์ ์ ์ ์คํ์ผ ์ฌ์ฉ ๋ถ๊ฐ
new TableOfContents("Table of Contents", { hyperlink: true, headingStyleRange: "1-3" })
๋จธ๋ฆฌ๊ธ/๋ฐ๋ฅ๊ธ
sections: [{
properties: {
page: { margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 } } // 1440 = 1์ธ์น
},
headers: {
default: new Header({ children: [new Paragraph({ children: [new TextRun("Header")] })] })
},
footers: {
default: new Footer({ children: [new Paragraph({
children: [new TextRun("Page "), new TextRun({ children: [PageNumber.CURRENT] })]
})] })
},
children: [/* content */]
}]
docx-js ํต์ฌ ๊ท์น
- ํ์ด์ง ํฌ๊ธฐ๋ฅผ ๋ช ์์ ์ผ๋ก ์ค์ - docx-js๋ ๊ธฐ๋ณธ๊ฐ์ด A4; ๋ฏธ๊ตญ ๋ฌธ์์๋ US Letter(12240 x 15840 DXA) ์ฌ์ฉ
- ๊ฐ๋ก ๋ฐฉํฅ: ์ธ๋ก ๋ฐฉํฅ ์น์๋ฅผ ์ ๋ฌ - docx-js๋ ๋ด๋ถ์ ์ผ๋ก ๋๋น/๋์ด๋ฅผ ๊ตํ; ์งง์ ๋ณ์
width๋ก, ๊ธด ๋ณ์height๋ก ์ ๋ฌํ๊ณorientation: PageOrientation.LANDSCAPE์ค์ - ์ ๋
\n์ฌ์ฉ ๊ธ์ง - ๋ณ๋์ Paragraph ์์ ์ฌ์ฉ - ์ ๋ ์ ๋์ฝ๋ ๊ธ๋จธ๋ฆฌ ๊ธฐํธ ์ฌ์ฉ ๊ธ์ง - numbering ์ค์ ๊ณผ
LevelFormat.BULLET์ฌ์ฉ - PageBreak๋ Paragraph ์์ ์์ด์ผ ํจ - ๋จ๋ ์ผ๋ก ์ฌ์ฉํ๋ฉด ์๋ชป๋ XML ์์ฑ
- ImageRun์๋
typeํ์ - ํญ์ png/jpg ๋ฑ ์ง์ - ํญ์ ํ
width๋ฅผ DXA๋ก ์ค์ - ์ ๋WidthType.PERCENTAGE์ฌ์ฉ ๊ธ์ง (Google Docs์์ ๊นจ์ง) - ํ์๋ ์ด์ค ๋๋น ํ์ -
columnWidths๋ฐฐ์ด๊ณผ ์ width, ๋ ๋ค ์ผ์นํด์ผ ํจ - ํ ๋๋น = columnWidths์ ํฉ - DXA์ ๊ฒฝ์ฐ ์ ํํ ํฉ์ด ๋ง๋์ง ํ์ธ
- ํญ์ ์
์ฌ๋ฐฑ ์ถ๊ฐ - ๊ฐ๋
์ฑ ์๋ ํจ๋ฉ์ ์ํด
margins: { top: 80, bottom: 80, left: 120, right: 120 }์ฌ์ฉ ShadingType.CLEAR์ฌ์ฉ - ํ ์์์ ์ ๋ SOLID ์ฌ์ฉ ๊ธ์ง- ์ ๋ ํ๋ฅผ ๊ตฌ๋ถ์ /๊ท์น์ผ๋ก ์ฌ์ฉ ๊ธ์ง - ์
์๋ ์ต์ ๋์ด๊ฐ ์์ด ๋น ์์๋ก ๋ ๋๋ง๋จ (๋จธ๋ฆฌ๊ธ/๋ฐ๋ฅ๊ธ ํฌํจ); ๋์ Paragraph์
border: { bottom: { style: BorderStyle.SINGLE, size: 6, color: "2E75B6", space: 1 } }์ฌ์ฉ. 2์ด ๋ฐ๋ฅ๊ธ์๋ ํ๊ฐ ์๋ ํญ ์ ์ง ์ฌ์ฉ (ํญ ์ ์ง ์น์ ์ฐธ์กฐ) - TOC์๋ HeadingLevel๋ง ํ์ - ์ ๋ชฉ ๋จ๋ฝ์ ์ฌ์ฉ์ ์ ์ ์คํ์ผ ์ฌ์ฉ ๋ถ๊ฐ
- ๋ด์ฅ ์คํ์ผ ์ฌ์ ์ - ์ ํํ ID ์ฌ์ฉ: "Heading1", "Heading2" ๋ฑ
outlineLevelํฌํจ - TOC์ ํ์ (H1์ 0, H2๋ 1 ๋ฑ)
๊ธฐ์กด ๋ฌธ์ ํธ์ง
3๋จ๊ณ๋ฅผ ์์๋๋ก ๋ชจ๋ ๋ฐ๋ฅด์ธ์.
1๋จ๊ณ: ์ธํฉ
python scripts/office/unpack.py document.docx unpacked/
XML์ ์ถ์ถํ๊ณ , ์ ๋ฆฌํ๊ณ , ์ธ์ ํ run์ ๋ณํฉํ๊ณ , ์ค๋งํธ ๋ฐ์ดํ๋ฅผ XML ์ํฐํฐ(“ ๋ฑ)๋ก ๋ณํํ์ฌ ํธ์ง ํ์๋ ์ ์ง๋๋๋ก ํฉ๋๋ค. run ๋ณํฉ์ ๊ฑด๋๋ฐ๋ ค๋ฉด --merge-runs false๋ฅผ ์ฌ์ฉํ์ธ์.
2๋จ๊ณ: XML ํธ์ง
unpacked/word/์ ํ์ผ์ ํธ์งํฉ๋๋ค. ํจํด์ ์๋ XML ์ฐธ์กฐ๋ฅผ ํ์ธํ์ธ์.
๋ณ๊ฒฝ ๋ด์ฉ ์ถ์ ๊ณผ ์ฝ๋ฉํธ์๋ ์์ฑ์๋ก "Claude"๋ฅผ ์ฌ์ฉํ์ธ์, ์ฌ์ฉ์๊ฐ ๋ค๋ฅธ ์ด๋ฆ ์ฌ์ฉ์ ๋ช ์์ ์ผ๋ก ์์ฒญํ์ง ์๋ ํ.
๋ฌธ์์ด ๊ต์ฒด์๋ Edit ๋๊ตฌ๋ฅผ ์ง์ ์ฌ์ฉํ์ธ์. Python ์คํฌ๋ฆฝํธ๋ฅผ ์์ฑํ์ง ๋ง์ธ์. ์คํฌ๋ฆฝํธ๋ ๋ถํ์ํ ๋ณต์ก์ฑ์ ์ถ๊ฐํฉ๋๋ค. Edit ๋๊ตฌ๋ ๋ฌด์์ด ๊ต์ฒด๋๋์ง ์ ํํ ๋ณด์ฌ์ค๋๋ค.
์ค์: ์ ์ฝํ ์ธ ์๋ ์ค๋งํธ ๋ฐ์ดํ๋ฅผ ์ฌ์ฉํ์ธ์. ์ํฌ์คํธ๋กํผ๋ ๋ฐ์ดํ๊ฐ ํฌํจ๋ ํ ์คํธ๋ฅผ ์ถ๊ฐํ ๋ XML ์ํฐํฐ๋ฅผ ์ฌ์ฉํ์ฌ ์ค๋งํธ ๋ฐ์ดํ๋ฅผ ์์ฑํ์ธ์:
<!-- ์ ๋ฌธ์ ์ธ ํ์ดํฌ๊ทธ๋ํผ๋ฅผ ์ํด ์ด ์ํฐํฐ๋ฅผ ์ฌ์ฉํ์ธ์ -->
<w:t>Here’s a quote: “Hello”</w:t>
| ์ํฐํฐ | ๋ฌธ์ |
|---|---|
‘ | ' (์ผ์ชฝ ์์๋ฐ์ดํ) |
’ | ' (์ค๋ฅธ์ชฝ ์์๋ฐ์ดํ / ์ํฌ์คํธ๋กํผ) |
“ | " (์ผ์ชฝ ํฐ๋ฐ์ดํ) |
” | " (์ค๋ฅธ์ชฝ ํฐ๋ฐ์ดํ) |
์ฝ๋ฉํธ ์ถ๊ฐ: ์ฌ๋ฌ XML ํ์ผ์ ๊ฑธ์น ๋ณด์ผ๋ฌํ๋ ์ดํธ๋ฅผ ์ฒ๋ฆฌํ๋ ค๋ฉด comment.py๋ฅผ ์ฌ์ฉํ์ธ์ (ํ
์คํธ๋ ์ฌ์ ์ ์ด์ค์ผ์ดํ๋ XML์ด์ด์ผ ํจ):
python scripts/comment.py unpacked/ 0 "Comment text with & and ’"
python scripts/comment.py unpacked/ 1 "Reply text" --parent 0 # ์ฝ๋ฉํธ 0์ ๋ํ ๋ต๊ธ
python scripts/comment.py unpacked/ 0 "Text" --author "Custom Author" # ์ฌ์ฉ์ ์ ์ ์์ฑ์ ์ด๋ฆ
๊ทธ๋ฐ ๋ค์ document.xml์ ๋ง์ปค๋ฅผ ์ถ๊ฐํ์ธ์ (XML ์ฐธ์กฐ์ ์ฝ๋ฉํธ ์น์ ์ฐธ์กฐ).
3๋จ๊ณ: ํฉ
python scripts/office/pack.py unpacked/ output.docx --original document.docx
์๋ ๋ณต๊ตฌ์ ํจ๊ป ๊ฒ์ฆํ๊ณ , XML์ ์์ถํ๊ณ , DOCX๋ฅผ ์์ฑํฉ๋๋ค. ๊ฑด๋๋ฐ๋ ค๋ฉด --validate false๋ฅผ ์ฌ์ฉํ์ธ์.
์๋ ๋ณต๊ตฌ๊ฐ ์์ ํ๋ ๊ฒ:
durableId>= 0x7FFFFFFF (์ ํจํ ID ์ฌ์์ฑ)- ๊ณต๋ฐฑ์ด ์๋
<w:t>์์ ๋๋ฝ๋xml:space="preserve"
์๋ ๋ณต๊ตฌ๊ฐ ์์ ํ์ง ์๋ ๊ฒ:
- ์๋ชป๋ XML, ์ ํจํ์ง ์์ ์์ ์ค์ฒฉ, ๋๋ฝ๋ ๊ด๊ณ, ์คํค๋ง ์๋ฐ
์ผ๋ฐ์ ์ธ ํจ์
- ์ ์ฒด
<w:r>์์๋ฅผ ๊ต์ฒดํ์ธ์: ๋ณ๊ฒฝ ๋ด์ฉ ์ถ์ ์ ์ถ๊ฐํ ๋ ์ ์ฒด<w:r>...</w:r>๋ธ๋ก์<w:del>...<w:ins>...(ํ์ ์์)๋ก ๊ต์ฒดํ์ธ์. run ๋ด๋ถ์ ๋ณ๊ฒฝ ๋ด์ฉ ์ถ์ ํ๊ทธ๋ฅผ ์ฝ์ ํ์ง ๋ง์ธ์. <w:rPr>์์ ์ ์ง: ์๋ณธ run์<w:rPr>๋ธ๋ก์ ๋ณ๊ฒฝ ๋ด์ฉ ์ถ์ run์ ๋ณต์ฌํ์ฌ ๋ณผ๋, ํฐํธ ํฌ๊ธฐ ๋ฑ์ ์ ์งํ์ธ์.
XML ์ฐธ์กฐ
์คํค๋ง ์ค์
<w:pPr>๋ด ์์ ์์:<w:pStyle>,<w:numPr>,<w:spacing>,<w:ind>,<w:jc>,<w:rPr>๋ง์ง๋ง- ๊ณต๋ฐฑ: ์๋ค ๊ณต๋ฐฑ์ด ์๋
<w:t>์xml:space="preserve"์ถ๊ฐ - RSID: 8์๋ฆฌ 16์ง์์ฌ์ผ ํจ (์:
00AB1234)
๋ณ๊ฒฝ ๋ด์ฉ ์ถ์
์ฝ์ :
<w:ins w:id="1" w:author="Claude" w:date="2025-01-01T00:00:00Z">
<w:r><w:t>inserted text</w:t></w:r>
</w:ins>
์ญ์ :
<w:del w:id="2" w:author="Claude" w:date="2025-01-01T00:00:00Z">
<w:r><w:delText>deleted text</w:delText></w:r>
</w:del>
<w:del> ๋ด๋ถ: <w:t> ๋์ <w:delText>, <w:instrText> ๋์ <w:delInstrText>์ ์ฌ์ฉํ์ธ์.
์ต์ํ์ ํธ์ง - ๋ณ๊ฒฝ๋ ๋ถ๋ถ๋ง ๋งํฌํ์ธ์:
<!-- "30 days"๋ฅผ "60 days"๋ก ๋ณ๊ฒฝ -->
<w:r><w:t>The term is </w:t></w:r>
<w:del w:id="1" w:author="Claude" w:date="...">
<w:r><w:delText>30</w:delText></w:r>
</w:del>
<w:ins w:id="2" w:author="Claude" w:date="...">
<w:r><w:t>60</w:t></w:r>
</w:ins>
<w:r><w:t> days.</w:t></w:r>
์ ์ฒด ๋จ๋ฝ/๋ชฉ๋ก ํญ๋ชฉ ์ญ์ - ๋จ๋ฝ์์ ๋ชจ๋ ๋ด์ฉ์ ์ ๊ฑฐํ ๋, ๋ค์ ๋จ๋ฝ๊ณผ ๋ณํฉ๋๋๋ก ๋จ๋ฝ ๋งํฌ๋ ์ญ์ ๋ก ํ์ํ์ธ์. <w:pPr><w:rPr> ์์ <w:del/>์ ์ถ๊ฐํ์ธ์:
<w:p>
<w:pPr>
<w:numPr>...</w:numPr> <!-- ์๋ ๊ฒฝ์ฐ ๋ชฉ๋ก ๋ฒํธ ๋งค๊น -->
<w:rPr>
<w:del w:id="1" w:author="Claude" w:date="2025-01-01T00:00:00Z"/>
</w:rPr>
</w:pPr>
<w:del w:id="2" w:author="Claude" w:date="2025-01-01T00:00:00Z">
<w:r><w:delText>Entire paragraph content being deleted...</w:delText></w:r>
</w:del>
</w:p>
<w:pPr><w:rPr>์ <w:del/>์ด ์์ผ๋ฉด ๋ณ๊ฒฝ ์ฌํญ์ ์น์ธํ์ ๋ ๋น ๋จ๋ฝ/๋ชฉ๋ก ํญ๋ชฉ์ด ๋จ์ต๋๋ค.
๋ค๋ฅธ ์์ฑ์์ ์ฝ์ ๊ฑฐ๋ถ - ๊ทธ๋ค์ ์ฝ์ ์์ ์ญ์ ๋ฅผ ์ค์ฒฉํ์ธ์:
<w:ins w:author="Jane" w:id="5">
<w:del w:author="Claude" w:id="10">
<w:r><w:delText>their inserted text</w:delText></w:r>
</w:del>
</w:ins>
๋ค๋ฅธ ์์ฑ์์ ์ญ์ ๋ณต์ - ๋ค์ ์ฝ์ ์ ์ถ๊ฐํ์ธ์ (๊ทธ๋ค์ ์ญ์ ๋ฅผ ์์ ํ์ง ๋ง์ธ์):
<w:del w:author="Jane" w:id="5">
<w:r><w:delText>deleted text</w:delText></w:r>
</w:del>
<w:ins w:author="Claude" w:id="10">
<w:r><w:t>deleted text</w:t></w:r>
</w:ins>
์ฝ๋ฉํธ
comment.py๋ฅผ ์คํํ ํ (2๋จ๊ณ ์ฐธ์กฐ), document.xml์ ๋ง์ปค๋ฅผ ์ถ๊ฐํ์ธ์. ๋ต๊ธ์ ๊ฒฝ์ฐ --parent ํ๋๊ทธ๋ฅผ ์ฌ์ฉํ๊ณ ๋ง์ปค๋ฅผ ๋ถ๋ชจ ์์ ์ค์ฒฉํ์ธ์.
์ค์: <w:commentRangeStart>์ <w:commentRangeEnd>๋ <w:r>์ ํ์ ์์์ด๋ฉฐ, ์ ๋ <w:r> ๋ด๋ถ์ ์์นํ๋ฉด ์ ๋ฉ๋๋ค.
<!-- ์ฝ๋ฉํธ ๋ง์ปค๋ w:p์ ์ง์ ์์์ด๋ฉฐ, ์ ๋ w:r ๋ด๋ถ์ ์์ผ๋ฉด ์ ๋จ -->
<w:commentRangeStart w:id="0"/>
<w:del w:id="1" w:author="Claude" w:date="2025-01-01T00:00:00Z">
<w:r><w:delText>deleted</w:delText></w:r>
</w:del>
<w:r><w:t> more text</w:t></w:r>
<w:commentRangeEnd w:id="0"/>
<w:r><w:rPr><w:rStyle w:val="CommentReference"/></w:rPr><w:commentReference w:id="0"/></w:r>
<!-- ๋ต๊ธ 1์ด ์ค์ฒฉ๋ ์ฝ๋ฉํธ 0 -->
<w:commentRangeStart w:id="0"/>
<w:commentRangeStart w:id="1"/>
<w:r><w:t>text</w:t></w:r>
<w:commentRangeEnd w:id="1"/>
<w:commentRangeEnd w:id="0"/>
<w:r><w:rPr><w:rStyle w:val="CommentReference"/></w:rPr><w:commentReference w:id="0"/></w:r>
<w:r><w:rPr><w:rStyle w:val="CommentReference"/></w:rPr><w:commentReference w:id="1"/></w:r>
์ด๋ฏธ์ง
word/media/์ ์ด๋ฏธ์ง ํ์ผ ์ถ๊ฐword/_rels/document.xml.rels์ ๊ด๊ณ ์ถ๊ฐ:
<Relationship Id="rId5" Type=".../image" Target="media/image1.png"/>
[Content_Types].xml์ ์ฝํ ์ธ ํ์ ์ถ๊ฐ:
<Default Extension="png" ContentType="image/png"/>
- document.xml์์ ์ฐธ์กฐ:
<w:drawing>
<wp:inline>
<wp:extent cx="914400" cy="914400"/> <!-- EMU ๋จ์: 914400 = 1์ธ์น -->
<a:graphic>
<a:graphicData uri=".../picture">
<pic:pic>
<pic:blipFill><a:blip r:embed="rId5"/></pic:blipFill>
</pic:pic>
</a:graphicData>
</a:graphic>
</wp:inline>
</w:drawing>
์์กด์ฑ
- pandoc: ํ ์คํธ ์ถ์ถ
- docx:
npm install -g docx(์ ๋ฌธ์) - LibreOffice: PDF ๋ณํ (์๋๋ฐ์ค ํ๊ฒฝ์์
scripts/office/soffice.py๋ฅผ ํตํด ์๋ ์ค์ ) - Poppler: ์ด๋ฏธ์ง์ฉ
pdftoppm