This commit is contained in:
2026-01-30 17:20:52 +09:00
commit 21b6332c9c
459 changed files with 190743 additions and 0 deletions

406
kngil/js/adm.js Normal file
View File

@@ -0,0 +1,406 @@
import { w2grid, w2ui, w2popup, w2alert } from 'https://cdn.jsdelivr.net/gh/vitmalina/w2ui@master/dist/w2ui.es6.min.js'
import { setUserGridMode, loadData } from './adm_comp.js'
export function destroyGrid(name) {
if (w2ui[name]) {
w2ui[name].destroy()
}
}
/* ----------------------------------------
Super Admin Grid 생성 (상단)
---------------------------------------- */
export function createSuperAdminGrid(boxId) {
destroyGrid('superGrid')
userGrid
const grid = new w2grid({
name: 'superGrid',
box: boxId,
show: {
// toolbar: true,
footer: true,
// toolbarReload: true,
// toolbarColumns: true,
// toolbarSearch: true
},
// multiSearch: true,
// searches: [
// { field: 'member_id', text: 'ID', type: 'text' },
// { field: 'user_nm', text: '회원명', type: 'text' },
// { field: 'co_nm', text: '회사명', type: 'text' },
// {
// field: 'stat_bc',
// text: '회원상태',
// type: 'list',
// options: {
// items: [
// { id: 'Y', text: '사용' },
// { id: 'N', text: '미사용' }
// ]
// }
// }
// ],
columns: [
{ field: 'recid', text: '#', size: '50px', attr: 'align=center', editable: false, resizable: true, sortable: true },
{ field: 'member_id', text: 'ID', size: '90px', editable: false, resizable: true, sortable: true },
{ field: 'user_nm', text: '회원명', size: '90px', editable: false, resizable: true, sortable: true },
{ field: 'tel_no', text: '연락처', size: '120px', editable: {type : 'text'}, resizable: true, sortable: true },
{ field: 'email', text: 'E-mail', size: '180px', editable: {type : 'text'}, resizable: true, sortable: true },
{ field: 'co_nm', text: '회사명', size: '120px', editable: {type : 'text'}, resizable: true, sortable: true },
{
field: 'bs_no',
text: '사업자번호',
size: '120px',
editable: { type: 'text' },
resizable: true,
sortable: true
},
{ field: 'join_dt', text: '가입일', size: '100px', editable: false, resizable: true, sortable: true },
{ field: 'user_y', text: '사용자수', size: '80px', attr: 'align=right', editable: false, resizable: true, sortable: true },
{ field: 'buy_area', text: '제공면적', size: '100px', attr: 'align=right', editable: false, resizable: true, sortable: true },
{ field: 'use_area', text: '사용면적', size: '100px', attr: 'align=right', editable: false, resizable: true, sortable: true },
{ field: 'rem_area', text: '잔여면적', size: '100px', attr: 'align=right', editable: false, resizable: true, sortable: true },
{
field: 'stat_bc',
text: '회원상태',
size: '80px',
resizable: true,
sortable: true,
editable: false,
render(record) {
switch (record.stat_bc) {
case 'SA100100':
return '사용중'
case 'SA100200':
return '탈퇴'
default:
return record.stat_bc || ''
}
}
},
{ field: 'memo', text: '메모', size: '100px', attr: 'align=right', editable: {type : 'text'}, resizable: true, sortable: true },
],
records: [],
/* --------------------------------
상단 회사 클릭 → 하단 사용자 Grid 갱신
-------------------------------- */
onClick(event) {
if (!event.detail.recid) return
const record = this.get(event.detail.recid)
if (!record) return
// 하단 영역 표시
document.getElementById('detailCard').style.display = 'block'
// 하단 사용자 로드
// fetch(`/kngil/bbs/adm.php?action=user_list&member_id=${record.member_id}`)
// .then(res => res.json())
// .then(d => {
// if (d.status !== 'success') {
// w2alert('사용자 조회 실패')
// return
// }
// const g = w2ui.detailGrid
// g.clear()
// g.add(d.records || [])
// })
import('./adm_comp.js').then(m => {
m.loadUsersByMember(record.member_id)
})
},
onSelect(event) {
event.onComplete = () => {
const btn = document.getElementById('btnServiceRegister')
if (btn) btn.disabled = false
}
},
onUnselect(event) {
event.onComplete = () => {
const btn = document.getElementById('btnServiceRegister')
if (btn && !this.getSelection().length) {
btn.disabled = true
}
}
},
// onEditField(event) {
// if (event.field !== 'bs_no') return
// event.onComplete = () => {
// const input = event.input
// if (!input) return
// input.addEventListener('input', () => {
// let v = input.value.replace(/\D/g, '').slice(0, 10)
// if (v.length >= 6) {
// v = `${v.slice(0, 3)}-${v.slice(3, 5)}-${v.slice(5)}`
// } else if (v.length >= 4) {
// v = `${v.slice(0, 3)}-${v.slice(3)}`
// }
// input.value = v
// })
// }
// },
onChange(event) {
event.onComplete = function (ev) {
let rec = grid.get(ev.recid);
if (!rec) return;
let field = grid.columns[ev.column].field;
let val = ev.value_new;
/* ===============================
🔴 사업자번호(bs_no) 전용 처리
=============================== */
if (field === 'bs_no') {
// 숫자만 추출
let digits = String(val || '').replace(/\D/g, '');
// 🔥 입력 중에는 아무 것도 안 건드린다
if (digits.length < 10) {
return;
}
// 10자리 초과 방지
digits = digits.slice(0, 10);
// 최종 포맷
let formatted = `${digits.slice(0, 3)}-${digits.slice(3, 5)}-${digits.slice(5)}`;
// 🔥 여기서만 set
grid.set(ev.recid, { bs_no: formatted });
grid.refreshRow(ev.recid);
return;
}
/* ===============================
🔵 기존 공통 로직
=============================== */
if (typeof val === "object" && val !== null) {
val = val.text;
}
grid.set(ev.recid, { [field]: val });
if (field === 'quantity' || field === 'unit_price' || field === 'discount') {
let qty = parseInt(rec.quantity) || 0;
let unit = parseInt(rec.unit_price) || 0;
let dc = parseInt(rec.discount) || 0;
let total = Math.max(0, qty * unit - dc);
grid.set(ev.recid, { total_amount: total });
}
grid.refresh();
};
}
// onDblClick(event) {
// if (!event.detail.recid) return
// const record = this.get(event.detail.recid)
// if (!record) return
// import('./adm_service.js').then(m => {
// m.openServiceRegisterPopup({
// memberId: record.member_id,
// memberName: record.user_nm,
// company: record.co_nm,
// bizNo: record.bs_no
// })
// })
// }
})
loadCompanies()
return grid
}
//사업자 번호 포맷
function formatBizNo(value) {
const digits = String(value || '').replace(/\D/g, '');
if (digits.length !== 10) return null;
return `${digits.slice(0, 3)}-${digits.slice(3, 5)}-${digits.slice(5)}`;
}
/* ----------------------------------------
상단 회사 목록 로드
---------------------------------------- */
function loadCompanies() {
fetch('/kngil/bbs/adm.php')
.then(res => res.json())
.then(json => {
if (!json.records) return
w2ui.superGrid.clear()
w2ui.superGrid.add(json.records)
})
.catch(err => {
console.error('회사 목록 로드 실패:', err)
})
}
// =========================
// 서비스등록 버튼
// =========================
document.getElementById('btnServiceRegister')
.addEventListener('click', () => {
const g = w2ui.superGrid
if (!g) return
const sel = g.getSelection()
if (!sel.length) {
w2alert('서비스를 등록할 회원을 선택하세요.')
return
}
const r = g.get(sel[0])
// 신규 행 방지
if (!r.member_id || String(r.member_id).startsWith('new_')) {
w2alert('저장된 회원만 서비스 등록이 가능합니다.')
return
}
import('/kngil/js/adm_service.js').then(m => {
m.openServiceRegisterPopup({
memberId: r.member_id,
memberName: r.user_nm,
company: r.co_nm,
bizNo: r.bs_no
})
})
})
//저장
let isSaveBound = false
export function bindSaveButton() {
if (isSaveBound) return
isSaveBound = true
const btn = document.getElementById('btnSave')
if (!btn) return
btn.addEventListener('click', () => {
const g = w2ui.superGrid
if (!g) return
document.activeElement?.blur()
g.finishEditing?.()
const changes = g.getChanges()
changes.forEach(ch => {
// bs_no가 변경 대상이면
if ('bs_no' in ch) {
const formatted = formatBizNo(ch.bs_no);
if (!formatted) {
w2alert('사업자번호는 숫자 10자리여야 합니다.\n예: 123-45-67890');
throw new Error('invalid bs_no');
}
// 🔥 changes 객체를 직접 수정
ch.bs_no = formatted;
}
});
if (!changes.length) {
w2alert('변경된 내용이 없습니다.')
return
}
// 🔥 recid 기준으로 원본 record + 변경값 병합
const updates = changes.map(c => {
const r = g.get(c.recid)
return { ...r, ...c }
})
// 🔥 member_id 필수 체크
if (!updates[0]?.member_id) {
w2alert('member_id가 없습니다.')
return
}
fetch('/kngil/bbs/adm.php?action=save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
member_id: updates[0].member_id, // ✅ 핵심
updates
})
})
.then(res => res.json())
.then(json => {
if (json.status !== 'success') {
throw new Error(json.message)
}
updates.forEach(u => {
const r = g.get(u.recid);
if (r && u.bs_no) {
r.bs_no = u.bs_no; // 하이픈 포함 값
g.refreshRow(u.recid);
}
});
// g.mergeChanges()
// changes 강제 제거
g.changes = [];
// 수정 상태 플래그 제거
g.records.forEach(r => {
if (r.w2ui) delete r.w2ui.changes;
});
// 화면 다시 그리기
g.refresh();
w2alert('저장되었습니다.')
})
.catch(err => {
console.error(err)
w2alert(err.message || '저장 실패')
})
})
}
export function createDetailGrid(boxId) {
if (w2ui.detailGrid) w2ui.detailGrid.destroy()
new w2grid({
name: 'detailGrid',
box: boxId,
show: {
footer: true
},
columns: [
{ field: 'recid', text: '#', size: '50px' },
{ field: 'user_id', text: 'ID', size: '120px' },
{ field: 'user_nm', text: '이름', size: '120px' },
{ field: 'dept_nm', text: '부서', size: '120px' },
{ field: 'email', text: 'E-mail', size: '200px' },
{ field: 'use_yn', text: '사용', size: '80px' }
],
records: []
})
}

163
kngil/js/adm_common.js Normal file
View File

@@ -0,0 +1,163 @@
import { w2grid, w2ui, w2popup, w2alert , w2confirm} from 'https://cdn.jsdelivr.net/gh/vitmalina/w2ui@master/dist/w2ui.es6.min.js'
export function bindProductPopupEvents() {
const g = w2ui.productGrid
//행 추가
document.getElementById('prodAdd').onclick = () => {
g.add({
recid: g.records.length + 1,
name: '',
volume: 0,
price: 0,
use_yn: 'Y'
})
}
//행 삭제
document.getElementById('prodRemove').onclick = () => {
const sel = g.getSelection()
if (!sel.length) {
w2alert('삭제할 항목을 선택하세요.')
return
}
g.remove(...sel)
}
//저장
document.getElementById('prodSave').onclick = () => {
console.log('상품 저장', g.records)
w2alert('저장 처리 예정')
}
}
// [추가] 공통 함수
export function commonAdd(gridName, defaultData = {}) {
const grid = w2ui[gridName];
if (!grid) return;
// 중복되지 않는 recid 생성 (신규 행 식별용)
const newRecid = 'new_' + Date.now();
grid.add({
recid: newRecid,
is_new: true, // ★ 핵심: 이 플래그가 있어야 commonSave에서 검색됨
...defaultData // 아래 2번 단계에서 전달받은 초기값들이 여기에 들어감
});
grid.scrollIntoView(newRecid);
}
// [행 삭제] 공통 함수 (체크박스 선택 기준)
export function commonRemove(gridName) {
// 인자가 문자열이 아닌 객체로 들어오는지 확인용
if (typeof gridName !== 'string') {
console.error("그리드 이름은 반드시 문자열이어야 합니다. 전달된 값:", gridName);
return;
}
const grid = w2ui[gridName];
if (!grid) {
console.error(`그리드 '${gridName}'을 찾을 수 없습니다.`, Object.keys(w2ui));
return;
}
const sel = grid.getSelection();
if (sel.length === 0) {
w2alert('삭제할 항목을 선택하세요.');
return;
}
w2confirm(`${sel.length}건을 삭제하시겠습니까?`).yes(() => {
grid.remove(...sel);
});
}
// /js/adm_common.js
export async function commonRemoveWithParams(gridName, deleteUrl) {
const grid = w2ui[gridName];
// 1. 그리드 존재 확인 (문자열로 잘 들어왔는지 체크)
if (!grid) {
console.error(`[삭제실패] ${gridName} 그리드를 찾을 수 없습니다.`, Object.keys(w2ui));
return;
}
const sel = grid.getSelection();
if (sel.length === 0) {
w2alert('삭제할 항목을 선택하세요.');
return;
}
w2confirm(`${sel.length}건을 삭제하시겠습니까?`).yes(async () => {
try {
grid.lock('삭제 중...', true);
// 2. 프로시저에 필요한 데이터(member_id, sq_no) 추출
const selectedRows = sel.map(id => grid.get(id));
const response = await fetch(deleteUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ items: selectedRows }) // 통째로 전달
});
const responseText = await response.text(); // JSON 변환 전에 텍스트로 먼저 받음
console.log("서버 실제 응답 원문:", responseText); // 여기서 HTML 에러 내용 확인 가능
const result = await response.json();
if (result.status === 'success') {
grid.remove(...sel);
w2alert('성공적으로 삭제되었습니다.');
} else {
throw new Error(result.message);
}
} catch (e) {
w2alert('삭제 오류: ' + e.message);
console.error("서버 응답이 JSON이 아닙니다:", responseText);
w2alert("서버 오류가 발생했습니다. 콘솔을 확인하세요.");
} finally {
grid.unlock();
}
});
}
// [수정 및 저장] 공통 함수
export async function commonSave(gridName, saveUrl) {
const grid = w2ui[gridName];
if (!grid) {
console.error(`[저장 실패] '${gridName}' 그리드를 찾을 수 없습니다.`);
return;
}
grid.save(); // 에디터에서 입력 중인 값 확정
// 변경된 내용이 있는지 확인
if (grid.getChanges().length === 0) {
w2alert('변경사항이 없습니다.');
return;
}
try {
grid.lock('저장 중...', true);
const response = await fetch(saveUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ items: grid.records }) // 전체 데이터를 보낼지 changes만 보낼지 선택
});
const result = await response.json();
if (result.status === 'success') {
w2alert('저장되었습니다.');
grid.mergeChanges(); // 그리드의 변경 표시(빨간색) 제거
} else {
throw new Error(result.message);
}
} catch (e) {
w2alert('저장 오류: ' + e.message);
} finally {
grid.unlock();
}
}

506
kngil/js/adm_comp copy.js Normal file
View File

@@ -0,0 +1,506 @@
import { w2grid, w2ui, w2popup, w2alert, w2confirm } from 'https://cdn.jsdelivr.net/gh/vitmalina/w2ui@master/dist/w2ui.es6.min.js'
/* -------------------------------------------------
공통 유틸
------------------------------------------------- */
function destroyGrid(name) {
if (w2ui[name]) {
w2ui[name].destroy()
}
}
function loadBaseCode(mainCd) {
return fetch(`/kngil/bbs/adm_comp.php?action=base_code&main_cd=${mainCd}`)
.then(res => res.json())
.then(json => {
if (json.status !== 'success') {
throw new Error(json.message || '공통코드 로딩 실패')
}
return json.items
})
}
/* -------------------------------------------------
기업 관리자 페이지 Grid
------------------------------------------------- */
export async function createUserGrid(boxId, options = {}) {
// 🔥 DB에서 권한 코드 로딩
const authItems = await loadBaseCode('BS100')
const { loadSummary = true, memberId = null } = options
destroyGrid('userGrid')
const grid = new w2grid({
name: boxId === '#detailGrid' ? 'detailGrid' : 'userGrid',
box: boxId,
show: {
footer: true,
selectColumn: true,
},
columns: [
{ field: 'recid', text: '#', size: '50px', attr: 'align=center', editable: false, resizable: true, sortable: true },
{ field: 'user_id', text: 'ID', size: '90px', editable: {type : 'text'}, resizable: true, sortable: true },
{
field: 'user_pw',
text: 'PW',
size: '90px',
resizable: true,
sortable: false,
editable: {
type: 'password' // ✅ 입력 시 ●●●●
},
render() {
return '********' // ✅ 항상 마스킹
}
},
{ field: 'user_nm', text: '이름', size: '90px', editable: {type : 'text'}, resizable: true, sortable: true },
{ field: 'tel_no', text: '연락처', size: '120px', editable: {type : 'text'}, resizable: true, sortable: true },
{ field: 'email', text: 'E-mail', size: '180px', editable: {type : 'text'}, resizable: true, sortable: true },
{ field: 'dept_nm', text: '부서', size: '120px', editable: {type : 'text'}, resizable: true, sortable: true },
{
field: 'use_area',
text: '사용량(㎡)',
size: '120px',
attr: 'align=right',
editable: false,
resizable: true,
sortable: true,
render(record) {
const v = Number(record.use_area) || 0
return v.toLocaleString() // ✅ 1,000단위 콤마
}
},
{ field: 'reg_date', text: '등록일', size: '100px', editable: false, resizable: true, sortable: true },
{
field: 'use_yn',
text: '사용',
size: '80px',
attr: 'align=center',
resizable: true,
sortable: true,
editable: {
type: 'list',
items: [
{ id: 'Y', text: 'Y' },
{ id: 'N', text: 'N' }
]
},
render(record) {
return record.use_yn === 'Y' ? 'Y' : 'N'
}
},
/* ✅ 권한 콤보 (DB 연동) */
{
field: 'auth_bc',
text: '권한',
size: '120px',
editable: {
type: 'list',
items: authItems
},
render(record) {
const item = authItems.find(i => i.id === record.auth_bc)
return item ? item.text : record.auth_bc
}
},
{ field: 'rmks', text: '비고', size: '120px', editable: { type: 'text' }, resizable: true, sortable: true }
],
onEditField(event) {
const pwColIndex = this.getColumn('user_pw').index
// 🔥 PW 컬럼일 때만 처리
if (event.column !== pwColIndex) return
event.onComplete = function () {
// 🔥 현재 편집 세션의 input만 정확히 집기
const box = event.box
if (!box) return
const input = box.querySelector('input[type="password"]')
if (!input) return
// PW만 초기화
input.value = ''
input.placeholder = '변경 시에만 입력'
}
},
records: [],
})
loadData({
loadSummary,
memberId
})
}
function loadUsers() {
fetch('/kngil/bbs/adm_comp.php')
.then(res => res.text()) // 🔥 먼저 text로 확인
.then(text => {
try {
const json = JSON.parse(text)
w2ui.userGrid.clear()
w2ui.userGrid.add(json.records)
} catch (e) {
console.error('JSON 파싱 실패:', text)
}
})
}
export function loadUsersByMember(member_id) {
if (!member_id) return
// 🔥 실제 존재하는 grid 찾기
const g = w2ui.detailGrid || w2ui.userGrid
if (!g) {
console.error('사용자 grid가 없습니다')
return
}
fetch('/kngil/bbs/adm_comp.php')
.then(res => res.json())
.then(json => {
g.clear()
g.add(json.records || [])
})
.catch(err => {
console.error('사용자 로드 실패', err)
})
}
export function setUserGridMode(mode = 'view') {
const g = w2ui.userGrid
if (!g) return
if (mode === 'view') {
g.show.toolbar = false
g.show.selectColumn = false
g.show.toolbarSave = false
} else {
g.show.toolbar = true
g.show.selectColumn = true
g.show.toolbarSave = true
}
g.refresh()
}
export function loadData({ loadSummary = true } = {}) {
fetch('/kngil/bbs/adm_comp.php')
.then(res => res.json())
.then(async d => {
if (d.status !== 'success') {
w2alert('데이터 로딩 실패')
return
}
const records = d.records || []
const memberId = d.member_id // ⭐ 여기서 확정
if (loadSummary && memberId) {
const totalArea = await loadTotalArea(memberId)
renderSummaryFromRecords({
memberId,
records,
totalArea
})
}
const gridName = w2ui.detailGrid ? 'detailGrid' : 'userGrid'
w2ui[gridName].clear()
w2ui[gridName].add(records)
})
.catch(err => {
console.error(err)
w2alert('서버 통신 오류')
})
}
function renderSummaryFromRecords({ memberId, records, totalArea }) {
if (!records.length) return
const first = records[0]
const issuedCnt = Number(first.users_tot) || 0
const usedCnt = Number(first.users_y) || 0
const term = first.term || ''
// 사용 중 면적 합계
let usedArea = 0
records.forEach(r => {
if (r.use_yn === 'Y') {
usedArea += Number(r.use_area || 0)
}
})
const percent = totalArea > 0
? Math.floor((usedArea / totalArea) * 100)
: 0
/* -------- 화면 바인딩 -------- */
document.getElementById('memberId').textContent = memberId
document.getElementById('planName').textContent = 'Silver'
document.getElementById('dateRange').textContent = term
document.getElementById('issuedCnt').textContent = issuedCnt
document.getElementById('usedCnt').textContent = usedCnt
document.getElementById('usedArea').textContent =
usedArea.toLocaleString() + '㎡'
document.getElementById('totalArea').textContent =
totalArea.toLocaleString() + '㎡'
document.getElementById('usedBar').style.width = percent + '%'
document.getElementById('usedBar').textContent = percent + '%'
document.getElementById('remainBar').style.width =
(100 - percent) + '%'
}
// ===============================
// 저장 버튼
// ===============================
document.getElementById('btnSave_comp')?.addEventListener('click', () => {
// 현재 사용 중인 grid
const g = w2ui.detailGrid || w2ui.userGrid
if (!g) return
const changes = g.getChanges()
if (!changes.length) {
w2alert('변경된 내용이 없습니다.')
return
}
const inserts = []
const updates = []
changes.forEach(c => {
const rec = g.get(c.recid)
if (!rec) return
// 🔥 핵심: 원본 rec + 변경값 c 병합
const merged = { ...rec, ...c }
if (rec.__isNew) {
// INSERT → 전체 row 필요
inserts.push(merged)
} else {
// UPDATE → PK + 변경 컬럼
updates.push({
member_id : merged.member_id,
user_id : merged.user_id,
user_nm : merged.user_nm,
dept_nm : merged.dept_nm,
tel_no : merged.tel_no,
email : merged.email,
use_yn : merged.use_yn,
auth_bc : merged.auth_bc,
rmks : (merged.memo !== undefined ? merged.memo : '')
})
}
})
console.log('INSERTS', inserts)
console.log('UPDATES', updates)
fetch('/kngil/bbs/adm_comp.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'save',
member_id: g.records[0]?.member_id,
inserts,
updates
})
})
.then(res => res.text())
.then(text => {
try {
const json = JSON.parse(text)
if (json.status === 'success') {
w2alert('저장 완료')
loadUsersByMember(g.records[0].member_id)
} else {
w2alert(json.message || '저장 실패')
}
} catch (e) {
console.error(text)
w2alert('서버 응답 오류 (JSON 아님)')
}
})
})
//추가(insert)
document.getElementById('btnAdd')?.addEventListener('click', () => {
const g = w2ui.userGrid || w2ui.detailGrid
if (!g) return
// 신규 row용 recid (음수로 충돌 방지)
const newRecid = -Date.now()
g.add({
recid: newRecid,
__isNew: true, // ⭐ 신규 플래그
user_id: '',
user_pw: '',
user_nm: '',
tel_no: '',
email: '',
dept_nm: '',
use_area: 0,
use_yn: 'Y',
auth_bc: 'BS100400',
memo: ''
}, true)
g.select(newRecid)
g.scrollIntoView(newRecid)
})
//삭제
document.getElementById('btnDelete')?.addEventListener('click', () => {
const g = w2ui.detailGrid || w2ui.userGrid
if (!g) return
const sel = g.getSelection()
if (!sel.length) {
w2alert('삭제할 사용자를 선택하세요.')
return
}
// 선택된 user_id 수집
const ids = sel
.map(recid => {
const r = g.get(recid)
return r?.user_id
})
.filter(Boolean)
if (!ids.length) {
w2alert('삭제 가능한 항목이 없습니다.')
return
}
w2confirm(`선택한 ${ids.length}명의 사용자를 삭제하시겠습니까?`)
.yes(() => {
fetch('/kngil/bbs/adm_comp.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'delete',
member_id: g.records[0]?.member_id,
ids
})
})
.then(res => res.text())
.then(text => {
try {
const json = JSON.parse(text)
if (json.status === 'success') {
w2alert('삭제 완료')
loadUsersByMember(g.records[0].member_id)
} else {
w2alert(json.message || '삭제 실패')
}
} catch (e) {
console.error(text)
w2alert('서버 응답 오류 (JSON 아님)')
}
})
})
})
function loadTotalArea(memberId) {
return fetch(`/kngil/bbs/adm_comp.php?action=total_area&member_id=${memberId}`)
.then(res => res.json())
.then(json => {
if (json.status !== 'success') {
throw new Error('총 구매면적 로딩 실패')
}
return Number(json.total_area || 0)
})
}
function doSearch() {
const keyword = document.getElementById('schKeyword').value.trim()
const type = document.getElementById('schType').value
const useYn = document.getElementById('schUseYn').value
let p_user_nm = ''
let p_dept_nm = ''
// DB로 보낼 검색 조건
if (type === 'name') {
p_user_nm = keyword
} else if (type === 'dept') {
p_dept_nm = keyword
} else if (type === '') {
// 전체 검색 → 이름 OR 부서
p_user_nm = keyword
p_dept_nm = keyword
}
// ⚠️ type === 'id' 는 DB로 안 보냄
fetch(`/kngil/bbs/adm_comp.php?action=list`
+ `&user_nm=${encodeURIComponent(p_user_nm)}`
+ `&dept_nm=${encodeURIComponent(p_dept_nm)}`
+ `&use_yn=${useYn}`
)
.then(res => res.json())
.then(d => {
if (d.status !== 'success') {
w2alert('검색 실패')
return
}
let records = d.records || []
// 🔥 ID 검색은 프론트 필터
if (type === 'id' && keyword) {
records = records.filter(r =>
(r.user_id || '').toLowerCase()
.includes(keyword.toLowerCase())
)
}
const g = w2ui.detailGrid || w2ui.userGrid
g.clear()
g.add(records)
})
}
document.getElementById('btnSearch')
?.addEventListener('click', doSearch)
document.getElementById('schKeyword')
?.addEventListener('keydown', e => {
if (e.key === 'Enter') doSearch()
})

798
kngil/js/adm_comp.js Normal file
View File

@@ -0,0 +1,798 @@
import { w2grid, w2ui, w2popup, w2alert, w2confirm } from 'https://cdn.jsdelivr.net/gh/vitmalina/w2ui@master/dist/w2ui.es6.min.js'
let AUTH_ITEMS = []
let CURRENT_MEMBER_ID = null;
const USE_ITEMS = [
{ id: 'Y', text: '사용' },
{ id: 'N', text: '미사용' }
]
/* -------------------------------------------------
공통 유틸
------------------------------------------------- */
function getTargetMemberId() {
return document.getElementById('targetMemberId')?.value?.trim()
}
function destroyGrid(name) {
if (w2ui[name]) {
w2ui[name].destroy()
}
}
function loadBaseCode(mainCd) {
return fetch(`/kngil/bbs/adm_comp.php?action=base_code&main_cd=${mainCd}`)
.then(res => res.json())
.then(json => {
if (json.status !== 'success') {
throw new Error(json.message || '공통코드 로딩 실패')
}
return json.items
})
}
function normalizeAuth(records) {
return records.map(r => {
const item = AUTH_ITEMS.find(a => a.id === r.auth_bc)
return {
...r,
auth_bc: item || null //객체 복사
}
})
}
function normalizeUseYn(records) {
return records.map(r => {
const item = USE_ITEMS.find(u => u.id === r.use_yn)
return {
...r,
use_yn: item || null
}
})
}
/* -------------------------------------------------
기업 관리자 페이지 Grid
------------------------------------------------- */
export async function createUserGrid(boxId, options = {}) {
console.log('🔥 createUserGrid 호출됨:', boxId)
// 🔥 DB에서 권한 코드 로딩
AUTH_ITEMS = (await loadBaseCode('BS100')).map(r => ({
id: r.id,
text: r.text
}))
const { loadSummary = true, memberId = null } = options
destroyGrid('userGrid')
const grid = new w2grid({
name: boxId === '#detailGrid' ? 'detailGrid' : 'userGrid',
box: boxId,
show: {
footer: true,
lineNumbers: true,
selectColumn: true,
},
columns: [
// {
// field: 'recid',
// text: '#',
// size: '50px',
// attr: 'align=center',
// sortable: true,
// },
{ field: 'user_id', text: 'ID', size: '90px', editable: {type : 'text'}, resizable: true, sortable: true },
{
field: 'user_pw',
text: 'PW',
size: '90px',
resizable: true,
sortable: false,
editable: {
type: 'password' // ✅ 입력 시 ●●●●
},
render() {
return '********' // ✅ 항상 마스킹
}
},
{ field: 'user_nm', text: '이름', size: '90px', editable: {type : 'text'}, resizable: true, sortable: true },
{
field: 'tel_no',
text: '연락처',
size: '120px',
editable: { type: 'text' },
resizable: true,
sortable: true
},
{ field: 'email', text: 'E-mail', size: '180px', editable: {type : 'text'}, resizable: true, sortable: true },
{ field: 'dept_nm', text: '부서', size: '120px', editable: {type : 'text'}, resizable: true, sortable: true },
{
field: 'use_area',
text: '사용량(㎡)',
size: '120px',
attr: 'align=right',
editable: false,
resizable: true,
sortable: true,
render(record) {
const v = Number(record.use_area) || 0
return v.toLocaleString() // ✅ 1,000단위 콤마
}
},
{ field: 'cdt', text: '등록일', size: '100px', editable: false, resizable: true, sortable: true },
{
field: 'use_yn',
text: '사용',
size: '90px',
attr: 'align=center',
sortable: true,
editable: {
type: 'list',
items: USE_ITEMS,
showAll: true,
openOnFocus: true
},
render(record, extra) {
return extra?.value?.text || ''
}
},
/* ✅ 권한 콤보 (DB 연동) */
{
field: 'auth_bc',
text: '권한',
size: '120px',
sortable: true,
editable: {
type: 'list',
items: AUTH_ITEMS,
showAll: true,
openOnFocus: true
},
render(record, extra) {
return extra?.value?.text || ''
}
},
{ field: 'rmks', text: '비고', size: '120px', editable: { type: 'text' }, resizable: true, sortable: true }
],
onEditField(event) {
const pwColIndex = this.getColumn('user_pw').index
// 🔥 PW 컬럼일 때만 처리
if (event.column !== pwColIndex) return
event.onComplete = function () {
// 🔥 현재 편집 세션의 input만 정확히 집기
const box = event.box
if (!box) return
const input = box.querySelector('input[type="password"]')
if (!input) return
// PW만 초기화
input.value = ''
input.placeholder = '변경 시에만 입력'
}
},
onChange(event) {
event.onComplete = function (ev) {
const rec = grid.get(ev.recid);
if (!rec) return;
const field = grid.columns[ev.column].field;
let val = ev.value_new;
/* ===============================
📞 전화번호(tel_no) 자동 하이픈
=============================== */
if (field === 'tel_no') {
let digits = String(val || '').replace(/\D/g, '');
// 입력 중이면 건드리지 않음
if (digits.length < 9) return;
let formatted = digits;
if (digits.length === 11) {
formatted =
`${digits.slice(0,3)}-${digits.slice(3,7)}-${digits.slice(7)}`;
} else if (digits.length === 10) {
formatted =
`${digits.slice(0,3)}-${digits.slice(3,6)}-${digits.slice(6)}`;
}
// ✅ 화면 값
grid.set(ev.recid, { tel_no: formatted });
// ✅ 변경사항으로 "확정" (이게 핵심)
grid.mergeChanges(ev.recid, { tel_no: formatted });
grid.refreshRow(ev.recid);
return;
}
/* ===============================
공통 처리
=============================== */
if (typeof val === 'object' && val !== null) {
val = val.text;
}
grid.set(ev.recid, { [field]: val });
grid.mergeChanges(ev.recid, { [field]: val });
};
},
records: [],
})
loadData({
loadSummary,
memberId
})
}
export function loadUsersByMember(memberId) {
if (!memberId) return
const g = w2ui.detailGrid
if (!g) {
console.error('detailGrid 없음')
return
}
fetch(`/kngil/bbs/adm_comp.php?action=list&member_id=${memberId}`)
.then(res => res.json())
.then(d => {
if (d.status !== 'success') {
w2alert('사용자 조회 실패')
return
}
const records = normalizeAuth(d.records || [])
records = normalizeUseYn(records)
g.clear()
g.add(d.records)
})
.catch(err => console.error(err))
}
export function setUserGridMode(mode = 'view') {
const g = w2ui.userGrid
if (!g) return
if (mode === 'view') {
g.show.toolbar = false
g.show.selectColumn = false
g.show.toolbarSave = false
} else {
g.show.toolbar = true
g.show.selectColumn = true
g.show.toolbarSave = true
}
g.refresh()
}
export function loadData({ loadSummary = true } = {}) {
fetch('/kngil/bbs/adm_comp.php?action=list')
.then(res => res.json())
.then(async d => {
if (d.status !== 'success') return
let records = normalizeAuth(d.records || [])
records = normalizeUseYn(records)
const gridName = w2ui.detailGrid ? 'detailGrid' : 'userGrid'
w2ui[gridName].clear()
w2ui[gridName].add(records)
if (loadSummary && d.member_id) {
const totalArea = await loadTotalArea()
renderSummaryFromRecords({
memberId: d.member_id,
records,
totalArea
})
}
})
}
function renderSummaryFromRecords({ memberId, records, totalArea }) {
if (!records.length) return
const first = records[0]
const issuedCnt = Number(first.users_tot) || 0
const usedCnt = Number(first.users_y) || 0
const term = first.term || ''
// 사용 중 면적 합계
let usedArea = 0
records.forEach(r => {
if (r.use_yn?.id === 'Y') {
usedArea += Number(r.use_area || 0)
}
})
const percent = totalArea > 0
? Math.floor((usedArea / totalArea) * 100)
: 0
/* -------- 화면 바인딩 -------- */
document.getElementById('memberId').textContent = memberId
document.getElementById('planName').textContent = first.itm_nm || '-'
document.getElementById('dateRange').textContent = term
document.getElementById('issuedCnt').textContent = issuedCnt
document.getElementById('usedCnt').textContent = usedCnt
document.getElementById('usedArea').textContent =
usedArea.toLocaleString() + '㎡'
document.getElementById('totalArea').textContent =
totalArea.toLocaleString() + '㎡'
/* -------- 사용량 바 -------- */
const bar = document.getElementById('usedBar')
bar.style.width = percent + '%'
}
// ===============================
// 저장 버튼
// ===============================
document.getElementById('btnSave_comp')?.addEventListener('click', () => {
// 현재 사용 중인 grid
const g = w2ui.detailGrid || w2ui.userGrid
if (!g) return
const changes = g.getChanges()
if (!changes.length) {
w2alert('변경된 내용이 없습니다.')
return
}
const inserts = []
const updates = []
changes.forEach(c => {
const rec = g.get(c.recid)
if (!rec) return
// 🔥 핵심: 원본 rec + 변경값 c 병합
const merged = { ...rec, ...c }
if (rec.__isNew) {
// INSERT → 전체 row 필요
inserts.push(merged)
} else {
// UPDATE → PK + 변경 컬럼
updates.push({
member_id : merged.member_id,
user_id : merged.user_id,
user_nm : merged.user_nm,
dept_nm : merged.dept_nm,
tel_no : merged.tel_no,
email : merged.email,
use_yn : merged.use_yn?.id || merged.use_yn,
auth_bc : merged.auth_bc?.id || merged.auth_bc,
rmks : (merged.memo !== undefined ? merged.memo : '')
})
}
})
console.log('INSERTS', inserts)
console.log('UPDATES', updates)
fetch('/kngil/bbs/adm_comp.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'save',
// member_id: g.records[0]?.member_id,
inserts,
updates
})
})
.then(res => res.text())
.then(text => {
try {
const json = JSON.parse(text)
if (json.status === 'success') {
w2alert('저장 완료')
loadData()
} else {
w2alert(json.message || '저장 실패')
}
} catch (e) {
console.error(text)
w2alert('서버 응답 오류 (JSON 아님)')
}
})
})
//추가(insert)
document.getElementById('btnAdd')?.addEventListener('click', () => {
const g = w2ui.userGrid || w2ui.detailGrid
if (!g) return
const defaultAuth = AUTH_ITEMS.find(a => a.id === 'BS100500') // 일반
// 신규 row용 recid (음수로 충돌 방지)
const newRecid = -Date.now()
g.add({
recid: newRecid,
__isNew: true,
user_id: '',
user_pw: '',
user_nm: '',
tel_no: '',
email: '',
dept_nm: '',
use_area: 0,
use_yn: USE_ITEMS[0],
auth_bc: defaultAuth || null,
memo: ''
}, true)
g.select(newRecid)
g.scrollIntoView(newRecid)
})
//삭제
document.getElementById('btnDelete')?.addEventListener('click', () => {
const g = w2ui.detailGrid || w2ui.userGrid
if (!g) return
const sel = g.getSelection()
if (!sel.length) {
w2alert('삭제할 사용자를 선택하세요.')
return
}
// 선택된 user_id 수집
const ids = sel
.map(recid => {
const r = g.get(recid)
return r?.user_id
})
.filter(Boolean)
if (!ids.length) {
w2alert('삭제 가능한 항목이 없습니다.')
return
}
w2confirm(`선택한 ${ids.length}명의 사용자를 삭제하시겠습니까?`)
.yes(() => {
fetch('/kngil/bbs/adm_comp.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'delete',
// member_id: g.records[0]?.member_id,
ids
})
})
.then(res => res.text())
.then(text => {
try {
const json = JSON.parse(text)
if (json.status === 'success') {
w2alert('삭제 완료')
loadData()
} else {
w2alert(json.message || '삭제 실패')
}
} catch (e) {
console.error(text)
w2alert('서버 응답 오류 (JSON 아님)')
}
})
})
})
function loadTotalArea(memberId) {
return fetch(`/kngil/bbs/adm_comp.php?action=total_area`)
.then(res => res.json())
.then(json => {
if (json.status !== 'success') {
throw new Error('총 구매면적 로딩 실패')
}
return Number(json.total_area || 0)
})
}
function doSearch() {
const keyword = document.getElementById('schKeyword').value.trim()
const type = document.getElementById('schType').value
const useYn = document.getElementById('schUseYn').value
let p_user_nm = ''
let p_dept_nm = ''
// DB로 보낼 검색 조건
if (type === 'name') {
p_user_nm = keyword
} else if (type === 'dept') {
p_dept_nm = keyword
} else if (type === '') {
// 전체 검색 → 이름 OR 부서
p_user_nm = keyword
p_dept_nm = keyword
}
// ⚠️ type === 'id' 는 DB로 안 보냄
fetch(`/kngil/bbs/adm_comp.php?action=list`
+ `&user_nm=${encodeURIComponent(p_user_nm)}`
+ `&dept_nm=${encodeURIComponent(p_dept_nm)}`
+ `&use_yn=${useYn}`
)
.then(res => res.json())
.then(d => {
if (d.status !== 'success') {
w2alert('검색 실패')
return
}
let records = d.records || []
// ID 검색은 프론트 필터
if (type === 'id' && keyword) {
records = records.filter(r =>
(r.user_id || '').toLowerCase()
.includes(keyword.toLowerCase())
)
}
const g = w2ui.detailGrid || w2ui.userGrid
records = normalizeAuth(records)
records = normalizeUseYn(records)
g.clear()
g.add(records)
})
}
document.getElementById('btnSearch')
?.addEventListener('click', () => {
const memberInput = document.getElementById('targetMemberId');
const memberId = memberInput ? memberInput.value.trim() : '';
if (memberInput && memberInput.style.display !== 'none') {
CURRENT_MEMBER_ID = memberId; // ⭐ 저장
loadDataByMemberId(memberId);
return;
}
doSearch();
});
document.getElementById('schKeyword')
?.addEventListener('keydown', e => {
if (e.key === 'Enter') doSearch()
})
document.getElementById('btnBulkCreate')
?.addEventListener('click', () => {
const memberId = getTargetMemberId()
if (!memberId) {
w2alert('회원ID를 입력하세요.')
return
}
// 🔥 여기서 CSV URL 팝업 띄움
openBulkCreatePopup(memberId)
})
function openBulkCreatePopup(memberId) {
// ⭐ 방어: 객체면 member_id만 사용
if (typeof memberId === 'object' && memberId !== null) {
memberId = memberId.member_id || '';
}
if (!memberId) {
w2alert('회원ID가 올바르지 않습니다.');
return;
}
w2popup.open({
title: '사용자 일괄 생성 (CSV)',
width: 520,
height: 220,
modal: true,
body: `
<div style="padding:20px">
<p style="margin-bottom:8px">
대상 회원ID: <b>${memberId}</b>
</p>
<input type="text"
id="csvUrl"
placeholder="CSV URL 입력"
style="width:100%; padding:6px" />
<div style="margin-top:15px; text-align:right">
<button class="w2ui-btn" id="btnCsvRun">실행</button>
</div>
</div>
`,
onOpen(event) {
event.onComplete = () => {
document
.getElementById('btnCsvRun')
.addEventListener('click', () => {
const url =
document.getElementById('csvUrl').value.trim()
if (!url) {
w2alert('CSV URL을 입력하세요.')
return
}
runBulkCreate(memberId, url)
})
}
}
})
}
function runBulkCreate(memberId, csvUrl) {
fetch('/kngil/bbs/adm_comp.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'bulk_create',
member_id: memberId,
csv_url: csvUrl
})
})
.then(res => res.text())
.then(text => {
let d;
try {
d = JSON.parse(text);
} catch (e) {
w2alert({
title: '서버 오류',
text: `<pre style="white-space:pre-wrap; text-align:left;">${text}</pre>`
});
return;
}
const successCnt = Number(d.success_cnt || 0);
const failCnt = Number(d.fail_cnt || 0);
let errors = d.errors || [];
// 배열이지만 요소가 객체일 수 있음 → 문자열로 강제
errors = errors.map(e => {
if (typeof e === 'string') return e;
try {
return JSON.stringify(e);
} catch {
return String(e);
}
});
// // ⭐ 핵심: object → array 변환
// if (!Array.isArray(errors) && typeof errors === 'object') {
// errors = Object.values(errors);
// }
console.log('errors raw =', d.errors);
// ❌ 전부 실패
if (successCnt === 0) {
w2popup.close(); // ⭐ 먼저 CSV 팝업 닫기
w2alert(`
<div style="
text-align:center;
line-height:1.6;
padding:10px;
font-size:16px;
">
<b>일괄 생성 실패</b><br><br>
${errors.map(e => `${e}`).join('<br>')}
</div>
`);
return;
}
// ✅ 성공 or 부분 성공
let msg = '';
msg += `✔ 성공: ${successCnt}명<br>`;
msg += `❌ 실패: ${failCnt}`;
if (errors.length > 0) {
msg += '<hr style="margin:8px 0">';
msg += '<div style="text-align:left; max-height:200px; overflow:auto;">';
errors.forEach(err => {
msg += `${err}<br>`;
});
msg += '</div>';
}
w2alert({
title: '일괄 생성 결과',
msg: msg
});
if (CURRENT_MEMBER_ID) {
loadDataByMemberId(CURRENT_MEMBER_ID);
} else {
loadData();
}
w2popup.close();
})
.catch(err => {
console.error(err);
w2alert('서버 통신 중 오류가 발생했습니다.');
});
}
function loadDataByMemberId(memberId) {
if (!memberId) {
w2alert('회원ID를 입력하세요.');
return;
}
fetch(`/kngil/bbs/adm_comp.php?action=list&member_id=${encodeURIComponent(memberId)}`)
.then(res => res.json())
.then(async d => {
if (d.status !== 'success') {
w2alert(d.message || '조회 실패');
return;
}
let records = normalizeAuth(d.records || []);
records = normalizeUseYn(records);
const gridName = w2ui.detailGrid ? 'detailGrid' : 'userGrid';
w2ui[gridName].clear();
w2ui[gridName].add(records);
// ✅ 상단 요약 갱신
const totalArea = await loadTotalArea(memberId);
renderSummaryFromRecords({
memberId,
records,
totalArea
});
});
}

285
kngil/js/adm_faq_popup.js Normal file
View File

@@ -0,0 +1,285 @@
import { w2grid, w2ui, w2popup, w2alert } from 'https://cdn.jsdelivr.net/gh/vitmalina/w2ui@master/dist/w2ui.es6.min.js'
/* -------------------------------------------------
공통 유틸
------------------------------------------------- */
function destroyGrid(name) {
if (w2ui[name]) {
w2ui[name].destroy()
}
}
function loadBaseCode(mainCd) {
return fetch(`/kngil/bbs/adm_comp.php?action=base_code&main_cd=${mainCd}`)
.then(res => res.json())
.then(json => {
if (json.status !== 'success') {
throw new Error(json.message || '공통코드 로딩 실패')
}
return json.items
})
}
/* =================================================
faq 팝업 (슈퍼관리자 전용)
================================================= */
export function openfaqPopup() {
if (w2ui.faqGrid) {
w2ui.faqGrid.destroy()
}
w2popup.open({
title: 'FAQ 내용등록',
width: 900,
height: 600,
modal: true,
body: `
<div class="faq-popup">
<div style="display: flex; justify-content: flex-end; padding: 10px; gap: 5px;">
<button id="faqAdd">추가</button>
<button id="faqRemove">삭제</button>
<button id="faqSave" class="btn-faq">저장</button>
</div>
<div id="faqGrid" style="height:460px;"></div>
</div>
`,
onOpen(event) {
event.onComplete = () => {
// 1. 그리드를 생성합니다.
createfaqGrid('#faqGrid');
// 1. 추가 버튼
document.getElementById('faqAdd').onclick = () => {
const g = w2ui.faqGrid
if (!g) return
// 신규 row용 recid (음수로 충돌 방지)
const newRecid = -Date.now()
g.add({
fa_subject: '',
recid: newRecid,
__isNew: true, // ⭐ 신규 플래그
fa_content: ''
}, true)
g.select(newRecid)
g.scrollIntoView(newRecid)
};
// 삭제 버튼 이벤트 연결
const removeBtn = document.getElementById('faqRemove');
if (removeBtn) {
removeBtn.onclick = () => {
const g = w2ui.faqGrid;
if (!g) return;
// 1. 선택된 행 가져오기
const sel = g.getSelection();
if (!sel.length) {
alert('삭제할 항목을 선택하세요.');
return;
}
// 2. faq 추출
const ids = sel
.map(recid => g.get(recid)?.fa_id)
.filter(Boolean);
if (!ids.length) {
alert('삭제 가능한 항목이 없습니다.');
return;
}
// 3. 브라우저 기본 확인창 사용 (가장 확실함)
if (confirm(`선택한 ${ids.length}개의 상품을 삭제하시겠습니까?`)) {
fetch('/kngil/bbs/adm_faq_popup_delete.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'delete', ids: ids })
})
.then(res => {
// 서버 응답이 오면 일단 텍스트로 받아서 확인
return res.text();
})
.then(text => {
try {
const json = JSON.parse(text);
if (json.status === 'success') {
alert('삭제 완료');
loadfaqData(); // 목록 새로고침
} else {
alert(json.message || '삭제 실패');
}
} catch (e) {
console.error('응답 파싱 에러:', text);
alert('서버 응답 처리 중 오류가 발생했습니다.');
}
})
.catch(err => {
console.error('통신 에러:', err);
alert('서버와 통신할 수 없습니다.');
});
}
};
}
// 3. 저장 버튼
document.getElementById('faqSave').onclick = () => {
// 현재 사용 중인 grid
const g = w2ui.faqGrid
if (!g) return
const changes = g.getChanges()
if (!changes.length) {
w2alert('변경된 내용이 없습니다.')
return
}
const inserts = []
const updates = []
changes.forEach(c => {
const rec = g.get(c.recid)
if (!rec) return
// 🔥 핵심: 원본 rec + 변경값 c 병합
const merged = { ...rec, ...c }
if (rec.__isNew) {
// INSERT → 전체 row 필요
inserts.push(merged)
} else {
// UPDATE → PK + 변경 컬럼
updates.push({
fa_id : merged.fa_id,
fa_subject : merged.fa_subject ,
fa_content : merged.fa_content ,
use_yn : merged.use_yn,
sq_no : merged.sq_no
})
}
})
console.log('INSERTS', inserts)
console.log('UPDATES', updates)
fetch('/kngil/bbs/adm_faq_popup_save.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'save',
inserts,
updates
})
})
.then(res => {
// 서버가 준 응답이 괜찮은지 확인
if (!res.ok) throw new Error('서버 응답 오류');
return res.json(); // 처음부터 깔끔하게 데이터로 읽기
})
.then(json => {
// 이제 json.status를 바로 쓸 수 있습니다.
if (json.status === 'success') {
w2alert('저장 완료');
w2ui.faqGrid.save(); // 빨간 삼각형 없애기
loadfaqData(); // 목록 새로고침
} else {
w2alert(json.message || '저장 실패');
}
})
.catch(err => {
console.error('에러 발생:', err);
w2alert('처리 중 오류가 발생했습니다.');
});
};
}
}
})
}
//faq 그리드
export async function createfaqGrid(boxId) {
if (w2ui.faqGrid) w2ui.faqGrid.destroy();
destroyGrid('faqGrid')
const authItems = await loadBaseCode('BS210')
const grid = new w2grid({
name: 'faqGrid',
box: boxId,
recordHeight: 80, // ⭐ 행 높이를 80px로 설정 (3줄 정도 높이)
show: {
footer: true,
selectColumn: true,
lineNumbers: true // 행 번호를 표시하면 디버깅이 편합니다.
},
columns: [
/*
{ field: 'row_status', text: ' ', size: '30px', attr: 'align=center',
render: function (record) {
// 1. 신규 추가된 행 (recid가 임시값이거나 DB에 없는 경우)
if (record.is_new) return '<span style="color: green;">I</span>';
// 2. 수정된 데이터 (w2ui.changes 객체가 존재하는 경우)
if (record.w2ui && record.w2ui.changes) return '<span style="color: blue;">U</span>';
// 3. 일반 상태
return '<span style="color: gray;"> </span>';
}
},
*/
{ field: 'fa_id', text: 'FAID', size: '60',attr: 'align=center',style: 'text-align: center', attr: 'align=center' ,hidden: true,sortable: true}, // name 아님!
{ field: 'fa_subject', text: '질문', size: '300', editable: { type: 'text' }
,render: function(record) {
if (!record.fa_content) return '';
// 엔터값(\n)을 브라우저가 인식하는 <br>로 바꿔서 리턴
return record.fa_content.replace(/\n/g, '<br>');
}
}, // name 아님!
{ field: 'fa_content', text: '답글', size: '300', editable: { type: 'text' } }, // volume 아님!
{ field: 'sq_no', text: '순번', size: '60', attr: 'align=center',style: 'text-align: center',render: 'number:0', editable: { type: 'int' } ,sortable: true}, //
{ field: 'use_yn', text: '사용여부', size: '80px',attr: 'align=center',style: 'text-align: center', attr: 'align=center',
editable: { type: 'list', items: authItems, filter: false ,showAll: true } ,
render(record) {
const item = authItems.find(i => i.id === record.use_yn)
return item ? item.text : record.use_yn
},sortable: true
},
{ field: 'rmks', text: '비고', size: '200px', attr: 'align=center', editable: { type: 'text' } } // memo 아님!
],
records: [] // 처음엔 비워둠
});
// 데이터 호출 함수 실행
loadfaqData();
return grid;
}
// 데이터를 서버에서 불러오는 함수
async function loadfaqData() {
try {
w2ui.faqGrid.lock('조회 중...', true);
const response = await fetch('/kngil/bbs/adm_faq_popup.php'); // PHP 파일 호출
const data = await response.json();
w2ui.faqGrid.clear();
w2ui.faqGrid.add(data);
w2ui.faqGrid.unlock();
} catch (e) {
console.error(e);
w2ui.faqGrid.unlock();
w2alert('데이터 로드 실패');
}
}

View File

@@ -0,0 +1,280 @@
import { w2grid, w2ui, w2popup, w2alert } from 'https://cdn.jsdelivr.net/gh/vitmalina/w2ui@master/dist/w2ui.es6.min.js'
/* -------------------------------------------------
공통 유틸
------------------------------------------------- */
function destroyGrid(name) {
if (w2ui[name]) {
w2ui[name].destroy()
}
}
function loadBaseCode(mainCd) {
return fetch(`/kngil/bbs/adm_comp.php?action=base_code&main_cd=${mainCd}`)
.then(res => res.json())
.then(json => {
if (json.status !== 'success') {
throw new Error(json.message || '공통코드 로딩 실패')
}
return json.items
})
}
/* =================================================
상품등록 팝업 (슈퍼관리자 전용)
================================================= */
export function openProductPopup() {
if (w2ui.productGrid) {
w2ui.productGrid.destroy()
}
w2popup.open({
title: '상품등록',
width: 900,
height: 600,
modal: true,
body: `
<div class="product-popup">
<div style="display: flex; justify-content: flex-end; padding: 1S0px; gap: 5px;">
<button id="prodAdd">추가</button>
<button id="prodRemove">삭제</button>
<button id="prodSave" class="btn-product">저장</button>
</div>
<div id="productGrid" style="height:460px;"></div>
</div>
`,
onOpen(event) {
event.onComplete = () => {
// 1. 그리드를 생성합니다.
createProductGrid('#productGrid');
// 1. 추가 버튼
document.getElementById('prodAdd').onclick = () => {
const g = w2ui.productGrid
if (!g) return
// 신규 row용 recid (음수로 충돌 방지)
const newRecid = -Date.now()
g.add({
row_status: 'I',
recid: newRecid,
__isNew: true, // ⭐ 신규 플래그
use_yn: 'Y'
}, true)
g.select(newRecid)
g.scrollIntoView(newRecid)
// commonAdd('productGrid', { use_yn: 'Y', itm_amt: 0, area: 0 });
};
// 삭제 버튼 이벤트 연결
const removeBtn = document.getElementById('prodRemove');
if (removeBtn) {
removeBtn.onclick = () => {
const g = w2ui.productGrid;
if (!g) return;
// 1. 선택된 행 가져오기
const sel = g.getSelection();
if (!sel.length) {
alert('삭제할 상품을 선택하세요.');
return;
}
// 2. 상품코드 추출
const ids = sel
.map(recid => g.get(recid)?.itm_cd)
.filter(Boolean);
if (!ids.length) {
alert('삭제 가능한 항목이 없습니다.');
return;
}
// 3. 브라우저 기본 확인창 사용 (가장 확실함)
if (confirm(`선택한 ${ids.length}개의 상품을 삭제하시겠습니까?`)) {
fetch('/kngil/bbs/adm_product_popup_delete.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'delete', ids: ids })
})
.then(res => {
// 서버 응답이 오면 일단 텍스트로 받아서 확인
return res.text();
})
.then(text => {
try {
const json = JSON.parse(text);
if (json.status === 'success') {
alert('삭제 완료');
loadProductData(); // 목록 새로고침
} else {
alert(json.message || '삭제 실패');
}
} catch (e) {
console.error('응답 파싱 에러:', text);
alert('서버 응답 처리 중 오류가 발생했습니다.');
}
})
.catch(err => {
console.error('통신 에러:', err);
alert('서버와 통신할 수 없습니다.');
});
}
};
}
// 3. 저장 버튼
document.getElementById('prodSave').onclick = () => {
// 현재 사용 중인 grid
const g = w2ui.productGrid
if (!g) return
const changes = g.getChanges()
if (!changes.length) {
w2alert('변경된 내용이 없습니다.')
return
}
const inserts = []
const updates = []
changes.forEach(c => {
const rec = g.get(c.recid)
if (!rec) return
// 🔥 핵심: 원본 rec + 변경값 c 병합
const merged = { ...rec, ...c }
if (rec.__isNew) {
// INSERT → 전체 row 필요
inserts.push(merged)
} else {
// UPDATE → PK + 변경 컬럼
updates.push({
itm_cd : merged.itm_cd,
itm_nm : merged.itm_nm,
area : merged.area,
itm_amt: merged.itm_amt,
use_yn : merged.use_yn,
rmks : merged.rmks
})
}
})
console.log('INSERTS', inserts)
console.log('UPDATES', updates)
fetch('/kngil/bbs/adm_product_popup_save.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'save',
inserts,
updates
})
})
.then(res => {
// 서버가 준 응답이 괜찮은지 확인
if (!res.ok) throw new Error('서버 응답 오류');
return res.json(); // 처음부터 깔끔하게 데이터로 읽기
})
.then(json => {
// 이제 json.status를 바로 쓸 수 있습니다.
if (json.status === 'success') {
w2alert('저장 완료');
w2ui.productGrid.save(); // 빨간 삼각형 없애기
loadProductData(); // 목록 새로고침
} else {
w2alert(json.message || '저장 실패');
}
})
.catch(err => {
console.error('에러 발생:', err);
w2alert('처리 중 오류가 발생했습니다.');
});
};
}
}
})
}
//상품등록 그리드
export async function createProductGrid(boxId) {
if (w2ui.productGrid) w2ui.productGrid.destroy();
destroyGrid('productGrid')
const authItems = await loadBaseCode('BS210')
const grid = new w2grid({
name: 'productGrid',
box: boxId,
show: {
footer: true,
selectColumn: true,
lineNumbers: true // 행 번호를 표시하면 디버깅이 편합니다.
},
columns: [
/*
{ field: 'row_status', text: ' ', size: '30px', attr: 'align=center',
render: function (record) {
// 1. 신규 추가된 행 (recid가 임시값이거나 DB에 없는 경우)
if (record.is_new) return '<span style="color: green;">I</span>';
// 2. 수정된 데이터 (w2ui.changes 객체가 존재하는 경우)
if (record.w2ui && record.w2ui.changes) return '<span style="color: blue;">U</span>';
// 3. 일반 상태
return '<span style="color: gray;"> </span>';
}
},
*/
{ field: 'itm_cd', text: '상품코드', size: '80px',attr: 'align=center',style: 'text-align: center', attr: 'align=center', editable: { type: 'text' } ,sortable: true}, // name 아님!
{ field: 'itm_nm', text: '상품명', size: '120px', attr: 'align=center',style: 'text-align: center', editable: { type: 'text' } ,sortable: true}, // name 아님!
{ field: 'area', text: '제공량', size: '100px', attr: 'align=center',render: 'number:0' , editable: { type: 'float' } ,sortable: true}, // volume 아님!
{ field: 'itm_amt', text: '단가', size: '120px', attr: 'align=center',render: 'number:0', editable: { type: 'int' } ,sortable: true}, //
{ field: 'use_yn', text: '사용여부', size: '80px',attr: 'align=center',style: 'text-align: center', attr: 'align=center',
editable: { type: 'list', items: authItems, filter: false ,showAll: true } ,
render(record) {
const item = authItems.find(i => i.id === record.use_yn)
return item ? item.text : record.use_yn
},sortable: true
},
{ field: 'rmks', text: '비고', size: '200px', attr: 'align=center', editable: { type: 'text' } ,sortable: true} // memo 아님!
],
records: [] // 처음엔 비워둠
});
// 데이터 호출 함수 실행
loadProductData();
return grid;
}
// 데이터를 서버에서 불러오는 함수
async function loadProductData() {
try {
w2ui.productGrid.lock('조회 중...', true);
const response = await fetch('/kngil/bbs/adm_product_popup.php'); // PHP 파일 호출
const data = await response.json();
w2ui.productGrid.clear();
w2ui.productGrid.add(data);
w2ui.productGrid.unlock();
} catch (e) {
console.error(e);
w2ui.productGrid.unlock();
w2alert('데이터 로드 실패');
}
}

144
kngil/js/adm_purch_popup.js Normal file
View File

@@ -0,0 +1,144 @@
import { w2grid, w2ui, w2popup, w2alert } from 'https://cdn.jsdelivr.net/gh/vitmalina/w2ui@master/dist/w2ui.es6.min.js'
// 파일이 실제 존재하는지 확인 필수! 존재하지 않으면 아래 코드가 모두 멈춥니다.
import { bindProductPopupEvents } from '/kngil/js/adm_common.js'
export function openPurchaseHistoryPopup(memberId = '',isSuperAdmin='') {
// 기존 그리드 안전하게 제거
if (w2ui.purchaseHistoryGrid) {
w2ui.purchaseHistoryGrid.destroy();
}
w2popup.open({
title: '서비스 구매 이력',
width: 1100,
height: 600,
modal: true,
body:
/*
`
<div style="padding:8px">
<div id="purchaseHistoryGrid" style="height:520px;"></div>
</div>
*/
`
<div id="popupMainContainer" style="display: flex; flex-direction: column; height: 100%; padding: 10px; gap: 10px; box-sizing: border-box;">
<div id="searchFormArea" style="display: flex; align-items: center; gap: 10px; padding: 15px; background: #f9f9f9; border: 1px solid #ddd; border-radius: 4px;">
<span style="font-weight: bold; color: #333;">회원 ID</span>
<input type="text" id="searchMemberId" value="${memberId}"
${!isSuperAdmin ? 'disabled' : ''}
style="width: 150px; padding: 6px; border: 1px solid #ccc; border-radius: 4px;"
placeholder="아이디 입력">
<button id="btnSearchHistory"
style="padding: 6px 20px; background: #1565c0; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold;">
검색
</button>
</div>
<div id="purchaseHistoryGrid" style="flex: 1; min-height: 450px; border: 1px solid #ddd;"></div>
</div>
`,
onOpen(event) {
event.onComplete = () => {
const searchBtn = document.getElementById('btnSearchHistory');
const searchInput = document.getElementById('searchMemberId');
// 조회 버튼 클릭 시 동작
if (searchBtn) {
searchBtn.onclick = () => {
loadPurchaseHistoryData(searchInput.value);
};
}
// 엔터키 지원
if (searchInput) {
searchInput.onkeydown = (e) => {
if (e.key === 'Enter') searchBtn.click();
};
}
createPurchaseHistoryGrid(memberId);
loadPurchaseHistoryData(memberId);
}
}
});
}
function createPurchaseHistoryGrid(memberId) {
new w2grid({
name: 'purchaseHistoryGrid',
box: '#purchaseHistoryGrid',
show: {
//toolbar: true,
footer: true,
lineNumbers: true // 행 번호를 표시하면 디버깅이 편합니다.
},
columns: [
// field 이름이 PHP에서 내려주는 JSON 키값과 정확히 일치해야 함!
{ field: 'member_id', text: '회원ID', size: '80px',style: 'text-align: center', attr: 'align=center',sortable: true }, // name 아님!
{ field: 'sq_no', text: '순번', size: '50px',style: 'text-align: center', attr: 'align=center' ,sortable: true}, // name 아님!
{ field: 'user_nm', text: '구매자', size: '80px',style: 'text-align: center', attr: 'align=center' ,sortable: true}, // name 아님!
{ field: 'co_nm', text: '회사명', size: '100px',style: 'text-align: center', attr: 'align=center' ,sortable: true}, // name 아님!
{ field: 'bs_no', text: '사업자번호', size: '100px',style: 'text-align: center', attr: 'align=center' ,sortable: true}, // name 아님!
{ field: 'buy_dt', text: '구입일자', size: '80px',style: 'text-align: center', attr: 'align=center',sortable: true }, // name 아님!
{ field: 'itm_nm', text: '상품명', size: '80px',style: 'text-align: center', attr: 'align=center' ,sortable: true}, // name 아님!
{ field: 'area', text: '상품면적', size: '80px',render: 'number:0' , attr: 'align=center' ,sortable: true}, // name 아님!
{ field: 'itm_qty', text: '수량', size: '80px',render: 'number:0' , attr: 'align=center' ,sortable: true}, // name 아님!
{ field: 'itm_area', text: '면적', size: '100px',render: 'number:0' , attr: 'align=center' ,sortable: true}, // name 아님!
{ field: 'add_area', text: '추가면적', size: '100px',render: 'number:0' , attr: 'align=center' ,sortable: true}, // name 아님!
{ field: 'sum_area', text: '합계면적', size: '100px',render: 'number:0' , attr: 'align=center' ,sortable: true}, // name 아님!
{ field: 'itm_amt', text: '단가', size: '100px',render: 'number:0' , attr: 'align=center' ,sortable: true}, // name 아님!
{ field: 'dis_rt', text: '할인율', size: '80px', attr: 'align=center' ,sortable: true}, // name 아님!
{ field: 'buy_amt', text: '공급금액', size: '100px',render: 'number:0' , attr: 'align=center' ,sortable: true}, // name 아님!
{ field: 'vat_amt', text: '부가세', size: '100px',render: 'number:0' , attr: 'align=center' ,sortable: true}, // name 아님!
{ field: 'sum_amt', text: '합계', size: '100px',render: 'number:0' , attr: 'align=center' ,sortable: true}, // name 아님!
{ field: 'end_dt', text: '종료일', size: '80px',style: 'text-align: center', attr: 'align=center' ,sortable: true}, // name 아님!
{ field: 'ok_yn', text: '승인여부', size: '80px',style: 'text-align: center', attr: 'align=center' ,sortable: true}, // name 아님!
{ field: 'rmks', text: '비고', size: '150px',style: 'text-align: center', attr: 'align=center' ,sortable: true} // name 아님!
]
});
}
async function loadPurchaseHistoryData(memberId) {
try {
const grid = w2ui.purchaseHistoryGrid;
if (!grid) return;
grid.lock('조회 중...', true);
const searchParams = new URLSearchParams();
searchParams.append('member_id', memberId);
searchParams.append('member_nm', '');
searchParams.append('fbuy_dt', '');
searchParams.append('tbuy_dt', '');
const response = await fetch('/kngil/bbs/adm_purch_popup.php', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: searchParams
});
if (!response.ok) throw new Error(`서버 응답 오류: ${response.status}`);
const data = await response.json();
if (!Array.isArray(data)) {
throw new Error('응답 데이터 형식 오류');
}
grid.clear();
grid.add(data);
grid.unlock();
} catch (e) {
console.error(e);
if (w2ui.purchaseHistoryGrid) w2ui.purchaseHistoryGrid.unlock();
w2alert('구매이력 조회 실패: ' + e.message);
}
}
////////////////////////////////////////////////////////////////////////

View File

@@ -0,0 +1,237 @@
import { w2grid, w2ui, w2popup, w2alert }
from 'https://cdn.jsdelivr.net/gh/vitmalina/w2ui@master/dist/w2ui.es6.min.js'
/* -------------------------------------------------
공통 유틸
------------------------------------------------- */
function destroyGrid(name) {
if (w2ui[name]) w2ui[name].destroy()
}
/* -------------------------------------------------
서비스 등록 팝업
------------------------------------------------- */
export function openServiceRegisterPopup(ctx) {
destroyGrid('serviceGrid')
destroyGrid('productList')
w2popup.open({
title: '서비스 등록',
width: 1300,
height: 720,
modal: true,
body: `
<div class="service-popup">
<!-- 상단 -->
<div class="service-top">
<!-- 좌측 : 회원정보 + 구매일 -->
<div class="service-member">
<div><b>회원ID</b> : ${ctx.memberId}</div>
<div><b>회원명</b> : ${ctx.memberName}</div>
<div><b>회사명</b> : ${ctx.company}</div>
<div><b>사업자번호</b> : ${ctx.bizNo}</div>
<div class="purchase-date">
<label>구매일</label>
<input type="date" id="buyDate">
<button id="btnSearchBuy">조회</button>
</div>
</div>
<!-- 우측 : 상품 선택 -->
<div class="service-product">
<div class="title">상품 선택 (더블클릭)</div>
<div id="productList" style="height:160px;"></div>
</div>
</div>
<!-- 중단 -->
<div class="service-grid">
<div id="serviceGrid" style="height:260px;"></div>
</div>
<!-- 하단 -->
<div class="service-summary">
<div>공급가액 <span id="sumSupply">0</span></div>
<div>부가세 <span id="sumVat">0</span></div>
<div>결제금액 <span id="sumTotal">0</span></div>
</div>
</div>
`,
onOpen(event) {
event.onComplete = () => {
createServiceGrid()
createProductList()
// ✅ 구매일 조회 버튼 바인딩 (여기서!)
document.getElementById('btnSearchBuy').addEventListener('click', () => {
const buyDate = document.getElementById('buyDate').value
if (!buyDate) {
w2alert('구매일을 선택하세요')
return
}
loadExistingPurchase(ctx.memberId, buyDate)
})
}
}
})
}
/* -------------------------------------------------
서비스 Grid
------------------------------------------------- */
function createServiceGrid() {
new w2grid({
name: 'serviceGrid',
box: '#serviceGrid',
show: {
footer: true,
// selectColumn: true
},
columns: [
{ field: 'recid', text: 'NO', size: '50px', attr: 'align=center' },
{ field: 'itm_cd', text: '상품명', size: '100px' },
{ field: 'itm_area', text: '면적(m²)', size: '100px', editable: { type: 'int' } },
{ field: 'itm_amt', text: '금액', size: '100px', attr: 'align=right' },
{ field: 'itm_qty', text: '수량', size: '80px', editable: { type: 'int' } },
{ field: 'dis_rt', text: '할인율', size: '80px', editable: { type: 'int' } },
{ field: 'supply', text: '공급가액', size: '120px', attr: 'align=right' },
{ field: 'vat_amt', text: '부가세', size: '100px', attr: 'align=right' },
{ field: 'sum_amt', text: '결제금액', size: '120px', attr: 'align=right' },
{ field: 'end_dt', text: '만료일자', size: '110px', editable: { type: 'date' } },
{ field: 'rmks', text: '비고', size: '150px', editable: { type: 'text' } }
],
records: [],
onChange() {
calcSummary()
}
})
}
/* -------------------------------------------------
상품 목록
------------------------------------------------- */
function createProductList() {
new w2grid({
name: 'productList',
box: '#productList',
columns: [
{ field: 'name', text: '상품명', size: '120px' },
{ field: 'area', text: '제공량', size: '100px', attr: 'align=right' },
{ field: 'price', text: '단가', size: '100px', attr: 'align=right' }
],
records: [
{ recid: 1, name: '다이아', area: 10000000, price: 10000000 },
{ recid: 2, name: '골드', area: 1000000, price: 5000000 },
{ recid: 3, name: '실버', area: 100000, price: 1000000 }
],
onDblClick(event) {
const rec = this.get(event.recid)
addServiceFromProduct(rec)
}
})
}
/* -------------------------------------------------
상품 → 서비스 Grid 추가
------------------------------------------------- */
function addServiceFromProduct(p) {
const g = w2ui.serviceGrid
const recid = g.records.length + 1
const supply = p.price
const vat = Math.floor(supply * 0.1)
const total = supply + vat
g.add({
recid,
itm_cd: p.name,
itm_area: p.area,
itm_amt: p.price,
itm_qty: 1,
dis_rt: 0,
supply,
vat_amt: vat,
sum_amt: total,
end_dt: '',
rmks: ''
})
calcSummary()
}
/* -------------------------------------------------
기존 구매 불러오기
------------------------------------------------- */
function loadExistingPurchase(memberId, buyDate) {
fetch(`/kngil/bbs/adm_service.php?member_id=${memberId}&buy_date=${buyDate}`)
.then(res => res.json())
.then(json => {
if (json.status !== 'success') {
w2alert(json.message || '조회 실패')
return
}
const g = w2ui.serviceGrid
g.clear()
json.records.forEach((r, i) => {
g.add({
recid: i + 1,
itm_cd: r.itm_cd,
itm_area: Number(r.itm_area),
itm_amt: Number(r.itm_amt),
itm_qty: Number(r.itm_qty),
dis_rt: Number(r.dis_rt),
supply: Number(r.supply),
vat_amt: Number(r.vat_amt),
sum_amt: Number(r.sum_amt),
end_dt: r.end_dt,
rmks: r.rmks,
_existing: true
})
// 🔒 기존 구매 회색 처리
g.set(i + 1, {
w2ui: { style: 'background:#f3f3f3;color:#555' }
})
})
g.refresh()
calcSummary()
})
.catch(err => {
console.error(err)
w2alert('서버 오류')
})
}
/* -------------------------------------------------
합계 계산
------------------------------------------------- */
function calcSummary() {
let supply = 0
let vat = 0
let total = 0
w2ui.serviceGrid.records.forEach(r => {
supply += Number(r.supply || 0)
vat += Number(r.vat_amt || 0)
total += Number(r.sum_amt || 0)
})
document.getElementById('sumSupply').innerText = supply.toLocaleString()
document.getElementById('sumVat').innerText = vat.toLocaleString()
document.getElementById('sumTotal').innerText = total.toLocaleString()
}

678
kngil/js/adm_service.js Normal file
View File

@@ -0,0 +1,678 @@
import { w2grid, w2ui, w2popup, w2alert, w2confirm }
from 'https://cdn.jsdelivr.net/gh/vitmalina/w2ui@master/dist/w2ui.es6.min.js'
/* -------------------------------------------------
공통 유틸
------------------------------------------------- */
function destroyGrid(name) {
if (w2ui[name]) w2ui[name].destroy()
}
function fmtNum(val) {
return Number(val || 0).toLocaleString()
}
function fmtPrice(val) {
const v = Number(val || 0)
return Number.isInteger(v)
? v.toLocaleString()
: v.toLocaleString(undefined, { minimumFractionDigits: 2 })
}
/* -------------------------------------------------
서비스 등록 팝업
------------------------------------------------- */
export function openServiceRegisterPopup(ctx) {
destroyGrid('serviceGrid')
destroyGrid('productList')
w2popup.open({
title: '서비스 등록',
width: 1300,
height: 720,
modal: true,
body: `
<div class="service-popup">
<!-- 상단 -->
<div class="service-top">
<!-- 좌측 : 회원정보 -->
<div class="service-member">
<div><b>회원ID</b> : ${ctx.memberId}</div>
<div><b>회원명</b> : ${ctx.memberName}</div>
<div><b>회사명</b> : ${ctx.company}</div>
<div><b>사업자번호</b> : ${ctx.bizNo}</div>
<div class="purchase-date">
<label>구매일</label>
<input type="date" id="buyDate">
<button id="btnSearchBuy">조회</button>
</div>
</div>
<!-- 우측 : 상품 선택 -->
<div class="service-product">
<div class="title">상품 선택 (더블클릭)</div>
<div id="productList" style="height:160px;"></div>
</div>
</div>
<!-- 중단 -->
<div class="service-save-bar">
<div class="right-actions">
<button id="btnDeleteRow" class="btn-delete">삭제</button>
<button id="btnSaveService" class="btn-save">저장</button>
</div>
</div>
<div class="service-grid">
<div id="serviceGrid" style="height:260px;"></div>
</div>
<!-- 하단 합계 -->
<div class="service-summary">
<div>공급가액 <span id="sumSupply">0</span></div>
<div>부가세 <span id="sumVat">0</span></div>
<div>결제금액 <span id="sumTotal">0</span></div>
</div>
</div>
`,
onOpen(event) {
event.onComplete = () => {
createServiceGrid()
createProductList()
const buyDateInput = document.getElementById('buyDate')
const today = new Date().toISOString().slice(0, 10)
// 기본 구매일 = 오늘
buyDateInput.value = today
// 자동 조회
loadExistingPurchase(ctx.memberId, today)
// 수동 조회 버튼
document.getElementById('btnSearchBuy').addEventListener('click', () => {
const buyDate = buyDateInput.value
if (!buyDate) {
w2alert('구매일을 선택하세요')
return
}
loadExistingPurchase(ctx.memberId, buyDate)
})
document.getElementById('btnDeleteRow')
.addEventListener('click', deleteSelectedRows)
document.getElementById('btnSaveService')
.addEventListener('click', () => saveService(ctx))
}
}
})
}
/* -------------------------------------------------
서비스 Grid
------------------------------------------------- */
function createServiceGrid() {
new w2grid({
name: 'serviceGrid',
box: '#serviceGrid',
show: {
footer: true,
selectColumn: true
},
multiSelect: false,
columns: [
{ field: 'recid', text: 'NO', size: '50px', attr: 'align=center' },
{ field: 'itm_nm', text: '상품명', size: '120px', editable: { type: 'text' } },
{ field: 'area', text: '제공량', size: '120px', editable: { type: 'text' } },
{ field: 'itm_cd', text: '상품코드', hidden: true },
{
field: 'itm_amt',
text: '단가',
size: '100px',
attr: 'align=right',
render: r => fmtPrice(r.itm_amt)
},
{
field: 'itm_qty',
text: '수량',
size: '80px',
attr: 'align=right',
editable: { type: 'int' },
render: r => fmtNum(r.itm_qty)
},
{
field: 'dis_rt',
text: '할인율',
size: '80px',
editable: { type: 'int' }
},
{
field: 'supply',
text: '공급가액',
size: '120px',
attr: 'align=right',
render: r => fmtNum(r.supply)
},
{
field: 'vat_amt',
text: '부가세',
size: '100px',
attr: 'align=right',
render: r => fmtNum(r.vat_amt)
},
{
field: 'sum_amt',
text: '결제금액',
size: '120px',
attr: 'align=right',
render: r => fmtNum(r.sum_amt)
},
{
field: 'itm_area',
text: '적용면적(m²)',
size: '100px',
attr: 'align=right',
editable: { type: 'int' },
render: r => fmtNum(r.itm_area)
},
{
field: 'add_area',
text: '추가면적(m²)',
size: '110px',
attr: 'align=right',
editable: { type: 'int' },
render: r => fmtNum(r.add_area)
},
{
field: 'sum_area',
text: '합계면적(m²)',
size: '110px',
attr: 'align=right',
editable: { type: 'int' },
render: r => fmtNum(r.sum_area)
},
{
field: 'end_dt',
text: '만료일자',
size: '110px',
editable: { type: 'date' }
},
{
field: 'ok_yn',
text: '승인여부',
size: '80px',
attr: 'align=center',
editable: {
type: 'combo',
items: [
{ id: 'N', text: '미승인' },
{ id: 'Y', text: '승인' }
],
openOnFocus: true
},
render(record) {
if (record.ok_yn === 'Y') return '승인'
return '미승인'
}
},
{ field: 'rmks', text: '비고', size: '150px', editable: { type: 'text' } }
],
records: [],
/* -----------------------------
일반 필드 처리 (date 제외)
----------------------------- */
onChange(event) {
event.onComplete = (ev) => {
const g = w2ui.serviceGrid
const field = g.columns[ev.column].field
let val = ev.value_new
if (typeof val === 'object' && val !== null) val = val.id
g.set(ev.recid, { [field]: val })
if (['itm_qty','dis_rt','add_area'].includes(field)) {
recalcRow(ev.recid)
}
}
},
/* -----------------------------
날짜 전용 처리 (핵심)
----------------------------- */
onEditField(event) {
if (event.field !== 'end_dt') return;
event.onComplete = () => {
const g = w2ui.serviceGrid;
const recid = event.recid;
const input = document.querySelector(
`#grid_${g.name}_edit input`
);
if (!input) return;
input.addEventListener('change', () => {
// 🔥 날짜는 문자열 그대로 즉시 확정
g.set(recid, {
end_dt: input.value // YYYY-MM-DD
});
// 🔥 w2ui 방식으로 edit 종료
input.blur();
g.refreshRow(recid);
});
};
}
});
}
/* -------------------------------------------------
상품 목록
------------------------------------------------- */
function createProductList() {
new w2grid({
name: 'productList',
box: '#productList',
url: '/kngil/bbs/adm_product_popup.php',
columns: [
{ field: 'itm_nm', text: '상품명', size: '120px' },
{
field: 'area',
text: '제공량',
size: '100px',
attr: 'align=right',
render(record) {
return Number(record.area || 0).toLocaleString()
}
},
{
field: 'itm_amt',
text: '단가',
size: '120px',
attr: 'align=right',
render(record) {
const v = Number(record.itm_amt || 0)
// 정수면 소수점 제거
return Number.isInteger(v)
? v.toLocaleString()
: v.toLocaleString(undefined, { minimumFractionDigits: 2 })
}
}
],
onDblClick(event) {
event.onComplete = () => {
const recid = event.detail?.recid
if (!recid) return
const rec = this.get(recid)
if (!rec) return
addServiceFromProduct(rec)
}
}
})
}
/* -------------------------------------------------
상품 → 서비스 Grid 추가
------------------------------------------------- */
function addServiceFromProduct(p) {
const g = w2ui.serviceGrid
const recid = g.records.length + 1
const qty = 1
const area = Number(p.area || 0)
const unit = Number(p.itm_amt || 0)
const today = new Date()
const endDt = `${today.getFullYear()}-12-31`
g.add({
recid,
itm_cd: p.itm_cd,
itm_nm: p.itm_nm,
area: area, // 제공량
itm_qty: qty,
itm_amt: unit,
dis_rt: 0,
itm_area: area * qty,
add_area: 0,
sum_area: area * qty,
supply: unit * qty,
vat_amt: Math.floor(unit * qty * 0.1),
sum_amt: Math.floor(unit * qty * 1.1),
end_dt: endDt,
ok_yn: 'N',
rmks: '',
_new: true
})
recalcRow(recid)
}
/* -------------------------------------------------
행 삭제
------------------------------------------------- */
function deleteSelectedRows() {
const g = w2ui.serviceGrid
const sel = g.getSelection()
if (!sel.length) {
w2alert('삭제할 서비스를 선택하세요')
return
}
const recid = sel[0]
const row = g.get(recid)
if (!row || !row.sq_no) {
w2alert('삭제할 수 없는 항목입니다.')
return
}
if (row.ok_yn === 'Y') {
w2alert('승인된 항목은 삭제할 수 없습니다.')
return
}
w2confirm('선택한 서비스를 삭제하시겠습니까?')
.yes(() => {
deleteServiceImmediately(row)
})
}
function deleteServiceImmediately(row) {
console.log({
action: 'delete',
member_id: row.member_id,
sq_no: row.sq_no
})
fetch('/kngil/bbs/adm_service.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'delete',
member_id: row.member_id, // ✅ 여기 중요
sq_no: row.sq_no
})
})
.then(res => res.json())
.then(res => {
if (res.status === 'success') {
w2alert('삭제되었습니다.')
w2ui.serviceGrid.remove(row.recid)
calcSummary()
} else {
w2alert(res.message || '삭제 실패')
}
})
.catch(() => w2alert('서버 오류'))
}
function isServiceItem(r) {
return r.itm_cd && r.itm_cd.startsWith('ZET01')
|| r.itm_nm === '서비스'
}
/* -------------------------------------------------
기존 구매 불러오기
------------------------------------------------- */
function loadExistingPurchase(memberId, buyDate) {
fetch('/kngil/bbs/adm_service.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'list',
member_id: memberId,
buy_date: buyDate
})
})
.then(res => res.json())
.then(json => {
if (json.status !== 'success') {
w2alert(json.message || '조회 실패')
return
}
const g = w2ui.serviceGrid
g.clear()
json.records.forEach((r, i) => {
g.add({
recid: i + 1,
member_id: memberId,
sq_no: r.sq_no,
itm_cd: r.itm_cd,
itm_nm: r.itm_nm,
itm_area: Number(r.itm_area || 0),
add_area: Number(r.add_area || 0),
itm_amt: Number(r.itm_amt),
itm_qty: Number(r.itm_qty),
// area
area: isServiceItem(r)
? 1
: (r.itm_qty ? Number(r.itm_area) / Number(r.itm_qty) : 0),
dis_rt: Number(r.dis_rt || 0), // 문자열 → 숫자
supply: Number(r.buy_amt),
vat_amt: Number(r.vat_amt),
sum_amt: Number(r.sum_amt),
end_dt: r.end_dt || '',
ok_yn: r.ok_yn || 'N',
rmks: r.rmks ?? '', // undefined 방지
_existing: true
})
g.set(i + 1, {
w2ui: { style: 'background:#f3f3f3;color:#555' }
})
})
g.refresh()
// 🔥 모든 행 재계산
g.records.forEach(r => {
if (!r._deleted) {
recalcRow(r.recid)
}
})
calcSummary()
})
.catch(() => w2alert('서버 오류'))
}
/* -------------------------------------------------
행 단위 재계산
------------------------------------------------- */
function recalcRow(recid) {
const g = w2ui.serviceGrid
const r = g.get(recid)
if (!r) return
const qty = Number(r.itm_qty || 0)
const unit = Number(r.itm_amt || 0)
const rate = Number(r.dis_rt || 0)
const area = Number(r.area || 0)
const add = Number(r.add_area || 0)
// 🔥 적용면적 = 제공량 * 수량
r.itm_area = area * qty
// 🔥 공급가액 = 단가 * 수량 * (1 - 할인율)
const base = unit * qty
const discount = Math.floor(base * (rate / 100))
r.supply = base - discount
// 🔥 부가세 / 결제금액
r.vat_amt = Math.floor(r.supply * 0.1)
r.sum_amt = r.supply + r.vat_amt
// 🔥 합계면적
r.sum_area = r.itm_area + add
g.refreshRow(recid)
calcSummary()
}
/* -------------------------------------------------
합계 계산
------------------------------------------------- */
function calcSummary() {
let supply = 0
let vat = 0
let total = 0
w2ui.serviceGrid.records.forEach(r => {
if (r._deleted) return
supply += Number(r.supply || 0)
vat += Number(r.vat_amt || 0)
total += Number(r.sum_amt || 0)
})
document.getElementById('sumSupply').innerText = supply.toLocaleString()
document.getElementById('sumVat').innerText = vat.toLocaleString()
document.getElementById('sumTotal').innerText = total.toLocaleString()
}
function saveService(ctx) {
const buyDate = document.getElementById('buyDate').value
if (!buyDate) {
w2alert('구매일을 선택하세요')
return
}
const g = w2ui.serviceGrid
// 🔥🔥🔥 핵심: 현재 편집중인 셀 강제 반영
g.save()
const items = g.records.map(r => ({
sq_no: r.sq_no || null,
itm_cd: r.itm_cd,
itm_area: r.itm_area,
add_area: r.add_area || 0,
itm_amt: r.itm_amt,
itm_qty: r.itm_qty,
dis_rt: r.dis_rt,
buy_amt: r.supply,
vat_amt: r.vat_amt,
sum_amt: r.sum_amt,
end_dt: dateToYMD(r.end_dt),
ok_yn: r.ok_yn || 'N',
rmks: r.rmks,
_new: r._new || false,
_existing: r._existing || false,
_deleted: r._deleted || false
}))
fetch('/kngil/bbs/adm_service.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'save',
member_id: ctx.memberId,
buy_date: buyDate,
items
})
})
.then(res => res.json())
.then(res => {
if (res.status === 'success') {
w2alert('저장되었습니다.')
loadExistingPurchase(ctx.memberId, buyDate)
} else {
w2alert(res.message || '저장 실패')
}
})
}
function normalizeDate(val) {
if (!val) return null
// 이미 YYYY-MM-DD면 그대로
if (/^\d{4}-\d{2}-\d{2}$/.test(val)) {
return val
}
// MM/DD/YYYY → YYYY-MM-DD
const d = new Date(val)
if (isNaN(d)) return null
return d.toISOString().slice(0, 10)
}
function dateToYMD(val) {
if (!val) return null
// Date 객체
if (val instanceof Date) {
return [
val.getFullYear(),
String(val.getMonth() + 1).padStart(2, '0'),
String(val.getDate()).padStart(2, '0')
].join('-')
}
// 문자열 (MM/DD/YYYY or YYYY-MM-DD)
if (typeof val === 'string') {
// 이미 YYYY-MM-DD
if (/^\d{4}-\d{2}-\d{2}$/.test(val)) {
return val
}
// MM/DD/YYYY
const d = new Date(val)
if (!isNaN(d)) {
return [
d.getFullYear(),
String(d.getMonth() + 1).padStart(2, '0'),
String(d.getDate()).padStart(2, '0')
].join('-')
}
}
return null
}

158
kngil/js/adm_use_history.js Normal file
View File

@@ -0,0 +1,158 @@
import { w2grid, w2ui, w2popup, w2alert } from 'https://cdn.jsdelivr.net/gh/vitmalina/w2ui@master/dist/w2ui.es6.min.js'
// 파일이 실제 존재하는지 확인 필수! 존재하지 않으면 아래 코드가 모두 멈춥니다.
import { bindProductPopupEvents } from '/kngil/js/adm_common.js'
export function openuseHistoryPopup(memberId = '',isSuperAdmin='') {
// 기존 그리드 안전하게 제거
if (w2ui.UseHistoryGrid) {
w2ui.UseHistoryGrid.destroy();
}
w2popup.open({
title: '서비스 사용 이력',
width: 1100,
height: 600,
modal: true,
body:
/*
`
<div style="padding:8px">
<div id="UseHistoryGrid" style="height:520px;"></div>
</div>
*/
`
<div id="popupMainContainer" style="display: flex; flex-direction: column; height: 100%; padding: 10px; gap: 10px; box-sizing: border-box;">
<div id="searchFormArea" style="display: flex; align-items: center; gap: 20px; padding: 15px; background: #f9f9f9; border: 1px solid #ddd; border-radius: 4px;">
<div style="display: flex; align-items: center; gap: 8px;">
<span style="font-weight: bold; color: #333; white-space: nowrap;">회원 ID</span>
<input type="text" id="searchMemberId" value="${memberId}"
${!isSuperAdmin ? 'disabled' : ''}
style="width: 120px; padding: 6px; border: 1px solid #ccc; border-radius: 4px;"
placeholder="아이디">
</div>
<div style="display: flex; align-items: center; gap: 8px;">
<span style="font-weight: bold; color: #333; white-space: nowrap;">성명</span>
<input type="text" id="searchUserNm"
style="width: 120px; padding: 6px; border: 1px solid #ccc; border-radius: 4px;"
placeholder="성명 입력">
</div>
<div style="display: flex; align-items: center; gap: 8px;">
<span style="font-weight: bold; color: #333; white-space: nowrap;">부서</span>
<input type="text" id="searchDeptNm"
style="width: 150px; padding: 6px; border: 1px solid #ccc; border-radius: 4px;"
placeholder="부서명 입력">
</div>
<button id="btnSearchHistory"
style="padding: 6px 20px; background: #1565c0; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; margin-left: auto;">
검색
</button>
</div>
<div id="UseHistoryGrid" style="flex: 1; min-height: 450px; border: 1px solid #ddd;"></div>
</div>
`,
onOpen(event) {
event.onComplete = () => {
const searchBtn = document.getElementById('btnSearchHistory');
const searchInput = document.getElementById('searchMemberId');
const inputUnm = document.getElementById('searchUserNm');
const inputDnm = document.getElementById('searchDeptNm');
// 조회 버튼 클릭 시 동작
if (searchBtn) {
searchBtn.onclick = () => {
loadUseHistoryData(searchInput.value,inputUnm,inputDnm);
};
}
// 엔터키 지원
if (searchInput,inputUnm,inputDnm) {
searchInput.onkeydown = (e) => {
if (e.key === 'Enter') searchBtn.click();
};
}
createUseHistoryGrid(memberId);
loadUseHistoryData(memberId);
}
}
});
}
function createUseHistoryGrid(memberId) {
new w2grid({
name: 'UseHistoryGrid',
box: '#UseHistoryGrid',
show: {
//toolbar: true,
footer: true,
lineNumbers: true // 행 번호를 표시하면 디버깅이 편합니다.
},
columns: [
// field 이름이 PHP에서 내려주는 JSON 키값과 정확히 일치해야 함!
{ field: 'member_id', text: '회원ID', size: '100px',style: 'text-align: center', attr: 'align=center',hidden: true }, // name 아님!
{ field: 'use_dt', text: '사용일자', size: '100px',style: 'text-align: center', attr: 'align=center' ,sortable: true}, // name 아님!
{ field: 'user_id', text: '사용자ID', size: '100px',style: 'text-align: center', attr: 'align=center' ,sortable: true}, // name 아님!
{ field: 'sq_no', text: '순번', size: '100px',style: 'text-align: center', attr: 'align=center' ,hidden: true}, // name 아님!
{ field: 'user_nm', text: '성명', size: '100px',style: 'text-align: center', attr: 'align=center' ,sortable: true}, // name 아님!
{ field: 'dept_nm', text: '부서', size: '100px',style: 'text-align: center', attr: 'align=center' ,sortable: true}, // name 아님!
{ field: 'posit_nm', text: '직위', size: '100px',style: 'text-align: center', attr: 'align=center' ,sortable: true}, // name 아님!
{ field: 'use_yn', text: '사용여부', size: '80px', attr: 'align=center' ,sortable: true}, // name 아님!
{ field: 'use_area', text: '사용면적', size: '100px',render: 'number:0' , attr: 'align=center' ,sortable: true}, // name 아님!
{ field: 'ser_bc', text: '서비스구분', size: '150px', attr: 'align=center' ,sortable: true}, // name 아님!
]
});
}
async function loadUseHistoryData(memberId = ''){
try {
const grid = w2ui.UseHistoryGrid;
if (!grid) return;
// DOM에서 현재 입력된 값을 실시간으로 읽어옴
const sMid = document.getElementById('searchMemberId')?.value || '';
const sUnm = document.getElementById('searchUserNm')?.value || '';
const sDnm = document.getElementById('searchDeptNm')?.value || '';
grid.lock('조회 중...', true);
const searchParams = new URLSearchParams();
searchParams.append('member_id', sMid);
searchParams.append('user_nm', sUnm);
searchParams.append('dept_nm', sDnm);
const response = await fetch('/kngil/bbs/adm_use_history.php', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: searchParams
});
if (!response.ok) throw new Error(`서버 응답 오류: ${response.status}`);
const data = await response.json();
if (!Array.isArray(data)) {
throw new Error('응답 데이터 형식 오류');
}
grid.clear();
grid.add(data);
grid.unlock();
} catch (e) {
console.error(e);
if (w2ui.UseHistoryGrid) w2ui.UseHistoryGrid.unlock();
w2alert('사용이력 조회 실패: ' + e.message);
}
}
////////////////////////////////////////////////////////////////////////

4289
kngil/js/common.js Normal file

File diff suppressed because one or more lines are too long

1325
kngil/js/faq/faq_common.js Normal file

File diff suppressed because it is too large Load Diff

256
kngil/js/faq/faq_popup.js Normal file
View File

@@ -0,0 +1,256 @@
window.onload = function() {
$.ajax({
url: "some_api_endpoint",
success: function(response) {
},
complete: function() {
// 팝업 닫기버튼
$(document).ready(function() {
$('.btn_close').click(function() {
$('.popup_wrap').hide();
$('body').css('overflow', ''); // 기본 스크롤 상태로 복귀
lenis.start();
console.log('lenis 재시작')
});
});
$(document).ready(function() {
$('.btn_map_close').click(function() {
$('.popup_sitemap').hide();
$('body').css('overflow', ''); // 기본 스크롤 상태로 복귀
lenis.start();
console.log('lenis 재시작')
});
});
// 이메일 직접입력
$(document).ready(function() {
$('#domain-list').change(function() {
if ($(this).val() === 'type') {
$('#custom-domain').show().focus();
} else {
$('#custom-domain').hide().val('');
}
});
});
// 인증번호 타이머
$(document).ready(function() {
// 인증번호 버튼 클릭 시
$('.cert_number').click(function() {
$('.code').show();
});
// 확인 버튼 클릭 시
$('.check').click(function() {
$(this).hide();
$('.check.complete').show();
clearInterval(interval); // 타이머 멈춤
$('.timer').remove(); // 타이머 요소 삭제
});
// 타이머 함수
var interval;
function startTimer(duration, display) {
clearInterval(interval);
var timer = duration, minutes, seconds;
function updateTimer() {
minutes = parseInt(timer / 60, 10);
seconds = parseInt(timer % 60, 10);
minutes = minutes < 10 ? "0" + minutes : minutes;
seconds = seconds < 10 ? "0" + seconds : seconds;
display.text(minutes + ":" + seconds);
if (--timer < 0) {
clearInterval(interval);
display.text("00:00");
}
}
updateTimer();
interval = setInterval(updateTimer, 1000);
}
var threeMinutes = 60 * 3,
display = $('.timer');
startTimer(threeMinutes, display);
$('.cert_number').click(function() {
startTimer(threeMinutes, display);
});
});
// 아이디찾기
$(document).ready(function(){
$('.find_email').click(function(){
$('.find_ph').removeClass('on').prop('checked', false);
$(this).addClass('on').prop('checked', true);
$('.ph').hide();
$('.email').show();
});
$('.find_ph').click(function(){
$('.find_email').removeClass('on').prop('checked', false);
$(this).addClass('on').prop('checked', true);
$('.email').hide();
$('.ph').show();
});
});
$(document).ready(function() {
$('.btn_id').on('click', function() {
$('.btn_id').addClass('on');
$('.btn_pw').removeClass('on');
$('.content.id').show();
$('.content.pw').hide();
});
$('.btn_pw').on('click', function() {
$('.btn_pw').addClass('on');
$('.btn_id').removeClass('on');
$('.content.pw').show();
$('.content.id').hide();
});
$('#domain-list').on('change', function() {
if ($(this).val() === 'type') {
$('#custom-domain').show();
} else {
$('#custom-domain').hide();
}
});
});
// 전체약관동의
// $(document).ready(function() {
// function toggleJoinButton() {
// // 모든 개별 체크박스가 체크되었는지 확인
// var allChecked = $('.terms_wrap input[type="checkbox"]').length === $('.terms_wrap input[type="checkbox"]:checked').length;
// // '약관에 모두 동의합니다' 체크박스 상태에 따라 조정
// $('.checkbox_wrap.all input[type="checkbox"]').prop('checked', allChecked);
// // 모든 체크박스가 체크되지 않은 경우 버튼에 'none' 클래스 추가하고 disabled 속성 추가
// if (allChecked) {
// $('.join_btn_wrap').removeClass('none');
// $('.join_btn_wrap button').prop('disabled', false);
// } else {
// $('.join_btn_wrap').addClass('none');
// $('.join_btn_wrap button').prop('disabled', true);
// }
// }
// // '약관에 모두 동의합니다' 체크박스의 변경 이벤트
// $('.checkbox_wrap.all input[type="checkbox"]').on('change', function() {
// var isChecked = $(this).is(':checked');
// $('.terms_wrap input[type="checkbox"]').prop('checked', isChecked);
// toggleJoinButton(); // 버튼 상태 업데이트
// });
// // 각 terms_wrap의 개별 체크박스 변경 이벤트
// $('.terms_wrap input[type="checkbox"]').on('change', function() {
// toggleJoinButton(); // 버튼 상태 업데이트
// });
// // 초기 상태 설정
// toggleJoinButton();
// });
// 전체약관동의 수정 250813
(function ($) {
if (window.__agreeBound) return; // 중복 바인딩 방지
window.__agreeBound = true;
const $container = $('#pop_agreement');
const $items = $container.find('.terms_wrap input[type="checkbox"]'); // agree11, agree21
const $all = $container.find('.checkbox_wrap.all input[type="checkbox"]');
const $btn = $container.find('#btn_agree');
function syncAll() {
const allChecked = $items.length > 0 && $items.filter(':checked').length === $items.length;
$all.prop('checked', allChecked);
// 버튼은 disable 하지 않음 (스타일만 조정하고 싶다면 클래스만 토글)
// $('.join_btn_wrap').toggleClass('none', !allChecked); <-- 필요 없으면 제거
}
// 전체동의 → 개별
$all.on('change', function () {
const on = $(this).is(':checked');
$items.prop('checked', on);
syncAll();
});
// 개별 → 전체동의 동기화
$items.on('change', syncAll);
// 동의 버튼 클릭 시에만 검사
$btn.off('click.agree').on('click.agree', function (e) {
e.preventDefault();
const allChecked = $items.filter(':checked').length === $items.length;
if (!allChecked) {
alert('약관에 모두 동의해주세요.');
return false;
}
// 통과 시 다음 단계로 진행(필요 시 주석 해제)
// $('#pop_agreement').hide();
// $('#pop_register_form').show();
// $('body').css('overflow','hidden');
});
// 외부에서 팝업 열 때 상태 초기화가 필요하면 이 함수 호출
window.resetAgreementUI = function () {
$items.prop('checked', false);
$all.prop('checked', false);
syncAll();
// 버튼은 항상 활성
$('#btn_agree, #fregister button[type=submit]')
.prop('disabled', false)
.css('pointer-events', 'auto');
};
})(jQuery);
// 가입완료
$(document).ready(function() {
$('.join.completion .join_btn_wrap button').click(function() {
$('.pop_input_wrap form').children().not('.messages').hide();
$('.messages').show();
});
});
$(document).ready(function() {
$('.join.completion .join_btn_wrap button').click(function() {
$('.pop_input_wrap form').children().not('.messages').hide();
$('.messages').show();
// 세 번째 단계에 'on' 클래스 추가하고, 다른 단계에서 'on' 클래스 제거
$('.join_progress .join_step').removeClass('on');
$('.join_progress .join_step').eq(2).addClass('on');
});
});
// 개인정보 보호정책 스크립트
$(document).ready(function() {
$('.tab_privacy').on('click', function() {
$(this).addClass('on');
$('.tab_agreement').removeClass('on');
$('.content.pri').addClass('show').removeClass('hide');
$('.content.agr').removeClass('show').addClass('hide');
});
$('.tab_agreement').on('click', function() {
$(this).addClass('on');
$('.tab_privacy').removeClass('on');
$('.content.agr').addClass('show').removeClass('hide');
$('.content.pri').removeClass('show').addClass('hide');
});
});
}
});
};

5
kngil/js/faq/jquery-1.12.4.min.js vendored Normal file

File diff suppressed because one or more lines are too long

2
kngil/js/faq/jquery-3.6.1.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

109
kngil/js/faq/jquery.menu.js Normal file
View File

@@ -0,0 +1,109 @@
$(function(){
var hide_menu = false;
var mouse_event = false;
var oldX = oldY = 0;
$(document).mousemove(function(e) {
if(oldX == 0) {
oldX = e.pageX;
oldY = e.pageY;
}
if(oldX != e.pageX || oldY != e.pageY) {
mouse_event = true;
}
});
// 주메뉴
var $gnb = $(".gnb_1dli > a");
$gnb.mouseover(function() {
if(mouse_event) {
$("#hd").addClass("hd_zindex");
$(".gnb_1dli").removeClass("gnb_1dli_over gnb_1dli_over2 gnb_1dli_on");
$(this).parent().addClass("gnb_1dli_over gnb_1dli_on");
menu_rearrange($(this).parent());
hide_menu = false;
}
});
$gnb.mouseout(function() {
hide_menu = true;
});
$(".gnb_2dli").mouseover(function() {
hide_menu = false;
});
$(".gnb_2dli").mouseout(function() {
hide_menu = true;
});
$gnb.focusin(function() {
$("#hd").addClass("hd_zindex");
$(".gnb_1dli").removeClass("gnb_1dli_over gnb_1dli_over2 gnb_1dli_on");
$(this).parent().addClass("gnb_1dli_over gnb_1dli_on");
menu_rearrange($(this).parent());
hide_menu = false;
});
$gnb.focusout(function() {
hide_menu = true;
});
$(".gnb_2da").focusin(function() {
$(".gnb_1dli").removeClass("gnb_1dli_over gnb_1dli_over2 gnb_1dli_on");
var $gnb_li = $(this).closest(".gnb_1dli").addClass("gnb_1dli_over gnb_1dli_on");
menu_rearrange($(this).closest(".gnb_1dli"));
hide_menu = false;
});
$(".gnb_2da").focusout(function() {
hide_menu = true;
});
$('#gnb_1dul>li').bind('mouseleave',function(){
submenu_hide();
});
$(document).bind('click focusin',function(){
if(hide_menu) {
submenu_hide();
}
});
});
function submenu_hide() {
$("#hd").removeClass("hd_zindex");
$(".gnb_1dli").removeClass("gnb_1dli_over gnb_1dli_over2 gnb_1dli_on");
}
function menu_rearrange(el)
{
var width = $("#gnb_1dul").width();
var left = w1 = w2 = 0;
var idx = $(".gnb_1dli").index(el);
var max_menu_count = 0;
var $gnb_1dli;
for(i=0; i<=idx; i++) {
$gnb_1dli = $(".gnb_1dli:eq("+i+")");
w1 = $gnb_1dli.outerWidth();
if($gnb_1dli.find(".gnb_2dul").length)
w2 = $gnb_1dli.find(".gnb_2dli > a").outerWidth(true);
else
w2 = w1;
if((left + w2) > width) {
if(max_menu_count == 0)
max_menu_count = i + 1;
}
if(max_menu_count > 0 && (idx + 1) % max_menu_count == 0) {
el.removeClass("gnb_1dli_over").addClass("gnb_1dli_over2");
left = 0;
} else {
left += w1;
}
}
}

5
kngil/js/faq/jquery.mousewheel.min.js vendored Normal file
View File

@@ -0,0 +1,5 @@
/*!
* jQuery Mousewheel 3.1.13
* Copyright OpenJS Foundation and other contributors
*/
!function(e){"function"==typeof define&&define.amd?define(["jquery"],e):"object"==typeof exports?module.exports=e:e(jQuery)}(function(a){var u,r,e=["wheel","mousewheel","DOMMouseScroll","MozMousePixelScroll"],t="onwheel"in window.document||9<=window.document.documentMode?["wheel"]:["mousewheel","DomMouseScroll","MozMousePixelScroll"],f=Array.prototype.slice;if(a.event.fixHooks)for(var n=e.length;n;)a.event.fixHooks[e[--n]]=a.event.mouseHooks;var d=a.event.special.mousewheel={version:"3.1.12",setup:function(){if(this.addEventListener)for(var e=t.length;e;)this.addEventListener(t[--e],i,!1);else this.onmousewheel=i;a.data(this,"mousewheel-line-height",d.getLineHeight(this)),a.data(this,"mousewheel-page-height",d.getPageHeight(this))},teardown:function(){if(this.removeEventListener)for(var e=t.length;e;)this.removeEventListener(t[--e],i,!1);else this.onmousewheel=null;a.removeData(this,"mousewheel-line-height"),a.removeData(this,"mousewheel-page-height")},getLineHeight:function(e){var t=a(e),e=t["offsetParent"in a.fn?"offsetParent":"parent"]();return e.length||(e=a("body")),parseInt(e.css("fontSize"),10)||parseInt(t.css("fontSize"),10)||16},getPageHeight:function(e){return a(e).height()},settings:{adjustOldDeltas:!0,normalizeOffset:!0}};function i(e){var t,n=e||window.event,i=f.call(arguments,1),o=0,l=0,s=0,h=0;if((e=a.event.fix(n)).type="mousewheel","detail"in n&&(s=-1*n.detail),"wheelDelta"in n&&(s=n.wheelDelta),"wheelDeltaY"in n&&(s=n.wheelDeltaY),"wheelDeltaX"in n&&(l=-1*n.wheelDeltaX),"axis"in n&&n.axis===n.HORIZONTAL_AXIS&&(l=-1*s,s=0),o=0===s?l:s,"deltaY"in n&&(o=s=-1*n.deltaY),"deltaX"in n&&(l=n.deltaX,0===s&&(o=-1*l)),0!==s||0!==l)return 1===n.deltaMode?(o*=t=a.data(this,"mousewheel-line-height"),s*=t,l*=t):2===n.deltaMode&&(o*=t=a.data(this,"mousewheel-page-height"),s*=t,l*=t),h=Math.max(Math.abs(s),Math.abs(l)),(!r||h<r)&&c(n,r=h)&&(r/=40),c(n,h)&&(o/=40,l/=40,s/=40),o=Math[1<=o?"floor":"ceil"](o/r),l=Math[1<=l?"floor":"ceil"](l/r),s=Math[1<=s?"floor":"ceil"](s/r),d.settings.normalizeOffset&&this.getBoundingClientRect&&(h=this.getBoundingClientRect(),e.offsetX=e.clientX-h.left,e.offsetY=e.clientY-h.top),e.deltaX=l,e.deltaY=s,e.deltaFactor=r,e.deltaMode=0,i.unshift(e,o,l,s),u&&window.clearTimeout(u),u=window.setTimeout(w,200),(a.event.dispatch||a.event.handle).apply(this,i)}function w(){r=null}function c(e,t){return d.settings.adjustOldDeltas&&"mousewheel"===e.type&&t%120==0}a.fn.extend({mousewheel:function(e){return e?this.on("mousewheel",e):this.trigger("mousewheel")},unmousewheel:function(e){return this.off("mousewheel",e)}})});

7
kngil/js/faq/owl.carousel.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,186 @@
/**
* Owl Carousel v2.3.4
* Copyright 2013-2018 David Deutsch
* Licensed under: SEE LICENSE IN https://github.com/OwlCarousel2/OwlCarousel2/blob/master/LICENSE
*/
/*
* Owl Carousel - Core
*/
.owl-carousel {
display: none;
width: 100%;
-webkit-tap-highlight-color: transparent;
/* position relative and z-index fix webkit rendering fonts issue */
position: relative;
z-index: 1; }
.owl-carousel .owl-stage {
position: relative;
-ms-touch-action: pan-Y;
touch-action: manipulation;
-moz-backface-visibility: hidden;
/* fix firefox animation glitch */ }
.owl-carousel .owl-stage:after {
content: ".";
display: block;
clear: both;
visibility: hidden;
line-height: 0;
height: 0; }
.owl-carousel .owl-stage-outer {
position: relative;
overflow: hidden;
/* fix for flashing background */
-webkit-transform: translate3d(0px, 0px, 0px); }
.owl-carousel .owl-wrapper,
.owl-carousel .owl-item {
-webkit-backface-visibility: hidden;
-moz-backface-visibility: hidden;
-ms-backface-visibility: hidden;
-webkit-transform: translate3d(0, 0, 0);
-moz-transform: translate3d(0, 0, 0);
-ms-transform: translate3d(0, 0, 0); }
.owl-carousel .owl-item {
position: relative;
min-height: 1px;
float: left;
-webkit-backface-visibility: hidden;
-webkit-tap-highlight-color: transparent;
-webkit-touch-callout: none; }
.owl-carousel .owl-item img {
display: block;
width: 100%; }
.owl-carousel .owl-nav.disabled,
.owl-carousel .owl-dots.disabled {
display: none; }
.owl-carousel .owl-nav .owl-prev,
.owl-carousel .owl-nav .owl-next,
.owl-carousel .owl-dot {
cursor: pointer;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none; }
.owl-carousel .owl-nav button.owl-prev,
.owl-carousel .owl-nav button.owl-next,
.owl-carousel button.owl-dot {
background: none;
color: inherit;
border: none;
padding: 0 !important;
font: inherit; }
.owl-carousel.owl-loaded {
display: block; }
.owl-carousel.owl-loading {
opacity: 0;
display: block; }
.owl-carousel.owl-hidden {
opacity: 0; }
.owl-carousel.owl-refresh .owl-item {
visibility: hidden; }
.owl-carousel.owl-drag .owl-item {
-ms-touch-action: pan-y;
touch-action: pan-y;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none; }
.owl-carousel.owl-grab {
cursor: move;
cursor: grab; }
.owl-carousel.owl-rtl {
direction: rtl; }
.owl-carousel.owl-rtl .owl-item {
float: right; }
/* No Js */
.no-js .owl-carousel {
display: block; }
/*
* Owl Carousel - Animate Plugin
*/
.owl-carousel .animated {
animation-duration: 1000ms;
animation-fill-mode: both; }
.owl-carousel .owl-animated-in {
z-index: 0; }
.owl-carousel .owl-animated-out {
z-index: 1; }
.owl-carousel .fadeOut {
animation-name: fadeOut; }
@keyframes fadeOut {
0% {
opacity: 1; }
100% {
opacity: 0; } }
/*
* Owl Carousel - Auto Height Plugin
*/
.owl-height {
transition: height 500ms ease-in-out; }
/*
* Owl Carousel - Lazy Load Plugin
*/
.owl-carousel .owl-item {
/**
This is introduced due to a bug in IE11 where lazy loading combined with autoheight plugin causes a wrong
calculation of the height of the owl-item that breaks page layouts
*/ }
.owl-carousel .owl-item .owl-lazy {
opacity: 0;
transition: opacity 400ms ease; }
.owl-carousel .owl-item .owl-lazy[src^=""], .owl-carousel .owl-item .owl-lazy:not([src]) {
max-height: 0; }
.owl-carousel .owl-item img.owl-lazy {
transform-style: preserve-3d; }
/*
* Owl Carousel - Video Plugin
*/
.owl-carousel .owl-video-wrapper {
position: relative;
height: 100%;
background: #000; }
.owl-carousel .owl-video-play-icon {
position: absolute;
height: 80px;
width: 80px;
left: 50%;
top: 50%;
margin-left: -40px;
margin-top: -40px;
background: url("owl.video.play.png") no-repeat;
cursor: pointer;
z-index: 1;
-webkit-backface-visibility: hidden;
transition: transform 100ms ease; }
.owl-carousel .owl-video-play-icon:hover {
-ms-transform: scale(1.3, 1.3);
transform: scale(1.3, 1.3); }
.owl-carousel .owl-video-playing .owl-video-tn,
.owl-carousel .owl-video-playing .owl-video-play-icon {
display: none; }
.owl-carousel .owl-video-tn {
opacity: 0;
height: 100%;
background-position: center center;
background-repeat: no-repeat;
background-size: contain;
transition: opacity 400ms ease; }
.owl-carousel .owl-video-frame {
position: relative;
z-index: 1;
height: 100%;
width: 100%; }

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,6 @@
/**
* Owl Carousel v2.3.4
* Copyright 2013-2018 David Deutsch
* Licensed under: SEE LICENSE IN https://github.com/OwlCarousel2/OwlCarousel2/blob/master/LICENSE
*/
.owl-carousel,.owl-carousel .owl-item{-webkit-tap-highlight-color:transparent;position:relative}.owl-carousel{display:none;width:100%;z-index:1}.owl-carousel .owl-stage{position:relative;-ms-touch-action:pan-Y;touch-action:manipulation;-moz-backface-visibility:hidden}.owl-carousel .owl-stage:after{content:".";display:block;clear:both;visibility:hidden;line-height:0;height:0}.owl-carousel .owl-stage-outer{position:relative;overflow:hidden;-webkit-transform:translate3d(0,0,0)}.owl-carousel .owl-item,.owl-carousel .owl-wrapper{-webkit-backface-visibility:hidden;-moz-backface-visibility:hidden;-ms-backface-visibility:hidden;-webkit-transform:translate3d(0,0,0);-moz-transform:translate3d(0,0,0);-ms-transform:translate3d(0,0,0)}.owl-carousel .owl-item{min-height:1px;float:left;-webkit-backface-visibility:hidden;-webkit-touch-callout:none}.owl-carousel .owl-item img{display:block;width:100%}.owl-carousel .owl-dots.disabled,.owl-carousel .owl-nav.disabled{display:none}.no-js .owl-carousel,.owl-carousel.owl-loaded{display:block}.owl-carousel .owl-dot,.owl-carousel .owl-nav .owl-next,.owl-carousel .owl-nav .owl-prev{cursor:pointer;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.owl-carousel .owl-nav button.owl-next,.owl-carousel .owl-nav button.owl-prev,.owl-carousel button.owl-dot{background:0 0;color:inherit;border:none;padding:0!important;font:inherit}.owl-carousel.owl-loading{opacity:0;display:block}.owl-carousel.owl-hidden{opacity:0}.owl-carousel.owl-refresh .owl-item{visibility:hidden}.owl-carousel.owl-drag .owl-item{-ms-touch-action:pan-y;touch-action:pan-y;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.owl-carousel.owl-grab{cursor:move;cursor:grab}.owl-carousel.owl-rtl{direction:rtl}.owl-carousel.owl-rtl .owl-item{float:right}.owl-carousel .animated{animation-duration:1s;animation-fill-mode:both}.owl-carousel .owl-animated-in{z-index:0}.owl-carousel .owl-animated-out{z-index:1}.owl-carousel .fadeOut{animation-name:fadeOut}@keyframes fadeOut{0%{opacity:1}100%{opacity:0}}.owl-height{transition:height .5s ease-in-out}.owl-carousel .owl-item .owl-lazy{opacity:0;transition:opacity .4s ease}.owl-carousel .owl-item .owl-lazy:not([src]),.owl-carousel .owl-item .owl-lazy[src^=""]{max-height:0}.owl-carousel .owl-item img.owl-lazy{transform-style:preserve-3d}.owl-carousel .owl-video-wrapper{position:relative;height:100%;background:#000}.owl-carousel .owl-video-play-icon{position:absolute;height:80px;width:80px;left:50%;top:50%;margin-left:-40px;margin-top:-40px;background:url(owl.video.play.png) no-repeat;cursor:pointer;z-index:1;-webkit-backface-visibility:hidden;transition:transform .1s ease}.owl-carousel .owl-video-play-icon:hover{-ms-transform:scale(1.3,1.3);transform:scale(1.3,1.3)}.owl-carousel .owl-video-playing .owl-video-play-icon,.owl-carousel .owl-video-playing .owl-video-tn{display:none}.owl-carousel .owl-video-tn{opacity:0;height:100%;background-position:center center;background-repeat:no-repeat;background-size:contain;transition:opacity .4s ease}.owl-carousel .owl-video-frame{position:relative;z-index:1;height:100%;width:100%}

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

25
kngil/js/faq/placeholders.min.js vendored Normal file
View File

@@ -0,0 +1,25 @@
/* Placeholders.js v4.0.1 */
/*!
* The MIT License
*
* Copyright (c) 2012 James Allardice
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
!function(a){"use strict";function b(){}function c(){try{return document.activeElement}catch(a){}}function d(a,b){for(var c=0,d=a.length;d>c;c++)if(a[c]===b)return!0;return!1}function e(a,b,c){return a.addEventListener?a.addEventListener(b,c,!1):a.attachEvent?a.attachEvent("on"+b,c):void 0}function f(a,b){var c;a.createTextRange?(c=a.createTextRange(),c.move("character",b),c.select()):a.selectionStart&&(a.focus(),a.setSelectionRange(b,b))}function g(a,b){try{return a.type=b,!0}catch(c){return!1}}function h(a,b){if(a&&a.getAttribute(B))b(a);else for(var c,d=a?a.getElementsByTagName("input"):N,e=a?a.getElementsByTagName("textarea"):O,f=d?d.length:0,g=e?e.length:0,h=f+g,i=0;h>i;i++)c=f>i?d[i]:e[i-f],b(c)}function i(a){h(a,k)}function j(a){h(a,l)}function k(a,b){var c=!!b&&a.value!==b,d=a.value===a.getAttribute(B);if((c||d)&&"true"===a.getAttribute(C)){a.removeAttribute(C),a.value=a.value.replace(a.getAttribute(B),""),a.className=a.className.replace(A,"");var e=a.getAttribute(I);parseInt(e,10)>=0&&(a.setAttribute("maxLength",e),a.removeAttribute(I));var f=a.getAttribute(D);return f&&(a.type=f),!0}return!1}function l(a){var b=a.getAttribute(B);if(""===a.value&&b){a.setAttribute(C,"true"),a.value=b,a.className+=" "+z;var c=a.getAttribute(I);c||(a.setAttribute(I,a.maxLength),a.removeAttribute("maxLength"));var d=a.getAttribute(D);return d?a.type="text":"password"===a.type&&g(a,"text")&&a.setAttribute(D,"password"),!0}return!1}function m(a){return function(){P&&a.value===a.getAttribute(B)&&"true"===a.getAttribute(C)?f(a,0):k(a)}}function n(a){return function(){l(a)}}function o(a){return function(){i(a)}}function p(a){return function(b){return v=a.value,"true"===a.getAttribute(C)&&v===a.getAttribute(B)&&d(x,b.keyCode)?(b.preventDefault&&b.preventDefault(),!1):void 0}}function q(a){return function(){k(a,v),""===a.value&&(a.blur(),f(a,0))}}function r(a){return function(){a===c()&&a.value===a.getAttribute(B)&&"true"===a.getAttribute(C)&&f(a,0)}}function s(a){var b=a.form;b&&"string"==typeof b&&(b=document.getElementById(b),b.getAttribute(E)||(e(b,"submit",o(b)),b.setAttribute(E,"true"))),e(a,"focus",m(a)),e(a,"blur",n(a)),P&&(e(a,"keydown",p(a)),e(a,"keyup",q(a)),e(a,"click",r(a))),a.setAttribute(F,"true"),a.setAttribute(B,T),(P||a!==c())&&l(a)}var t=document.createElement("input"),u=void 0!==t.placeholder;if(a.Placeholders={nativeSupport:u,disable:u?b:i,enable:u?b:j},!u){var v,w=["text","search","url","tel","email","password","number","textarea"],x=[27,33,34,35,36,37,38,39,40,8,46],y="#ccc",z="placeholdersjs",A=new RegExp("(?:^|\\s)"+z+"(?!\\S)"),B="data-placeholder-value",C="data-placeholder-active",D="data-placeholder-type",E="data-placeholder-submit",F="data-placeholder-bound",G="data-placeholder-focus",H="data-placeholder-live",I="data-placeholder-maxlength",J=100,K=document.getElementsByTagName("head")[0],L=document.documentElement,M=a.Placeholders,N=document.getElementsByTagName("input"),O=document.getElementsByTagName("textarea"),P="false"===L.getAttribute(G),Q="false"!==L.getAttribute(H),R=document.createElement("style");R.type="text/css";var S=document.createTextNode("."+z+" {color:"+y+";}");R.styleSheet?R.styleSheet.cssText=S.nodeValue:R.appendChild(S),K.insertBefore(R,K.firstChild);for(var T,U,V=0,W=N.length+O.length;W>V;V++)U=V<N.length?N[V]:O[V-N.length],T=U.attributes.placeholder,T&&(T=T.nodeValue,T&&d(w,U.type)&&s(U));var X=setInterval(function(){for(var a=0,b=N.length+O.length;b>a;a++)U=a<N.length?N[a]:O[a-N.length],T=U.attributes.placeholder,T?(T=T.nodeValue,T&&d(w,U.type)&&(U.getAttribute(F)||s(U),(T!==U.getAttribute(B)||"password"===U.type&&!U.getAttribute(D))&&("password"===U.type&&!U.getAttribute(D)&&g(U,"text")&&U.setAttribute(D,"password"),U.value===U.getAttribute(B)&&(U.value=T),U.setAttribute(B,T)))):U.getAttribute(C)&&(k(U),U.removeAttribute(B));Q||clearInterval(X)},J);e(a,"beforeunload",function(){M.disable()})}}(this);

371
kngil/js/faq/wrest.js Normal file
View File

@@ -0,0 +1,371 @@
var wrestMsg = "";
var wrestFld = null;
var wrestFldDefaultColor = "";
//var wrestFldBackColor = "#ff3061";
// subject 속성값을 얻어 return, 없으면 tag의 name을 넘김
function wrestItemname(fld)
{
//return fld.getAttribute("title") ? fld.getAttribute("title") : ( fld.getAttribute("alt") ? fld.getAttribute("alt") : fld.name );
var id = fld.getAttribute("id");
var labels = document.getElementsByTagName("label");
var el = null;
for(i=0; i<labels.length; i++) {
if(id == labels[i].htmlFor) {
el = labels[i];
break;
}
}
if(el != null) {
var text = el.innerHTML.replace(/[<].*[>].*[<]\/+.*[>]/gi, "");
if(text == '') {
return fld.getAttribute("title") ? fld.getAttribute("title") : ( fld.getAttribute("placeholder") ? fld.getAttribute("placeholder") : fld.name );
} else {
return text;
}
} else {
return fld.getAttribute("title") ? fld.getAttribute("title") : ( fld.getAttribute("placeholder") ? fld.getAttribute("placeholder") : fld.name );
}
}
// 양쪽 공백 없애기
function wrestTrim(fld)
{
var pattern = /(^\s+)|(\s+$)/g; // \s 공백 문자
return fld.value.replace(pattern, "");
}
// 필수 입력 검사
function wrestRequired(fld)
{
if (wrestTrim(fld) == "") {
if (wrestFld == null) {
// 셀렉트박스일 경우에도 필수 선택 검사합니다.
wrestMsg = wrestItemname(fld) + " : 필수 "+(fld.type=="select-one"?"선택":"입력")+"입니다.\n";
wrestFld = fld;
}
}
}
// 김선용 2006.3 - 전화번호(휴대폰) 형식 검사 : 123-123(4)-5678
function wrestTelNum(fld)
{
if (!wrestTrim(fld)) return;
var pattern = /^[0-9]{2,3}-[0-9]{3,4}-[0-9]{4}$/;
if(!pattern.test(fld.value)){
if(wrestFld == null){
wrestMsg = wrestItemname(fld)+" : 전화번호 형식이 올바르지 않습니다.\n\n하이픈(-)을 포함하여 입력하세요.\n";
wrestFld = fld;
fld.select();
}
}
}
// 이메일주소 형식 검사
function wrestEmail(fld)
{
if (!wrestTrim(fld)) return;
//var pattern = /(\S+)@(\S+)\.(\S+)/; 이메일주소에 한글 사용시
var pattern = /([0-9a-zA-Z_-]+)@([0-9a-zA-Z_-]+)\.([0-9a-zA-Z_-]+)/;
if (!pattern.test(fld.value)) {
if (wrestFld == null) {
wrestMsg = wrestItemname(fld) + " : 이메일주소 형식이 아닙니다.\n";
wrestFld = fld;
}
}
}
// 한글인지 검사 (자음, 모음 조합된 한글만 가능)
function wrestHangul(fld)
{
if (!wrestTrim(fld)) return;
//var pattern = /([^가-힣\x20])/i;
var pattern = /([^가-힣\x20])/;
if (pattern.test(fld.value)) {
if (wrestFld == null) {
wrestMsg = wrestItemname(fld) + ' : 한글이 아닙니다. (자음, 모음 조합된 한글만 가능)\n';
wrestFld = fld;
}
}
}
// 한글인지 검사2 (자음, 모음만 있는 한글도 가능)
function wrestHangul2(fld)
{
if (!wrestTrim(fld)) return;
var pattern = /([^가-힣ㄱ-ㅎㅏ-ㅣ\x20])/i;
//var pattern = /([^가-힣ㄱ-ㅎㅏ-ㅣ\x20])/;
if (pattern.test(fld.value)) {
if (wrestFld == null) {
wrestMsg = wrestItemname(fld) + ' : 한글이 아닙니다.\n';
wrestFld = fld;
}
}
}
// 한글,영문,숫자인지 검사3
function wrestHangulAlNum(fld)
{
if (!wrestTrim(fld)) return;
var pattern = /([^가-힣\x20^a-z^A-Z^0-9])/i;
if (pattern.test(fld.value)) {
if (wrestFld == null) {
wrestMsg = wrestItemname(fld) + ' : 한글, 영문, 숫자가 아닙니다.\n';
wrestFld = fld;
}
}
}
// 한글,영문 인지 검사
function wrestHangulAlpha(fld)
{
if (!wrestTrim(fld)) return;
var pattern = /([^가-힣\x20^a-z^A-Z])/i;
if (pattern.test(fld.value)) {
if (wrestFld == null) {
wrestMsg = wrestItemname(fld) + ' : 한글, 영문이 아닙니다.\n';
wrestFld = fld;
}
}
}
// 숫자인지검사
// 배부른꿀꿀이님 추가 (http://dasir.com) 2003-06-24
function wrestNumeric(fld)
{
if (fld.value.length > 0) {
for (i = 0; i < fld.value.length; i++) {
if (fld.value.charAt(i) < '0' || fld.value.charAt(i) > '9') {
wrestMsg = wrestItemname(fld) + " : 숫자가 아닙니다.\n";
wrestFld = fld;
}
}
}
}
// 영문자 검사
// 배부른꿀꿀이님 추가 (http://dasir.com) 2003-06-24
function wrestAlpha(fld)
{
if (!wrestTrim(fld)) return;
var pattern = /(^[a-zA-Z]+$)/;
if (!pattern.test(fld.value)) {
if (wrestFld == null) {
wrestMsg = wrestItemname(fld) + " : 영문이 아닙니다.\n";
wrestFld = fld;
}
}
}
// 영문자와 숫자 검사
// 배부른꿀꿀이님 추가 (http://dasir.com) 2003-07-07
function wrestAlNum(fld)
{
if (!wrestTrim(fld)) return;
var pattern = /(^[a-zA-Z0-9]+$)/;
if (!pattern.test(fld.value)) {
if (wrestFld == null) {
wrestMsg = wrestItemname(fld) + " : 영문 또는 숫자가 아닙니다.\n";
wrestFld = fld;
}
}
}
// 영문자와 숫자 그리고 _ 검사
function wrestAlNum_(fld)
{
if (!wrestTrim(fld)) return;
var pattern = /(^[a-zA-Z0-9\_]+$)/;
if (!pattern.test(fld.value)) {
if (wrestFld == null) {
wrestMsg = wrestItemname(fld) + " : 영문, 숫자, _ 가 아닙니다.\n";
wrestFld = fld;
}
}
}
// 최소 길이 검사
function wrestMinLength(fld)
{
if (!wrestTrim(fld)) return;
var minlength = fld.getAttribute("minlength");
if (wrestFld == null) {
if (fld.value.length < parseInt(minlength)) {
wrestMsg = wrestItemname(fld) + " : 최소 "+minlength+"글자 이상 입력하세요.\n";
wrestFld = fld;
}
}
}
// 이미지 확장자
function wrestImgExt(fld)
{
if (!wrestTrim(fld)) return;
var pattern = /\.(gif|jpg|png)$/i; // jpeg 는 제외
if(!pattern.test(fld.value)){
if(wrestFld == null){
wrestMsg = wrestItemname(fld)+" : 이미지 파일이 아닙니다.\n.gif .jpg .png 파일만 가능합니다.\n";
wrestFld = fld;
fld.select();
}
}
}
// 확장자
function wrestExtension(fld, css)
{
if (!wrestTrim(fld)) return;
var str = css.split("="); // ext=?? <-- str[1]
var src = fld.value.split(".");
var ext = src[src.length - 1];
if (wrestFld == null) {
if (ext.toLowerCase() < str[1].toLowerCase()) {
wrestMsg = wrestItemname(fld) + " : ."+str[1]+" 파일만 가능합니다.\n";
wrestFld = fld;
}
}
}
// 공백 검사후 공백을 "" 로 변환
function wrestNospace(fld)
{
var pattern = /(\s)/g; // \s 공백 문자
if (pattern.test(fld.value)) {
if (wrestFld == null) {
wrestMsg = wrestItemname(fld) + " : 공백이 없어야 합니다.\n";
wrestFld = fld;
}
}
}
// submit 할 때 속성을 검사한다.
function wrestSubmit()
{
wrestMsg = "";
wrestFld = null;
var attr = null;
// 해당폼에 대한 요소의 개수만큼 돌려라
for (var i=0; i<this.elements.length; i++) {
var el = this.elements[i];
// Input tag 의 type 이 text, file, password 일때만
// 셀렉트 박스일때도 필수 선택 검사합니다. select-one
if (el.type=="text" || el.type=="hidden" || el.type=="file" || el.type=="password" || el.type=="select-one" || el.type=="textarea") {
if (el.getAttribute("required") != null) {
wrestRequired(el);
}
if (el.getAttribute("minlength") != null) {
wrestMinLength(el);
}
var array_css = el.className.split(" "); // class 를 공백으로 나눔
el.style.backgroundColor = wrestFldDefaultColor;
// 배열의 길이만큼 돌려라
for (var k=0; k<array_css.length; k++) {
var css = array_css[k];
switch (css) {
case "required" : wrestRequired(el); break;
case "trim" : wrestTrim(el); break;
case "email" : wrestEmail(el); break;
case "hangul" : wrestHangul(el); break;
case "hangul2" : wrestHangul2(el); break;
case "hangulalpha" : wrestHangulAlpha(el); break;
case "hangulalnum" : wrestHangulAlNum(el); break;
case "nospace" : wrestNospace(el); break;
case "numeric" : wrestNumeric(el); break;
case "alpha" : wrestAlpha(el); break;
case "alnum" : wrestAlNum(el); break;
case "alnum_" : wrestAlNum_(el); break;
case "telnum" : wrestTelNum(el); break; // 김선용 2006.3 - 전화번호 형식 검사
case "imgext" : wrestImgExt(el); break;
default :
if (/^extension\=/.test(css)) {
wrestExtension(el, css); break;
}
} // switch (css)
} // for (k)
} // if (el)
} // for (i)
// 필드가 null 이 아니라면 오류메세지 출력후 포커스를 해당 오류 필드로 옮김
// 오류 필드는 배경색상을 바꾼다.
if (wrestFld != null) {
// 경고메세지 출력
alert(wrestMsg);
if (wrestFld.style.display != "none") {
var id = wrestFld.getAttribute("id");
// 오류메세지를 위한 element 추가
var msg_el = document.createElement("strong");
msg_el.id = "msg_"+id;
msg_el.className = "msg_sound_only";
msg_el.innerHTML = wrestMsg;
wrestFld.parentNode.insertBefore(msg_el, wrestFld);
var new_href = document.location.href.replace(/#msg.+$/, "")+"#msg_"+id;
document.location.href = new_href;
//wrestFld.style.backgroundColor = wrestFldBackColor;
if (typeof(wrestFld.select) != "undefined")
wrestFld.select();
wrestFld.focus();
}
return false;
}
if (this.oldsubmit && this.oldsubmit() == false)
return false;
return true;
}
// 초기에 onsubmit을 가로채도록 한다.
function wrestInitialized()
{
for (var i = 0; i < document.forms.length; i++) {
// onsubmit 이벤트가 있다면 저장해 놓는다.
if (document.forms[i].onsubmit) {
document.forms[i].oldsubmit = document.forms[i].onsubmit;
}
document.forms[i].onsubmit = wrestSubmit;
}
}
// 폼필드 자동검사
$(document).ready(function(){
// onload
wrestInitialized();
});

523
kngil/js/index.js Normal file
View File

@@ -0,0 +1,523 @@
/**
* Main Page Video Player & Navigation Controller
* 모듈화된 구조로 리팩토링
*/
(function() {
'use strict';
// ============================================
// Configuration
// ============================================
const CONFIG = {
TOTAL_PAGES: 5,
INTRO_DELAY: 2800,
VISITED_STORAGE_KEY: 'visited',
VIDEO_BASE_PATH: '/kngil/img/video',
PAGE_LINKS: {
1: './value.html',
2: './provided.html',
3: './primary.html',
4: './analysis.html',
5: './results.html'
},
SELECTORS: {
video: '#video_play',
videoLink: '#main_video_link',
pagination: '.main-pagination',
paginationItem: '.main-pagination div',
intro: '.intro-wrap',
mainMask: '.main-mask',
footer: 'footer',
footerClose: '.footer-close',
main: '.main'
},
CLASSES: {
pageOn: 'page-on',
footerOn: 'on',
footerOff: ' ',
skip: 'skip'
}
};
// ============================================
// Utility Functions
// ============================================
const Utils = {
/**
* DOM 요소 선택 (jQuery 대체)
*/
$(selector) {
return document.querySelector(selector);
},
/**
* DOM 요소들 선택 (jQuery 대체)
*/
$$(selector) {
return document.querySelectorAll(selector);
},
/**
* 숫자 유효성 검사
*/
isValidPageNum(pageNum) {
return Number.isInteger(pageNum) &&
pageNum >= 1 &&
pageNum <= CONFIG.TOTAL_PAGES;
},
/**
* 안전한 이벤트 리스너 추가
*/
safeAddEventListener(element, event, handler) {
if (element && typeof handler === 'function') {
element.addEventListener(event, handler);
return true;
}
return false;
},
/**
* 클래스 토글 헬퍼
*/
toggleClass(element, className, force) {
if (!element) return;
if (force === undefined) {
element.classList.toggle(className);
} else {
element.classList.toggle(className, force);
}
}
};
// ============================================
// Video Player Module
// ============================================
const VideoPlayer = {
currentPage: 1,
videoElement: null,
sourceElement: null,
init() {
this.videoElement = Utils.$(CONFIG.SELECTORS.video);
if (!this.videoElement) {
console.warn('[VideoPlayer] Video element not found');
return false;
}
this.sourceElement = this.videoElement.querySelector('source');
if (!this.sourceElement) {
console.warn('[VideoPlayer] Source element not found');
return false;
}
this.setupEventListeners();
return true;
},
setupEventListeners() {
// 영상 종료 후 다음 영상 실행
Utils.safeAddEventListener(this.videoElement, 'ended', () => {
this.playNext();
});
},
loadVideo(pageNum) {
if (!Utils.isValidPageNum(pageNum)) {
console.warn(`[VideoPlayer] Invalid page number: ${pageNum}`);
return false;
}
const videoSource = `${CONFIG.VIDEO_BASE_PATH}/main_${pageNum}.mp4`;
try {
this.sourceElement.src = videoSource;
this.videoElement.load();
return true;
} catch (error) {
console.error('[VideoPlayer] Failed to load video:', error);
return false;
}
},
play() {
if (!this.videoElement) return false;
return this.videoElement.play()
.then(() => true)
.catch(err => {
console.warn('[VideoPlayer] Play failed:', err);
return false;
});
},
pause() {
if (this.videoElement) {
this.videoElement.pause();
}
},
playNext() {
this.currentPage = this.currentPage < CONFIG.TOTAL_PAGES
? this.currentPage + 1
: 1;
if (this.loadVideo(this.currentPage)) {
Pagination.updateState(this.currentPage);
this.play();
}
}
};
// ============================================
// Pagination Module
// ============================================
const Pagination = {
paginationElement: null,
items: null,
init() {
this.paginationElement = Utils.$(CONFIG.SELECTORS.pagination);
if (!this.paginationElement) {
console.warn('[Pagination] Pagination element not found');
return;
}
this.items = Utils.$$(CONFIG.SELECTORS.paginationItem);
this.setupClickHandlers();
},
setupClickHandlers() {
// 이벤트 위임 사용
Utils.safeAddEventListener(
this.paginationElement,
'click',
this.handleClick.bind(this)
);
},
handleClick(e) {
// 클릭된 요소 또는 그 부모 요소에서 data-page 속성을 찾음
let target = e.target.closest('[data-page]');
// 만약 li 요소를 직접 클릭한 경우, 그 안의 div를 찾음
if (!target) {
const liElement = e.target.closest('li');
if (liElement) {
target = liElement.querySelector('[data-page]');
}
}
if (!target) return;
e.preventDefault();
e.stopPropagation();
const pageNum = parseInt(target.getAttribute('data-page'), 10);
if (Utils.isValidPageNum(pageNum)) {
this.handlePageClick(pageNum);
}
},
handlePageClick(pageNum) {
VideoPlayer.currentPage = pageNum;
if (VideoPlayer.loadVideo(pageNum)) {
VideoPlayer.play();
this.updateState(pageNum);
this.updateLink(pageNum);
}
},
updateState(pageNum) {
if (!this.items || !Utils.isValidPageNum(pageNum)) return;
// 모든 아이템에서 활성 클래스 제거
this.items.forEach(item => {
item.classList.remove(CONFIG.CLASSES.pageOn);
});
// 해당 페이지 아이템에 활성 클래스 추가
const targetItem = Array.from(this.items).find(item =>
item.classList.contains(`page-0${pageNum}`)
);
if (targetItem) {
targetItem.classList.add(CONFIG.CLASSES.pageOn);
}
},
updateLink(pageNum) {
if (!Utils.isValidPageNum(pageNum)) return;
const linkUrl = CONFIG.PAGE_LINKS[pageNum];
const linkElement = Utils.$(CONFIG.SELECTORS.videoLink);
if (linkElement && linkUrl) {
linkElement.href = linkUrl;
}
},
show() {
if (this.paginationElement) {
this.paginationElement.style.display = '';
}
},
hide() {
if (this.paginationElement) {
this.paginationElement.style.display = 'none';
}
}
};
// ============================================
// Intro Controller Module
// ============================================
const IntroController = {
init() {
const visited = sessionStorage.getItem(CONFIG.VISITED_STORAGE_KEY);
const intro = Utils.$(CONFIG.SELECTORS.intro);
const mainMask = Utils.$(CONFIG.SELECTORS.mainMask);
if (visited) {
this.hideIntro(intro, mainMask);
} else {
// 첫 방문 시 세션 스토리지에 저장
setTimeout(() => {
try {
sessionStorage.setItem(CONFIG.VISITED_STORAGE_KEY, 'true');
} catch (error) {
console.warn('[IntroController] Failed to set sessionStorage:', error);
}
}, 1000);
}
},
hideIntro(intro, mainMask) {
if (intro) {
intro.style.display = 'none';
}
if (mainMask) {
mainMask.classList.add(CONFIG.CLASSES.skip);
}
}
};
// ============================================
// Cursor Text Controller Module
// ============================================
const CursorTextController = {
cursorTextElement: null,
init() {
// 메인 페이지인지 확인 (.wrap.main 클래스 존재 여부)
const mainElement = Utils.$(CONFIG.SELECTORS.main);
if (!mainElement) {
return;
}
// 커서 따라다니는 텍스트 요소 생성
this.cursorTextElement = document.createElement('div');
this.cursorTextElement.textContent = 'Click!';
this.cursorTextElement.style.cssText = `
position: fixed;
pointer-events: none;
z-index: 9999;
font-size: 20px;
color: #fff;
font-weight: 500;
white-space: nowrap;
transform: translate(-0%, -50%);
transition: opacity 0.3s;
opacity: 0;
`;
document.body.appendChild(this.cursorTextElement);
this.setupEventListeners();
},
setupEventListeners() {
// 마우스 움직임 추적
document.addEventListener('mousemove', (e) => {
this.handleMouseMove(e);
});
// 마우스가 페이지를 벗어나면 숨김
document.addEventListener('mouseleave', () => {
if (this.cursorTextElement) {
this.cursorTextElement.style.opacity = '0';
}
});
document.addEventListener('mouseenter', () => {
if (this.cursorTextElement) {
this.cursorTextElement.style.opacity = '1';
}
});
},
handleMouseMove(e) {
if (!this.cursorTextElement) return;
// 마우스 위치의 요소 확인
const elementUnderMouse = document.elementFromPoint(e.clientX, e.clientY);
// 특정 영역인지 확인
const isInFooter = elementUnderMouse?.closest('footer');
const isInPopup = elementUnderMouse?.closest('.popup_wrap') || elementUnderMouse?.closest('.popup-wrap');
const isInFloating = elementUnderMouse?.closest('.floating_menu');
const isInPagination = elementUnderMouse?.closest('.main-pagination');
const isInHeader = elementUnderMouse?.closest('.header');
const isInSitemap = elementUnderMouse?.closest('.popup_sitemap') || elementUnderMouse?.closest('.sitemap');
// 특정 영역이면 숨김
if (isInFooter || isInPopup || isInFloating || isInPagination || isInHeader || isInSitemap) {
this.cursorTextElement.style.opacity = '0';
} else {
this.cursorTextElement.style.opacity = '1';
// 커서 오른쪽 아래에 위치
this.cursorTextElement.style.left = (e.clientX + 15) + 'px';
this.cursorTextElement.style.top = (e.clientY + 15) + 'px';
}
}
};
// ============================================
// Footer Controller Module
// ============================================
const FooterController = {
footerElement: null,
mainElement: null,
footerCloseElement: null,
init() {
this.footerElement = Utils.$(CONFIG.SELECTORS.footer);
this.mainElement = Utils.$(CONFIG.SELECTORS.main);
this.footerCloseElement = Utils.$(CONFIG.SELECTORS.footerClose);
if (!this.footerElement || !this.mainElement) {
console.warn('[FooterController] Required elements not found');
return;
}
this.setupMousewheelHandler();
this.setupCloseHandler();
},
setupMousewheelHandler() {
// jQuery mousewheel 이벤트 대신 wheel 이벤트 사용
Utils.safeAddEventListener(
this.mainElement,
'wheel',
this.handleWheel.bind(this),
{ passive: true }
);
},
handleWheel(e) {
// deltaY가 양수면 아래로 스크롤 (footer 표시 - on 클래스 추가)
// deltaY가 음수면 위로 스크롤 (footer 숨김 - on 클래스 제거)
if (e.deltaY > 0) {
this.show();
} else if (e.deltaY < 0) {
this.hide();
}
},
setupCloseHandler() {
if (this.footerCloseElement) {
Utils.safeAddEventListener(
this.footerCloseElement,
'click',
() => this.hide()
);
}
},
show() {
if (!this.footerElement) return;
this.footerElement.classList.add(CONFIG.CLASSES.footerOn);
// footerOff가 유효한 클래스 이름인 경우에만 제거
const footerOff = CONFIG.CLASSES.footerOff.trim();
if (footerOff) {
this.footerElement.classList.remove(footerOff);
}
},
hide() {
if (!this.footerElement) return;
// footerOff가 유효한 클래스 이름인 경우에만 추가
const footerOff = CONFIG.CLASSES.footerOff.trim();
if (footerOff) {
this.footerElement.classList.add(footerOff);
}
this.footerElement.classList.remove(CONFIG.CLASSES.footerOn);
}
};
// ============================================
// Main Initialization
// ============================================
const MainController = {
init() {
// DOM이 준비되면 초기화
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
this.start();
});
} else {
this.start();
}
},
start() {
// Video Player 초기화
if (!VideoPlayer.init()) {
console.warn('[MainController] Video player initialization failed');
return;
}
// 초기 비디오 일시정지 및 페이지네이션 숨김
VideoPlayer.pause();
Pagination.hide();
// 방문 여부 확인 및 비디오 재생
const visited = sessionStorage.getItem(CONFIG.VISITED_STORAGE_KEY);
if (visited) {
this.startVideoPlayback();
} else {
setTimeout(() => {
this.startVideoPlayback();
}, CONFIG.INTRO_DELAY);
}
// 모듈 초기화
Pagination.init();
IntroController.init();
FooterController.init();
CursorTextController.init();
// 초기 링크 설정
Pagination.updateLink(VideoPlayer.currentPage);
},
startVideoPlayback() {
VideoPlayer.play();
Pagination.show();
}
};
// ============================================
// Start Application
// ============================================
MainController.init();
})();

326
kngil/js/join copy.js Normal file
View File

@@ -0,0 +1,326 @@
/* =========================
전역 상태값
========================= */
let isIdChecked = false;
let checkedUserId = '';
let isPhoneVerified = false;
let authTimer = null;
const REGEX = {
id: /^[a-zA-Z][a-zA-Z0-9]{3,11}$/,
password: /^(?=.*[A-Za-z])(?=.*\d)(?=.*[!@#$%^&*]).{8,}$/,
name: /^[가-힣a-zA-Z\s]+$/,
phone: /^\d{3}-\d{4}-\d{4}$/
};
/* =========================
DOM 로드 후 초기화
========================= */
document.addEventListener('DOMContentLoaded', () => {
initMemberType();
initEmailDomain();
initIdCheck();
initPhoneAuth();
initFormSubmit();
initPasswordValidation(); // 비밀번호 규칙
initNameValidation(); // 이름 특수문자 차단
initPhoneFormat(); // 휴대폰 자동 하이픈
// initFormValidation(); // submit 최종 검증
});
/* =========================
1. 회원유형
========================= */
function initMemberType() {
const radios = document.querySelectorAll('input[name="memberType"]');
const companyRow = document.querySelector('.company-group');
radios.forEach(radio => {
radio.addEventListener('change', () => {
if (radio.value === '1') {
companyRow.style.display = '';
} else {
companyRow.style.display = 'none';
companyRow.querySelectorAll('input').forEach(i => i.value = '');
}
});
});
}
/* =========================
2. 이메일 도메인
========================= */
function initEmailDomain() {
const select = document.getElementById('domain_list');
const custom = document.getElementById('custom_domain');
select.addEventListener('change', () => {
if (select.value === 'type') {
custom.classList.remove('d-none');
custom.required = true;
} else {
custom.classList.add('d-none');
custom.required = false;
custom.value = '';
}
});
}
/* =========================
3. 아이디 중복확인
========================= */
function initIdCheck() {
const input = document.getElementById('user_id');
const btn = document.getElementById('btn_id_check');
if (!btn || !input) {
console.error('ID input 또는 중복확인 버튼 없음');
return;
}
// ✅ 아이디 입력값 변경 시 → 중복확인 무효화
input.addEventListener('input', () => {
isIdChecked = false;
checkedUserId = '';
input.readOnly = false;
});
btn.addEventListener('click', async (e) => {
e.preventDefault(); // ⭐ 중요 (폼 submit 방지)
const userId = input.value.trim();
if (!/^[a-zA-Z][a-zA-Z0-9]{3,11}$/.test(userId)) {
alert('아이디 형식이 올바르지 않습니다.');
return;
}
const res = await fetch('/kngil/bbs/join.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'check_id',
userId: userId
})
});
const data = await res.json();
if (data.available === true) {
alert('사용 가능한 아이디입니다.');
isIdChecked = true;
checkedUserId = userId;
// UX: 아이디 수정 못 하게
input.readOnly = true;
} else {
alert(data.message || '이미 사용 중인 아이디입니다.');
isIdChecked = false;
checkedUserId = '';
}
});
}
/* =========================
4. 휴대폰 인증번호
========================= */
function initPhoneAuth() {
const btn = document.querySelector('.btn-code');
const phoneInput = document.getElementById('user_phone');
const timerEl = document.querySelector('.timer');
btn.addEventListener('click', async () => {
const phone = phoneInput.value.trim();
if (!/^\d{3}-\d{4}-\d{4}$/.test(phone)) {
alert('휴대폰 번호 형식이 올바르지 않습니다.');
return;
}
await fetch('/api/send_sms.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ phone })
});
startTimer(timerEl, 180);
alert('인증번호가 발송되었습니다.');
});
}
function startTimer(el, seconds) {
clearInterval(authTimer);
el.classList.remove('d-none');
let remain = seconds;
authTimer = setInterval(() => {
const m = String(Math.floor(remain / 60)).padStart(2, '0');
const s = String(remain % 60).padStart(2, '0');
el.textContent = `${m}:${s}`;
remain--;
if (remain < 0) {
clearInterval(authTimer);
el.textContent = '만료';
}
}, 1000);
}
/* =========================
5. 비밀번호 검증
========================= */
function validatePassword() {
const pw = document.getElementById('user_password').value;
const pw2 = document.getElementById('user_password_confirm').value;
if (!PASSWORD_REGEX.test(pw)) {
alert('비밀번호는 영문, 숫자, 특수기호를 포함한 8자 이상이어야 합니다.');
return false;
}
if (pw !== pw2) {
alert('비밀번호가 일치하지 않습니다.');
return false;
}
return true;
}
/* =========================
6. 가입하기 submit
========================= */
function initFormSubmit() {
const form = document.forms.joinForm;
form.addEventListener('submit', async (e) => {
e.preventDefault();
if (!isIdChecked) {
alert('아이디 중복확인을 해주세요.');
return;
}
if (!validatePassword()) return;
const formData = new FormData(form);
const email =
formData.get('userEmail') + '@' +
(formData.get('emailDomain') === 'type'
? formData.get('customDomain')
: formData.get('emailDomain'));
const payload = {
memberType: formData.get('memberType'),
userId: formData.get('userId'),
password: formData.get('userPassword'),
userName: formData.get('userName'),
email,
phone: formData.get('userPhone'),
company: formData.get('companyName'),
department: formData.get('departmentName')
};
const res = await fetch('/kngil/bbs/join.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await res.json();
if (data.success) {
form.style.display = 'none';
document.querySelector('.messages').style.display = 'block';
} else {
alert(data.message);
}
});
}
/* =========================
회원 가입 유효성 체크
========================= */
function initPasswordValidation() {
const pw = document.getElementById('user_password');
const pw2 = document.getElementById('user_password_confirm');
const msg = document.getElementById('password_help');
function validate() {
if (!pw.value) return;
if (!PASSWORD_REGEX.test(pw.value)) {
msg.textContent = '영문 + 숫자 + 특수기호 포함 8자 이상';
msg.classList.remove('success');
msg.classList.add('error');
return;
}
if (pw2.value && pw.value !== pw2.value) {
msg.textContent = '비밀번호가 일치하지 않습니다.';
msg.classList.remove('success');
msg.classList.add('error');
return;
}
msg.textContent = '사용 가능한 비밀번호입니다.';
msg.classList.remove('error');
msg.classList.add('success');
}
pw.addEventListener('input', validate);
pw2.addEventListener('input', validate);
}
// function initFormValidation() {
// const form = document.forms.joinForm;
// form.addEventListener('submit', e => {
// e.preventDefault();
// const id = form.userId.value.trim();
// const pw = form.userPassword.value;
// const pw2 = form.userPasswordConfirm.value;
// const name = form.userName.value.trim();
// const email = form.userEmail.value.trim();
// const phone = form.userPhone.value.trim();
// if (!isIdChecked || checkedUserId !== form.userId.value.trim()) {
// alert('아이디 중복확인을 해주세요.');
// return;
// }
// if (!id || !pw || !pw2 || !name || !email || !phone) {
// alert('필수 항목을 모두 입력하세요.');
// return;
// }
// if (!REGEX.password.test(pw)) {
// alert('비밀번호 규칙을 확인하세요.');
// return;
// }
// if (pw !== pw2) {
// alert('비밀번호가 일치하지 않습니다.');
// return;
// }
// if (!REGEX.name.test(name)) {
// alert('이름에 특수문자는 사용할 수 없습니다.');
// return;
// }
// if (!REGEX.phone.test(phone)) {
// alert('휴대전화번호 형식이 올바르지 않습니다.');
// return;
// }
// // 여기까지 왔으면 → 서버 signup 요청
// submitSignup();
// });
// }

308
kngil/js/join.js Normal file
View File

@@ -0,0 +1,308 @@
/* =========================
전역 상태값
========================= */
let isIdChecked = false;
let checkedUserId = '';
const REGEX = {
id: /^[a-zA-Z][a-zA-Z0-9]{3,11}$/,
password: /^(?=.*[A-Za-z])(?=.*\d)(?=.*[!@#$%^&*]).{8,}$/,
name: /^[가-힣a-zA-Z\s]+$/,
phone: /^\d{3}-\d{4}-\d{4}$/
};
/* =========================
DOM 로드 후 초기화
========================= */
document.addEventListener('DOMContentLoaded', () => {
initMemberType();
initEmailDomain();
initIdCheck();
initPasswordValidation();
// initNameValidation();
initPhoneFormat();
initFormSubmit();
});
/* =========================
1. 회원유형
========================= */
function initMemberType() {
const radios = document.querySelectorAll('input[name="memberType"]');
const companyRow = document.querySelector('.company-group');
radios.forEach(radio => {
radio.addEventListener('change', () => {
if (radio.value === '1') {
companyRow.style.display = '';
} else {
companyRow.style.display = 'none';
companyRow.querySelectorAll('input').forEach(i => i.value = '');
}
});
});
}
/* =========================
2. 이메일 도메인
========================= */
function initEmailDomain() {
const select = document.getElementById('domain_list');
const custom = document.getElementById('custom_domain');
select.addEventListener('change', () => {
if (select.value === 'type') {
custom.classList.remove('d-none');
custom.required = true;
} else {
custom.classList.add('d-none');
custom.required = false;
custom.value = '';
}
});
}
/* =========================
3. 아이디 중복확인
========================= */
function initIdCheck() {
const input = document.getElementById('user_id');
const btn = document.getElementById('btn_id_check');
input.addEventListener('input', () => {
isIdChecked = false;
checkedUserId = '';
input.readOnly = false;
});
btn.addEventListener('click', async () => {
const userId = input.value.trim();
if (!userId) {
alert('아이디를 입력해주세요.');
input.focus();
return;
}
if (!REGEX.id.test(userId)) {
alert('아이디 형식이 올바르지 않습니다.');
return;
}
const res = await fetch('/kngil/bbs/join.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'check_id', userId })
});
const data = await res.json();
if (data.available) {
alert('사용 가능한 아이디입니다.');
isIdChecked = true;
checkedUserId = userId;
input.readOnly = true;
} else {
alert(data.message || '이미 사용 중인 아이디입니다.');
isIdChecked = false;
}
});
}
/* =========================
4. 휴대폰 인증번호
========================= */
function initPhoneAuth() {
const btn = document.querySelector('.btn-code');
const phoneInput = document.getElementById('user_phone');
const timerEl = document.querySelector('.timer');
btn.addEventListener('click', async () => {
const phone = phoneInput.value.trim();
if (!/^\d{3}-\d{4}-\d{4}$/.test(phone)) {
alert('휴대폰 번호 형식이 올바르지 않습니다.');
return;
}
await fetch('/api/send_sms.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ phone })
});
startTimer(timerEl, 180);
alert('인증번호가 발송되었습니다.');
});
}
function startTimer(el, seconds) {
clearInterval(authTimer);
el.classList.remove('d-none');
let remain = seconds;
authTimer = setInterval(() => {
const m = String(Math.floor(remain / 60)).padStart(2, '0');
const s = String(remain % 60).padStart(2, '0');
el.textContent = `${m}:${s}`;
remain--;
if (remain < 0) {
clearInterval(authTimer);
el.textContent = '만료';
}
}, 1000);
}
/* =========================
5. 비밀번호 검증
========================= */
function validatePassword() {
const pw = document.getElementById('user_password').value;
const pw2 = document.getElementById('user_password_confirm').value;
if (!PASSWORD_REGEX.test(pw)) {
alert('비밀번호는 영문, 숫자, 특수기호를 포함한 8자 이상이어야 합니다.');
return false;
}
if (pw !== pw2) {
alert('비밀번호가 일치하지 않습니다.');
return false;
}
return true;
}
/* =========================
6. 가입하기 submit
========================= */
function initFormSubmit() {
const form = document.forms.joinForm;
form.addEventListener('submit', async (e) => {
e.preventDefault();
if (!isIdChecked || checkedUserId !== form.userId.value.trim()) {
alert('아이디 중복확인을 해주세요.');
return;
}
if (!REGEX.password.test(form.userPassword.value)) {
alert('비밀번호 규칙을 확인하세요.');
return;
}
if (form.userPassword.value !== form.userPasswordConfirm.value) {
alert('비밀번호가 일치하지 않습니다.');
return;
}
if (!REGEX.name.test(form.userName.value.trim())) {
alert('이름에 특수문자는 사용할 수 없습니다.');
return;
}
// if (!REGEX.phone.test(form.userPhone.value.trim())) {
// alert('휴대전화번호 형식이 올바르지 않습니다.');
// return;
// }
// ✅ 서버 전송
const formData = new FormData(form);
const email =
formData.get('userEmail') + '@' +
(formData.get('emailDomain') === 'type'
? formData.get('customDomain')
: formData.get('emailDomain'));
const payload = {
action: 'signup',
memberType: formData.get('memberType'),
userId: formData.get('userId'),
password: formData.get('userPassword'),
userName: formData.get('userName'),
email,
phone: formData.get('userPhone'),
company: formData.get('companyName') || null,
department: formData.get('departmentName') || null
};
const res = await fetch('/kngil/bbs/join.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await res.json();
if (data.success) {
form.style.display = 'none';
document.querySelector('.messages').style.display = 'block';
} else {
alert(data.message || '회원가입 실패');
}
});
}
/* =========================
회원 가입 유효성 체크
========================= */
function initPasswordValidation() {
const pw = document.getElementById('user_password');
const pw2 = document.getElementById('user_password_confirm');
const msg = document.getElementById('password_help');
function validate() {
if (!pw.value) {
msg.textContent = '영문+숫자+특수기호 8자 이상';
msg.className = 'info-msg';
return;
}
if (!REGEX.password.test(pw.value)) {
msg.textContent = '비밀번호 규칙을 만족하지 않습니다.';
msg.className = 'info-msg error';
return;
}
if (pw2.value && pw.value !== pw2.value) {
msg.textContent = '비밀번호가 일치하지 않습니다.';
msg.className = 'info-msg error';
return;
}
msg.textContent = '사용 가능한 비밀번호입니다.';
msg.className = 'info-msg success';
}
pw.addEventListener('input', validate);
pw2.addEventListener('input', validate);
}
function initPhoneFormat() {
const phone = document.getElementById('user_phone');
phone.addEventListener('input', () => {
let v = phone.value.replace(/\D/g, '');
if (v.length <= 3) phone.value = v;
else if (v.length <= 7) phone.value = `${v.slice(0,3)}-${v.slice(3)}`;
else phone.value = `${v.slice(0,3)}-${v.slice(3,7)}-${v.slice(7,11)}`;
});
}
// function initNameValidation() {
// const nameInput = document.getElementById('user_name');
// if (!nameInput) return;
// nameInput.addEventListener('input', () => {
// nameInput.value = nameInput.value.replace(/[^가-힣a-zA-Z\s]/g, '');
// });
// }

1533
kngil/js/lib/MotionPathPlugin.min.js vendored Normal file

File diff suppressed because it is too large Load Diff

1
kngil/js/lib/aos.min.js vendored Normal file

File diff suppressed because one or more lines are too long

10
kngil/js/lib/gsap.min.js vendored Normal file

File diff suppressed because one or more lines are too long

2
kngil/js/lib/jquery-3.6.1.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

5
kngil/js/lib/jquery.mousewheel.min.js vendored Normal file
View File

@@ -0,0 +1,5 @@
/*!
* jQuery Mousewheel 3.1.13
* Copyright OpenJS Foundation and other contributors
*/
!function(e){"function"==typeof define&&define.amd?define(["jquery"],e):"object"==typeof exports?module.exports=e:e(jQuery)}(function(a){var u,r,e=["wheel","mousewheel","DOMMouseScroll","MozMousePixelScroll"],t="onwheel"in window.document||9<=window.document.documentMode?["wheel"]:["mousewheel","DomMouseScroll","MozMousePixelScroll"],f=Array.prototype.slice;if(a.event.fixHooks)for(var n=e.length;n;)a.event.fixHooks[e[--n]]=a.event.mouseHooks;var d=a.event.special.mousewheel={version:"3.1.12",setup:function(){if(this.addEventListener)for(var e=t.length;e;)this.addEventListener(t[--e],i,!1);else this.onmousewheel=i;a.data(this,"mousewheel-line-height",d.getLineHeight(this)),a.data(this,"mousewheel-page-height",d.getPageHeight(this))},teardown:function(){if(this.removeEventListener)for(var e=t.length;e;)this.removeEventListener(t[--e],i,!1);else this.onmousewheel=null;a.removeData(this,"mousewheel-line-height"),a.removeData(this,"mousewheel-page-height")},getLineHeight:function(e){var t=a(e),e=t["offsetParent"in a.fn?"offsetParent":"parent"]();return e.length||(e=a("body")),parseInt(e.css("fontSize"),10)||parseInt(t.css("fontSize"),10)||16},getPageHeight:function(e){return a(e).height()},settings:{adjustOldDeltas:!0,normalizeOffset:!0}};function i(e){var t,n=e||window.event,i=f.call(arguments,1),o=0,l=0,s=0,h=0;if((e=a.event.fix(n)).type="mousewheel","detail"in n&&(s=-1*n.detail),"wheelDelta"in n&&(s=n.wheelDelta),"wheelDeltaY"in n&&(s=n.wheelDeltaY),"wheelDeltaX"in n&&(l=-1*n.wheelDeltaX),"axis"in n&&n.axis===n.HORIZONTAL_AXIS&&(l=-1*s,s=0),o=0===s?l:s,"deltaY"in n&&(o=s=-1*n.deltaY),"deltaX"in n&&(l=n.deltaX,0===s&&(o=-1*l)),0!==s||0!==l)return 1===n.deltaMode?(o*=t=a.data(this,"mousewheel-line-height"),s*=t,l*=t):2===n.deltaMode&&(o*=t=a.data(this,"mousewheel-page-height"),s*=t,l*=t),h=Math.max(Math.abs(s),Math.abs(l)),(!r||h<r)&&c(n,r=h)&&(r/=40),c(n,h)&&(o/=40,l/=40,s/=40),o=Math[1<=o?"floor":"ceil"](o/r),l=Math[1<=l?"floor":"ceil"](l/r),s=Math[1<=s?"floor":"ceil"](s/r),d.settings.normalizeOffset&&this.getBoundingClientRect&&(h=this.getBoundingClientRect(),e.offsetX=e.clientX-h.left,e.offsetY=e.clientY-h.top),e.deltaX=l,e.deltaY=s,e.deltaFactor=r,e.deltaMode=0,i.unshift(e,o,l,s),u&&window.clearTimeout(u),u=window.setTimeout(w,200),(a.event.dispatch||a.event.handle).apply(this,i)}function w(){r=null}function c(e,t){return d.settings.adjustOldDeltas&&"mousewheel"===e.type&&t%120==0}a.fn.extend({mousewheel:function(e){return e?this.on("mousewheel",e):this.trigger("mousewheel")},unmousewheel:function(e){return this.off("mousewheel",e)}})});

2
kngil/js/lib/lenis.min.js vendored Normal file

File diff suppressed because one or more lines are too long

15
kngil/js/lib/lottie.min.js vendored Normal file

File diff suppressed because one or more lines are too long

10
kngil/js/lib/scrollToPlugin.min.js vendored Normal file
View File

@@ -0,0 +1,10 @@
/*!
* ScrollToPlugin 3.12.5
* https://gsap.com
*
* @license Copyright 2024, GreenSock. All rights reserved.
* Subject to the terms at https://gsap.com/standard-license or for Club GSAP members, the agreement issued with that membership.
* @author: Jack Doyle, jack@greensock.com
*/
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e=e||self).window=e.window||{})}(this,function(e){"use strict";function l(){return"undefined"!=typeof window}function m(){return f||l()&&(f=window.gsap)&&f.registerPlugin&&f}function n(e){return"string"==typeof e}function o(e){return"function"==typeof e}function p(e,t){var o="x"===t?"Width":"Height",n="scroll"+o,r="client"+o;return e===T||e===i||e===c?Math.max(i[n],c[n])-(T["inner"+o]||i[r]||c[r]):e[n]-e["offset"+o]}function q(e,t){var o="scroll"+("x"===t?"Left":"Top");return e===T&&(null!=e.pageXOffset?o="page"+t.toUpperCase()+"Offset":e=null!=i[o]?i:c),function(){return e[o]}}function s(e,t){if(!(e=y(e)[0])||!e.getBoundingClientRect)return console.warn("scrollTo target doesn't exist. Using 0")||{x:0,y:0};var o=e.getBoundingClientRect(),n=!t||t===T||t===c,r=n?{top:i.clientTop-(T.pageYOffset||i.scrollTop||c.scrollTop||0),left:i.clientLeft-(T.pageXOffset||i.scrollLeft||c.scrollLeft||0)}:t.getBoundingClientRect(),l={x:o.left-r.left,y:o.top-r.top};return!n&&t&&(l.x+=q(t,"x")(),l.y+=q(t,"y")()),l}function t(e,t,o,r,l){return isNaN(e)||"object"==typeof e?n(e)&&"="===e.charAt(1)?parseFloat(e.substr(2))*("-"===e.charAt(0)?-1:1)+r-l:"max"===e?p(t,o)-l:Math.min(p(t,o),s(e,t)[o]-l):parseFloat(e)-l}function u(){f=m(),l()&&f&&"undefined"!=typeof document&&document.body&&(T=window,c=document.body,i=document.documentElement,y=f.utils.toArray,f.config({autoKillThreshold:7}),h=f.config(),a=1)}var f,a,T,i,c,y,h,v,r={version:"3.12.5",name:"scrollTo",rawVars:1,register:function register(e){f=e,u()},init:function init(e,r,l,s,i){a||u();var p=this,c=f.getProperty(e,"scrollSnapType");p.isWin=e===T,p.target=e,p.tween=l,r=function _clean(e,t,r,l){if(o(e)&&(e=e(t,r,l)),"object"!=typeof e)return n(e)&&"max"!==e&&"="!==e.charAt(1)?{x:e,y:e}:{y:e};if(e.nodeType)return{y:e,x:e};var s,i={};for(s in e)i[s]="onAutoKill"!==s&&o(e[s])?e[s](t,r,l):e[s];return i}(r,s,e,i),p.vars=r,p.autoKill=!!r.autoKill,p.getX=q(e,"x"),p.getY=q(e,"y"),p.x=p.xPrev=p.getX(),p.y=p.yPrev=p.getY(),v=v||f.core.globals().ScrollTrigger,"smooth"===f.getProperty(e,"scrollBehavior")&&f.set(e,{scrollBehavior:"auto"}),c&&"none"!==c&&(p.snap=1,p.snapInline=e.style.scrollSnapType,e.style.scrollSnapType="none"),null!=r.x?(p.add(p,"x",p.x,t(r.x,e,"x",p.x,r.offsetX||0),s,i),p._props.push("scrollTo_x")):p.skipX=1,null!=r.y?(p.add(p,"y",p.y,t(r.y,e,"y",p.y,r.offsetY||0),s,i),p._props.push("scrollTo_y")):p.skipY=1},render:function render(e,t){for(var o,n,r,l,s,i=t._pt,c=t.target,u=t.tween,f=t.autoKill,a=t.xPrev,y=t.yPrev,d=t.isWin,g=t.snap,x=t.snapInline;i;)i.r(e,i.d),i=i._next;o=d||!t.skipX?t.getX():a,r=(n=d||!t.skipY?t.getY():y)-y,l=o-a,s=h.autoKillThreshold,t.x<0&&(t.x=0),t.y<0&&(t.y=0),f&&(!t.skipX&&(s<l||l<-s)&&o<p(c,"x")&&(t.skipX=1),!t.skipY&&(s<r||r<-s)&&n<p(c,"y")&&(t.skipY=1),t.skipX&&t.skipY&&(u.kill(),t.vars.onAutoKill&&t.vars.onAutoKill.apply(u,t.vars.onAutoKillParams||[]))),d?T.scrollTo(t.skipX?o:t.x,t.skipY?n:t.y):(t.skipY||(c.scrollTop=t.y),t.skipX||(c.scrollLeft=t.x)),!g||1!==e&&0!==e||(n=c.scrollTop,o=c.scrollLeft,x?c.style.scrollSnapType=x:c.style.removeProperty("scroll-snap-type"),c.scrollTop=n+1,c.scrollLeft=o+1,c.scrollTop=n,c.scrollLeft=o),t.xPrev=t.x,t.yPrev=t.y,v&&v.update()},kill:function kill(e){var t="scrollTo"===e,o=this._props.indexOf(e);return!t&&"scrollTo_x"!==e||(this.skipX=1),!t&&"scrollTo_y"!==e||(this.skipY=1),-1<o&&this._props.splice(o,1),!this._props.length}};r.max=p,r.getOffset=s,r.buildGetter=q,m()&&f.registerPlugin(r),e.ScrollToPlugin=r,e.default=r;if (typeof(window)==="undefined"||window!==e){Object.defineProperty(e,"__esModule",{value:!0})} else {delete e.default}});

10
kngil/js/lib/scrolltrigger.min.js vendored Normal file

File diff suppressed because one or more lines are too long

14
kngil/js/lib/swiper11.min.js vendored Normal file

File diff suppressed because one or more lines are too long

36
kngil/js/login.js Normal file
View File

@@ -0,0 +1,36 @@
const form = document.querySelector('.tab-content.id form')
if (form) {
form.addEventListener('submit', async (e) => {
e.preventDefault()
const id = document.getElementById('login_id').value.trim()
const pw = document.getElementById('login_password').value.trim()
if (!id || !pw) {
alert('아이디와 비밀번호를 입력하세요.')
return
}
try {
const res = await fetch('/kngil/bbs/login.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id, pw })
})
console.log('login.js loaded')
const json = await res.json()
if (json.status === 'success') {
console.log('LOGIN OK, reload')
document.getElementById('pop_login').style.display = 'none'
location.reload()
} else {
alert(json.message)
}
} catch (err) {
console.error(err)
alert('로그인 중 오류가 발생했습니다.')
}
})
}

102
kngil/js/login_sms.js Normal file
View File

@@ -0,0 +1,102 @@
document.addEventListener('DOMContentLoaded', () => {
const smsBtn = document.getElementById('sms_button');
const phoneInp = document.getElementById('login_phone');
if (!smsBtn || !phoneInp) return;
const form = smsBtn.closest('form');
const infoBox = form.querySelector('.info-box');
const timerEl = form.querySelector('.timer');
let pollTimer = null;
let timerIntv = null;
let remain = 180; // 3분
function startTimer() {
clearInterval(timerIntv);
remain = 180;
timerEl.classList.remove('d-none');
updateTimer();
timerIntv = setInterval(() => {
remain--;
updateTimer();
if (remain <= 0) {
clearInterval(timerIntv);
smsBtn.disabled = false;
timerEl.textContent = '00:00';
}
}, 1000);
}
function updateTimer() {
const m = String(Math.floor(remain / 60)).padStart(2, '0');
const s = String(remain % 60).padStart(2, '0');
timerEl.textContent = `${m}:${s}`;
}
smsBtn.addEventListener('click', async (e) => {
e.preventDefault();
const phone = phoneInp.value.trim();
if (!phone) {
alert('휴대폰 번호를 입력하세요.');
phoneInp.focus();
return;
}
smsBtn.disabled = true;
try {
const res = await fetch('/kngil/bbs/login_sms.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
mode: 'request',
phone
})
});
const json = await res.json();
if (json.status !== 'success') {
alert(json.message || '인증 링크 요청 실패');
smsBtn.disabled = false;
return;
}
// UI 처리
infoBox.classList.remove('d-none');
startTimer();
startPolling();
} catch (err) {
console.error(err);
alert('인증 요청 중 오류');
smsBtn.disabled = false;
}
});
function startPolling() {
clearInterval(pollTimer);
pollTimer = setInterval(async () => {
try {
const res = await fetch('/kngil/bbs/login_sms.php?mode=status');
const json = await res.json();
if (json.status === 'success') {
clearInterval(pollTimer);
clearInterval(timerIntv);
location.reload();
}
} catch (e) {
console.error(e);
}
}, 3000);
}
});

457
kngil/js/mypage.js Normal file
View File

@@ -0,0 +1,457 @@
/**
* mypage.js
* 마이페이지 관련 모든 모달 흐름 관리
*/
/* =========================
공통 유틸
========================= */
function show(id) {
const el = document.getElementById(id)
if (el) el.style.display = 'block'
}
function hide(id) {
const el = document.getElementById(id)
if (el) el.style.display = 'none'
}
/* =========================
마이페이지 1단계 (비밀번호 인증)
- 헤더에서 호출
========================= */
window.mypage01 = function () {
if (!window.IS_LOGIN) {
if (typeof window.login === 'function') {
window.login()
}
return
}
show('pop_mypage01')
}
/* =========================
마이페이지 2단계 (실제 마이페이지)
========================= */
let currentPage = 1
window.mypage02 = async function (page = 1) {
currentPage = page
hide('pop_mypage01')
try {
const res = await fetch(`/kngil/bbs/mypage02.php?page=${page}`)
const json = await res.json()
if (json.status !== 'success') {
alert(json.message || '데이터 로드 실패')
return
}
renderMyPage02(json.user)
renderMyPageHistory(json.history)
renderPagination(json.pagination)
show('pop_mypage02')
} catch (e) {
console.error(e)
alert('마이페이지 로딩 오류')
}
}
function renderMyPage02(data) {
// 이름 / ID
document.getElementById('mp_user_nm').textContent = data.user_nm
document.getElementById('mp_user_id').textContent = `(${data.user_id})`
// 연락처
document.getElementById('mp_tel').textContent = data.tel_no || '-'
document.getElementById('mp_email').textContent = data.email || '-'
// 회사 정보
document.getElementById('mp_co_nm').textContent = data.co_nm || '-'
document.getElementById('mp_dept_nm').textContent = data.dept_nm || '-'
// 사용량
document.getElementById('mp_tot_use').textContent =
Number(data.tot_use || 0).toLocaleString()
document.getElementById('mp_year_use').textContent =
Number(data.year_use || 0).toLocaleString()
}
function formatDate(dateStr) {
if (!dateStr) return '-'
const d = new Date(dateStr)
const yy = String(d.getFullYear()).slice(2)
const mm = String(d.getMonth() + 1).padStart(2, '0')
const dd = String(d.getDate()).padStart(2, '0')
return `${yy}-${mm}-${dd}`
}
function renderMyPageHistory(list) {
const tbody = document.getElementById('mp_history_body')
tbody.innerHTML = ''
if (!list.length) {
tbody.innerHTML = `
<tr>
<td colspan="4" style="text-align:center;">사용 이력이 없습니다.</td>
</tr>
`
return
}
list.forEach((row, idx) => {
const tr = document.createElement('tr')
tr.innerHTML = `
<td>${(currentPage - 1) * 5 + idx + 1}</td>
<td class="tit">${row.ser_bc}</td>
<td>${formatDate(row.use_dt)}</td>
<td><em>${Number(row.use_area).toLocaleString()}</em></td>
`
tbody.appendChild(tr)
})
}
function renderPagination(p) {
const ul = document.querySelector('.pagination-list')
if (!ul) return
ul.innerHTML = ''
for (let i = 1; i <= p.totalPages; i++) {
const li = document.createElement('li')
li.className = (i === p.page) ? 'on' : ''
const a = document.createElement('a')
a.href = '#'
a.textContent = i
a.addEventListener('click', (e) => {
e.preventDefault()
mypage02(i)
})
li.appendChild(a)
ul.appendChild(li)
}
}
/* =========================
마이페이지 3단계 (마이페이지 정보수정)
========================= */
window.mypage03 = async function () {
// ✅ 비밀번호 변경 영역 초기 상태 강제
const rowCurrent = document.getElementById('row-pw-current')
const rowNew = document.getElementById('row-pw-new')
const rowView = document.querySelector('.btn-sm.change')?.closest('tr')
rowCurrent && (rowCurrent.style.display = 'none')
rowNew && (rowNew.style.display = 'none')
rowView && (rowView.style.display = '')
try {
const res = await fetch('/kngil/bbs/mypage03.php')
const json = await res.json()
if (json.status !== 'success') {
alert(json.message)
return
}
fillMyPage03(json.data)
bindPasswordValidation()
hide('pop_mypage02')
show('pop_mypage03')
bindPasswordChangeUI()
} catch (e) {
console.error(e)
alert('회원정보 조회 오류')
}
};
;(function bindMyPage03Save() {
const popup = document.getElementById('pop_mypage03')
if (!popup) return
const btnSave = popup.querySelector('.btn-full')
btnSave.addEventListener('click', async () => {
const password = popup.querySelector('#new_pw')?.value.trim() || ''
const emailId = popup.querySelector('.e-id')?.value.trim()
const domainSel = popup.querySelector('#domain_list')
const customDomain = popup.querySelector('#custom_domain')
let email = ''
if (emailId) {
const domain =
domainSel.value === 'type'
? customDomain.value.trim()
: domainSel.options[domainSel.selectedIndex].text
email = `${emailId}@${domain}`
}
const telNo = popup.querySelector('#user_phone')?.value.trim()
const deptNm = popup.querySelector('#department_name')?.value.trim()
try {
const res = await fetch('/kngil/bbs/mypage03.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
password,
email,
tel_no: telNo,
dept_nm: deptNm
})
})
const json = await res.json()
if (json.status === 'success') {
alert('정보가 수정되었습니다.')
hide('pop_mypage03')
mypage02()
} else {
alert(json.message)
}
} catch (e) {
console.error(e)
alert('저장 중 오류 발생')
}
})
})();
function fillMyPage03(data) {
const popup = document.getElementById('pop_mypage03')
if (!popup) return
/* =========================
아이디
========================= */
const idInput = popup.querySelector('input[readonly]')
if (idInput) idInput.value = data.user_id || ''
/* =========================
이름
========================= */
const nameInput = popup.querySelector('input[placeholder="이지빔"]')
if (nameInput) nameInput.value = data.user_nm || ''
/* =========================
이메일
========================= */
if (data.email) {
const [emailId, domain] = data.email.split('@')
const emailInput = popup.querySelector('.e-id')
const domainList = popup.querySelector('#domain_list')
const customDomain = popup.querySelector('#custom_domain')
if (emailInput) emailInput.value = emailId
// 도메인 목록에 있으면 선택
const option = [...domainList.options].find(opt => opt.text === domain)
if (option) {
domainList.value = option.value
customDomain.classList.add('d-none')
customDomain.value = ''
} else {
domainList.value = 'type'
customDomain.classList.remove('d-none')
customDomain.value = domain
}
}
/* =========================
부서명
========================= */
const deptInput = popup.querySelector('#department_name')
if (deptInput) deptInput.value = data.dept_nm || ''
/* =========================
휴대폰 (표시만)
========================= */
const phoneInput = popup.querySelector('#user_phone')
if (phoneInput) phoneInput.value = data.tel_no || ''
}
/* =========================
비밀번호 재인증 처리
========================= */
;(function bindPasswordVerify() {
const popup = document.getElementById('pop_mypage01')
if (!popup) return
const form = popup.querySelector('.tab-content.id form')
if (!form) return
form.addEventListener('submit', async (e) => {
e.preventDefault()
const pwInput = popup.querySelector('#login_password')
const pw = pwInput?.value.trim()
if (!pw) {
alert('비밀번호를 입력하세요.')
pwInput?.focus()
return
}
try {
const res = await fetch('/kngil/bbs/mypage01.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ pw })
})
const json = await res.json()
if (json.status === 'success') {
pwInput.value = ''
window.mypage02()
} else {
alert(json.message || '비밀번호가 올바르지 않습니다.')
pwInput.focus()
}
} catch (err) {
console.error('[mypage verify error]', err)
alert('인증 중 오류가 발생했습니다.')
}
})
})(); // ✅ IIFE 종료
/* =========================
공통 닫기
========================= */
window.closeMyPage = function () {
hide('pop_mypage01')
hide('pop_mypage02')
hide('pop_mypage03')
}
/* =========================
새로고침 / bfcache 대응 (script-only)
========================= */
window.addEventListener('pageshow', () => {
['pop_mypage01','pop_mypage02','pop_mypage03']
.forEach(hide)
})
/* =========================
비밀번호 변경 UI 토글
========================= */
function bindPasswordChangeUI() {
const rowOriginal = document.getElementById('row-pw-original')
const rowNew = document.getElementById('row-pw-new')
const btnChange = document.getElementById('btnPwChange')
const btnCancel = document.getElementById('btnPwCancel')
if (!rowOriginal || !rowNew || !btnChange || !btnCancel) {
console.warn('[mypage] password toggle elements missing')
return
}
// 초기 상태
rowNew.style.display = 'none'
// 비밀번호 변경
btnChange.addEventListener('click', () => {
rowOriginal.style.display = 'none'
rowNew.style.display = ''
})
// 취소
btnCancel.addEventListener('click', () => {
rowOriginal.style.display = ''
rowNew.style.display = 'none'
document.getElementById('current_pw').value = ''
document.getElementById('new_pw').value = ''
document.getElementById('new_pw_confirm').value = ''
})
}
/* =========================
회원정보 유효성 체크
========================= */
function bindPasswordValidation() {
const pw = document.getElementById('new_pw')
const pwConfirm = document.getElementById('new_pw_confirm')
const msg = document.getElementById('pwPolicyMsg')
if (!pw || !pwConfirm || !msg) return
function updateMessage(text, isError = true) {
msg.textContent = text
msg.classList.remove('d-none')
msg.style.color = isError ? '#e60000' : '#2e7d32'
}
function validate() {
const v = pw.value
const c = pwConfirm.value
if (!v && !c) {
msg.classList.add('d-none')
return
}
const policy = checkPasswordPolicy(v)
if (!policy.length) {
updateMessage('비밀번호는 8자 이상이어야 합니다.')
return
}
if (!policy.eng) {
updateMessage('영문자를 포함해야 합니다.')
return
}
if (!policy.num) {
updateMessage('숫자를 포함해야 합니다.')
return
}
if (!policy.special) {
updateMessage('특수문자를 포함해야 합니다.')
return
}
if (c && v !== c) {
updateMessage('비밀번호가 일치하지 않습니다.')
return
}
updateMessage('사용 가능한 비밀번호입니다.', false)
}
pw.addEventListener('input', validate)
pwConfirm.addEventListener('input', validate)
}
function checkPasswordPolicy(pw) {
return {
length: pw.length >= 8,
eng: /[a-zA-Z]/.test(pw),
num: /[0-9]/.test(pw),
special: /[^a-zA-Z0-9]/.test(pw)
}
}

680
kngil/js/popup.js Normal file
View File

@@ -0,0 +1,680 @@
/**
* Popup Controller Module
* 팝업 관련 기능을 관리하는 모듈
*/
(function() {
'use strict';
// ============================================
// Configuration
// ============================================
const CONFIG = {
SELECTORS: {
btnClose: '.btn-close',
btnMapClose: '.btn-map-close',
popupWrap: '.popup-wrap',
popupSitemap: '.popup-sitemap',
domainList: '#domain-list',
customDomain: '#custom-domain',
certNumber: '.cert-number',
code: '.code',
check: '.check',
checkComplete: '.check.complete',
timer: '.timer',
findEmail: '.find-email',
findPh: '.find-ph',
btnId: '.btn-id',
btnPw: '.btn-pw',
contentId: '.content.id',
contentPw: '.content.pw',
termsWrap: '.terms-wrap',
checkboxWrap: '.checkbox-wrap.all',
joinBtnWrap: '.join-btn-wrap',
popInputWrap: '.pop-input-wrap',
joinProgress: '.join-progress',
joinStep: '.join-step',
tabPrivacy: '.tab-privacy',
tabAgreement: '.tab-agreement',
tabContentPri: '.tab-content.pri',
tabContentAgr: '.tab-content.agr',
tabWrap: '.tab-menu',
contentsWrap: '.contents_wrap',
messages: '.messages'
},
TIMER: {
DURATION: 60 * 3, // 3분
INTERVAL: 1000 // 1초
},
CLASSES: {
on: 'on',
none: 'none',
show: 'show',
hide: 'hide',
complete: 'complete'
}
};
// ============================================
// Utility Functions
// ============================================
const Utils = {
/**
* Lenis 스크롤 시작
*/
startLenis() {
if (typeof lenis !== 'undefined' && lenis) {
lenis.start();
}
},
/**
* 스크롤 복원
*/
restoreScroll() {
$('body').css('overflow', '');
this.startLenis();
}
};
// ============================================
// Popup Close Controller
// ============================================
const PopupCloseController = {
init() {
// 팝업 닫기 버튼
$(document).on('click', CONFIG.SELECTORS.btnClose, () => {
$(CONFIG.SELECTORS.popupWrap).hide();
Utils.restoreScroll();
});
// 사이트맵 닫기 버튼
$(document).on('click', CONFIG.SELECTORS.btnMapClose, () => {
$(CONFIG.SELECTORS.popupSitemap).hide();
Utils.restoreScroll();
});
}
};
// ============================================
// Email Domain Controller
// ============================================
const EmailDomainController = {
init() {
// ID 기반 선택자 (기존 호환성)
$(document).on('change', CONFIG.SELECTORS.domainList, function() {
const $select = $(this);
const $selectBox = $select.closest('.select-box');
const $customDomain = $selectBox.find(CONFIG.SELECTORS.customDomain);
if ($select.val() === 'type') {
// 직접입력 선택 시 d-none 클래스 제거
$customDomain.removeClass('d-none').focus();
} else {
// 다른 선택지 선택 시 d-none 클래스 추가 및 값 초기화
$customDomain.addClass('d-none').val('');
}
});
// 클래스 기반 선택자 (.domain-list)
$(document).on('change', '.domain-list', function() {
const $select = $(this);
const $inputBox = $select.closest('.input-box');
const $customDomain = $inputBox.find('.domain-domain');
if ($select.val() === 'type') {
// 직접입력 선택 시 d-none 클래스 제거
$customDomain.removeClass('d-none').focus();
} else {
// 다른 선택지 선택 시 d-none 클래스 추가 및 값 초기화
$customDomain.addClass('d-none').val('');
}
});
}
};
// ============================================
// Timer Controller
// ============================================
// const TimerController = {
// interval: null,
// display: null,
// init() {
// this.display = $(CONFIG.SELECTORS.timer);
// // 인증번호 버튼 클릭 시
// $(document).on('click', CONFIG.SELECTORS.certNumber, () => {
// $(CONFIG.SELECTORS.code).show();
// this.start();
// });
// // 확인 버튼 클릭 시
// $(document).on('click', CONFIG.SELECTORS.check, function() {
// $(this).hide();
// $(CONFIG.SELECTORS.checkComplete).show();
// TimerController.stop();
// $(CONFIG.SELECTORS.timer).remove();
// });
// // 로그인 페이지 휴대폰 인증 폼의 인증 링크 요청 버튼 클릭 시
// $(document).on('submit', '#pop_login .tab-content.phone form', (e) => {
// e.preventDefault();
// const $form = $(e.target);
// const $timer = $form.find('.timer');
// const $infoBox = $form.find('.info-box');
// // 타이머와 info-box 표시
// // $timer.removeClass('hide');
// $timer.removeClass('d-none');
// $infoBox.removeClass('d-none');
// // 타이머 시작 (해당 폼 내의 타이머만)
// if ($timer.length) {
// this.stop();
// this.display = $timer;
// this.start();
// }
// });
// // 초기 타이머 시작 (다른 페이지에서 사용하는 경우를 위해)
// // 숨겨지지 않은 타이머만 자동 시작
// const $visibleTimers = $(CONFIG.SELECTORS.timer).not('.hide').not('.d-none');
// if ($visibleTimers.length > 0) {
// this.display = $visibleTimers.first();
// this.start();
// }
// },
// start() {
// this.stop();
// this.startTimer(CONFIG.TIMER.DURATION, this.display);
// },
// stop() {
// if (this.interval) {
// clearInterval(this.interval);
// this.interval = null;
// }
// },
// startTimer(duration, display) {
// let timer = duration;
// const updateTimer = () => {
// const minutes = Math.floor(timer / 60);
// const seconds = timer % 60;
// const formattedMinutes = minutes < 10 ? `0${minutes}` : minutes;
// const formattedSeconds = seconds < 10 ? `0${seconds}` : seconds;
// if (display && display.length) {
// display.text(`${formattedMinutes}:${formattedSeconds}`);
// }
// if (--timer < 0) {
// this.stop();
// if (display && display.length) {
// display.text('00:00');
// }
// }
// };
// updateTimer();
// this.interval = setInterval(updateTimer, CONFIG.TIMER.INTERVAL);
// }
// };
// ============================================
// Find Account Controller
// ============================================
const FindAccountController = {
/**
* 라디오 버튼 전환 처리
* @param {jQuery} $radio - 클릭된 라디오 버튼
* @param {string} type - 'ph' 또는 'email'
*/
switchRadio($radio, type) {
const $radioWrap = $radio.closest('.radio-wrap');
const $tabContent = $radioWrap.closest('.tab-content');
// 같은 라디오 그룹의 다른 라디오 버튼들
const $otherRadio = type === 'ph'
? $radioWrap.find(CONFIG.SELECTORS.findEmail)
: $radioWrap.find(CONFIG.SELECTORS.findPh);
// 라디오 버튼 상태 변경
$otherRadio.removeClass(CONFIG.CLASSES.on).prop('checked', false);
$radio.addClass(CONFIG.CLASSES.on).prop('checked', true);
// 테이블 표시/숨김 (해당 탭 콘텐츠 내에서만)
if (type === 'ph') {
$tabContent.find('table.email').hide();
$tabContent.find('table.ph').show();
} else {
$tabContent.find('table.ph').hide();
$tabContent.find('table.email').show();
}
},
/**
* 초기 상태 설정
*/
initRadioState() {
// 각 탭 콘텐츠별로 초기 상태 설정
$('.tab-content').each(function() {
const $tabContent = $(this);
const $checkedRadio = $tabContent.find('.radio-wrap input[type="radio"]:checked');
if ($checkedRadio.length) {
if ($checkedRadio.hasClass('find-ph')) {
$tabContent.find('table.ph').show();
$tabContent.find('table.email').hide();
} else if ($checkedRadio.hasClass('find-email')) {
$tabContent.find('table.email').show();
$tabContent.find('table.ph').hide();
}
}
});
},
init() {
// 이메일 찾기 라디오 버튼 클릭
$(document).on('click', CONFIG.SELECTORS.findEmail, function(e) {
e.preventDefault();
FindAccountController.switchRadio($(this), 'email');
});
// 전화번호 찾기 라디오 버튼 클릭
$(document).on('click', CONFIG.SELECTORS.findPh, function(e) {
e.preventDefault();
FindAccountController.switchRadio($(this), 'ph');
});
// 라디오 버튼 변경 이벤트 (체크 상태 동기화)
$(document).on('change', '.radio-wrap input[type="radio"]', function() {
const $radio = $(this);
if ($radio.hasClass('find-ph')) {
FindAccountController.switchRadio($radio, 'ph');
} else if ($radio.hasClass('find-email')) {
FindAccountController.switchRadio($radio, 'email');
}
});
// 초기 상태 설정
this.initRadioState();
}
};
// ============================================
// Login Tab Controller
// ============================================
const LoginTabController = {
init() {
// ID 찾기 탭
$(document).on('click', CONFIG.SELECTORS.btnId, () => {
$(CONFIG.SELECTORS.btnId).addClass(CONFIG.CLASSES.on);
$(CONFIG.SELECTORS.btnPw).removeClass(CONFIG.CLASSES.on);
$(CONFIG.SELECTORS.contentId).show();
$(CONFIG.SELECTORS.contentPw).hide();
});
// 비밀번호 찾기 탭
$(document).on('click', CONFIG.SELECTORS.btnPw, () => {
$(CONFIG.SELECTORS.btnPw).addClass(CONFIG.CLASSES.on);
$(CONFIG.SELECTORS.btnId).removeClass(CONFIG.CLASSES.on);
$(CONFIG.SELECTORS.contentPw).show();
$(CONFIG.SELECTORS.contentId).hide();
});
// 도메인 선택 (중복 제거)
$(CONFIG.SELECTORS.domainList).on('change', function() {
const $customDomain = $(CONFIG.SELECTORS.customDomain);
if ($(this).val() === 'type') {
$customDomain.show();
} else {
$customDomain.hide();
}
});
}
};
// ============================================
// Terms Agreement Controller
// ============================================
const TermsAgreementController = {
init() {
// 약관 동의 상태 업데이트
this.updateJoinButton();
// 전체 동의 체크박스 (기존 호환성)
$(CONFIG.SELECTORS.checkboxWrap).find('input[type="checkbox"]').on('change', (e) => {
const isChecked = $(e.target).is(':checked');
$(CONFIG.SELECTORS.termsWrap).find('input[type="checkbox"]').prop('checked', isChecked);
this.updateJoinButton();
});
// .chk-all 클래스를 가진 체크박스 (전체 동의)
$(document).on('change click', '.chk-all', function(e) {
e.stopPropagation(); // 이벤트 전파 중지
const $chkAll = $(this);
const isChecked = $chkAll.is(':checked');
const $termsWrap = $chkAll.closest('.terms-wrap');
// .chk-all을 제외한 모든 체크박스 선택/해제
$termsWrap.find('input[type="checkbox"]:not(.chk-all)').prop('checked', isChecked);
// updateJoinButton 호출 시 무한 루프 방지를 위해 플래그 설정
TermsAgreementController._updatingFromChkAll = true;
TermsAgreementController.updateJoinButton();
TermsAgreementController._updatingFromChkAll = false;
});
// 개별 체크박스
$(CONFIG.SELECTORS.termsWrap).find('input[type="checkbox"]').on('change', () => {
this.updateJoinButton();
});
// .terms-wrap 내의 개별 체크박스 (chk-all 제외)
$(document).on('change', '.terms-wrap input[type="checkbox"]:not(.chk-all)', () => {
this.updateJoinButton();
});
},
updateJoinButton() {
// .terms-wrap 내의 모든 체크박스 확인 (chk-all 제외)
const $allTermsWraps = $('.terms-wrap');
let allChecked = true;
$allTermsWraps.each(function() {
const $checkboxes = $(this).find('input[type="checkbox"]:not(.chk-all)');
const $checked = $checkboxes.filter(':checked');
if ($checkboxes.length > 0 && $checkboxes.length !== $checked.length) {
allChecked = false;
return false; // break
}
});
// .chk-all 체크박스 상태 업데이트 (무한 루프 방지)
if (!TermsAgreementController._updatingFromChkAll) {
$('.chk-all').prop('checked', allChecked);
}
// 전체 동의 체크박스 상태 업데이트 (기존 호환성)
$(CONFIG.SELECTORS.checkboxWrap).find('input[type="checkbox"]').prop('checked', allChecked);
// 버튼 상태 업데이트
const $joinBtnWrap = $(CONFIG.SELECTORS.joinBtnWrap);
if (allChecked) {
$joinBtnWrap.removeClass(CONFIG.CLASSES.none);
$joinBtnWrap.find('button').prop('disabled', false);
} else {
$joinBtnWrap.addClass(CONFIG.CLASSES.none);
$joinBtnWrap.find('button').prop('disabled', true);
}
}
};
// ============================================
// Join Completion Controller
// ============================================
const JoinCompletionController = {
init() {
// 전송완료
$(document).on('click', '.pw ' + CONFIG.SELECTORS.joinBtnWrap + ' button', () => {
$(CONFIG.SELECTORS.contentsWrap).children().not(CONFIG.SELECTORS.messages).hide();
$(CONFIG.SELECTORS.messages).show();
});
// 가입완료
$(document).on('click', '.join.completion ' + CONFIG.SELECTORS.joinBtnWrap + ' button', () => {
$(CONFIG.SELECTORS.popInputWrap).find('form').children().not(CONFIG.SELECTORS.messages).hide();
$(CONFIG.SELECTORS.messages).show();
// 진행 단계 업데이트
$(CONFIG.SELECTORS.joinProgress).find(CONFIG.SELECTORS.joinStep).removeClass(CONFIG.CLASSES.on);
$(CONFIG.SELECTORS.joinProgress).find(CONFIG.SELECTORS.joinStep).eq(2).addClass(CONFIG.CLASSES.on);
});
}
};
// ============================================
// Universal Tab Controller
// ============================================
const TabController = {
/**
* 탭 클래스명에서 콘텐츠 클래스명 추출
* @param {jQuery} $tab - 탭 요소
* @returns {string} 콘텐츠 클래스명
*/
getContentClass($tab) {
// 탭 클래스명에서 tab- 접두사 제거
const tabClasses = $tab.attr('class').split(' ');
let contentClass = '';
for (let i = 0; i < tabClasses.length; i++) {
const className = tabClasses[i];
if (className.startsWith('tab-')) {
const tabName = className.replace('tab-', '');
// 특수 케이스 매핑
const specialCases = {
'privacy': 'pri',
'agreement': 'agr'
};
contentClass = specialCases[tabName] || tabName;
break;
}
}
return contentClass;
},
/**
* 슬라이딩 인디케이터 위치 업데이트 (round 타입 탭 메뉴용)
* @param {jQuery} $tabMenu - 탭 메뉴 요소
* @param {jQuery} $activeTab - 활성화된 탭 요소
*/
updateSliderPosition($tabMenu, $activeTab) {
// round 클래스가 있는 경우에만 처리
if (!$tabMenu.hasClass('round')) {
return;
}
const $tabs = $tabMenu.find('li');
const activeIndex = $tabs.index($activeTab);
const tabCount = $tabs.length;
if (tabCount === 0) return;
// 탭 메뉴의 실제 너비와 패딩 확인
const tabMenuWidth = $tabMenu.width();
const padding = 0; // padding: 4px
const gap = 4; // gap: 4px
// 사용 가능한 너비 (패딩 제외)
const availableWidth = tabMenuWidth - (padding * 2);
// 각 탭의 너비 계산 (gap 포함)
// gap은 탭 사이에만 있으므로, 탭 개수 - 1개의 gap이 있음
const totalGapWidth = gap * (tabCount - 1);
const tabWidth = (availableWidth - totalGapWidth) / tabCount;
// 인디케이터 위치 계산 (패딩 + 탭 너비 * 인덱스 + gap * 인덱스)
const indicatorPosition = padding + (tabWidth + gap) * activeIndex;
// CSS 변수로 위치 설정 (픽셀 단위)
$tabMenu[0].style.setProperty('--tab-indicator-position', `${indicatorPosition}px`);
// 인디케이터 너비도 동적으로 설정
$tabMenu[0].style.setProperty('--tab-indicator-width', `${tabWidth}px`);
},
/**
* 탭 전환 처리
* @param {jQuery} $clickedTab - 클릭된 탭 요소
*/
switchTab($clickedTab) {
const $tabMenu = $clickedTab.closest(CONFIG.SELECTORS.tabWrap);
const $allTabs = $tabMenu.find('li');
// 콘텐츠 컨테이너 찾기 (pop-contents 또는 contents-wrap)
const $contentContainer = $clickedTab.closest('.pop-contents, .contents-wrap');
const $allContents = $contentContainer.find('.tab-content');
// 모든 탭에서 on 클래스 제거
$allTabs.removeClass(CONFIG.CLASSES.on);
// 클릭된 탭에 on 클래스 추가
$clickedTab.addClass(CONFIG.CLASSES.on);
// 슬라이딩 인디케이터 위치 업데이트 (round 타입인 경우)
this.updateSliderPosition($tabMenu, $clickedTab);
// 모든 콘텐츠 숨기기
$allContents.removeClass(CONFIG.CLASSES.show);
// 해당하는 콘텐츠만 표시
const contentClass = this.getContentClass($clickedTab);
if (contentClass) {
const $targetContent = $contentContainer.find('.tab-content.' + contentClass);
if ($targetContent.length) {
$targetContent.addClass(CONFIG.CLASSES.show);
}
}
},
/**
* 탭 초기 상태 설정
*/
initTabState() {
// 모든 tab-menu 찾기
$(CONFIG.SELECTORS.tabWrap).each(function() {
const $tabMenu = $(this);
const $tabs = $tabMenu.find('li');
const $contentContainer = $tabMenu.closest('.pop-contents, .contents-wrap');
if ($tabs.length === 0 || !$contentContainer.length) return;
// 활성화된 탭 찾기
const $activeTab = $tabs.filter('.' + CONFIG.CLASSES.on);
if ($activeTab.length > 0) {
// 활성화된 탭이 있으면 해당 콘텐츠 표시
TabController.switchTab($activeTab);
} else {
// 활성화된 탭이 없으면 첫 번째 탭 활성화
const $firstTab = $tabs.first();
$firstTab.addClass(CONFIG.CLASSES.on);
TabController.switchTab($firstTab);
}
// 리사이즈 시 인디케이터 위치 재계산
if ($tabMenu.hasClass('round')) {
$(window).on('resize.tabSlider', function() {
const $active = $tabMenu.find('li.' + CONFIG.CLASSES.on);
if ($active.length) {
TabController.updateSliderPosition($tabMenu, $active);
}
});
}
});
},
init() {
// 모든 tab-menu의 탭 클릭 이벤트
$(document).on('click', CONFIG.SELECTORS.tabWrap + ' li', function(e) {
e.preventDefault();
TabController.switchTab($(this));
});
// 키보드 접근성 지원 (탭 + 엔터/스페이스)
$(document).on('keydown', CONFIG.SELECTORS.tabWrap + ' li', function(e) {
// Enter 또는 Space 키
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
TabController.switchTab($(this));
}
// 화살표 키로 탭 전환
const $tabMenu = $(this).closest(CONFIG.SELECTORS.tabWrap);
const $tabs = $tabMenu.find('li');
const currentIndex = $tabs.index(this);
if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
e.preventDefault();
let targetIndex;
if (e.key === 'ArrowLeft') {
targetIndex = currentIndex > 0 ? currentIndex - 1 : $tabs.length - 1;
} else {
targetIndex = currentIndex < $tabs.length - 1 ? currentIndex + 1 : 0;
}
const $targetTab = $tabs.eq(targetIndex);
$targetTab.focus();
TabController.switchTab($targetTab);
}
});
// 초기 상태 설정
this.initTabState();
}
};
// ============================================
// Privacy Tab Controller (하위 호환성 유지)
// ============================================
const PrivacyTabController = {
switchTab: function($clickedTab) {
return TabController.switchTab.call(TabController, $clickedTab);
},
init: function() {
return TabController.init.call(TabController);
},
initTabState: function() {
return TabController.initTabState.call(TabController);
}
};
// ============================================
// Main Initialization
// ============================================
const PopupController = {
init() {
// DOM이 준비되면 초기화
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
this.start();
});
} else {
this.start();
}
},
start() {
PopupCloseController.init();
EmailDomainController.init();
TimerController.init();
FindAccountController.init();
LoginTabController.init();
TermsAgreementController.init();
JoinCompletionController.init();
TabController.init();
}
};
// ============================================
// Start Application
// ============================================
PopupController.init();
// ============================================
// Global Functions
// ============================================
// 전역에서 접근 가능하도록 함수 노출
window.PopupController = PopupController;
window.TabController = TabController;
window.PrivacyTabController = PrivacyTabController; // 하위 호환성 유지
})();

2
kngil/js/qa/jquery-3.6.1.min.js vendored Normal file

File diff suppressed because one or more lines are too long

5
kngil/js/qa/jquery.mousewheel.min.js vendored Normal file
View File

@@ -0,0 +1,5 @@
/*!
* jQuery Mousewheel 3.1.13
* Copyright OpenJS Foundation and other contributors
*/
!function(e){"function"==typeof define&&define.amd?define(["jquery"],e):"object"==typeof exports?module.exports=e:e(jQuery)}(function(a){var u,r,e=["wheel","mousewheel","DOMMouseScroll","MozMousePixelScroll"],t="onwheel"in window.document||9<=window.document.documentMode?["wheel"]:["mousewheel","DomMouseScroll","MozMousePixelScroll"],f=Array.prototype.slice;if(a.event.fixHooks)for(var n=e.length;n;)a.event.fixHooks[e[--n]]=a.event.mouseHooks;var d=a.event.special.mousewheel={version:"3.1.12",setup:function(){if(this.addEventListener)for(var e=t.length;e;)this.addEventListener(t[--e],i,!1);else this.onmousewheel=i;a.data(this,"mousewheel-line-height",d.getLineHeight(this)),a.data(this,"mousewheel-page-height",d.getPageHeight(this))},teardown:function(){if(this.removeEventListener)for(var e=t.length;e;)this.removeEventListener(t[--e],i,!1);else this.onmousewheel=null;a.removeData(this,"mousewheel-line-height"),a.removeData(this,"mousewheel-page-height")},getLineHeight:function(e){var t=a(e),e=t["offsetParent"in a.fn?"offsetParent":"parent"]();return e.length||(e=a("body")),parseInt(e.css("fontSize"),10)||parseInt(t.css("fontSize"),10)||16},getPageHeight:function(e){return a(e).height()},settings:{adjustOldDeltas:!0,normalizeOffset:!0}};function i(e){var t,n=e||window.event,i=f.call(arguments,1),o=0,l=0,s=0,h=0;if((e=a.event.fix(n)).type="mousewheel","detail"in n&&(s=-1*n.detail),"wheelDelta"in n&&(s=n.wheelDelta),"wheelDeltaY"in n&&(s=n.wheelDeltaY),"wheelDeltaX"in n&&(l=-1*n.wheelDeltaX),"axis"in n&&n.axis===n.HORIZONTAL_AXIS&&(l=-1*s,s=0),o=0===s?l:s,"deltaY"in n&&(o=s=-1*n.deltaY),"deltaX"in n&&(l=n.deltaX,0===s&&(o=-1*l)),0!==s||0!==l)return 1===n.deltaMode?(o*=t=a.data(this,"mousewheel-line-height"),s*=t,l*=t):2===n.deltaMode&&(o*=t=a.data(this,"mousewheel-page-height"),s*=t,l*=t),h=Math.max(Math.abs(s),Math.abs(l)),(!r||h<r)&&c(n,r=h)&&(r/=40),c(n,h)&&(o/=40,l/=40,s/=40),o=Math[1<=o?"floor":"ceil"](o/r),l=Math[1<=l?"floor":"ceil"](l/r),s=Math[1<=s?"floor":"ceil"](s/r),d.settings.normalizeOffset&&this.getBoundingClientRect&&(h=this.getBoundingClientRect(),e.offsetX=e.clientX-h.left,e.offsetY=e.clientY-h.top),e.deltaX=l,e.deltaY=s,e.deltaFactor=r,e.deltaMode=0,i.unshift(e,o,l,s),u&&window.clearTimeout(u),u=window.setTimeout(w,200),(a.event.dispatch||a.event.handle).apply(this,i)}function w(){r=null}function c(e,t){return d.settings.adjustOldDeltas&&"mousewheel"===e.type&&t%120==0}a.fn.extend({mousewheel:function(e){return e?this.on("mousewheel",e):this.trigger("mousewheel")},unmousewheel:function(e){return this.off("mousewheel",e)}})});

1325
kngil/js/qa/qa_common.js Normal file

File diff suppressed because it is too large Load Diff

198
kngil/js/qa/qa_index.js Normal file
View File

@@ -0,0 +1,198 @@
$(function () {
var obj = document.getElementById("video_play");
var video = $("#video_play").get(0);
var i = 1;
// 영상 소스를 비율에 맞게 설정하는 함수
function updateVideoSource() {
var width = $(window).width();
var height = $(window).height();
var ratio = width / height;
// 비율이 가로가 더 길면 기본 영상, 세로가 더 길면 '_v'가 붙은 영상
if (ratio > 1) {
$("#video_play").attr("src", "img/main_" + i + ".mp4");
$("ul.pagination_main").removeClass("m");
} else {
$("#video_play").attr("src", "img/main_" + i + "_v.mp4");
$("ul.pagination_main").addClass("m");
}
// 영상 로드 및 자동 재생
video.load();
video.play();
}
// 페이지 로드 시 비율에 맞는 영상 설정
updateVideoSource();
// 화면 사이즈가 변경될 때마다 비율에 맞는 영상 설정
$(window).resize(function () {
updateVideoSource();
});
console.log("리사이징 완료");
// 인트로 페이지 종료 후 첫 영상 실행
video.pause();
$(".pagination_main").hide();
if (sessionStorage.getItem("visited")) {
video.play();
$(".pagination_main").show();
} else {
setTimeout(function () {
video.play();
$(".pagination_main").show();
}, 2800);
}
// 페이지 네이션 - 클릭하면 해당 영상 실행
function setPageVideo(pageNum) {
i = pageNum;
updateVideoSource(); // 비율에 맞는 영상으로 설정
$(".page_0" + i).addClass("page_on");
$(".pagination_main div")
.not(".page_0" + i)
.removeClass("page_on");
$(".main_link_0" + i).addClass("link_on");
$(".main_link a")
.not(".main_link_0" + i)
.removeClass("link_on");
}
// 각 페이지 클릭 시 영상 변경
$(".page_01").click(function () {
setPageVideo(1);
console.log("영상1 재생");
});
$(".page_02").click(function () {
setPageVideo(2);
console.log("영상2 재생");
});
$(".page_03").click(function () {
setPageVideo(3);
console.log("영상3 재생");
});
$(".page_04").click(function () {
setPageVideo(4);
console.log("영상4 재생");
});
$(".page_05").click(function () {
setPageVideo(5);
console.log("영상5 재생");
});
// 영상 종료 후 다음 영상 실행
$("#video_play").on("ended", function () {
if (i < 5) {
i = i + 1;
updateVideoSource();
$(".page_0" + i).addClass("page_on");
$(".pagination_main div")
.not(".page_0" + i)
.removeClass("page_on");
$(".main_link_0" + i).addClass("link_on");
$(".main_link a")
.not(".main_link_0" + i)
.removeClass("link_on");
} else {
i = 1;
updateVideoSource();
$(".page_0" + i).addClass("page_on");
$(".pagination_main div")
.not(".page_0" + i)
.removeClass("page_on");
$(".main_link_0" + i).addClass("link_on");
$(".main_link a")
.not(".main_link_0" + i)
.removeClass("link_on");
}
video.play(); // 영상 자동 재생
});
});
// index footer 동작
$(function () {
let currentMode = null;
// 디바이스 체크 함수
function getDeviceType() {
const ua = navigator.userAgent.toLowerCase();
const isMobile =
/android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(ua);
const isTablet = /(ipad|tablet|playbook|silk)|(android(?!.*mobile))/i.test(
ua
);
// 터치 지원 여부도 체크
const hasTouch = "ontouchstart" in window || navigator.maxTouchPoints > 0;
// 모바일 또는 태블릿이면서 터치 지원
if ((isMobile || isTablet) && hasTouch) {
return "mo";
}
return "pc";
}
function setupFooterEvents() {
const newMode = getDeviceType();
// 모드가 변경되지 않았으면 리턴
if (currentMode === newMode) return;
currentMode = newMode;
// 기존 이벤트 제거
$(".main").off("wheel mousewheel touchmove");
if (newMode === "pc") {
// PC: 기본 footer_off 상태
$("footer").addClass("footer_off").removeClass("footer_on");
// wheel 이벤트로 토글
$(".main").on("wheel", function (e) {
if (e.originalEvent.deltaY < 0) {
// 위로 스크롤
$("footer").addClass("footer_off").removeClass("footer_on");
} else {
// 아래로 스크롤
$("footer").addClass("footer_on").removeClass("footer_off");
}
});
} else {
// 모바일/태블릿: touchmove 시 footer_on
$(".main").on("touchmove", function () {
$("footer").addClass("footer_on").removeClass("footer_off");
});
}
}
// 초기 설정
setupFooterEvents();
// footer 닫기
$(".footer_close")
.off("click")
.on("click", function () {
$("footer").addClass("footer_off").removeClass("footer_on");
});
});
// 인트로 없애기
// 1. 홈페이지에 들어오면 sessionStorage에 visited 추가
// 2. visited가 있는 동안에는 인트로 삭제
// 3. 브라우저 종료 or 탭 닫으면 visited 자동 삭제
document.addEventListener("DOMContentLoaded", function () {
const intro = document.querySelector(".intro_wrap");
if (sessionStorage.getItem("visited")) {
console.log("visited");
intro.style.display = "none";
document.querySelector(".main_mask").classList.add("skip");
} else {
setTimeout(() => {
sessionStorage.setItem("visited", "true");
}, 1000);
}
});

256
kngil/js/qa/qa_popup.js Normal file
View File

@@ -0,0 +1,256 @@
window.onload = function() {
$.ajax({
url: "some_api_endpoint",
success: function(response) {
},
complete: function() {
// 팝업 닫기버튼
$(document).ready(function() {
$('.btn_close').click(function() {
$('.popup_wrap').hide();
$('body').css('overflow', ''); // 기본 스크롤 상태로 복귀
lenis.start();
console.log('lenis 재시작')
});
});
$(document).ready(function() {
$('.btn_map_close').click(function() {
$('.popup_sitemap').hide();
$('body').css('overflow', ''); // 기본 스크롤 상태로 복귀
lenis.start();
console.log('lenis 재시작')
});
});
// 이메일 직접입력
$(document).ready(function() {
$('#domain-list').change(function() {
if ($(this).val() === 'type') {
$('#custom-domain').show().focus();
} else {
$('#custom-domain').hide().val('');
}
});
});
// 인증번호 타이머
$(document).ready(function() {
// 인증번호 버튼 클릭 시
$('.cert_number').click(function() {
$('.code').show();
});
// 확인 버튼 클릭 시
$('.check').click(function() {
$(this).hide();
$('.check.complete').show();
clearInterval(interval); // 타이머 멈춤
$('.timer').remove(); // 타이머 요소 삭제
});
// 타이머 함수
var interval;
function startTimer(duration, display) {
clearInterval(interval);
var timer = duration, minutes, seconds;
function updateTimer() {
minutes = parseInt(timer / 60, 10);
seconds = parseInt(timer % 60, 10);
minutes = minutes < 10 ? "0" + minutes : minutes;
seconds = seconds < 10 ? "0" + seconds : seconds;
display.text(minutes + ":" + seconds);
if (--timer < 0) {
clearInterval(interval);
display.text("00:00");
}
}
updateTimer();
interval = setInterval(updateTimer, 1000);
}
var threeMinutes = 60 * 3,
display = $('.timer');
startTimer(threeMinutes, display);
$('.cert_number').click(function() {
startTimer(threeMinutes, display);
});
});
// 아이디찾기
$(document).ready(function(){
$('.find_email').click(function(){
$('.find_ph').removeClass('on').prop('checked', false);
$(this).addClass('on').prop('checked', true);
$('.ph').hide();
$('.email').show();
});
$('.find_ph').click(function(){
$('.find_email').removeClass('on').prop('checked', false);
$(this).addClass('on').prop('checked', true);
$('.email').hide();
$('.ph').show();
});
});
$(document).ready(function() {
$('.btn_id').on('click', function() {
$('.btn_id').addClass('on');
$('.btn_pw').removeClass('on');
$('.content.id').show();
$('.content.pw').hide();
});
$('.btn_pw').on('click', function() {
$('.btn_pw').addClass('on');
$('.btn_id').removeClass('on');
$('.content.pw').show();
$('.content.id').hide();
});
$('#domain-list').on('change', function() {
if ($(this).val() === 'type') {
$('#custom-domain').show();
} else {
$('#custom-domain').hide();
}
});
});
// 전체약관동의
// $(document).ready(function() {
// function toggleJoinButton() {
// // 모든 개별 체크박스가 체크되었는지 확인
// var allChecked = $('.terms_wrap input[type="checkbox"]').length === $('.terms_wrap input[type="checkbox"]:checked').length;
// // '약관에 모두 동의합니다' 체크박스 상태에 따라 조정
// $('.checkbox_wrap.all input[type="checkbox"]').prop('checked', allChecked);
// // 모든 체크박스가 체크되지 않은 경우 버튼에 'none' 클래스 추가하고 disabled 속성 추가
// if (allChecked) {
// $('.join_btn_wrap').removeClass('none');
// $('.join_btn_wrap button').prop('disabled', false);
// } else {
// $('.join_btn_wrap').addClass('none');
// $('.join_btn_wrap button').prop('disabled', true);
// }
// }
// // '약관에 모두 동의합니다' 체크박스의 변경 이벤트
// $('.checkbox_wrap.all input[type="checkbox"]').on('change', function() {
// var isChecked = $(this).is(':checked');
// $('.terms_wrap input[type="checkbox"]').prop('checked', isChecked);
// toggleJoinButton(); // 버튼 상태 업데이트
// });
// // 각 terms_wrap의 개별 체크박스 변경 이벤트
// $('.terms_wrap input[type="checkbox"]').on('change', function() {
// toggleJoinButton(); // 버튼 상태 업데이트
// });
// // 초기 상태 설정
// toggleJoinButton();
// });
// 전체약관동의 수정 250813
(function ($) {
if (window.__agreeBound) return; // 중복 바인딩 방지
window.__agreeBound = true;
const $container = $('#pop_agreement');
const $items = $container.find('.terms_wrap input[type="checkbox"]'); // agree11, agree21
const $all = $container.find('.checkbox_wrap.all input[type="checkbox"]');
const $btn = $container.find('#btn_agree');
function syncAll() {
const allChecked = $items.length > 0 && $items.filter(':checked').length === $items.length;
$all.prop('checked', allChecked);
// 버튼은 disable 하지 않음 (스타일만 조정하고 싶다면 클래스만 토글)
// $('.join_btn_wrap').toggleClass('none', !allChecked); <-- 필요 없으면 제거
}
// 전체동의 → 개별
$all.on('change', function () {
const on = $(this).is(':checked');
$items.prop('checked', on);
syncAll();
});
// 개별 → 전체동의 동기화
$items.on('change', syncAll);
// 동의 버튼 클릭 시에만 검사
$btn.off('click.agree').on('click.agree', function (e) {
e.preventDefault();
const allChecked = $items.filter(':checked').length === $items.length;
if (!allChecked) {
alert('약관에 모두 동의해주세요.');
return false;
}
// 통과 시 다음 단계로 진행(필요 시 주석 해제)
// $('#pop_agreement').hide();
// $('#pop_register_form').show();
// $('body').css('overflow','hidden');
});
// 외부에서 팝업 열 때 상태 초기화가 필요하면 이 함수 호출
window.resetAgreementUI = function () {
$items.prop('checked', false);
$all.prop('checked', false);
syncAll();
// 버튼은 항상 활성
$('#btn_agree, #fregister button[type=submit]')
.prop('disabled', false)
.css('pointer-events', 'auto');
};
})(jQuery);
// 가입완료
$(document).ready(function() {
$('.join.completion .join_btn_wrap button').click(function() {
$('.pop_input_wrap form').children().not('.messages').hide();
$('.messages').show();
});
});
$(document).ready(function() {
$('.join.completion .join_btn_wrap button').click(function() {
$('.pop_input_wrap form').children().not('.messages').hide();
$('.messages').show();
// 세 번째 단계에 'on' 클래스 추가하고, 다른 단계에서 'on' 클래스 제거
$('.join_progress .join_step').removeClass('on');
$('.join_progress .join_step').eq(2).addClass('on');
});
});
// 개인정보 보호정책 스크립트
$(document).ready(function() {
$('.tab_privacy').on('click', function() {
$(this).addClass('on');
$('.tab_agreement').removeClass('on');
$('.content.pri').addClass('show').removeClass('hide');
$('.content.agr').removeClass('show').addClass('hide');
});
$('.tab_agreement').on('click', function() {
$(this).addClass('on');
$('.tab_privacy').removeClass('on');
$('.content.agr').addClass('show').removeClass('hide');
$('.content.pri').removeClass('show').addClass('hide');
});
});
}
});
};

109
kngil/js/qa_write.js Normal file
View File

@@ -0,0 +1,109 @@
// /kngil/js/qa/qa_write.js
document.addEventListener('DOMContentLoaded', () => {
/* ==========================
* 1. CKEditor 초기화
* ========================== */
let editorInstance = null;
const contentEl = document.querySelector('#content');
if (contentEl) {
ClassicEditor
.create(contentEl, {
toolbar: [
'heading','|',
'bold','italic','link',
'bulletedList','numberedList','|',
'fontColor','fontBackgroundColor','fontSize',
'|','blockQuote','insertTable','imageUpload',
'undo','redo'
],
language: 'ko',
ckfinder: {
uploadUrl: '/kngil/bbs/qa_img_upload.php'
}
})
.then(editor => {
editorInstance = editor;
})
.catch(err => console.error(err));
}
/* ==========================
* 2. 폼 submit 검증
* ========================== */
const form = document.getElementById('qaForm');
if (form) {
form.addEventListener('submit', (e) => {
if (!editorInstance) return;
const data = editorInstance.getData().trim();
if (!data) {
e.preventDefault();
alert('내용을 입력해주세요.');
editorInstance.editing.view.focus();
return;
}
contentEl.value = data; // textarea에 반영
});
}
/* ==========================
* 3. 첨부파일 Drag & Drop
* ========================== */
const dropZone = document.getElementById('drop-zone');
const fileInput = document.getElementById('attach');
const fileList = document.getElementById('file-list');
if (dropZone && fileInput && fileList) {
let uploadFiles = [];
dropZone.addEventListener('click', () => fileInput.click());
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('dragover');
});
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('dragover');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('dragover');
addFiles(e.dataTransfer.files);
});
fileInput.addEventListener('change', () => {
addFiles(fileInput.files);
});
function addFiles(files) {
for (const file of files) {
uploadFiles.push(file);
}
render();
syncInput();
}
function render() {
fileList.innerHTML = '';
uploadFiles.forEach(file => {
const li = document.createElement('li');
li.textContent = `${file.name} (${(file.size / 1024).toFixed(1)} KB)`;
fileList.appendChild(li);
});
}
function syncInput() {
const dt = new DataTransfer();
uploadFiles.forEach(f => dt.items.add(f));
fileInput.files = dt.files;
}
}
});

View File

@@ -0,0 +1,96 @@
(function($) {
$.fn.viewimageresize = function(selector)
{
var cfg = {
selector: "img"
};
if(typeof selector == "object") {
cfg = $.extend(cfg, selector);
} else {
if(selector) {
cfg = $.extend({ selector: selector });
}
}
var $img = this.find(cfg.selector);
var $this = this;
$img.removeAttr("height")
.css("height", "");
function image_resize()
{
var width = $this.width();
$img.each(function() {
if($(this).data("width") == undefined)
$(this).data("width", $(this).width());
if($(this).data("width") > width) {
$(this).removeAttr("width")
.removeAttr("height")
.css("width","")
.css("height", "");
if($(this).data("width") > width) {
$(this).css("width", "100%");
}
} else {
$(this).attr("width", $(this).data("width"));
}
});
}
$(window).on("load", function() {
image_resize();
});
$(window).on("resize", function() {
image_resize();
});
}
$.fn.viewimageresize2 = function(selector)
{
var cfg = {
selector: "img"
};
if(typeof selector == "object") {
cfg = $.extend(cfg, selector);
} else {
if(selector) {
cfg = $.extend({ selector: selector });
}
}
var $img = this.find(cfg.selector);
var $this = this;
function image_resize()
{
var width = $this.width();
$img.each(function() {
$(this).removeAttr("width")
.removeAttr("height")
.css("width","")
.css("height", "");
if($(this).data("width") == undefined)
$(this).data("width", $(this).width());
if($(this).data("width") > width) {
$(this).css("width", "100%");
}
});
}
$(window).on("resize", function() {
image_resize();
});
image_resize();
}
}(jQuery));