PageSourceSearch

https://ssonda.ai/assets/js/pages/site/my-info.js;jsessionid=3F248D56BE6DAB1154E1FCC0E706408B?v=1790258051475

js ssonda.ai collected 2026-09-24 13:55:42 UTC 34,508 bytes, 931 lines download raw bytes

1// DOM 로드 후 실행
2document.addEventListener("DOMContentLoaded", () => {
3    // let searchInput = document.querySelector('input[name=search]');
4
5    // 비밀번호 변경 시 이벤트 처리
6    document.querySelector("form#updatePassword")?.addEventListener("submit", async (e) => {
7        e.preventDefault();
8        e.stopImmediatePropagation();
9
10        updatePassword(e.target);
11    });
12
13    // 공지사항 화면 호출 시 이벤트 처리
14    if (document.querySelector("#noticeList")) {
15        let noticeType = "notice";
16
17        getNoticeList();
18
19        document.querySelector("#searchFormAjax")?.addEventListener("submit", async (e) => {
20            e.preventDefault();
21            e.stopImmediatePropagation();
22
23            // if (searchInput?.value != '' && searchInput?.value.length < 2) {
24            //   searchInput?.focus();
25            //   toastr.error('검색어는 2글자 이상 입력해 주세요.');
26            //   return;
27            // }
28
29            // getNoticeList(noticeType, 1, searchInput?.value);
30            getNoticeList(noticeType, 1);
31        });
32
33        document.querySelector("#noticeType")?.addEventListener("click", (e) => {
34            e.preventDefault();
35            e.stopImmediatePropagation();
36
37            let targetEl = e.target.closest(".nav-link");
38            if (targetEl.classList.contains("active")) {
39                return;
40            }
41
42            targetEl.closest(".nav").querySelector(".nav-link.active").classList.remove("active");
43            targetEl.classList.add("active");
44
45            noticeType = targetEl.dataset.type;
46
47            // searchInput.value = '';
48
49            getNoticeList(noticeType);
50        });
51    }
52
53    // FAQ 화면 호출 시 이벤트 처리
54    if (document.querySelector("#faqList")) {
55        let faqType = document.querySelector("#faqType")?.dataset?.selectedType ?? "faq";
56
57        if (faqType == "faq") {
58            getFaqList();
59        } else {
60            getQnaList();
61        }
62
63        document.querySelector("#searchFormAjax")?.addEventListener("submit", async (e) => {
64            e.preventDefault();
65            e.stopImmediatePropagation();
66
67            if (searchInput.value != "" && searchInput.value.length < 2) {
68                searchInput.focus();
69                toastr.error("검색어는 2글자 이상 입력해 주세요.");
70                return;
71            }
72
73            getFaqList(faqType, 1, searchInput.value);
74        });
75    }
76
77    // 정책 및 약관 화면 호출 시 이벤트 처리
78    if (document.querySelector("#termsList")) {
79        let termsType = "terms";
80
81        getTermsList();
82
83        document.querySelector("#searchFormAjax")?.addEventListener("submit", async (e) => {
84            e.preventDefault();
85            e.stopImmediatePropagation();
86
87            if (searchInput.value != "" && searchInput.value.length < 2) {
88                searchInput.focus();
89                toastr.error("검색어는 2글자 이상 입력해 주세요.");
90                return;
91            }
92
93            getTermsList(termsType, 1, searchInput.value);
94        });
95    }
96
97    // 정책 및 약관 화면 호출 시 이벤트 처리
98    if (document.querySelector("#qnaList")) {
99        getQnaList();
100
101        document.querySelector("#searchFormAjax")?.addEventListener("submit", async (e) => {
102            e.preventDefault();
103            e.stopImmediatePropagation();
104
105            if (searchInput.value != "" && searchInput.value.length < 2) {
106                searchInput.focus();
107                toastr.error("검색어는 2글자 이상 입력해 주세요.");
108                return;
109            }
110
111            getTermsList(1, searchInput.value);
112        });
113    }
114
115    if (document.querySelector("form[name=qna_add]")) {
116        let form = document.querySelector("form[name=qna_add]");
117
118        form.addEventListener("submit", async (e) => {
119            e.preventDefault();
120            e.stopImmediatePropagation();
121
122            onSubmitQna(form);
123        });
124    }
125
126    let userEditForm = document.querySelector("form[name=user_edit]");
127    if (userEditForm) {
128        userEditForm.querySelector(".id-verify")?.addEventListener("click", async (e) => {
129            e.preventDefault();
130            e.stopImmediatePropagation();
131
132            idVerify();
133        });
134
135        // 내 정보 변경 시 이벤트 처리
136        userEditForm?.addEventListener("submit", async (e) => {
137            e.preventDefault();
138
139            let emailInput = userEditForm.querySelector("input[name=email]");
140            if (!(await validateSingleField(emailInput))) {
141                return;
142            }
143
144            let phoneInput = userEditForm.querySelector("input[name=phone_number]");
145            if (phoneInput.dataset.original != phoneInput.value) {
146                if (!(await validateSingleField(phoneInput))) {
147                    return;
148                }
149
150                if (!window.idVerifyResult) {
151                    toastr.error("휴대전화번호 변경을 위한 본인인증이 필요합니다.");
152                    return;
153                }
154            }
155
156            submit(userEditForm, {
157                onComplete: (data) => {
158                    // 임시 저장된 휴대전화번호 필드 삭제 및 본인인증 결과 초기화
159                    userEditForm.querySelector("input[name=phone]")?.remove();
160                    window.idVerifyResult = false;
161
162                    if (data.code == RESULT_CODE_SUCCESS || data.code == 0) {
163                        toastr.success(i18n.t('msg.saveSuccess'));
164                    } else {
165                        errorHandling(data.code);
166                        return;
167                    }
168
169                    setTimeout(() => {
170                        history.back();
171                    }, 1000);
172                },
173            });
174        });
175    }
176
177    if (document.querySelector("#qnaDetailSection")) {
178        getQnaDetail();
179    }
180
181    if (document.querySelector("#noticeDetailSection")) {
182        postNoticeDetail();
183    }
184});
185
186// 공지사항 조회
187async function getNoticeList(type, page, search, lang) {
188    let sendHttp = new SendHttp("/post/listJson");
189    let formData = new FormData();
190    formData.append("type", type ?? "notice");
191    formData.append("page", page ?? 1);
192    formData.append("search", search ?? "");
193    formData.append("lang", lang ?? "ko");
194    formData.append("targetList", "all");
195    formData.append("targetList", "ceo");
196    formData.append("allItem", "1");
197
198    sendHttp.data = Array.from(formData.entries());
199
200    JsLoadingOverlay.setOptions({ containerID: "noticeListSection", overlayOpacity: 0.0 });
201    JsLoadingOverlay.show();
202    let resultMap = await sendHttp.send();
203    JsLoadingOverlay.hide();
204
205    if (resultMap != null) {
206        // 리스트
207        let list = resultMap.list;
208        let listContainer = document.querySelector("#noticeList");
209
210        // document.querySelector('.total-count').innerText = numberWithCommas(list.length);
211
212        listContainer.innerHTML = "";
213        if (list.length == 0) {
214            listContainer.innerHTML = `
215        <div class="my-6 text-center text-gray-400 fs-base min-h-300px d-flex align-items-center justify-content-center">등록된 공지사항이 없습니다.</div>
216      `;
217        } else {
218            listContainer.classList.remove("d-none");
219            list.forEach((item) => {
220                let listEl = document.createElement("div");
221                listEl.classList.add("list-group-item", "list-group-item-action", "w-auto", "d-flex", "justify-content-between", "align-items-center", "rounded-4", "border-0", "cursor-pointer", "mx-n3");
222                listEl.dataset.id = item.id;
223
224                listEl.innerHTML = `
225          <div class="d-flex flex-column py-2">
226            <div class="fs-base fw-semibold">${item.title ?? "-"}</div>
227            <div class="fs-7 text-gray-600">${formatDateTime(item.created, "dayTimeMin")}</div>
228          </div>
229        `;
230
231                listEl.addEventListener("click", async (e) => {
232                    e.preventDefault();
233                    e.stopImmediatePropagation();
234
235                    const id = e.currentTarget.dataset.id;
236                    location.href = `/post/detail?id=${id}`;
237                });
238
239                listContainer.append(listEl);
240            });
241        }
242    }
243}
244
245// FAQ 조회
246async function getFaqList(type, page, search, lang) {
247    let sendHttp = new SendHttp("/post/listJson");
248
249    let formData = new FormData();
250    formData.append("type", type ?? "faq");
251    formData.append("page", page ?? 1);
252    formData.append("search", search ?? "");
253    formData.append("lang", lang ?? "ko");
254    formData.append("targetList", "all");
255    formData.append("targetList", "ceo");
256    formData.append("allItem", "1");
257
258    sendHttp.data = Array.from(formData.entries());
259
260    JsLoadingOverlay.setOptions({ containerID: "faqListSection", overlayOpacity: 0.0 });
261    JsLoadingOverlay.show();
262    let resultMap = await sendHttp.send();
263    JsLoadingOverlay.hide();
264
265    if (document.querySelector('#supportBanner')) {
266        document.querySelector('#supportBanner').classList.remove('d-none');
267    }
268
269    if (resultMap != null) {
270        // 리스트
271        let list = resultMap.list;
272        let listContainer = document.querySelector("#faqList");
273
274        // document.querySelector('.total-count').innerText = numberWithCommas(list.length);
275
276        listContainer.innerHTML = "";
277        if (list.length == 0) {
278            listContainer.innerHTML = `
279                <div class="my-6 text-center text-gray-400 fs-base min-h-300px d-flex align-items-center justify-content-center">등록된 FAQ가 없습니다.</div>
280            `;
281        } else {
282            listContainer.classList.remove("d-none");
283            list.forEach((item) => {
284                let listEl = document.createElement("div");
285                listEl.dataset.id = item.id;
286                listEl.classList.add('mx-n3');
287
288                let fileList = item.fileList;
289                let fileHtml = '';
290                if (fileList.length > 0) {
291                    fileList.forEach((file) => {
292                        fileHtml += `<img src="${resultMap.filePath}/${file.name}" alt="${file.name}" class="img-fluid">`;
293                    });
294                }
295
296                listEl.innerHTML = `
297                    <div class="faq-list-item bg-hover-light-dark text-hover-primary curs
297or-pointer d-flex align-items-baseline gap-3 p-4 rounded-4 w-100 rotate collapsible collapsed" data-bs-toggle="collapse" href="#faq${item.id}">
298                        <div class="d-flex align-items-baseline gap-3">
299                            <span class="fs-4 fw-bold text-brand-1-dark">Q</span>
300                            <div class="flex-fill">
301                                <span class="fw-semibold">${item.title}</span>
302                                <div id="faq${item.id}" class="collapse mt-5">
303                                    <div class="d-flex flex-column gap-3">
304                                        <div class="img-list ${fileList.length > 0 ? 'd-flex' : 'd-none'} flex-column gap-3">${fileHtml}</div>
305                                        <div class="text-dark contents">${item.contents}</div>
306                                    </div>
307                                </div>
308                            </div>
309                        </div>
310                        <i class="fa-solid fa-chevron-down rotate-n180 ms-auto"></i>
311                    </div>
312                `;
313
314                listContainer.append(listEl);
315            });
316        }
317    }
318}
319
320// FAQ 상세 조회
321async function getFaqDetail(id) {
322    let sendHttp = new SendHttp("/post/get");
323    sendHttp.isStoreData = true;
324    sendHttp.data = {
325        id: id,
326        bGetDetail: true,
327    };
328    
329    let resultMap = await sendHttp.send();
330    
331    if (resultMap == null || resultMap.code != RESULT_CODE_SUCCESS) {
332        return;
333    }
334    
335    let filePath = resultMap.filePath;
336
337    let faqItem = document.querySelector(`#faq${id}`);
338    let imgListEl = faqItem.querySelector(".img-list");
339    let imgList = resultMap.item.fileList;
340
341    if (imgList.length > 0) {
342        imgListEl.classList.remove("d-none");
343        imgListEl.classList.add("d-flex");
344        imgListEl.innerHTML = "";
345        imgList.forEach((img) => {
346            imgListEl.insertAdjacentHTML("beforeend", `<img src="${filePath}/${img.name}" alt="${img.name}" class="img-fluid">`);
347        });
348    }
349}
350
351// 정책 및 약관 조회
352async function getTermsList(type, page, search, lang) {
353    let sendHttp = new SendHttp("/post/listJson");
354    sendHttp.data = {
355        type: type ?? "terms",
356        page: page ?? 1,
357        search: search ?? "",
358        lang: lang ?? "ko",
359        allItem: "1",
360    };
361
362    JsLoadingOverlay.show();
363    let resultMap = await sendHttp.send();
364    JsLoadingOverlay.hide();
365
366    if (resultMap != null) {
367        // 리스트
368        let list = resultMap.list;
369        let listContainer = document.querySelector("#termsList");
370
371        // document.querySelector('.total-count').innerText = numberWithCommas(list.length);
372
373        listContainer.innerHTML = "";
374        if (list.length == 0) {
375            listContainer.innerHTML = `
376        <div class="my-6 text-center text-gray-400 fs-base min-h-300px d-flex align-items-center justify-content-center">등록된 약관 및 정책이 없습니다.</div>
377      `;
378        } else {
379            listContainer.classList.remove("d-none");
380            list.forEach((item) => {
381                let listEl = document.createElement("div");
382                listEl.classList.add("list-group-item", "list-group-item-action", "w-auto", "d-flex", "justify-content-between", "align-items-center", "rounded-4", "border-0", "cursor-pointer", "mx-n3");
383                listEl.dataset.id = item.id;
384
385                listEl.innerHTML = `
386          <div class="d-flex flex-column py-2">
387            <div class="fs-base fw-semibold">${item.title ?? "-"}</div>
388          </div>
389        `;
390
391                listEl.addEventListener("click", async (e) => {
392                    e.preventDefault();
393                    e.stopImmediatePropagation();
394
395                    postDetail(e.currentTarget.dataset.id);
396                });
397
398                listContainer.append(listEl);
399            });
400        }
401    }
402}
403
404// 1:1문의 조회
405async function getQnaList(page, search, lang) {
406    let sendHttp = new SendHttp("/qna/listJson");
407    sendHttp.data = {
408        page: page ?? 1,
409        search: search ?? "",
410        lang: lang ?? "ko",
411        allItem: "1",
412    };
413
414    JsLoadingOverlay.setOptions({ containerID: "qnaListSection", overlayOpacity: 0.0 });
415    JsLoadingOverlay.show();
416    let resultMap = await sendHttp.send();
417    JsLoadingOverlay.hide();
418
419    if (resultMap != null) {
420        // 리스트
421        let list = resultMap.list;
422        let listContainer = document.querySelector("#qnaList");
423
424        // document.querySelector('.total-count').innerText = numberWithCommas(list.length);
425
426        listContainer.innerHTML = "";
427        if (list.length == 0) {
428            listContainer.innerHTML = `
429        <div class="my-6 text-center text-gray-400 fs-base min-h-300px d-flex align-items-center justify-content-center">등록된 문의가 없습니다.</div>
430      `;
431        } else {
432            listContainer.classList.remove("d-none");
433            list.forEach((item) => {
434                let listEl = document.createElement("div");
435                listEl.classList.add("list-group-item", "list-group-item-action", "w-auto", "d-flex", "justify-content-between", "align-items-center", "rounded-4", "border-0", "cursor-pointer", "mx-n3");
436                listEl.dataset.id = item.id;
437
438                const badge = qnaStatusBadge(item.status, "sm");
439
440                listEl.innerHTML = `
441                    <div id="qnaListItem" class="d-flex flex-column py-2">
442                        <div>${badge}</div>
443                        <div class="fs-base fw-semibold mt-1 line-clamp-1 ignore-line-break">${item.contents ?? "-"}</div>
444                        <div class="fs-7 text-gray-600">${formatDateTime(item.created, "dayTimeMin")}</div>
445                    </div>
446                `;
447
448                listEl.addEventListener("click", async (e) => {
449                    e.preventDefault();
450                    e.stopImmediatePropagation();
451
452                    const id = e.currentTarget.dataset.id;
453                    location.href = `/qna/detail?id=${id}`;
454                });
455
456                listContainer.append(listEl);
457            });
458        }
459    }
460}
461
462async function postDetail(id) {
463    let sendHttp = new SendHttp("/post/get");
464    sendHttp.data = {
465        id: id,
466    };
467
468    JsLoadingOverlay.show();
469    let resultMap = await sendHttp.send();
470    JsLoadingOverlay.hide();
471
472    if (resultMap != null) {
473        let modal = new DynamicModal();
474        modal.setTitle(resultMap.item.title);
475        modal.setOptions({ scrollBody: true });   // 본문이 길면 모달 내부에서 스크롤
476        modal.removeConfirmButton();
477        modal.setCancelButton("닫기");
478
479        let imageList = "";
480        let filePath = resultMap.filePath;
481        if (resultMap.item.fileList != null && resultMap.item.fileList.length > 0) {
482            resultMap.item.fileList.forEach((file) => {
483                imageList += `
484                    <div class="text-center mb-3 mx-n6">
485                        <img src="${filePath}/${file.name}" class="img-fluid" onerror="this.src='/assets/images/image-error.png'">
486                    </div>
487                `;
488            });
489        }
490
491        // 약관 개정이력 섹션 (BE 에서 약관일 때만 동봉)
492        let historyList = resultMap.historyList || [];
493        let historyHtml = '';
494        if (historyList.length > 0) {
495            let rows = historyList.map((h) => `
496                <div>
497                    <a href="javascript:void(0)" class="term-history-item text-decoration-underline fs-7 text-gray-600" data-history-id="${h.id}">
498                        ${i18n.t('terms.history.title')} v${h.version} ${h.title} : ${formatDateTime(h.created, "yyyy. MM. dd")}
499                    </a>
500                </div>
501            `).join('');
502
503            historyHtml = `
504                <div class="mt-5 pt-3 border-top">${rows}</div>
505            `;
506        }
507
508        modal.setContents(`
509            ${imageList}
510            <div class="fs-base contents">${resultMap.item.contents}</div>
511            ${historyHtml}
512        `);
513
514        // 개정이력 행 클릭 → 해당 버전 본문을 스택 모달로 표시
515        if (historyList.length > 0) {
516            let baseTitle = resultMap.item.title;
517            modal.container.addEventListener('click', (e) => {
518                let btn = e.target.closest('.term-history-item');
519                if (!btn) return;
520                let historyId = parseInt(btn.dataset.historyId);
521                let history = historyList.find((h) => h.id === historyId);
522                if (!history) return;
523
524                let stackedModal = new DynamicModal(`${baseTitle} (v${history.version})`, '');
525                stackedModal.setOptions({ isStack: true });
526                stackedModal.container.querySelector('.modal-dialog').classList.add('modal-dialog-scrollable');
527                stackedModal.removeConfirmButton();
528                stackedModal.setCancelButton("닫기");
529                stackedModal.setContents(`
530                    <div class="w-100">
531                        <div style="white-space: pre-line">${(history.contents || '').trim()}</div>
532                    </div>
533                `);
534                stackedModal.show();
535            });
536        }
537
538        modal.show();
539    }
540}
541
542// 공지사항 상세보기
543async function postNoticeDetail() {
544    // 현재 페이지의 URL에서 쿼리 문자열 가져오기
545    const params = new URLSearchParams(window.location.search);
546    const id = params.get("id");
547
548    let sendHttp = new SendHttp("/post/get");
549    sendHttp.data = {
550        id: id,
551    };
552
553    let noticeDetailSection = document.querySelector("#noticeDetailSection .content-section");
554    let noticeDetailCard = document.getElementById("noticeDetailSection").closest(".card-body");
555    noticeDetailCard.id = "noticeDetailCard";
556
557    JsLoadingOverlay.show({ containerID: "noticeDetailCard", overlayOpacity: 0.0 });
558    let resultMap = await sendHttp.send();
559    JsLoadingOverlay.hide();
560
561    if (resultMap != null) {
562        let imageList = "";
563        let filePath = resultMap.filePath;
564
565        if (resultMap.item.fileList != null && resultMap.item.fileList.length > 0) {
566            resultMap.item.fileList.forEach((file) => {
567                imageList += `
568                    <div class="text-center mt-6 mx-n6">
569                        <img src="${filePath}/${file.name}" class="img-fluid" onerror="this.src='/assets/images/image-error.png'">
570                    </div>
571                `;
572            });
573        }
574
575        const noticeDetails = `
576            <h3 id="detailTitle" class="mb-0 fw-semibold">${resultMap.item.title}</h3>
577            <div class="mt-2 d-flex align-items-center">
578                <span class="text-gray-600 fs-base">${formatDateTime(resultMap.item.created, "dayTimeMin")}</span>
579            </div>
580            ${imageList}
581            <div class="mt-6 fs-base contents">${convertUrlToLink(resultMap.item.contents)}</div>
582        `;
583
584        noticeDetailSection.insertAdjacentHTML("afterbegin", noticeDetails);
585    }
586}
587
588// 1:1문의 상세보기
589async function getQnaDetail() {
590    // 현재 페이지의 URL에서 쿼리 문자열 가져오기
591    const params = new URLSearchParams(window.location.search);
592    const id = params.get("id");
593
594    let sendHttp = new SendHttp("/qna/get");
595    sendHttp.data = {
596        id: id,
597    };
598
599    let loaderContainer = document.getElementById("qnaDetailSection").closest(".card-body");
600    loaderContainer.id = "qnaDetailCard";
601
602    JsLoadingOverlay.show({ containerID: "qnaDetailCard", overlayOpacity: 0.0 });
603    let resultMap = await sendHttp.send();
604    JsLoadingOverlay.hide();
605
606    if (resultMap == null) {
607        return;
608    }
609
610    const recentItem = resultMap.recentItem; // 최신 문의
611    const filePath = resultMap.filePath;
612    // BE는 최신순(DESC)으로 내려줌 → 대화 흐름대로 시간순(ASC)으로 변환
613    const list = (resultMap.list || []).slice().reverse();
614    if (list.length === 0) {
615        return;
616    }
617    const origin = list[0]; // 원문 (첨부 정보가 채워진 list 항목)
618
619    const detailContainer = document.querySelector("#qnaDetailSection");
620
621    // 문의 유형(스레드 공통) + 현재 상태 뱃지
622    detailContainer.querySelector("#detailInfo").classList.remove("d-none");
623    detailContainer.querySelector("#detailStatus").textContent = getQnaTypeStr(origin.type);
624    document.getElementById("detailInfo").insertAdjacentHTML("afterbegin", qnaStatusBadge(origin.status, "xl"));
625
626    const thread = detailContainer.querySelector("#qnaReplyList");
627    thread.innerHTML = "";
628
629    // ── 원문 문의 본문 (박스 없이 상단) ──
630    const originBody = document.createElement("div");
631    originBody.classList.add("mt-4");
632    const originContents = document.createElement("div");
633    originContents.classList.add("contents");
634    originContents.textContent = origin.contents;
635    originBody.append(originContents);
636    originBody.insertAdjacentHTML("beforeend", buildQnaImages(origin.askFileList, filePath));
637    thread.append(originBody);
638
639    // ── 대화 스레드 (재문의/답변, bg-light 박스) ──
640    const convo = document.createElement("div");
641    convo.classList.add("d-flex", "flex-column", "gap-4", "mt-6");
642
643    // 스레드 최신 상태가 답변대기면 최신 라운드 답변은 숨김(이전 라운드 답변은 유지).
644    // 재문의로 원문 status가 0으로 리셋될 수 있어, 최신 라운드만 status 기준으로 가린다.
645    const threadAnswered = origin.status === "1"; // 1: 답변완료
646    const lastIdx = list.length - 1;
647    // 실제로 노출될 답변이 하나라도 있을 때만 안내 문구 표시
648    const hasVisibleAnswer = list.some((it, i) => it.answer && (threadAnswered || i !== lastIdx));
649    if (hasVisibleAnswer) {
650        convo.insertAdjacentHTML("beforeend", `<h3 class="fw-semibold mt-8 mb-1">문의에 대한 답변입니다</h3>`);
651    }
652
653    list.forEach((item, idx) => {
654        const isReinquiry = idx > 0; // 원문 질문은 위에 이미 표시
655        const isLatest = idx === list.length - 1;
656
657        // 재문의 질문 메시지
658        if (isReinquiry) {
659            const q = document.createElement("div");
660            // 사용자(재문의) 메시지는 배경·패딩 없이 평문 + ms-6 들여쓰기 (바로쏜다 답변만 박스)
661            q.classList.add("ms-6");
662            q.innerHTML = `
663                <div class="d-flex align-items-center gap-2">
664                    <span class="fw-semibold">${item.fullName ?? "-"}</span>
665                    <span class="text-gray-500 fs-8">${formatDateTime(item.created, "dayTimeMin")}</span>
666                </div>
667            `;
668            const qc = document.createElement("div");
669            qc.classList.add("mt-2", "contents");
670            qc.textContent = item.contents;
671            q.append(qc);
672            q.insertAdjacentHTML("beforeend", buildQnaThumbnails(item.askFileList, filePath));
673            convo.append(q);
674        }
675
676        // 최신 라운드가 답변대기면 그 라운드 답변은 숨기고, 답변완료된 라운드만 노출
677        const isPendingLatest = isLatest && !threadAnswered;
678        if (item.answer && !isPendingLatest) {
679            const a = document.createElement("div");
680            a.classList.add("bg-light", "rounded-4", "p-4");
681            a.innerHTML = `
682                <div class="d-flex align-items-center gap-2">
683                    <div class="symbol symbol-25px">
684                        <img src="/assets/images/app-icon.png" alt="바로쏜다" />
685                    </div>
686                    <span class="fw-semibold">바로쏜다</span>
687                    <span class="text-gray-500 fs-8">${formatDateTime(item.answerd, "dayTimeMin")}</span>
688                </div>
689            `;
690            const ac = document.createElement("div");
691            ac.classList.add("mt-2", "contents");
692            ac.innerHTML = convertUrlToLink(item.answer ?? "");
693            a.append(ac);
694            a.insertAdjacentHTML("beforeend", buildQnaThumbnails(item.answerFileList, filePath));
695            convo.append(a);
696        } else if (isPendingLatest) {
697            // 최신 문의가 답변대기면 안내 문구
698            convo.insertAdjacentHTML(
699                "beforeend",
700                `
701                <div class="fs-8 text-gray-600 mt-5">
702                    <p class="mb-0">업무시간 내 순차적으로 답변드려요. 1~3일 정도 걸려요</p>
703                </div>
704            `
705            );
706        }
707    });
708
709    thread.append(convo);
710
711    // 재문의 버튼: 스레드 최신 상태가 답변완료일 때만 노출 (BE 가드와 동일)
712    if (recentItem && threadAnswered) {
713        const retryBtn = document.getElementById("retryQnaBtn");
714        retryBtn.querySelector("a").href = `/qna/add?parentId=${origin.id}&type=${origin.type}`;
715        retryBtn.classList.remove("d-none");
716    }
717
718    window.refreshFsLightbox();
719}
720
721// 문의/답변 첨부 이미지 HTML 생성 (원문 본문용 — 풀폭 표시, fslightbox 그룹은 파일별)
722function buildQnaImages(fileList, filePath) {
723    if (fileList == null || fileList.length === 0) {
724        return "";
725    }
726
727    return fileList
728        .map(
729            (file) => `
730            <div class="text-center mt-6 mx-n6">
731                <a href="${filePath}/${file.name}" data-fslightbox="image_${file.name}">
732                    <img src="${filePath}/${file.name}" class="img-fluid" onerror="this.src='/assets/images/image-error.png'">
733                </a>
734            </div>
735        `
736        )
737        .join("");
738}
739
740// 대화 박스 안 첨부 이미지 (썸네일 — 박스 폭에 맞게)
741function buildQnaThumbnails(fileList, filePath) {
742    if (fileList == null || fileList.length === 0) {
743        return "";
744    }
745
746    return (
747        `<div class="d-flex gap-2 flex-wrap mt-3">` +
748        fileList
749            .map(
750                (file) => `
751                <a href="${filePath}/${file.name}" data-fslightbox="image_${file.name}">
752                    <img class="thumbnail thumbnail-lg rounded-4" src="${filePath}/${file.name}" onerror="this.src='/assets/images/image-error.png'">
753                </a>
754            `
755            )
756            .join("") +
757        `</div>`
758    );
759}
760
761async function onSubmitQna(form) {
762    // let titleInput = form.querySelector("input[name=title]");
763    let contentsInput = form.querySelector("textarea[name=contents]");
764
765    // if (!(await validateSingleField(titleInput))) {
766    //     titleInput.focus();
767    //     return;
768    // }
769
770    if (!(await validateSingleField(contentsInput))) {
771        contentsInput.focus();
772        return;
773    }
774
775    // 제출 시 에러 처리: 일일 한도 초과(W2158)는 모달 안내, 그 외는 기존 처리
776    await submit(form, {
777        onError: (data) => {
778            if (data.code === 'W2158') {
779                showQnaLimitModal(data.data?.limitCount);
780                return;
781            }
782            errorHandling(data.code, data.message);
783        },
784    });
785}
786
787async function idVerify() {
788    let phoneInput = document.querySelector("input[name=phone_number]");
789
790    if (!(await validateSingleField(phoneInput))) {
791        return;
792    }
793
794    // 전화번호 중복 확인
795    let chkUrl = "/validDuplicatePhone";
796    let sendHttp = new SendHttp(chkUrl);
797    sendHttp.data = {
798        phone: phoneInput.value,
799        role: 1,
800    };
801
802    JsLoadingOverlay.show();
803    let resultMap = await sendHttp.send();
804    JsLoadingOverlay.hide();
805
806    if (resultMap.code != RESULT_CODE_AVAILABLE) {
807        errorHandling(resultMap.code)
808        return false;
809    }
810
811    let popup = window.open(`/requestIdVerify?phone=${phoneInput.value}`, "_blank", "width=430,height=640,scrollbars=yes");
812    idVerifyRequested = true;
813}
814
815async function onCompleteIdVerify(resultMap) {
816    let phoneInput = document.querySelector("input[name=phone_number]");
817    let isVerified = true; // for test
818    // let isVerified = resultMap.phone == phoneInput.value.replaceAll('-', '');
819
820    if (isVerified) {
821        toastr.success("본인인증이 완료되었습니다.");
822        document.querySelector(".phone-message").innerHTML = '<span class="text-success fs-8">본인인증이 완료되었습니다.</span>';
823        let form = document.querySelector("form[name=user_edit]");
824        form.querySelector("input[name=phone]")?.remove();
825        form.insertAdjacentHTML("beforeend", `<input type="hidden" name="phone" value="${escapeHtml(phoneInput.value)}">`);
826        if (resultMap.telecom) {
827            form.querySelector("input[name=telecomProvider]")?.remove();
828            form.insertAdjacentHTML("beforeend", `<input type="hidden" name="telecomProvider" value="${escapeHtml(resultMap.telecom)}">`);
829        }
830        window.idVerifyResult = true;
831    } else {
832        toastr.error("입력하신 전화번호와 본인인증 시 입력한 전화번호가 일치하지 않습니다.");
833        phoneInput.focus();
834        return false;
835    }
836}
837
838window.addEventListener("message", (event) => {
839    // 보안 검증: 메시지를 보낸 도메인이 일치하는지 확인
840    // if (event.origin !== "https://popup-domain.com") return;
841
842    // 받은 데이터 처리
843    const data = event.data;
844    onCompleteIdVerify(data);
845});
846
847// 인증 결과화면이 opener 없이(새 탭 복귀 등) 열려 postMessage가 불가능할 때 localStorage로 남긴 결과 수신
848const ID_VERIFY_RESULT_KEY = "idVerifyResult";
849let idVerifyRequested = false;
850
851function consumeIdVerifyResult() {
852    if (!idVerifyRequested) {
853        return; // 이 화면에서 본인인증을 시작한 적 없으면 무시 (다른 화면의 잔여 결과 오인 방지)
854    }
855
856    let raw = localStorage.getItem(ID_VERIFY_RESULT_KEY);
857    if (raw == null) {
858        return;
859    }
860    localStorage.removeItem(ID_VERIFY_RESULT_KEY); // 중복 처리 방지를 위해 읽는 즉시 제거
861
862    try {
863        let stored = JSON.parse(raw);
864        let data = stored?.data;
865
866        // 5분 이내 결과만 유효 처리
867        if (Date.now() - stored.time < 5 * 60 * 1000 && data != null) {
868            onCompleteIdVerify(data);
869        }
870    } catch (e) {
871        // 파싱 불가한 결과는 무시
872    }
873}
874
875window.addEventListener("storage", (event) => {
876    if (event.key === ID_VERIFY_RESULT_KEY) {
877        consumeIdVerifyResult();
878    }
879});
880
881// 백그라운드 탭에는 storage 이벤트가 지연/누락될 수 있어(Safari 등) 탭 복귀 시에도 확인
882document.addEventListener("visibilitychange", () => {
883    if (!document.hidden) {
884        consumeIdVerifyResult();
885    }
886});
887
888// 비밀번호 변경
889async function updatePassword(form) {
890    let currentPasswordInput = form.querySelector("input[name=password]");
891    let passwordInput = form.querySelector("input[name=newPassword]");
892    let passwordConfirmInput = form.querySelector("input[name=passwordConfirm]");
893
894    if (validator == null || validator.form.name != form.name) {
895        validInit(form);
896    }
897
898    if (!(await validateSingleField(currentPasswordInput))) {
899        currentPasswordInput.focus();
900        return;
901    }
902
903    if (!(await validateSingleField(passwordInput))) {
904        passwordInput.focus();
905        return;
906    }
907
908    if (!(await validateSingleField(passwordConfirmInput))) {
909        passwordConfirmInput.focus();
910        return;
911    }
912
913    if (passwordInput.value != passwordConfirmInput.value) {
914        toastr.error("비밀번호가 일치하지 않습니다.");
915        passwordConfirmInput.focus();
916        return;
917    }
918
919    submit(form, {
920        onComplete: (data) => {
921            if (data.code == RESULT_CODE_SUCCESS || data.code == 0) {
922                toastr.success(i18n.t('msg.updateSuccess'));
923                setTimeout(() => {
924                    history.back();
925                }, 1000);
926            } else {
927                errorHandling(data.code);
928            }
929        },
930    });
931}

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.