File size: 4,232 Bytes
3798d02
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
/**
 * ๋ฒ•์ œ์ฒ˜ API XML ์‘๋‹ต ํŒŒ์„œ.
 * ๋‹จ์ˆœ ํƒœ๊ทธ ์ถ”์ถœ์€ regex(๋น ๋ฅด๊ณ  ๊ฐ€๋ฒผ์›€), ๊ตฌ์กฐํ™” ํŒŒ์‹ฑ์€ fast-xml-parser ์‚ฌ์šฉ.
 */

import { XMLParser } from "fast-xml-parser";

/**
 * `<![CDATA[...]]>` wrapper ์ž๋™ ์ œ๊ฑฐ.
 * ๋ฒ•์ œ์ฒ˜ XML์ด ํ•œ๊ธ€ยทํŠน์ˆ˜๋ฌธ์ž ํ•„๋“œ๋ฅผ CDATA๋กœ ๊ฐ์‹ธ์„œ ๋ณด๋‚ด๊ธฐ ๋•Œ๋ฌธ์— ํ•„์ˆ˜.
 */
function unwrapCdata(s: string): string {
  const m = s.match(/^<!\[CDATA\[([\s\S]*?)\]\]>$/);
  return m && m[1] !== undefined ? m[1] : s;
}

/**
 * ํƒœ๊ทธ 1๊ฐœ ์ถ”์ถœ โ€” ์ฒซ ๋งค์นญ๋งŒ ๋ฐ˜ํ™˜.
 * **case-sensitive** โ€” ๋ฒ•์ œ์ฒ˜ XML ์‘๋‹ต์— `<Ppc>`(rootTag)์™€ `<ppc>`(itemTag)๊ฐ€
 * ์ผ€์ด์Šค๋งŒ ๋‹ค๋ฅธ ์ฑ„ ๊ณต์กดํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ์žˆ์–ด case-insensitive ๋งค์นญ ์‹œ outer ํƒœ๊ทธ๊ฐ€
 * inner์˜ ๋‹ซ๋Š” ํƒœ๊ทธ(`</ppc>`)์— ์ž˜๋ฆผ.
 *
 * CDATA wrapper ์ž๋™ ์ œ๊ฑฐ โ€” ํ•œ๊ธ€ ํ•„๋“œ(๋ฒ•๋ น๋ช…ยท์•ˆ๊ฑด๋ช… ๋“ฑ)๋Š” ๊ฑฐ์˜ ๋ชจ๋‘ CDATA๋กœ ๊ฐ์‹ธ์ง.
 */
export function extractTag(xml: string, tagName: string): string {
  // ์•ˆ์ „ํ•œ ํƒœ๊ทธ๋ช… (์˜๋ฌธยทํ•œ๊ธ€ยท์ˆซ์ž๋งŒ ํ—ˆ์šฉ)
  if (!/^[๊ฐ€-ํžฃA-Za-z0-9_]+$/.test(tagName)) {
    return "";
  }
  const re = new RegExp(`<${tagName}[^>]*>([\\s\\S]*?)</${tagName}>`);
  const m = xml.match(re);
  if (!m || !m[1]) return "";
  return unwrapCdata(m[1].trim());
}

/** ํƒœ๊ทธ N๊ฐœ ์ถ”์ถœ โ€” ๋ชจ๋“  ๋งค์นญ ๋ฐ˜ํ™˜. case-sensitive + CDATA unwrap (`extractTag`์™€ ๋™์ผ). */
export function extractTagAll(xml: string, tagName: string): string[] {
  if (!/^[๊ฐ€-ํžฃA-Za-z0-9_]+$/.test(tagName)) {
    return [];
  }
  const re = new RegExp(`<${tagName}[^>]*>([\\s\\S]*?)</${tagName}>`, "g");
  const out: string[] = [];
  let m: RegExpExecArray | null;
  while ((m = re.exec(xml)) !== null) {
    if (m[1] !== undefined) out.push(unwrapCdata(m[1].trim()));
  }
  return out;
}

export interface SearchXmlResult<T> {
  totalCnt: number;
  page: number;
  items: T[];
}

export interface ParseSearchOptions {
  /** ์ด๊ฑด์ˆ˜ ํƒœ๊ทธ๋ช… (๊ธฐ๋ณธ "totalCnt"). aiSearch๋Š” "๊ฒ€์ƒ‰๊ฒฐ๊ณผ๊ฐœ์ˆ˜" ์‚ฌ์šฉ. */
  totalCntTag?: string;
  /** ํŽ˜์ด์ง€ ํƒœ๊ทธ๋ช… (๊ธฐ๋ณธ "page") */
  pageTag?: string;
}

/**
 * ๋ฒ•์ œ์ฒ˜ ๊ฒ€์ƒ‰ ์‘๋‹ต ํŒŒ์‹ฑ.
 * <{rootTag}><totalCnt>N</totalCnt><page>N</page><{itemTag}>...</{itemTag}>...</{rootTag}>
 *
 * mapper๋Š” ๊ฐ itemTag ๋‚ด๋ถ€ XML์„ ๋ฐ›์•„ ๋„๋ฉ”์ธ ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜.
 * ์ผ๋ถ€ endpoint๋Š” totalCnt ํƒœ๊ทธ๋ช…์ด ๋‹ค๋ฆ„ (์˜ˆ: aiSearch=๊ฒ€์ƒ‰๊ฒฐ๊ณผ๊ฐœ์ˆ˜) โ€” options๋กœ override.
 */
export function parseSearchXML<T>(
  xml: string,
  rootTag: string,
  itemTag: string,
  mapper: (itemXml: string) => T,
  options: ParseSearchOptions = {}
): SearchXmlResult<T> {
  const totalCntTag = options.totalCntTag ?? "totalCnt";
  const pageTag = options.pageTag ?? "page";

  const totalCnt = parseInt(extractTag(xml, totalCntTag), 10) || 0;
  const page = parseInt(extractTag(xml, pageTag), 10) || 1;

  // rootTag ๋‚ด๋ถ€์—์„œ๋งŒ itemTag ๊ฒ€์ƒ‰ (์„ ํƒ์  โ€” ๋ถ€์ •ํ™•ํ•œ ํ•„ํ„ฐ๋ง ํšŒํ”ผ)
  const rootContent = extractTag(xml, rootTag) || xml;
  const itemContents = extractTagAll(rootContent, itemTag);

  const items = itemContents.map(mapper);
  return { totalCnt, page, items };
}

const DEFAULT_PARSER = new XMLParser({
  ignoreAttributes: false,
  attributeNamePrefix: "@_",
  parseTagValue: false,
  parseAttributeValue: false,
  trimValues: true,
});

/**
 * ์ „์ฒด XML โ†’ ์ค‘์ฒฉ ๊ฐ์ฒด. ๋ณต์žกํ•œ ๊ตฌ์กฐ(๋ฒ•๋ น ๋ณธ๋ฌธ ๋“ฑ) ํŒŒ์‹ฑ์šฉ.
 * ๋‹จ์ผ vs ๋ฐฐ์—ด ์ •๊ทœํ™”๋Š” ํ˜ธ์ถœ์ž ์ฑ…์ž„.
 */
export function parseXML(xml: string): unknown {
  return DEFAULT_PARSER.parse(xml);
}

/** ๋‹จ์ผ ๊ฐ์ฒด ๋˜๋Š” ๋ฐฐ์—ด์„ ํ•ญ์ƒ ๋ฐฐ์—ด๋กœ ์ •๊ทœํ™” (fast-xml-parser quirk ์ฒ˜๋ฆฌ) */
export function asArray<T>(value: T | T[] | undefined | null): T[] {
  if (value === undefined || value === null) return [];
  return Array.isArray(value) ? value : [value];
}

/**
 * ๋ณธ๋ฌธ ์•ˆ์˜ HTML ํƒœ๊ทธ ์ œ๊ฑฐ + ์—”ํ‹ฐํ‹ฐ ๋””์ฝ”๋”ฉ.
 * ์˜๋ฌธ๋ฒ•๋ น(elaw) ์‘๋‹ต์— ๊ฒ€์ƒ‰ ๊ฐ•์กฐ `<strong>` ํƒœ๊ทธ๊ฐ€ ๋“ค์–ด์˜ค๋Š” quirk ๋“ฑ ์ฒ˜๋ฆฌ.
 */
export function stripHtmlTags(s: string): string {
  if (!s) return s;
  return s
    .replace(/<[^>]+>/g, "")
    .replace(/&lt;/g, "<")
    .replace(/&gt;/g, ">")
    .replace(/&amp;/g, "&")
    .replace(/&quot;/g, '"')
    .replace(/&apos;/g, "'")
    .replace(/&nbsp;/g, " ")
    .trim();
}