PageSourceSearch

https://0404.go.kr/js/commons/common.js?ver=1790913594003

js 0404.go.kr collected 2026-10-02 04:00:38 UTC 24,599 bytes, 646 lines download raw bytes

1
2// null or undefined 체크 함수
3function isNull(value){
4    return value === null || value === undefined || (typeof value === 'string' && value.trim() === "");
5}
6
7function isEmptyObject(obj) {
8  return obj && Object.keys(obj).length === 0 && obj.constructor === Object;
9}
10
11// 객체 또는 배열을 깊은 복사한다.
12function deepClone(obj) {
13    return JSON.parse(JSON.stringify(obj));
14}
15
16// 배열에서 중복항목을 제거한다.
17function uniqueArray(arr) {
18    return [...new Set(arr)];
19}
20
21// text를 클립보드로 복사해주는 함수
22function copyToClipboard(text) {
23    return navigator.clipboard.writeText(text);
24}
25
26// 현재 페이지 URL을 클립보드에 복사하는 기능
27function copyCurrentUrlToClipboard() {
28  const currentUrl = window.location.href;
29
30  navigator.clipboard.writeText(currentUrl)
31    .then(() => {
32      alert("URL이 클립보드에 복사되었습니다!");
33    })
34    .catch(err => {
35      console.error("클립보드 복사 실패:", err);
36    });
37}
38
39// URL의 쿼리 매개변수를 객체로 변환
40function getQueryParams() {
41    return Object.fromEntries(new URLSearchParams(window.location.search));
42}
43
44// 페이지를 스크롤하는 함수, 0이면 맨 위로 스크롤한다.
45function scrollToTop(top) {
46    window.scrollTo({ top: top, behavior: 'smooth' });
47}
48
49// 문자열(str)을 지정된 길이(length)로 자른뒤에 ...를 추가한다.
50function textTruncate(str, length) {
51    return str.length > length ? str.slice(0, length) + '...' : str;
52}
53
54// 문자열의 첫 글자를 대문자로 변환한다.
55function capitalize(str) {
56    return str.charAt(0).toUpperCase() + str.slice(1);
57}
58
59// ip4 검증
60function isValidIPv4(ip) {
61  if (typeof ip !== 'string') return false;
62
63  const parts = ip.trim().split('.');
64  if (parts.length !== 4) return false;
65
66  for (const part of parts) {
67    // 빈 문자열 or 숫자가 아님
68    if (!/^\d+$/.test(part)) return false;
69
70    const num = Number(part);
71    if (num < 0 || num > 255) return false;
72
73    // "01", "001" 같은 선행 0 금지 (단, "0"은 허용)
74    if (part.length > 1 && part.startsWith('0')) return false;
75  }
76
77  return true;
78}
79
80
81/**
82 * 필수 입력값이 포함되어 있는지 확인하는 함수
83 * @param {Object} valuesObj - 사용자가 입력한 값 객체 (key-value 쌍)
84 * @param {Array} requiredList - 필수 입력값 목록 (키, 제목, 선택자 정보 포함)
85 * @returns {boolean} - 필수값이 모두 포함되어 있으면 `true`, 누락된 값이 있으면 `false`
86 */
87function checkRequired(valuesObj, requiredList) {
88    // 필수값이 포함되어있는지 확인
89    for (let requiredObj of requiredList) {
90        const key = requiredObj.key; // 필수 입력값의 키
91        const title = requiredObj.title; // 필수 입력값의 제목 (라벨)
92        const values = valuesObj[key]; // 실제 입력값
93
94        // 입력값이 비어 있으면 경고창을 띄우고, 해당 요소에 포커스를 맞춘 후 `false` 반환
95        if (isNull(values)) {
96            alert(title + getMessage("confirm.required"));
97            requiredObj.selector.focus();
98            return false;
99        }
100    }
101    return true; // 모든 필수값이 입력된 경우 `true` 반환
102}
103
104/**
105 * 특정 컨테이너 내에서 `required` 필수 입력값 요소를 추출하는 함수
106 * @param {string} containerSelector - 필수 입력값을 찾을 컨테이너의 CSS 선택자
107 * @returns {Array | null} - 필수 입력 요소 객체 배열 (컨테이너가 없으면 `null` 반환)
108 */
109function getRequired(containerSelector) {
110    const container = document.querySelector(containerSelector);
111    if (!container) return null;
112    return getRequiredContainer(container);
113}
114
115/**
116 * 특정 컨테이너 내에서 `required` 필수 입력값 요소를 추출하는 함수
117 * @param {HTMLElement} container - 폼 입력 필드가 포함된 컨테이너 요소
118 * @returns {Array} - 필수 입력 요소 정보 객체 배열
119 */
120function getRequiredContainer(container) {
121    // 컨테이너 내의 모든 입력 요소 (`input`, `select`, `textarea`)를 가져옴
122    const elements = container.querySelectorAll("input, select, textarea");
123    const values = [];
124
125    elements.forEach(element => {
126        let key = element.name || element.id; // `name`이 없으면 `id` 사용
127        if (!key) return; // `name`과 `id` 둘 다 없으면 무시
128        if (!element.required) return; // 필수 입력값이 아닌 요소는 제외
129
130        // 필수 입력 요소 객체 생성
131        const obj = {};
132        obj.key = key; // 필수 입력값의 키 (폼 요소의 `name` 또는 `id`)
133        obj.title = element.title; // 필수 입력값의 라벨 (입력 요소의 `title` 속성)
134        obj.required = element.required; // `required` 여부
135        obj.selector = element; // 해당 입력 요소 객체
136        values.push(obj);
137    });
138
139    return values; // 필수 입력 요소 객체 목록 반환
140}
141
142
143/**
144 * 컨테이너 요소 내의 모든 입력값을 가져오는 함수
145 * @param {string} containerSelector - 폼 요소가 포함된 컨테이너의 CSS 선택자
146 * @returns {Object | null} - 컨테이너 내 폼 요소의 값을 포함하는 객체 (컨테이너가 없으면 `null` 반환)
147 */
148function getFormValues(containerSelector) {
149    // 특정 div(container) 안의 모든 form 요소를 가져옴
150    const container = document.querySelector(containerSelector);
151    if (!container) return null;
152    return getFormValuesContainer(container);
153}
154
155/**
156 * 컨테이너 요소 내의 모든 입력값을 가져오는 함수
157 * @param {HTMLElement} container - 폼 입력 필드를 포함하는 컨테이너 요소
158 * @returns {Object} - 입력값을 키-값 쌍으로 반환하는 객체
159 */
160function getFormValuesContainer(container) {
161    // 컨테이너 내 모든 input, select, textarea 요소를 선택
162    const elements = container.querySelectorAll("input, select, textarea");
163    const values = {};
164
165    elements.forEach(element => {
166        let key = element.name || element.id; // name이 없으면 id 사용
167        if (!key) return; // name과 id 둘 다 없으면 무시
168
169        const isMultiple = container.querySelectorAll(`[name="${element.name}"]`).length > 1;
170
171        if (element.type === "checkbox") {
172            if (isMultiple) {
173                if (!values[key]) values[key] = [];
174                if (element.checked) values[key].push(element.value);
175            } else {
176                if(element.checked){
177                    values[key] = element.value;
178                }
179            }
180        } else if (element.type === "radio") {
181            if (element.checked) {
182                values[key] = element.value;
183            }
184        } else {
185            if (isMultiple) {
186                if (!values[key]) values[key] = [];
187                values[key].push(element.value);
188            } else {
189                values[key] = element.value;
190            }
191        }
192    });
193
194    return values;
195}
196
197
198/**
199 * 컨테이너 요소 내의 모든 `<input type="file">`에서 파일을 가져오는 함수
200 * @param {string} containerSelector - 파일 입력 필드가 포함된 컨테이너의 CSS 선택자
201 * @returns {File[] | null} - 컨테이너 내 모든 파일 입력 필드에서 선택된 파일 목록 (파일이 없으면 `null` 반환)
202 */
203function getFormFiles(containerSelector) {
204    const container = document.querySelector(containerSelector);
205    if (!container) return null;
206    return getFormFilesContainer(container);
207}
208
209/**
210 * 컨테이너 요소 내의 모든 `<input type="file">`에서 파일을 가져오는 함수
211 * @param {HTMLElement} container - 파일 입력 필드를 포함하는 컨테이너 요소
212 * @returns {File[]} - 선택된 파일 객체 배열 (파일이 없으면 빈 배열 반환)
213 */
214function getFormFilesContainer(container) {
215    // 컨테이너 내의 모든 파일 입력 필드 가져오기
216    const inputFileList = container.querySelectorAll('input[type="file"]');
217    const fileList = [];
218
219    // 각 파일 입력 필드에서 첫 번째 파일을 가져와 배열에 추가
220    for (let fileBox of inputFileList) {
221        const file = fileBox.files[0];
222        if (file) fileList.push(file);
223    }
224
225    return fileList;
226}
227
228
229/**
230 * JS 객체 데이터를 특정 div 내 form 요소에 동적으로 입력하는 함수
231 * @param {Object} data - 입력할 데이터 객체 (key: 요소의 name 속성)
232 * @param {string} containerSelector - 입력할 폼이 포함된 div의 선택자
233 */
234function populateFormValues(data, containerSelector) {
235    const container = document.querySelector(containerSelector);
236    if (!container) return;
237
238    resetFormValues(containerSelector); // 값 초기화
239
240    // container 내부의 모든 input, select, textarea 요소 선택
241    const elements = container.querySelectorAll("input, select, textarea");
242
243    elements.forEach(element => {
244        const name = element.name;
245        if (!name || !(name in data)) return; // name이 없거나 data에 해당 key가 없으면 스킵
246
247        const value = data[name];
248
249        switch (element.type) {
250            case "radio":
251                // name이 동일한 라디오 버튼 중, value가 일치하는 것 체크
252                const radioGroup = container.querySelectorAll(`input[type="radio"][name="${name}"]`);
253                radioGroup.forEach(radio => {
254                    radio.checked = radio.value === String(value);
255                });
256                break;
257            case "checkbox":
258                // 체크박스는 배열일 경우 여러 개 체크 가능
259                if (Array.isArray(value)) {
260                    element.checked = value.includes(element.value);
261                } else {
262                    element.checked = Boolean(value);
263                }
264                break;
265            case "select-one":
266                // select 요소 값 설정
267                element.value = value;
268                break;
269            default:
270                // 일반 input, textarea 값 설정
271                element.value = value;
272                break;
273        }
274    });
275}
276
277/**
278 * 특정 컨테이너 내의 모든 입력 요소 값을 초기화하는 함수
279 * @param {string} containerSelector - 값을 초기화할 컨테이너의 CSS 선택자
280 */
281function resetFormValues(containerSelector) {
282    const container = document.querySelector(containerSelector);
283    if (!container) return;
284
285    // 컨테이너 내의 모든 input, select, textarea 요소 가져오기
286    const elements = container.querySelectorAll("input, select, textarea");
287
288    // 반복문으로 값 초기화
289    elements.forEach(element => {
290        if ((element.tagName === "INPUT") && (element.type === "checkbox" || element.type === "radio")) {
291            element.checked = false; // 체크박스와 radio버튼은 체크를 해제한다.
292        } else if(element.tagName === "INPUT" || element.tagName === "TEXTAREA"){
293            element.value = ""; //값 초기화
294        } else if (element.tagName === "SELECT") {
295            element.selectedIndex = 0; // 첫 번째 옵션 선택
296        }
297    });
298}
299
300
301/**
302 * 동적으로 HTML form을 생성하고 특정 URL로 데이터를 제출하는 함수
303 *
304 * @param {string} url - 데이터를 전송할 대상 URL
305 * @param {Object} options - 옵션 객체
306 * @param {string} [options.method="GET"] - HTTP 요청 메서드 ("GET" 또는 "POST" 등)
307 * @param {Object} [options.data={}] - 폼 데이터 객체 (키-값 쌍)
308 * @param {boolean} [options.viewLoading=true] - 로딩 화면 표시 여부
309 */
310function submitForm(url, { method = "GET", data = {}, target = "_self", enctype = 'application/x-www-form-urlencoded', viewLoading = true} = {}) {
311    if(viewLoading) showLoading(); // 로딩 화면을 표시할지 여부 확인 후 실행
312
313    method = method.toUpperCase(); // HTTP 메서드를 대문자로 변환 (예: "post" -> "POST")
314
315    // 1. form 요소 생성 및 기본 설정
316    const form = document.createElement('form');
317    form.method = method;  // HTTP 메서드 설정 (GET 또는 POST)
318    form.action = url;  // 요청을 보낼 URL 지정
319    form.target = target;  // target 옵션
320    form.enctype = enctype;  // enctype 옵션
321
322    // 2. data 객체를 기반으로 input 요소를 생성하여 form에 추가
323    if (!isEmptyObject(data) && Object.keys(data).length > 0 && typeof data === 'object') {
324        for (const key in data) {
325            const input = document.createElement('input');
326            input.type = 'hidden';  // 화면에 보이지 않는 hidden input
327            input.name = key;  // input의 name 속성 설정
328            input.value = data[key];
328  // input의 값 설정
329            form.appendChild(input);  // form에 추가
330        }
331    }
332
333    // 3. form을 body에 추가한 후 자동 제출
334    document.body.appendChild(form);
335    form.submit();
336}
337
338
339// 자주사용하는 fetch기능을 묶어서 템플릿화한 function
340/**
341 * HTTP 요청을 수행하는 `fetch` 래퍼 함수
342 *
343 * @param {string} url - 요청을 보낼 URL
344 * @param {Object} [data={}] - 전송할 데이터 (GET 요청의 경우 쿼리 스트링으로 변환됨)
345 * @param {Object} [options={}] - 추가 옵션 객체
346 * @param {string} [options.method="POST"] - HTTP 요청 메서드 ("GET", "POST", "PUT", "DELETE" 등)
347 * @param {Object} [options.headers={}] - 추가 요청 헤더 (기본적으로 JSON 헤더 포함)
348 * @param {AbortSignal} [options.signal=null] - 요청 취소를 위한 AbortController 신호
349 * @param {boolean} [options.isShowLoading=true] - 요청 중 로딩 UI 표시 여부
350 * @returns {Promise<Object|string>} - 응답 데이터를 JSON 또는 텍스트 형태로 반환
351 */
352async function fetchWrapper(url, data = {}, { method = "POST", headers = {}, signal = null, isShowLoading = true } = {}) {
353    method = method.toUpperCase(); // HTTP 메서드를 대문자로 변환 ("post" -> "POST")
354
355    // 기본 헤더 정의 (JSON 요청을 기본으로 설정)
356    const defaultHeaders = {
357        "Content-Type": "application/json",
358        "Accept": "application/json",
359    };
360
361    // 사용자 정의 헤더와 기본 헤더 병합
362    const mergedHeaders = Object.assign({}, defaultHeaders, headers);
363    //console.log(mergedHeaders);
364
365    // signal 기본값 세팅
366    if(signal === null){
367        const controller = new AbortController(); // 기본값
368        signal = controller.signal;
369
370        // 새로고침/탭 닫기/페이지 이동 시 요청 중단
371        window.addEventListener("beforeunload", () => {
372            console.log("beforeunload 페이지 떠남 감지 - > controller.abort()");
373            controller.abort("페이지 떠남");
374        });
375    }
376
377    // 요청 옵션 객체 구성
378    const options = {
379        method,
380        headers: mergedHeaders,
381        signal : signal,
382    };
383
384    /*
385     * mode, cache, credentials, redirect, referrerPolicy는 기본값을 사용
386     *
387     * 기본값:
388     * mode: "cors"
389     * cache: "default"
390     * credentials: "same-origin"
391     * redirect: "follow"
392     * referrerPolicy: "strict-origin-when-cross-origin"
393     */
394
395    // GET 요청일 경우, data를 쿼리스트링으로 변환하여 URL에 추가
396    if (method === "GET" && Object.keys(data).length > 0) {
397        url += `?${new URLSearchParams(data).toString()}`;
398    }
399    // GET이 아닌 경우, 요청 body에 JSON 데이터를 포함
400    else if (method !== "GET" && data !== null && Object.keys(data).length > 0) {
401        options.body = JSON.stringify(data);
402    }
403
404    try {
405        //console.log(`🚀 Fetching: ${url} [${method}]`); // 요청 정보 콘솔에 출력
406
407        if (isShowLoading) showLoading(); // 요청 중 로딩 표시
408        const response = await fetch(url, options); // fetch API로 요청 실행
409        if (isShowLoading) showLoading(false); // 요청 종료 후 로딩 제거
410
411        // 세션이 해제되었을 경우 (401 Unauthorized)
412        if (!response.ok && response.status === 401) {
413            const contentType = response.headers.get("content-type");
414
415            // 응답이 JSON 형식이면, 메시지를 가져와 경고창 표시 후 로그인 페이지로 이동
416            if (contentType && contentType.includes("application/json")) {
417                const errorJson = await response.json();
418                alert(errorJson.message);
419                location.href = '/'; // 로그인 페이지로 이동
420                return;
421            }
422
423            // 기본적인 에러 처리
424            alert("에러가 발생했습니다.");
425            location.href = '/';
426            return;
427        }
428
429        // 응답 Content-Type 확인 후 JSON 또는 텍스트로 변환
430        const contentType = response.headers.get("content-type");
431        if (contentType && contentType.includes("application/json")) {
432            return await response.json(); // JSON 응답 반환
433        } else {
434            return await response.text(); // 텍스트 응답 반환
435        }
436
437    } catch (error) {
438        if (isShowLoading) showLoading(false);
439        let message = error.message;
440
441        console.error("Fetch error:", error);
442        console.error("fetchWrapper - error - error.message : ", error.message);
443        console.error("fetchWrapper - error - error.name : ", error.name);
444        console.error("fetchWrapper - error - isUnloading : ", isUnloading);
445
446        if (isUnloading) {
447            message = "페이지를 벗어나면서 요청이 취소되었습니다.";
448        } else if(error.name === 'AbortError'){
449            message = "요청이 중단되었습니다."
450        } else if (message === "Failed to fetch") {
451            message = "서버 응답 실패. 네트워크 상태를 확인해주세요.";
452            //return { status: "error"};
453        }
454
455        return { status: "suceess", message: "" }; // 오류 발생 시 에러 객체 반환
456    }
457}
458
459/*
460// signal 사용방법
461const controller = new AbortController();
462const signal = controller.signal;
463
464setTimeout(() => controller.abort(), 1000);
464 // 1초 후 요청 취소
465
466fetchWrapper("/api/search", { params: { query: "Alice" }, signal })
467    .then(data => console.log("검색 결과:", data))
468    .catch(error => console.error("요청 취소됨:", error.message));
469*/
470
471/**
472 * 서버에 파일을 업로드하는 함수
473 *
474 * @param {File[]} fileList - 업로드할 파일 배열
475 * @param {string | null} [fileId=null] - 파일을 식별할 ID (선택적)
476 * @returns {Promise<Object | null>} - 업로드된 파일 정보 (성공 시), 실패 시 `null`
477 */
478async function fileUpload(fileList, fileId = null) {
479
480    const formData = new FormData();
481
482    // 파일 목록을 FormData에 추가
483    for (const file of fileList) {
484        formData.append('fileList', file);
485    }
486
487    // fileId가 존재하면 FormData에 추가
488    if (!isNull(fileId)) formData.append("fileId", fileId);
489
490    const options = {
491        method: "POST",
492        body: formData,
493        headers: {
494            "Accept": "application/json",
495        }
496    };
497
498    // 서버에 파일 업로드 요청
499    const response = await fetch("/files/upload", options);
500
501    // 요청이 실패한 경우 null 반환
502    if (!response.ok) return null;
503
504    const data = await response.json();
505
506    // 업로드 결과가 실패한 경우 null 반환
507    if (!data.success) return null;
508
509    return data.data; // 업로드된 파일 데이터 반환
510}
511
512/**
513 * 로딩 화면을 표시 또는 숨기는 함수
514 * @param {boolean} [bool=true] - `true`이면 로딩 화면 표시, `false`이면 숨김
515 */
516function showLoading(bool = true) {
517    const loadingOverlay = document.querySelector("#loadingOverlay");
518    if (loadingOverlay) {
519        loadingOverlay.style.display = bool ? "block" : "none";
520    }
521}
522
523/**
524 * 특정 HTML 요소를 숨기는 함수
525 * @param {HTMLElement} el - 숨길 대상 요소
526 */
527function hideElement(el) {
528    el.style.display = "none";
529}
530
531/**
532 * 특정 HTML 요소를 표시하는 함수
533 * @param {HTMLElement} el - 표시할 대상 요소
534 */
535function showElement(el) {
536    el.style.display = "block";
537}
538
539/**
540 * input태그의 file을 validate하는 함수
541 * @param inputElement - input el요소
542 * @param isCheckImage - check할 것이 이미지인지 아닌지 여부
543 */
544function validateFile(inputElement, isCheckImage = false) {
545    const file = inputElement.files[0];
546    if (!file) return;
547
548    // 허용된 확장자 & MIME 타입
549    const allowedExtensions = fileProperties.allowExtensions;
550    const allowExtensionsImages = fileProperties.allowExtensionsImages;
551    const allowedMimeTypes = fileProperties.allowMimeTypes;
552    /*
553    // 허용된 확장자 & MIME 타입
554    const allowedExtensions = [".doc", ".docx", ".pdf", ".txt", ".hwp", ".hwpx",
555                               ".xls", ".xlsx", ".csv",
556                               ".gif", ".jpg", ".jpeg", ".png", ".bmp", ".webp", ".tiff"];
557
558    const allowedMimeTypes = ["application/msword", "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
559                              "application/pdf", "text/plain", "application/x-hwp",
560                              "application/vnd.ms-excel", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
561                              "image/gif", "image/jpeg", "image/png", "image/bmp", "image/webp", "image/tiff", "text/csv"];
562    // MIME 타입 검사, MIME 검사는 backend에서
563    const isValidMimeType = allowedMimeTypes.includes(file.type);
564    * */
565
566    // 확장자 검사
567    const fileName = file.name.toLowerCase();
568
569    // 이미지만 체크하는지 여부에 따라 허용확장자가 달라짐.
570    const isValidExtension = isCheckImage ? allowExtensionsImages.some(ext => fileName.endsWith(ext)) : allowedExtensions.some(ext => fileName.endsWith(ext));
571
572    // MIME 타입 검사
573    if (!isValidExtension) {
574        alert('허용되지 않은 파일 형식입니다!');
575        inputElement.value = ""; // 입력 필드 초기화
576    }
577    return isValidExtension;
578}
579
580
581/**
582 * iframe을 resize하는 함수
583 * @param iframe - iframe 객체
584 * @param {Object} [options={}] - 추가 옵션 객체
585 */
586function resizeIframe(iframe, { minHeight = 400, maxHeight = 1600 }={}) {
587
588  if (!iframe.contentWindow || !iframe.contentWindow.document.body) return;
589
590  const scrollHeight = iframe.contentWindow.document.body.scrollHeight + 100;
591  const adjustedHeight = Math.max(minHeight, Math.min(scrollHeight, maxHeight));
592  iframe.style.height = adjustedHeight + 'px';
593}
594
595function getMobileOS() {
596  const userAgent = navigator.userAgent || navigator.vendor || window.opera;
597
598  if (/android/i.test(userAgent)) {
599    return "Android";
600  }
601
602  if (/iPad|iPhone|iPod/.test(userAgent) && !window.MSStream) {
603    return "iOS";
604  }
605
606  return "unknown";
607}
608
609
610/**
611 * 특정 조건이 true가 될 때까지 기다린다.
612 */
613function waitForTrue(getFlagFn, checkInterval = 100, timeout = 5000) {
614  return new Promise((resolve, reject) => {
615    const timer = setInterval(() => {
616      if (getFlagFn()) {
617        clearInterval(timer);
618        clearTimeout(timeoutTimer);
619        resolve();
620      }
621    }, checkInterval);
622
623    const timeoutTimer = setTimeout(() => {
624      clearInterval(timer);
625      reject(new Error('Timeout: 조건이 true가 되지 않았습니다.'));
626    }, timeout);
627  });
628}
629/**
630 * like 검색 자음 모음
631 */
632function getChoseong(text) {
633    const CHO = ["ㄱ","ㄲ","ㄴ","ㄷ","ㄸ","ㄹ","ㅁ","ㅂ","ㅃ","ㅅ","ㅆ","ㅇ","ㅈ","ㅉ","ㅊ","ㅋ","ㅌ","ㅍ","ㅎ"];
634    let result = "";
635
636    for (let i = 0; i < text.length; i++) {
637        const code = text.charCodeAt(i) - 44032;
638        if (code >= 0 && code <= 11171) {
639            const choIndex = Math.floor(code / 588);
640            result += CHO[choIndex];
641        } else {
642            result += text[i];  // 한글 아닌 경우는 그대로
643        }
644    }
645    return result;
646}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.