hwpforge-smithy-hwpx 0.5.1

HWPX format codec (encoder + decoder) for HwpForge
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
//! 청운시 문화유산 발굴연표 — wide table split across facing pages.
//!
//! This file contains completely fictional example data (~80 entries)
//! for the fictional city of 청운시. All site names, institution names,
//! district names, and publication titles are invented.
//! Creates interleaved left/right sections for paired page layout:
//!   - Odd pages (1,3,5,...):  연번, 시대, 연대, 유적명/위치, 출토유물
//!   - Even pages (2,4,6,...): 유적·유물의 내용, 조사기간, 조사기관, 출처, 비고, pdf
//!
//! Usage:
//!   cargo run -p hwpforge-smithy-hwpx --example large_table
//!
//! Output:
//!   temp/large_table.hwpx

use hwpforge_core::document::Document;
use hwpforge_core::image::ImageStore;
use hwpforge_core::paragraph::Paragraph;
use hwpforge_core::run::Run;
use hwpforge_core::section::Section;
use hwpforge_core::table::{Table, TableCell, TablePageBreak, TableRow};
use hwpforge_core::PageSettings;
use hwpforge_foundation::{
    Alignment, CharShapeIndex, Color, HwpUnit, LineSpacingType, ParaShapeIndex,
};
use hwpforge_smithy_hwpx::style_store::{HwpxCharShape, HwpxFont, HwpxParaShape, HwpxStyleStore};
use hwpforge_smithy_hwpx::HwpxEncoder;

// ── Style indices ────────────────────────────────────────────────

const CS_NORMAL: usize = 0;
const CS_HEADER: usize = 1;
const CS_SMALL: usize = 2;
const PS_NORMAL: usize = 0;
const PS_CENTER: usize = 1;

// ── Column definitions for automatic page splitting ─────────────

/// Column metadata for the splitting/distribution algorithm.
struct ColumnDef {
    name: &'static str,
    min_width_mm: f64,
    para_shape_idx: usize,
    char_shape_idx: usize,
}

/// All 11 columns in reading order. The algorithm splits them across pages
/// based on which columns fit within the A4 portrait printable width (170mm).
const ALL_COLUMNS: [ColumnDef; 11] = [
    ColumnDef {
        name: "연번",
        min_width_mm: 8.0,
        para_shape_idx: PS_CENTER,
        char_shape_idx: CS_NORMAL,
    },
    ColumnDef {
        name: "시대",
        min_width_mm: 20.0,
        para_shape_idx: PS_CENTER,
        char_shape_idx: CS_NORMAL,
    },
    ColumnDef {
        name: "연대",
        min_width_mm: 24.0,
        para_shape_idx: PS_CENTER,
        char_shape_idx: CS_NORMAL,
    },
    ColumnDef {
        name: "유적명 / 위치",
        min_width_mm: 48.0,
        para_shape_idx: PS_NORMAL,
        char_shape_idx: CS_NORMAL,
    },
    ColumnDef {
        name: "출토유물",
        min_width_mm: 70.0,
        para_shape_idx: PS_NORMAL,
        char_shape_idx: CS_NORMAL,
    },
    ColumnDef {
        name: "유적·유물의 내용",
        min_width_mm: 60.0,
        para_shape_idx: PS_NORMAL,
        char_shape_idx: CS_NORMAL,
    },
    ColumnDef {
        name: "조사기간",
        min_width_mm: 18.0,
        para_shape_idx: PS_CENTER,
        char_shape_idx: CS_NORMAL,
    },
    ColumnDef {
        name: "조사기관",
        min_width_mm: 25.0,
        para_shape_idx: PS_CENTER,
        char_shape_idx: CS_NORMAL,
    },
    ColumnDef {
        name: "출처",
        min_width_mm: 40.0,
        para_shape_idx: PS_NORMAL,
        char_shape_idx: CS_SMALL,
    },
    ColumnDef {
        name: "비고",
        min_width_mm: 15.0,
        para_shape_idx: PS_NORMAL,
        char_shape_idx: CS_SMALL,
    },
    ColumnDef {
        name: "pdf",
        min_width_mm: 12.0,
        para_shape_idx: PS_NORMAL,
        char_shape_idx: CS_SMALL,
    },
];

/// Font size used in data cells (pt).
const FONT_SIZE_PT: f64 = 8.0;
/// Line spacing percentage (130% = 1.3x font size).
const LINE_SPACING_PCT: u32 = 130;

// ── Helpers ──────────────────────────────────────────────────────

fn p(text: &str, cs: usize, ps: usize) -> Paragraph {
    Paragraph::with_runs(vec![Run::text(text, CharShapeIndex::new(cs))], ParaShapeIndex::new(ps))
}

/// Cell with multiple paragraphs (splits text on `\n`).
fn cell_lines(text: &str, width: HwpUnit, cs: usize, ps: usize) -> TableCell {
    let paras: Vec<Paragraph> = text.split('\n').map(|line| p(line, cs, ps)).collect();
    TableCell::new(paras, width)
}

/// Header cell with gray background.
fn header_cell(text: &str, width: HwpUnit) -> TableCell {
    cell_lines(text, width, CS_HEADER, PS_CENTER).with_background(Color::from_rgb(220, 220, 220))
}

// ── Data ────────────────────────────────────────────────────────

#[derive(Clone)]
struct Row {
    num: &'static str,
    era: &'static str,
    era_span: u16,
    date: &'static str,
    site: &'static str,
    artifacts: &'static str,
    findings: &'static str,
    period: &'static str,
    institution: &'static str,
    source: &'static str,
    notes: &'static str,
    pdf: &'static str,
}

fn data() -> Vec<Row> {
    vec![
        // ── 구석기 (10) ──────────────────────────────────────────
        Row { num: "1", era: "구석기", era_span: 10, date: "전기구석기", site: "청운 운봉리 구석기유적\n운봉면 운봉리 12, 134번지 일원", artifacts: "찍개, 몸돌, 격지, 깨진 자갈돌, 부스러기 등 34점", findings: "석기 지표수습, 유물출토 문화층 미확인\n-표본조사 중 확인조사", period: "1998", institution: "청운문화재연구원", source: "청운문화재연구원, 2000,\n『청운 운봉리 구석기유적』", notes: "청동기시대 토기편 지표수습", pdf: "" },
        Row { num: "2", era: "구석기", era_span: 0, date: "중기구석기", site: "청운 하늘재 구석기유적\n하늘읍 하늘재리 산45-2번지 일원", artifacts: "흑요석, 석영제 긁개, 밀개, 홈날, 찌르개, 몸돌, 망치, 격지 등 2,184점", findings: "중기~후기구석기 문화층 3개소 확인", period: "2003~2004", institution: "하늘고고학연구소", source: "하늘고고학연구소, 2006,\n『청운 하늘재 구석기유적』", notes: "", pdf: "" },
        Row { num: "3", era: "구석기", era_span: 0, date: "중기구석기", site: "청운 구름봉 유적(1지구)\n구름동 산22-1번지 일원", artifacts: "몸돌, 주먹도끼, 주먹찌르개, 주먹대패, 밀개, 긁개, 홈날, 격지 등 3,021점", findings: "중기구석기 문화층 4개소 확인", period: "2007~2009", institution: "별빛문화유산연구원", source: "별빛문화유산연구원, 2011,\n『청운 구름봉 유적 1지구』", notes: "", pdf: "" },
        Row { num: "4", era: "구석기", era_span: 0, date: "중기구석기", site: "청운 구름봉 유적(2지구)\n구름동 산23-5번지 일원", artifacts: "찍개, 여러면석기, 긁개, 홈날, 복합석기 등 487점", findings: "후기구석기 문화층 2개소 확인", period: "2009~2011", institution: "달빛역사연구원", source: "달빛역사연구원, 2013,\n『청운 구름봉 유적 2지구』", notes: "", pdf: "" },
        Row { num: "5", era: "구석기", era_span: 0, date: "후기구석기", site: "청운 별빛마을 구석기유적\n별내면 별빛리 89번지 일원", artifacts: "슴베찌르개, 좀돌날, 좀돌날몸돌, 흑요석제 새기개, 긁개 등 1,572점", findings: "후기구석기 문화층 3개소 확인\n-중기→후기 전환기 양상 확인", period: "2012~2013", institution: "청운문화재연구원", source: "청운문화재연구원, 2015,\n『청운 별빛마을 구석기유적』", notes: "", pdf: "" },
        Row { num: "6", era: "구석기", era_span: 0, date: "후기구석기", site: "청운 솔밭들 구석기유적\n솔밭면 솔밭리 산78번지 일원", artifacts: "주먹도끼, 찍개, 뚜르개, 톱니날, 긁개, 밀개 등 891점", findings: "후기구석기 문화층 2개소 확인", period: "2014", institution: "하늘고고학연구소", source: "하늘고고학연구소, 2016,\n『청운 솔밭들 구석기유적』", notes: "", pdf: "" },
        Row { num: "7", era: "구석기", era_span: 0, date: "후기구석기", site: "청운 바람골 구석기유적\n바람골면 바람리 386-3번지 일원", artifacts: "흑요석제 화살촉, 슴베찌르개, 돌날 등 204점", findings: "후기구석기 초기~중기 2개 문화층 확인", period: "2005~2006", institution: "별빛문화유산연구원", source: "별빛문화유산연구원, 2008,\n『청운 바람골 구석기유적』", notes: "", pdf: "" },
        Row { num: "8", era: "구석기", era_span: 0, date: "후기구석기", site: "청운 수정터 구석기유적\n수정동 산56번지 일원", artifacts: "안팎날찍개, 뚜르개, 몸돌, 부스러기, 깨진 자갈돌 등 12점", findings: "시굴조사\n유물출토 문화층 미확인", period: "2016", institution: "달빛역사연구원", source: "달빛역사연구원, 2018,\n『청운 수정터 구석기유적』", notes: "", pdf: "" },
        Row { num: "9", era: "구석기", era_span: 0, date: "후기구석기", site: "청운 은하벌 구석기유적\n은하면 은하리 일원", artifacts: "몸돌, 격지, 돌날몸돌, 돌날, 찍개, 여러면석기, 주먹대패 등 743점", findings: "문화층 2개소 확인", period: "2018~2019", institution: "청운문화재연구원", source: "청운문화재연구원, 2021,\n『청운 은하벌 구석기유적』", notes: "", pdf: "" },
        Row { num: "10", era: "구석기", era_span: 0, date: "후기구석기", site: "청운 새벽골 구석기유적\n새벽면 새벽리 115번지 일원", artifacts: "몸돌, 돌날, 밀개, 긁개, 슴베찌르개, 갈돌, 흑요석제 새기개 등 2,310점", findings: "문화층 3개소 확인", period: "2019~2021", institution: "하늘고고학연구소", source: "하늘고고학연구소, 2023,\n『청운 새벽골 구석기유적』", notes: "", pdf: "" },

        // ── 신석기시대 (5) ────────────────────────────────────────
        Row { num: "11", era: "신석기시대", era_span: 5, date: "", site: "청운 노을리 신석기유적\n노을면 노을리 250-3번지 일원", artifacts: "빗살무늬토기편, 석촉 등 42점", findings: "수혈유구 1기, 노지 1기\n-선사시대 취락 존재가능성 확인", period: "2001", institution: "청운문화재연구원", source: "청운문화재연구원, 2003,\n『청운 노을리 신석기유적』", notes: "", pdf: "" },
        Row { num: "12", era: "신석기시대", era_span: 0, date: "", site: "청운 달빛고개 신석기유적\n달빛면 달빛리 303번지 일원", artifacts: "빗살무늬토기 구연부편, 갈돌, 고석 등 29점", findings: "수혈유구 2기\n-내부에 무시설식 노지 1기 확인", period: "2008~2009", institution: "별빛문화유산연구원", source: "별빛문화유산연구원, 2011,\n『청운 달빛고개 신석기유적』", notes: "", pdf: "" },
        Row { num: "13", era: "신석기시대", era_span: 0, date: "", site: "청운 운봉리 신석기 주거지\n운봉면 운봉리 370-10번지 일원", artifacts: "즐문토기 발, 구연부편, 저부편, 긁개, 갈돌, 몸돌 등 56점", findings: "주거지 1기\n-평면 방형, 위석식 화덕시설 확인\n-대형에 속하는 주거지", period: "2013~2014", institution: "달빛역사연구원", source: "달빛역사연구원, 2016,\n『청운 운봉리 신석기 주거지』", notes: "", pdf: "" },
        Row { num: "14", era: "신석기시대", era_span: 0, date: "", site: "청운 수정동 선사유적\n수정동 산18번지 일원", artifacts: "즐문토기편, 결합식 낚시바늘, 뼈바늘 등 18점", findings: "패총 흔적 1개소, 수혈유구 1기\n-동물뼈 다수 출토", period: "2017", institution: "청운문화재연구원", source: "청운문화재연구원, 2019,\n『청운 수정동 선사유적』", notes: "지표조사 포함", pdf: "" },
        Row { num: "15", era: "신석기시대", era_span: 0, date: "", site: "청운 은하리 신석기유적\n은하면 은하리 188번지 일원", artifacts: "빗살무늬토기, 갈판, 갈돌, 석도편 등 33점", findings: "주거지 2기, 수혈 3기\n-취락 형성 초기 단계 양상 추정", period: "2020~2021", institution: "하늘고고학연구소", source: "하늘고고학연구소, 2023,\n『청운 은하리 신석기유적』", notes: "", pdf: "" },

        // ── 청동기 (12) ───────────────────────────────────────────
        Row { num: "16", era: "청동기", era_span: 12, date: "전기", site: "청운 운봉리 청동기유적\n운봉면 운봉리, 달빛면 달빛리 일원", artifacts: "빗살무늬토기, 공렬토기, 무문토기,\n반달돌칼, 화살촉, 숫돌 등", findings: "주거지 5동", period: "2004~2005", institution: "청운문화재연구원", source: "청운문화재연구원, 2007,\n『청운 운봉리 청동기유적』", notes: "", pdf: "" },
        Row { num: "17", era: "청동기", era_span: 0, date: "전기", site: "청운 달빛리 마을유적\n달빛면 달빛리 303번지 일원", artifacts: "무문토기 호, 석촉, 반월형석도", findings: "주거지 3기\n-역삼동형 주거지 확인", period: "2006", institution: "별빛문화유산연구원", source: "별빛문화유산연구원, 2008,\n『청운 달빛리 마을유적』", notes: "", pdf: "" },
        Row { num: "18", era: "청동기", era_span: 0, date: "전기", site: "청운 하늘재 청동기유적\n하늘읍 하늘재리 94번지 일원", artifacts: "무문토기편, 갈돌, 대석 등 21점", findings: "주거지 2기\n-세장방형 주거지\n-생활공간 외 작업공간 추정", period: "2009", institution: "달빛역사연구원", source: "달빛역사연구원, 2011,\n『청운 하늘재 청동기유적』", notes: "", pdf: "" },
        Row { num: "19", era: "청동기", era_span: 0, date: "전기", site: "청운 구름봉 고인돌\n구름동 산16-1번지 일원", artifacts: "", findings: "탁자식 지석묘 2기\n-재질 화강암, 남북방향\n*향토유적 제12호", period: "2002~2003", institution: "청운시문화원", source: "청운시문화원, 2004,\n『청운시 고인돌 조사보고서』", notes: "지표조사", pdf: "" },
        Row { num: "20", era: "청동기", era_span: 0, date: "전기", site: "청운 별빛리 고인돌\n별내면 별빛리 일원", artifacts: "", findings: "개석식 지석묘 3기", period: "2002~2003", institution: "청운시문화원", source: "청운시문화원, 2004,\n『청운시 고인돌 조사보고서』", notes: "지표조사", pdf: "" },
        Row { num: "21", era: "청동기", era_span: 0, date: "전기", site: "청운 솔밭리 고인돌\n솔밭면 솔밭리 일원", artifacts: "", findings: "탁자식 지석묘 1기 외 석재노출", period: "2002~2003", institution: "청운시문화원", source: "청운시문화원, 2004,\n『청운시 고인돌 조사보고서』", notes: "지표조사", pdf: "" },
        Row { num: "22", era: "청동기", era_span: 0, date: "전기", site: "청운 바람리 고인돌\n바람골면 바람리 8번지 일원", artifacts: "", findings: "탁자식 지석묘 2기\n*향토유적 제18호", period: "2002~2003", institution: "청운시문화원", source: "청운시문화원, 2004,\n『청운시 고인돌 조사보고서』", notes: "지표조사", pdf: "" },
        Row { num: "23", era: "청동기", era_span: 0, date: "전기", site: "청운 노을리 고인돌군\n노을면 노을리 251번지 일원", artifacts: "", findings: "탁자식 지석묘 1기\n*향토유적 제3호", period: "2002~2003", institution: "청운시문화원", source: "청운시문화원, 2004,\n『청운시 고인돌 조사보고서』", notes: "지표조사", pdf: "" },
        Row { num: "24", era: "청동기", era_span: 0, date: "전기", site: "청운 수정동 청동기유적\n수정동 434-4번지 일원", artifacts: "무문토기 4점, 석촉 2점", findings: "고인돌 3기 확인\n탁자식, 개석식 확인\n-유아묘 존재 가능성 추정", period: "2010~2011", institution: "하늘고고학연구소", source: "하늘고고학연구소, 2013,\n『청운 수정동 청동기유적』", notes: "", pdf: "" },
        Row { num: "25", era: "청동기", era_span: 0, date: "중기", site: "청운 은하벌 청동기 취락\n은하면 은하리 일원", artifacts: "구순각목, 이중구연단사선문, 무문토기, 반월형석도, 석촉", findings: "주거지 4기, 수혈유구 5기\n역삼동식 주거지 확인", period: "2015~2016", institution: "별빛문화유산연구원", source: "별빛문화유산연구원, 2018,\n『청운 은하벌 청동기 취락유적』", notes: "", pdf: "" },
        Row { num: "26", era: "청동기", era_span: 0, date: "중기", site: "청운 새벽리 유적\n새벽면 새벽리, 노을리 일원", artifacts: "신석기시대 토기편, 공열문토기, 무문토기, 반월형석도, 석촉, 석착, 지석 등", findings: "주거지 2기, 수혈 4기\n-북한강유형과 유사\n-임시거처로 추정", period: "2018~2019", institution: "달빛역사연구원", source: "달빛역사연구원, 2021,\n『청운 새벽리 유적』", notes: "", pdf: "" },
        Row { num: "27", era: "청동기", era_span: 0, date: "후기", site: "청운 은하면 늘터 유적\n은하면 은하리 133-1번지 일원", artifacts: "주상편인석부, 반월형석도, 무문토기 호편 등 28점", findings: "주거지 2기, 소성유구 1기\n-송국리형 주거지 확인", period: "2021~2022", institution: "청운문화재연구원", source: "청운문화재연구원, 2024,\n『청운 은하면 늘터 유적』", notes: "", pdf: "" },

        // ── 초기철기~원삼국 (10) ──────────────────────────────────
        Row { num: "28", era: "초기철기~원삼국", era_span: 10, date: "1~2세기", site: "청운 달빛리 유적\n달빛면 달빛리 374-3번지 일원", artifacts: "민무늬토기, 타날문토기편, 돌끌, 갈판, 토우, 대롱옥 등", findings: "주거지 2기\n-평면형태 장방형, 내부시설 노지, 주혈", period: "1997", institution: "청운대학교 박물관", source: "청운대학교 박물관, 1999,\n『청운 달빛리 유적』", notes: "", pdf: "" },
        Row { num: "29", era: "초기철기~원삼국", era_span: 0, date: "1~2세기", site: "청운 솔밭리 취락유적\n솔밭면 솔밭리 1014-5번지 일원", artifacts: "무문토기편, 경질무문토기편, 타날문토기편, 화분형토기편, 토제방추차, 석제그물추, 석제화살촉, 숫돌, 철경동촉 등 188점", findings: "주거지 8기, 불탄자리 2기, 구상유구 4기\n-철자형, 장방형 주거지 확인\n-돌 위에 기둥을 세운 양상 확인", period: "2000~2001", institution: "청운문화재연구원", source: "청운문화재연구원, 2003,\n『청운 솔밭리 취락유적』", notes: "", pdf: "" },
        Row { num: "30", era: "초기철기~원삼국", era_span: 0, date: "1~2세기", site: "청운 구름동 421번지 유적\n구름동 421번지 일원", artifacts: "경질무문토기 구연부편, 기저부편", findings: "주거지 2기, 지상식건물지 1기\n-평면형태 장방형\n-내부시설 노지, 주혈", period: "2006", institution: "별빛문화유산연구원", source: "별빛문화유산연구원, 2008,\n『청운 구름동 421번지 유적』", notes: "", pdf: "" },
        Row { num: "31", era: "초기철기~원삼국", era_span: 0, date: "1~2세기", site: "청운 바람리 모래내유적\n바람골면 바람리 35번지 일원", artifacts: "중도식무문토기, 타날문토기, 시루, 연질토기, 토제방추차, 원형토제품, 지석, 구슬 등 132점", findings: "주거지 22기, 수혈유구 10기\n-구릉 정상부에 밀집분포\n-凸자형 주거지 주류\n-외줄구들, 부뚜막, 노지, 주공 확인", period: "2009", institution: "달빛역사연구원", source: "달빛역사연구원, 2011,\n『청운 바람리 모래내유적』", notes: "", pdf: "" },
        Row { num: "32", era: "초기철기~원삼국", era_span: 0, date: "1~2세기", site: "청운 노을리 유적(1지점)\n노을면 노을리 291-2번지", artifacts: "경질무문토기 외반구연 옹, 호 등", findings: "주거지 3기, 수혈유구 2기, 주혈군 2기\n-주거지 평면형태 말각방형\n-'ㅡ','ㄱ'자형 부뚜막, 주혈 확인", period: "2012", institution: "하늘고고학연구소", source: "하늘고고학연구소, 2014,\n『청운 노을리 유적 1·2지점』", notes: "", pdf: "" },
        Row { num: "33", era: "초기철기~원삼국", era_span: 0, date: "2~3세기", site: "청운 별빛리 선사유적\n별내면 별빛리 269 외", artifacts: "즐문토기편, 중도식외반구연호, 심발형토기, 승석문단경호, 호형토기, 무문토기시루편, 대옹, 토제방추차, 지석, 철모, 철도자 등", findings: "원삼국시대 주거지 6기, 미상수혈유구 2기\n-포천지역 처음 이루어진 발굴조사와 유사\n-중소형 철자형주거지 확인\n-외줄구들의 부뚜막 확인", period: "1994", institution: "청운대학교 박물관", source: "청운대학교 박물관, 1996,\n『청운 별빛리 선사유적』", notes: "", pdf: "" },
        Row { num: "34", era: "초기철기~원삼국", era_span: 0, date: "2~3세기", site: "청운 노을동 마을유적\n노을면 노을리 일원", artifacts: "즐문토기편, 단경호, 타날문토기편, 심발형토기편, 회청색경질토기편, 대부완, 동이, 고배, 외반구연옹, 기대, 주조철부편, 철편, 철촉, 철겸 등 621점", findings: "원삼국시대 주거지 2기, 한성기 주거지 3기, 소형유구 8기, 구상유구 1기\n-성동리산성과 연관된 유적과 유사 양상\n-한성기 유구의 분포양상 구분", period: "2001", institution: "청운문화재연구원", source: "청운문화재연구원, 2003,\n『청운 노을동 마을유적』", notes: "", pdf: "" },
        Row { num: "35", era: "초기철기~원삼국", era_span: 0, date: "2~3세기", site: "청운 구름동 취락유적Ⅰ\n구름동 251-2번지 일원", artifacts: "격자·사격자문·무문 암키와, 대옹, 대호, 직구호, 동이, 원저단경호, 장란형토기, 심발형토기, 원통형기대, 철도자, 철정 등", findings: "주거지 3기, 소형유구 5기, 구상유구 3기, 굴립주건물지 1기\n-대형 주거지\n-위계가 높은 집단으로 추정\n-자체 철기 생산 가능성 추정", period: "2003~2004", institution: "별빛문화유산연구원", source: "별빛문화유산연구원, 2006,\n『청운 구름동 취락유적Ⅰ』", notes: "", pdf: "" },
        Row { num: "36", era: "초기철기~원삼국", era_span: 0, date: "2~3세기", site: "청운 달빛리 취락유적Ⅱ\n달빛면 달빛리 250-3번지 일원", artifacts: "대옹, 원통형기대편, 소형기대편, 원저단경호, 타날문토기편, 경질무문토기편, 철촉편, 철도자편, 지석 등", findings: "주거지 35~38기 존재 추정, 소형유구 70기 내외, 구상유구 3기\n-시굴조사로 유적 범위 확인\n-백제시대 주거지 비율 높음", period: "2005", institution: "달빛역사연구원", source: "달빛역사연구원, 2007,\n『청운 달빛리 취락유적Ⅱ』", notes: "", pdf: "" },
        Row { num: "37", era: "초기철기~원삼국", era_span: 0, date: "2~3세기", site: "청운 바람리 용수골 유적\n바람골면 바람리 552번지 일원", artifacts: "경질무문토기, 타날문토기, 이형토기, 회색무문양토기, 토제품, 석제품, 마노구슬, 철촉, 환두소도 등", findings: "주거지 28기, 수혈유구 46기, 소성유구 4기, 고상가옥 5기, 목책 3기\n-凸자형 주거지 주류\n-낙랑계 기술영향 확인", period: "2014~2016", institution: "청운문화재연구원", source: "청운문화재연구원, 2018,\n『청운 바람리 용수골 유적』", notes: "", pdf: "" },

        // ── 삼국~통일신라 (10) ────────────────────────────────────
        Row { num: "38", era: "삼국~통일신라", era_span: 10, date: "", site: "청운 고소산성\n달빛면 달빛리 산2-1번지 일원", artifacts: "토기편, 기와편 등", findings: "테뫼식 석축산성\n둘레 512m\n토기편으로 볼 때 삼국시대로 추정", period: "1997", institution: "청운대학교 박물관", source: "청운대학교 박물관, 1998,\n『청운시 군사유적 지표조사 보고서』", notes: "지표조사", pdf: "" },
        Row { num: "39", era: "삼국~통일신라", era_span: 0, date: "", site: "청운 소봉산성\n솔밭면 솔밭리 할미산", artifacts: "불명철기편, 토기편 등", findings: "테뫼식 석축산성\n둘레 94m\n막돌허튼층쌓기\n소규모 보루로 교통로 차단 목적 추정", period: "1997", institution: "청운대학교 박물관", source: "청운대학교 박물관, 1998,\n『청운시 군사유적 지표조사 보고서』", notes: "지표조사", pdf: "" },
        Row { num: "40", era: "삼국~통일신라", era_span: 0, date: "", site: "청운 바람산성\n바람골면 바람리 산225-6 일원", artifacts: "고배편, 인화문토기편, 토기편, 기와편 등", findings: "원형의 석축산성\n-해발 215m, 둘레 280m\n-서쪽성벽 일부 잔존 확인\n-단기적 방어를 위한 성 추정", period: "1998", institution: "청운대학교 박물관", source: "청운대학교 박물관, 1999,\n『청운시 군사유적 지표조사 보고서』", notes: "지표조사", pdf: "" },
        Row { num: "41", era: "삼국~통일신라", era_span: 0, date: "", site: "청운 달빛산성\n달빛면 달빛리 산61-1번지 일원", artifacts: "개배, 고배, 완, 뚜껑, 합, 심발형토기, 타날문토기편, 중국제 시유도기편 등", findings: "동벽일대 석축성벽, 수혈유구\n-2단의 석축부, 기초부만 잔존\n-토축성벽을 석축성벽으로 개축\n-성 내 정상부 생활시설 잔존 가능성", period: "2007~2008", institution: "별빛문화유산연구원", source: "별빛문화유산연구원, 2010,\n『청운 달빛산성Ⅰ』", notes: "", pdf: "" },
        Row { num: "42", era: "삼국~통일신라", era_span: 0, date: "", site: "청운 노을리 유적(1·2지점)\n노을면 노을리 291-2번지, 산23-1번지 일원", artifacts: "단경호, 완, 호, 파수부호, 파수부완, 대부완, 옹, 병, 고배, 부가구연대부장경호, 암키와편, 철제테두리 등", findings: "수혈주거지 7기, 수혈 9개, 주혈군 3개소, 석실묘 5기, 석곽묘 3기\n-주거지 평면형태 방형, 장방형, 凸자형\n-석실묘 평면형태 대부분 장방형", period: "2014", institution: "하늘고고학연구소", source: "하늘고고학연구소, 2016,\n『청운 노을리 유적 1·2지점』", notes: "", pdf: "" },
        Row { num: "43", era: "삼국~통일신라", era_span: 0, date: "", site: "청운 구름봉 취락유적Ⅱ\n구름동 250-3번지 일원", artifacts: "수키와편, 암키와편, 경질무문토기, 장란형토기, 심발형토기, 단경호, 회색무문양토기 호, 대옹, 시루, 도자편, 주조괭이, 철정, 숫돌 등", findings: "주거지 18기, 지상식건물지 1기, 수혈 60기, 구상유구 5기\n-凸, 呂자형 출입구의 오각형 주거지 주류\n-내부시설 'ㄱ','ㅡ'자형 부뚜막\n-저장용 수혈 다수", period: "2010", institution: "청운문화재연구원", source: "청운문화재연구원, 2012,\n『청운 구름봉 취락유적Ⅱ』", notes: "", pdf: "" },
        Row { num: "44", era: "삼국~통일신라", era_span: 0, date: "", site: "청운 수정동 419-32번지 유적\n수정동 419-32번지 일원", artifacts: "적갈색 연질토기 완, 고배편", findings: "수혈식석곽묘 2기\n-비교적 대형에 속하는 것\n-내부시설 시상", period: "2016", institution: "별빛문화유산연구원", source: "별빛문화유산연구원, 2018,\n『청운 수정동 419-32번지 유적』", notes: "", pdf: "" },
        Row { num: "45", era: "삼국~통일신라", era_span: 0, date: "", site: "청운 은하리 기와가마\n은하면 은하리 산15-31번지 일원", artifacts: "암키와편, 수키와편, 수막새편", findings: "기와가마 2기, 용도미상가마 1기", period: "2018~2019", institution: "달빛역사연구원", source: "달빛역사연구원, 2021,\n『청운 은하리 기와가마유적』", notes: "", pdf: "" },
        Row { num: "46", era: "삼국~통일신라", era_span: 0, date: "", site: "청운 새벽리 복합유적\n새벽면 새벽리 373-2번지 일원", artifacts: "기와편, 토기 호, 대호, 도기뚜껑, 완, 매병편, 청자화형접시, 분청접시, 백자발, 철솥 편 등", findings: "주거지 3기, 삼가마 1기, 구들, 소성유구 2기, 석렬 2기\n-주거지 평면형태 장방형\n-삼가마 일체형 1기", period: "2019", institution: "하늘고고학연구소", source: "하늘고고학연구소, 2021,\n『청운 새벽리 복합유적』", notes: "", pdf: "" },
        Row { num: "47", era: "삼국~통일신라", era_span: 0, date: "", site: "청운 반월성\n하늘읍 하늘리 산 5-1번지 일원", artifacts: "명문기와, 수막새, 귀면와, 무문·격자문·사격자문 평기와, 장동호, 호, 단경호, 완, 대부발, 벼루편, 고배, 철제도끼, 철겸, 철정, 방추차, 어망추 등", findings: "장대지, 건물지 2기, 성벽 단면, 북문지 조사\n*도기념물\n-장대지, 건물지 대지조성 확인\n-체성벽 수직에 가깝게 품자형 축조\n-북문지 평거식 확인", period: "1999", institution: "청운대학교 박물관", source: "청운대학교 박물관, 2000,\n『청운 반월성 1차 발굴조사 보고서』", notes: "", pdf: "" },

        // ── 고려 (12) ─────────────────────────────────────────────
        Row { num: "48", era: "고려", era_span: 12, date: "", site: "청운 보가산성\n바람골면 바람리 산251-1번지 일원", artifacts: "기와편, 청자편", findings: "포곡식 석축산성\n-전체 둘레 3.8㎞\n-성문지, 수구부, 추정건물지 확인\n-고려시대에 축조 추정", period: "1996", institution: "청운대학교 박물관", source: "청운대학교 박물관, 1997,\n『청운시 군사유적 지표조사 보고서』", notes: "지표조사", pdf: "" },
        Row { num: "49", era: "고려", era_span: 0, date: "", site: "청운 운악산성\n달빛면 산202번지 일원", artifacts: "어골문, 격자문 기와편, 회청색 경질토기", findings: "편축과 협축을 혼용한 석축산성\n-해발 870m\n-내부에서 추정문지, 탄요 흔적 확인\n-입보형 성곽 추정", period: "1998", institution: "청운대학교 박물관", source: "청운대학교 박물관, 1999,\n『청운시 군사유적 지표조사 보고서』", notes: "지표조사", pdf: "" },
        Row { num: "50", era: "고려", era_span: 0, date: "", site: "청운 노을리 태봉\n노을면 노을리 산28-2 일원", artifacts: "", findings: "왕녀 아기의 재를 묻은 곳\n-태항 도굴, 석대와 개석만 잔존", period: "2000~2001", institution: "청운대학교 박물관", source: "청운대학교 박물관, 2002,\n『청운시 문화유적분포지도』", notes: "", pdf: "" },
        Row { num: "51", era: "고려", era_span: 0, date: "1185년", site: "청운향교\n하늘읍 하늘리 176", artifacts: "", findings: "외삼문, 내삼문, 명륜당, 대성전 등\n*문화유산자료\n-1185년 창건, 1592년 소실\n-1598년 중건, 1975년 중수", period: "2000~2001", institution: "청운대학교 박물관", source: "청운대학교 박물관, 2002,\n『청운시 문화유적분포지도』", notes: "지표조사", pdf: "" },
        Row { num: "52", era: "고려", era_span: 0, date: "", site: "청운 솔밭리 유적\n솔밭면 솔밭리 산70-11 일원", artifacts: "토기편, 도기 호, 백자종지, 주조괭이, 물미, 쇠삽날, 철검, 철촉, 철정 등", findings: "수혈건물지 2기, 수혈유구 1기\n-2열의 구들열, 아궁이, 배연시설 확인\n-철기유물 다량 확인\n조선시대 화전민 거주 추정", period: "2005", institution: "청운문화재연구원", source: "청운문화재연구원, 2007,\n『청운 솔밭리 유적』", notes: "", pdf: "" },
        Row { num: "53", era: "고려", era_span: 0, date: "", site: "청운 달빛리 마산유적\n달빛면 달빛리 303번지 일원", artifacts: "분청자, 백자, 동이, 도기편, 기와편 등", findings: "고려~조선시대 건물지 1기,\n조선시대 주거지 1기, 수혈유구 12기\n유물지 1기", period: "2008~2009", institution: "하늘고고학연구소", source: "하늘고고학연구소, 2011,\n『청운 달빛리 마산유적』", notes: "", pdf: "" },
        Row { num: "54", era: "고려", era_span: 0, date: "", site: "청운 구름봉 유적\n구름동, 달빛면 달빛리 일원", artifacts: "청자접시 및 완, 백자접시 및 병,\n도기 완, 시루, 평기와, 철부, 청동숟가락 등", findings: "조선시대 건물지 2동, 흑탄요 1기, 집석유구 1기, 수혈유구 3기, 토광묘 3기, 회곽묘 1기\n-고려 말~조선 후기 출토유물", period: "2009~2010", institution: "별빛문화유산연구원", source: "별빛문화유산연구원, 2012,\n『청운 구름봉 유적』", notes: "", pdf: "" },
        Row { num: "55", era: "고려", era_span: 0, date: "", site: "청운 바람리 유적\n바람골면 바람리 산15-31번지 일원", artifacts: "암키와편, 수키와편", findings: "건물지 2기, 석렬 1기, 집석 1기, 토광묘 5기, 회곽묘 6기, 구상유구 2기, 수혈 5기", period: "2011~2012", institution: "달빛역사연구원", source: "달빛역사연구원, 2014,\n『청운 바람리 유적』", notes: "", pdf: "" },
        Row { num: "56", era: "고려", era_span: 0, date: "", site: "청운 은하리 청자가마\n은하면 은하리 산82번지 일원", artifacts: "분청자기 호편, 청자발·접시·호·병·잔·매병, 초벌 저부편, 흑유주자, 도침, 시침, 지석, 동전 등", findings: "고려시대 청자가마 1기, 폐기장 2기, 석곽묘 1기, 수혈유구 2기, 조선시대 숯가마 3기, 토광묘 6기, 회곽묘 2기", period: "2015~2016", institution: "청운문화재연구원", source: "청운문화재연구원, 2018,\n『청운 은하리 청자가마유적』", notes: "", pdf: "" },
        Row { num: "57", era: "고려", era_span: 0, date: "", site: "청운 새벽리 산성지\n새벽면 새벽리 산110번지 일원", artifacts: "기와편, 청자편, 소형토기 등", findings: "테뫼식 석축산성\n둘레 320m\n-배수로, 문지 추정지 확인", period: "2017", institution: "하늘고고학연구소", source: "하늘고고학연구소, 2019,\n『청운 새벽리 산성지 시굴조사 보고서』", notes: "지표조사 포함", pdf: "" },
        Row { num: "58", era: "고려", era_span: 0, date: "", site: "청운 노을동 고분군\n노을면 노을리 산45번지 일원", artifacts: "청자 접시, 청자 대접, 철기편, 동경편 등", findings: "석실묘 3기, 석곽묘 5기\n-고려 중기~후기 조성으로 추정\n-내부 시상대 확인", period: "2019~2020", institution: "별빛문화유산연구원", source: "별빛문화유산연구원, 2022,\n『청운 노을동 고분군』", notes: "", pdf: "" },
        Row { num: "59", era: "고려", era_span: 0, date: "", site: "청운 별빛마을 건물지\n별내면 별빛리 668-1번지 일원", artifacts: "기와편, 청자편, 소형호 등", findings: "건물지 2동, 우물 1기, 석렬 3기\n-고려 후기 건물 배치 양상 확인", period: "2021~2022", institution: "달빛역사연구원", source: "달빛역사연구원, 2024,\n『청운 별빛마을 건물지』", notes: "", pdf: "" },

        // ── 조선 (21) ─────────────────────────────────────────────
        Row { num: "60", era: "조선", era_span: 21, date: "전기", site: "청운 노을리 조선고분\n노을면 노을리 산98 일원", artifacts: "백자접시·종지·병·발, 청동숟가락 등", findings: "토광묘 4기", period: "2003~2004", institution: "청운문화재연구원", source: "청운문화재연구원, 2006,\n『청운 노을리 조선고분』", notes: "", pdf: "" },
        Row { num: "61", era: "조선", era_span: 0, date: "전기", site: "청운 수정동 조선유적\n수정동 136-1번지", artifacts: "수키와, 암키와, 도기 호, 청자 접시편, 분청사기 접시편, 백자 발, 철솥, 철도자, 철정 등", findings: "주거지 4기, 수혈 1기\n-평면형태 방형, 장방형\n-내부시설 구들, 부뚜막, 주혈 등", period: "2007", institution: "하늘고고학연구소", source: "하늘고고학연구소, 2009,\n『청운 수정동 136-1번지 유적』", notes: "", pdf: "" },
        Row { num: "62", era: "조선", era_span: 0, date: "전기", site: "청운정\n솔밭면 솔밭리 산547", artifacts: "", findings: "조선시대 정자\n*향토유적 제8호\n-세종 때 처음 세워짐\n-한국전쟁 때 해체, 이후 복원", period: "1997", institution: "청운군지 편찬위원회", source: "청운군지편찬위원회, 1997,\n『청운군지』", notes: "", pdf: "" },
        Row { num: "63", era: "조선", era_span: 0, date: "전기", site: "청운 달빛리 분청사기 요지\n달빛면 산190 일원", artifacts: "분청사기 발, 접시, 종지, 잔, 병, 호, 합, 백자 등", findings: "가마 1기 확인\n*향토유적 제22호\n-반지하식 단실 등요\n-분청사기 초기단계 인화기법 주류\n-광주지역 가마와 유사", period: "2008", institution: "청운문화재연구원", source: "청운문화재연구원, 2010,\n『청운 달빛리 분청사기 요지 발굴조사보고서』", notes: "", pdf: "" },
        Row { num: "64", era: "조선", era_span: 0, date: "전기", site: "청운서원\n솔밭면 솔밭리 산210 일원", artifacts: "", findings: "조선 중기 인물을 기리는 서원\n-1652년 사우 창건\n-1714년 청운이라는 사액 받음\n-1871년 훼철, 1980년 복원", period: "2000~2001", institution: "청운대학교 박물관", source: "청운대학교 박물관, 2002,\n『청운시 문화유적분포지도』", notes: "", pdf: "" },
        Row { num: "65", era: "조선", era_span: 0, date: "전기", site: "달빛서원\n달빛면 달빛리 산16-1 일원", artifacts: "", findings: "조선 후기 유학자를 기리는 서원\n-1638년 사우 창건\n-1868년 훼철, 1973년 복원시작\n-강당, 동재, 서재 등 복원", period: "2000~2001", institution: "청운대학교 박물관", source: "청운대학교 박물관, 2002,\n『청운시 문화유적분포지도』", notes: "", pdf: "" },
        Row { num: "66", era: "조선", era_span: 0, date: "전기", site: "청운 노을리 봉수지\n노을면 노을리 봉화골 봉화뚝", artifacts: "병 구연부편, 도기편, 자기편 등", findings: "연조에 사용된 돌무지, 길이 18m 정도의 석축 추정건물지 확인\n해발 172m\n북쪽 솔밭봉수, 남쪽 달빛봉수와 응함", period: "1999", institution: "청운대학교 박물관", source: "청운대학교 박물관, 2000,\n『청운시 군사유적 지표조사 보고서』", notes: "지표조사", pdf: "" },
        Row { num: "67", era: "조선", era_span: 0, date: "전기", site: "청운 바람리 봉수지\n바람골면 바람리 산86-1번지", artifacts: "회흑색 연질 토기 뚜껑, 연질토기편 등", findings: "평면 타원형 토축기단부의 봉수군 보호시설 조성, 4기의 석축연조 흔적\n-북쪽 노을봉수, 남쪽 별빛봉수 응함", period: "1999", institution: "청운대학교 박물관", source: "청운대학교 박물관, 2000,\n『청운시 군사유적 지표조사 보고서』", notes: "지표조사", pdf: "" },
        Row { num: "68", era: "조선", era_span: 0, date: "전기", site: "청운 솔밭산 봉수지\n솔밭면 솔밭리 중군봉", artifacts: "기와편", findings: "산 정상의 평탄면 확인\n해발 230m\n북쪽 바람봉수-남쪽 노을봉수 연결", period: "1999", institution: "청운대학교 박물관", source: "청운대학교 박물관, 2000,\n『청운시 군사유적 지표조사 보고서』", notes: "지표조사", pdf: "" },
        Row { num: "69", era: "조선", era_span: 0, date: "전기", site: "청운 달빛리 흑유자 가마\n달빛면 달빛리 350-50번지 일원", artifacts: "흑유자, 백자, 초벌편, 요도구, 도기편 등 2,847점", findings: "자기가마 1기, 주거지 1기, 수혈유구 2기, 소성유구 4기, 구들 1기, 폐기장 2기, 석렬 1기\n-세장방형 가마\n-주변 유구는 가마 부속시설로 추정", period: "2004~2005", institution: "별빛문화유산연구원", source: "별빛문화유산연구원, 2007,\n『청운 달빛리 흑유자 가마유적』", notes: "", pdf: "" },
        Row { num: "70", era: "조선", era_span: 0, date: "전기", site: "청운 별빛리 탄요 유적\n별내면 별빛리 산153번지 일원", artifacts: "도기편, 자기편 등 6점", findings: "탄요 2기\n-산지형 탄요, 원형의 반지하 등요", period: "2006", institution: "달빛역사연구원", source: "달빛역사연구원, 2008,\n『청운 별빛리 탄요 발굴조사 보고서』", notes: "", pdf: "" },
        Row { num: "71", era: "조선", era_span: 0, date: "전기", site: "청운 운봉리 조선취락\n운봉면 운봉리 521-1번지 일원", artifacts: "백자, 도기장군, 흑갈유호, 청동숟가락, 청동합, 구슬, 철제가위 등", findings: "측구부탄요 1기, 회곽묘 4기, 토광묘 15기, 추정 석곽묘 1기\n-석곽묘는 조선시대 이전으로 불가 추정", period: "2009", institution: "청운문화재연구원", source: "청운문화재연구원, 2011,\n『청운 운봉리 조선취락유적』", notes: "", pdf: "" },
        Row { num: "72", era: "조선", era_span: 0, date: "중기", site: "청운 하늘재 건물지 유적\n하늘읍 하늘재리 산38번지 일원", artifacts: "청해파문·종선문 수키와, 청해파문 암키와, 백자 대접, 발, 접시, 철제도구 등", findings: "건물지 2동\n-1호 건물지 정면 2칸, 측면 1칸\n-2호 건물지 정면, 측면 2칸\n-민가 혹은 사찰 관련 가능성", period: "2012", institution: "하늘고고학연구소", source: "하늘고고학연구소, 2014,\n『청운 하늘재 건물지 유적』", notes: "", pdf: "" },
        Row { num: "73", era: "조선", era_span: 0, date: "중기", site: "청운 구름동 조선유적(설운동 유물산포지)\n구름동 산34-8번지 일원", artifacts: "", findings: "조선시대 추정주거지 1기, 석축 1기, 시대미상 석곽고, 이장묘 1기\n-원형녹지보존지역으로 발굴유예", period: "2013", institution: "별빛문화유산연구원", source: "별빛문화유산연구원, 2015,\n『청운 구름동 조선유적 시굴조사 보고서』", notes: "", pdf: "" },
        Row { num: "74", era: "조선", era_span: 0, date: "중기", site: "청운 바람리 조선취락유적\n바람골면 바람리 389번지 일원", artifacts: "출토유물 없음", findings: "토광묘 1기\n-이단굴광 토광묘", period: "2015", institution: "달빛역사연구원", source: "달빛역사연구원, 2017,\n『청운 바람리 조선취락유적』", notes: "", pdf: "" },
        Row { num: "75", era: "조선", era_span: 0, date: "중기", site: "청운 노을리 회곽묘군\n노을면 노을리 초가팔리 389번지 일원", artifacts: "분청자접시편, 백자편 등", findings: "회곽묘 3기, 토광묘 7기, 수혈 2기, 구 1기", period: "2015~2016", institution: "청운문화재연구원", source: "청운문화재연구원, 2018,\n『청운 노을리 회곽묘군』", notes: "", pdf: "" },
        Row { num: "76", era: "조선", era_span: 0, date: "후기", site: "청운 달빛리 종가집터\n달빛면 달빛리 557번지 일원", artifacts: "명문 망새, 기하문 수막새, 파상문 수키와, 백자 제기접시·발, 청화백자 접시, 절구공이, 맷돌", findings: "건물지 4동(안채, 중문 및 광, 솟을대문 및 행랑마당, 사랑채), 담장, 구들\n-'ㄱ','ㅡ'자형 평면구조 건물\n-일반 종가에 비해 규모가 작음\n-조선 후기 건축양상", period: "2017", institution: "하늘고고학연구소", source: "하늘고고학연구소, 2019,\n『청운 달빛리 종가집터 발굴조사 보고서』", notes: "", pdf: "" },
        Row { num: "77", era: "조선", era_span: 0, date: "후기", site: "청운 독산봉수지\n하늘읍 하늘리 590번지 일원", artifacts: "수키와, 도기편, 분청사기편, 백자접시·발, 갈유자기편, 석환 등", findings: "봉수대(방호벽, 연대, 연조 3기, 고사, 망덕 등 확인)\n*향토유적\n-지표, 시굴조사만 진행\n-조선 전기부터 운영시작", period: "2018~2019", institution: "별빛문화유산연구원", source: "별빛문화유산연구원, 2021,\n『청운 독산봉수지』", notes: "", pdf: "" },
        Row { num: "78", era: "조선", era_span: 0, date: "후기", site: "청운 노을리 유적(1·2지점)\n노을면 노을리 291-2번지, 산23-1번지 일원", artifacts: "명문 암키와, 명문수키와편, 단경호, 청자완편, 백자 발, 엽전, 동경, 동전, 석제관옥 등", findings: "주거지 7기, 건물지 2기, 우물 1기, 수혈유구 35기, 토광묘 18기, 회곽묘 5기, 소성유구 3기", period: "2019", institution: "달빛역사연구원", source: "달빛역사연구원, 2021,\n『청운 노을리 유적 1·2지점』", notes: "", pdf: "" },
        Row { num: "79", era: "조선", era_span: 0, date: "후기", site: "청운 구름동 530-4번지 유적\n구름동 530-4번지 일원", artifacts: "암막새, 수키와, 암키와, 청자 접시, 백자 발·접시·종지·잔, 도기 호·소호, 상평통보, 철정, 편자 등", findings: "건물지 2기, 계단 1기, 축대 5기, 석열 2기\n-조선시대 청운현 관아터\n-계획적인 대지조성 확인\n-문지, 잡석지정, 초석, 온돌시설 확인\n-관아건물로 판단", period: "2020", institution: "청운문화재연구원", source: "청운문화재연구원, 2022,\n『청운 구름동 530-4번지 유적』", notes: "", pdf: "" },
        Row { num: "80", era: "조선", era_span: 0, date: "후기", site: "청운 은하리 479-2번지\n은하면 은하리 479-2번지 일원", artifacts: "출토유물 없음", findings: "탄요 1기, 수혈 5기", period: "2021~2022", institution: "하늘고고학연구소", source: "하늘고고학연구소, 2024,\n『청운 은하리 479-2번지 일원』", notes: "", pdf: "" },
    ]
}

fn build_store() -> HwpxStyleStore {
    let mut store = HwpxStyleStore::new();
    store.push_font(HwpxFont::new(0, "함초롬바탕", "HANGUL"));
    // CS0: normal 8pt
    let mut cs0 = HwpxCharShape::default();
    cs0.height = HwpUnit::from_pt(8.0).unwrap();
    store.push_char_shape(cs0);
    // CS1: header 8pt bold
    let mut cs1 = HwpxCharShape::default();
    cs1.height = HwpUnit::from_pt(8.0).unwrap();
    cs1.bold = true;
    store.push_char_shape(cs1);
    // CS2: small 7pt
    let mut cs2 = HwpxCharShape::default();
    cs2.height = HwpUnit::from_pt(7.0).unwrap();
    store.push_char_shape(cs2);
    // PS0: justify, 130% line spacing
    let mut ps0 = HwpxParaShape::default();
    ps0.alignment = Alignment::Justify;
    ps0.line_spacing_type = LineSpacingType::Percentage;
    ps0.line_spacing = 130;
    store.push_para_shape(ps0);
    // PS1: center, 130% line spacing
    let mut ps1 = HwpxParaShape::default();
    ps1.alignment = Alignment::Center;
    ps1.line_spacing_type = LineSpacingType::Percentage;
    ps1.line_spacing = 130;
    store.push_para_shape(ps1);
    store
}

/// A chunk of rows for one page, with recalculated era_span values.
struct Chunk {
    rows: Vec<Row>,
}

/// Splits data into page-sized chunks, respecting era boundaries when possible.
/// `max_rows` is computed dynamically by `estimate_max_rows()`.
fn chunk_data(data: &[Row], max_rows: usize) -> Vec<Chunk> {
    let mut chunks: Vec<Chunk> = Vec::new();
    let mut current: Vec<Row> = Vec::new();
    let mut i: usize = 0;
    while i < data.len() {
        let era_start: usize = i;
        let mut era_end: usize = i + 1;
        while era_end < data.len() && data[era_end].era_span == 0 {
            era_end += 1;
        }
        let era_size: usize = era_end - era_start;

        if current.len() + era_size <= max_rows {
            for item in data.iter().take(era_end).skip(era_start) {
                current.push(item.clone());
            }
            i = era_end;
        } else if current.is_empty() {
            let take: usize = max_rows.min(era_size);
            for item in data.iter().skip(era_start).take(take) {
                current.push(item.clone());
            }
            current[0].era_span = take as u16;
            chunks.push(finalize_chunk(current));
            current = Vec::new();
            if era_size - take > 0 {
                i = era_start + take;
            } else {
                i = era_end;
            }
        } else {
            // Era doesn't fit whole — fill remaining space with partial era rows
            let remaining: usize = max_rows - current.len();
            if remaining > 0 {
                let take: usize = remaining.min(era_size);
                for item in data.iter().skip(era_start).take(take) {
                    current.push(item.clone());
                }
                i = era_start + take;
            }
            chunks.push(finalize_chunk(current));
            current = Vec::new();
        }
    }
    if !current.is_empty() {
        chunks.push(finalize_chunk(current));
    }
    chunks
}

/// Recalculate era_span values within a chunk.
fn finalize_chunk(mut rows: Vec<Row>) -> Chunk {
    // Rebuild era_span: walk through, find era groups within this chunk
    let mut i = 0;
    while i < rows.len() {
        let era = rows[i].era;
        let mut count = 1;
        while i + count < rows.len() && rows[i + count].era == era && rows[i + count].era_span == 0
        {
            count += 1;
        }
        rows[i].era_span = count as u16;
        // Clear era_span for continuation rows
        for j in 1..count {
            rows[i + j].era_span = 0;
        }
        i += count;
    }
    Chunk { rows }
}

// ── Automatic column splitting algorithm ─────────────────────────

/// Greedy left-to-right split: assigns columns to page 1 until the
/// accumulated min_width exceeds printable width, rest go to page 2.
fn split_columns(columns: &[ColumnDef], page_width_mm: f64) -> (Vec<usize>, Vec<usize>) {
    let mut left: Vec<usize> = Vec::new();
    let mut right: Vec<usize> = Vec::new();
    let mut accumulated: f64 = 0.0;
    let mut overflow: bool = false;

    for (i, col) in columns.iter().enumerate() {
        if !overflow && accumulated + col.min_width_mm <= page_width_mm {
            left.push(i);
            accumulated += col.min_width_mm;
        } else {
            overflow = true;
            right.push(i);
        }
    }
    (left, right)
}

/// Distributes remaining page width equally across all columns in the group.
fn distribute_widths(columns: &[ColumnDef], indices: &[usize], page_width_mm: f64) -> Vec<f64> {
    let total_min: f64 = indices.iter().map(|&i| columns[i].min_width_mm).sum();
    let bonus: f64 = if indices.is_empty() {
        0.0
    } else {
        (page_width_mm - total_min).max(0.0) / indices.len() as f64
    };
    indices.iter().map(|&i| columns[i].min_width_mm + bonus).collect()
}

/// Maps column index (0-10) to the corresponding Row field.
fn extract_field(row: &Row, col_index: usize) -> &str {
    match col_index {
        0 => row.num,
        1 => row.era,
        2 => row.date,
        3 => row.site,
        4 => row.artifacts,
        5 => row.findings,
        6 => row.period,
        7 => row.institution,
        8 => row.source,
        9 => row.notes,
        10 => row.pdf,
        _ => "",
    }
}

/// Estimates rendered line count for a cell, including word wrap.
/// Splits on `\n`, then for each line measures total visual width
/// accounting for full-width (Korean/CJK) and half-width (ASCII) characters.
fn estimate_cell_lines(text: &str, col_width_mm: f64) -> usize {
    let cell_margin_mm: f64 = 1.5; // left + right internal cell margins
    let usable_mm: f64 = (col_width_mm - cell_margin_mm).max(5.0);
    let full_width_mm: f64 = FONT_SIZE_PT * 0.3528; // Korean/CJK character width
    let half_width_mm: f64 = full_width_mm * 0.5; // ASCII/Latin character width

    let mut total_lines: usize = 0;
    for line in text.split('\n') {
        if line.is_empty() {
            total_lines += 1;
            continue;
        }
        // Measure total visual width of this line
        let mut line_width_mm: f64 = 0.0;
        let mut lines_for_segment: usize = 1;
        for ch in line.chars() {
            let w: f64 = if ch > '\u{FF}' { full_width_mm } else { half_width_mm };
            if line_width_mm + w > usable_mm {
                lines_for_segment += 1;
                line_width_mm = w;
            } else {
                line_width_mm += w;
            }
        }
        total_lines += lines_for_segment;
    }
    total_lines.max(1)
}

/// Computes the synchronized row height (in mm) for each data row,
/// considering ALL 11 columns across both facing pages.
/// Both left and right tables will use the same heights so rows stay aligned.
fn compute_row_heights(
    data: &[Row],
    left_indices: &[usize],
    right_indices: &[usize],
    left_widths: &[f64],
    right_widths: &[f64],
) -> Vec<f64> {
    let line_height_mm: f64 = FONT_SIZE_PT * 0.3528 * (LINE_SPACING_PCT as f64 / 100.0);
    let cell_padding_mm: f64 = 1.5;

    let col_widths: Vec<(usize, f64)> = left_indices
        .iter()
        .zip(left_widths.iter())
        .chain(right_indices.iter().zip(right_widths.iter()))
        .map(|(&idx, &w)| (idx, w))
        .collect();

    data.iter()
        .map(|row| {
            let max_lines: usize = col_widths
                .iter()
                .map(|&(idx, w)| estimate_cell_lines(extract_field(row, idx), w))
                .max()
                .unwrap_or(1);
            max_lines as f64 * line_height_mm + cell_padding_mm
        })
        .collect()
}

/// Computes how many rows fit on one page using pre-computed row heights.
fn rows_that_fit(row_heights: &[f64], page_height_mm: f64) -> usize {
    let line_height_mm: f64 = FONT_SIZE_PT * 0.3528 * (LINE_SPACING_PCT as f64 / 100.0);
    let cell_padding_mm: f64 = 1.5;
    let title_height_mm: f64 = 10.0 * 0.3528 * 1.3;
    let overhead_mm: f64 = title_height_mm + line_height_mm + (line_height_mm + cell_padding_mm);
    let available_mm: f64 = (page_height_mm - overhead_mm) * 0.97;

    let mut accumulated_mm: f64 = 0.0;
    let mut count: usize = 0;
    for &h in row_heights {
        if accumulated_mm + h > available_mm {
            break;
        }
        accumulated_mm += h;
        count += 1;
    }
    count.max(1)
}

/// Builds a Table from a subset of columns with distributed widths and
/// explicit row heights (synchronized across facing pages).
fn build_table_for_page(
    data: &[Row],
    col_indices: &[usize],
    widths_mm: &[f64],
    columns: &[ColumnDef],
    table_width_mm: f64,
    row_heights_mm: &[f64],
) -> Table {
    let widths_hwp: Vec<HwpUnit> =
        widths_mm.iter().map(|&w| HwpUnit::from_mm(w).unwrap()).collect();

    // Header row
    let header_cells: Vec<TableCell> = col_indices
        .iter()
        .zip(widths_hwp.iter())
        .map(|(&idx, &w)| header_cell(columns[idx].name, w))
        .collect();
    let header: TableRow = TableRow::new(header_cells).with_header(true);

    // Data rows with synchronized heights
    let mut rows: Vec<TableRow> = vec![header];
    for (i, entry) in data.iter().enumerate() {
        let mut cells: Vec<TableCell> = Vec::new();
        for (&idx, &w) in col_indices.iter().zip(widths_hwp.iter()) {
            let text: &str = extract_field(entry, idx);
            let cs: usize = columns[idx].char_shape_idx;
            let ps: usize = columns[idx].para_shape_idx;
            cells.push(cell_lines(text, w, cs, ps));
        }
        let h: Option<HwpUnit> = row_heights_mm.get(i).map(|&mm| HwpUnit::from_mm(mm).unwrap());
        let row: TableRow = match h {
            Some(height) => TableRow::with_height(cells, height),
            None => TableRow::new(cells),
        };
        rows.push(row);
    }

    Table::new(rows)
        .with_width(HwpUnit::from_mm(table_width_mm).unwrap())
        .with_page_break(TablePageBreak::Cell)
}

fn main() {
    println!("=== 청운시 문화유산 발굴연표 ===\n");
    std::fs::create_dir_all("temp").unwrap();
    let store = build_store();
    let all_data = data();
    let page = PageSettings::a4();
    let printable_width_mm = page.printable_width().to_mm();
    let printable_height_mm = page.printable_height().to_mm();

    // Step 1: Split columns across two pages
    let (left_indices, right_indices) = split_columns(&ALL_COLUMNS, printable_width_mm);
    let left_names: Vec<&str> = left_indices.iter().map(|&i| ALL_COLUMNS[i].name).collect();
    let right_names: Vec<&str> = right_indices.iter().map(|&i| ALL_COLUMNS[i].name).collect();
    println!("  좌측 컬럼 ({}): {:?}", left_indices.len(), left_names);
    println!("  우측 컬럼 ({}): {:?}", right_indices.len(), right_names);

    // Step 2: Distribute widths equally within each group
    let left_widths = distribute_widths(&ALL_COLUMNS, &left_indices, printable_width_mm);
    let right_widths = distribute_widths(&ALL_COLUMNS, &right_indices, printable_width_mm);

    // Step 3: Compute synchronized row heights (same for both tables)
    let row_heights =
        compute_row_heights(&all_data, &left_indices, &right_indices, &left_widths, &right_widths);
    let max_rows = rows_that_fit(&row_heights, printable_height_mm);
    println!("  페이지당 최대 행 수: {}", max_rows);

    // Step 4: Chunk data respecting era boundaries
    let chunks = chunk_data(&all_data, max_rows);
    println!(
        "  총 {}건 → {}개 청크 → {}개 섹션 (페이지 페어)",
        all_data.len(),
        chunks.len(),
        chunks.len() * 2
    );

    let mut doc = Document::new();
    let mut row_offset: usize = 0;
    for (i, chunk) in chunks.iter().enumerate() {
        let chunk_label = if i == 0 {
            "청운시 문화유산 발굴연표".to_string()
        } else {
            format!("청운시 문화유산 발굴연표 ({})", i + 1)
        };

        // Slice row heights for this chunk
        let chunk_heights = &row_heights[row_offset..row_offset + chunk.rows.len()];
        row_offset += chunk.rows.len();

        // Left section (odd page)
        let left_table = build_table_for_page(
            &chunk.rows,
            &left_indices,
            &left_widths,
            &ALL_COLUMNS,
            printable_width_mm,
            chunk_heights,
        );
        println!(
            "  청크 {}: 좌 {}행×{}열, 우 {}행×{}열",
            i + 1,
            left_table.row_count(),
            left_table.col_count(),
            chunk.rows.len() + 1,
            right_indices.len()
        );
        let left_para = Paragraph::with_runs(
            vec![Run::table(left_table, CharShapeIndex::new(CS_NORMAL))],
            ParaShapeIndex::new(PS_NORMAL),
        );
        let left_section = Section::with_paragraphs(
            vec![p(&chunk_label, CS_HEADER, PS_CENTER), p("", CS_NORMAL, PS_NORMAL), left_para],
            PageSettings::a4(),
        );
        doc.add_section(left_section);

        // Right section (even page) — same heights as left
        let right_table = build_table_for_page(
            &chunk.rows,
            &right_indices,
            &right_widths,
            &ALL_COLUMNS,
            printable_width_mm,
            chunk_heights,
        );
        let right_label = format!("{} (계속)", chunk_label);
        let right_para = Paragraph::with_runs(
            vec![Run::table(right_table, CharShapeIndex::new(CS_NORMAL))],
            ParaShapeIndex::new(PS_NORMAL),
        );
        let right_section = Section::with_paragraphs(
            vec![p(&right_label, CS_HEADER, PS_CENTER), p("", CS_NORMAL, PS_NORMAL), right_para],
            PageSettings::a4(),
        );
        doc.add_section(right_section);
    }

    let validated = doc.validate().expect("validation failed");
    let image_store = ImageStore::new();
    let bytes = HwpxEncoder::encode(&validated, &store, &image_store).expect("encode failed");
    std::fs::write("temp/large_table.hwpx", &bytes).unwrap();
    println!("  ✅ temp/large_table.hwpx 생성 완료 ({} bytes)", bytes.len());
}