{"id":1852,"date":"2025-09-16T19:06:56","date_gmt":"2025-09-16T11:06:56","guid":{"rendered":"https:\/\/stayeasyinn.net\/?page_id=1852"},"modified":"2025-10-02T18:37:58","modified_gmt":"2025-10-02T10:37:58","slug":"profile","status":"publish","type":"page","link":"https:\/\/stayeasyinn.net\/ko\/profile\/","title":{"rendered":"\u6703\u54e1\u8cc7\u6599"},"content":{"rendered":"<div id=\"lml-wrap\" class=\"lml-profile\">\n  <div id=\"lml-notice\" style=\"margin-bottom:12px;\"><\/div>\n\n  <div id=\"lml-loading\">\u6b63\u5728\u8f09\u5165\u6703\u54e1\u8cc7\u6599\u2026<\/div>\n\n  <div id=\"lml-profile\" style=\"display:none;\">\n    <h3>\u6211\u7684\u6703\u54e1\u8cc7\u6599<\/h3>\n    <table style=\"width:100%;max-width:640px;\">\n      <tbody>\n        <tr><th style=\"text-align:left;width:140px\">LINE ID<\/th><td id=\"mp_line_user_id\"><\/td><\/tr>\n        <tr><th style=\"text-align:left\">\u59d3\u540d<\/th><td id=\"mp_name\"><\/td><\/tr>\n        <tr><th style=\"text-align:left\">\u624b\u6a5f<\/th><td id=\"mp_phone\"><\/td><\/tr>\n        <tr><th style=\"text-align:left\">Email<\/th><td id=\"mp_email\"><\/td><\/tr>\n        <tr><th style=\"text-align:left\">\u8eab\u5206\u8b49<\/th><td id=\"mp_nid\"><\/td><\/tr>\n        <tr><th style=\"text-align:left\">\u751f\u65e5<\/th><td id=\"mp_birthday\"><\/td><\/tr>\n        <tr><th style=\"text-align:left\">\u884c\u92b7\u540c\u610f<\/th><td id=\"mp_consent\"><\/td><\/tr>\n        <tr><th style=\"text-align:left\">\u72c0\u614b<\/th><td id=\"mp_status\"><\/td><\/tr>\n        <tr><th style=\"text-align:left\">\u5efa\u7acb\u6642\u9593<\/th><td id=\"mp_created\"><\/td><\/tr>\n        <tr><th style=\"text-align:left\">\u66f4\u65b0\u6642\u9593<\/th><td id=\"mp_updated\"><\/td><\/tr>\n      <\/tbody>\n    <\/table>\n\n    <hr style=\"margin:20px 0;\">\n    <h3>\u6211\u7684\u6298\u6263\u78bc<\/h3>\n    <div id=\"lml-coupon-loading\" style=\"margin:8px 0; display:none;\">\u6b63\u5728\u8f09\u5165\u6298\u6263\u78bc\u2026<\/div>\n    <div id=\"lml-coupons-empty\" style=\"margin:8px 0; display:none; color:#666;\">\u76ee\u524d\u6c92\u6709\u53ef\u7528\u7684\u6298\u6263\u78bc\u3002<\/div>\n\n    <div style=\"margin:8px 0;\">\n      <label style=\"margin-right:6px;\">\u72c0\u614b\uff1a<\/label>\n      <select id=\"lml-status-filter\" onchange=\"try{ lmlFetchCoupons(1); }catch(e){ console.error(e); }\">\n        <option value=\"AVAILABLE\" selected>\u50c5\u986f\u793a\u53ef\u7528<\/option>\n        <option value=\"\">\u5168\u90e8<\/option>\n        <option value=\"EXPIRED\">\u5df2\u904e\u671f<\/option>\n      <\/select>\n      <label style=\"margin-left:14px; margin-right:6px;\">\u6392\u5e8f\uff1a<\/label>\n      <select id=\"lml-sort\" onchange=\"try{ lmlFetchCoupons(1); }catch(e){ console.error(e); }\">\n        <option value=\"expiry\" selected>\u6709\u6548\u671f\u9650<\/option>\n        <option value=\"name\">\u6d3b\u52d5\u540d\u7a31<\/option>\n        <option value=\"status\">\u72c0\u614b<\/option>\n      <\/select>\n    <\/div>\n\n    <div style=\"overflow:auto;\">\n      <table id=\"lml-coupons\" style=\"width:100%;max-width:820px; border-collapse:collapse; display:none;\">\n        <thead>\n          <tr style=\"border-bottom:1px solid #ddd;\">\n            <th style=\"text-align:left; padding:6px 8px;\">\u4ee3\u78bc<\/th>\n            <th style=\"text-align:left; padding:6px 8px;\">\u6d3b\u52d5<\/th>\n            <th style=\"text-align:left; padding:6px 8px;\">\u6298\u6263<\/th>\n            <th style=\"text-align:left; padding:6px 8px;\">\u6709\u6548\u671f\u9650<\/th>\n            <th style=\"text-align:left; padding:6px 8px;\">\u72c0\u614b<\/th>\n            <th style=\"text-align:left; padding:6px 8px;\">\u64cd\u4f5c<\/th>\n          <\/tr>\n        <\/thead>\n        <tbody id=\"lml-coupons-tbody\"><\/tbody>\n      <\/table>\n    <\/div>\n\n    <div id=\"lml-pagination\" style=\"margin-top:10px; display:none;\">\n      <button id=\"lml-prev\" type=\"button\" class=\"button\" disabled>\u4e0a\u4e00\u9801<\/button>\n      <span id=\"lml-pageinfo\" style=\"margin:0 10px; color:#555;\"><\/span>\n      <button id=\"lml-next\" type=\"button\" class=\"button\">\u4e0b\u4e00\u9801<\/button>\n    <\/div>\n\n    <div style=\"margin-top:12px\">\n      <a id=\"lml-edit-link\" class=\"button\" href=\"#\" style=\"display:none;\">\u7de8\u8f2f\u8cc7\u6599<\/a>\n    <\/div>\n  <\/div>\n<\/div>\n\n<script src=\"https:\/\/static.line-scdn.net\/liff\/edge\/2\/sdk.js\"><\/script>\n<script>\n(function(){\n  const LIFF_ID     = \"2008012141-25KYXEZ4\";         \/\/ \u672c\u9801\uff08Profile\uff09\u9019\u9846 LIFF\n  const JOIN_LIFF_ID= \"2008012141-bWgDmRL3\";     \/\/ \u300c\u52a0\u5165\u6703\u54e1\u300d\u90a3\u9846 LIFF\uff08\u53ef\u9078\uff0c\u5efa\u8b70\u586b\uff09\n  const ENDPOINT    = \"https:\/\/stayeasyinn.net\/ko\/wp-json\/line\/v1\/member\/get\";\n  const noticeEl    = document.getElementById('lml-notice');\n  const loading     = document.getElementById('lml-loading');\n  const profileBox  = document.getElementById('lml-profile');\n\n  const COUPON_API  = \"https:\/\/stayeasyinn.net\/ko\/wp-json\/line\/v1\/coupons\/my\";\n  const BOOKING_URL = \"https:\/\/book-directonline.com\/properties\/StayEasyInnDirect?locale=zh-TW\";\n  const BOOKING_QS  = \"coupon\";\n  \n  const JOIN_PAGE_URL = \"https:\/\/stayeasyinn.net\/ko\/join\/\";\n\nfunction buildLiffUrl(liffId, redirect){\n  return 'https:\/\/liff.line.me\/' + encodeURIComponent(liffId) +\n         '?liff.redirectUri=' + encodeURIComponent(redirect);\n}\n\nconst JOIN_URL = buildLiffUrl(JOIN_LIFF_ID, JOIN_PAGE_URL);\n\n  const couponLoading = document.getElementById('lml-coupon-loading');\n  const couponEmpty   = document.getElementById('lml-coupons-empty');\n  const couponTable   = document.getElementById('lml-coupons');\n  const couponTbody   = document.getElementById('lml-coupons-tbody');\n  const statusFilter  = document.getElementById('lml-status-filter');\n  const sortSelect    = document.getElementById('lml-sort');\n  const pagWrap       = document.getElementById('lml-pagination');\n  const btnPrev       = document.getElementById('lml-prev');\n  const btnNext       = document.getElementById('lml-next');\n  const pageInfo      = document.getElementById('lml-pageinfo');\n\n  let currentUserId = null;\n  let currentPage = 1;\n  const PER_PAGE = 10;\n\n  function showNotice(msg, type){ noticeEl.innerText = msg; noticeEl.style.color = (type==='error'?'#b00':'#080'); }\n  function showError(msg){ loading.style.display='none'; profileBox.style.display='none'; showNotice(msg,'error'); }\n\n  \/\/ ====== \u95dc\u9375\uff1a\u667a\u6167\u8df3 join\uff0c\u5168\u529b\u7dad\u6301\u5728 LINE App \u5167 ======\n  function buildDeepLink(liffId, redirect){\n    return 'line:\/\/app\/' + encodeURIComponent(liffId) + '?liff.redirectUri=' + encodeURIComponent(redirect);\n  }\n  function buildWebLink(liffId, redirect){\n    return 'https:\/\/liff.line.me\/' + encodeURIComponent(liffId) + '?liff.redirectUri=' + encodeURIComponent(redirect);\n  }\n  function buildAndroidIntent(liffId, redirect){\n    const fallback = buildWebLink(liffId, redirect);\n    return 'intent:\/\/app\/' + encodeURIComponent(liffId)\n      + '#Intent;scheme=line;package=jp.naver.line.android;S.browser_fallback_url='\n      + encodeURIComponent(fallback) + ';end';\n  }\n  async function goJoinSmart(){\n    try{\n      \/\/ 1) \u5728 LINE App webview\uff1a\u76f4\u63a5\u5167\u5d4c\u958b\u555f\uff08\u4e0d\u8df3\u5916\u90e8\uff09\n      if (typeof liff !== 'undefined' && typeof liff.isInClient === 'function' && liff.isInClient()){\n        await liff.openWindow({ url: JOIN_URL, external: false });\n        return;\n      }\n    }catch(_){}\n\n    \/\/ 2) \u4e0d\u5728 LINE App\uff0c\u4f46\u4f60\u6709\u8a2d\u5b9a JOIN_LIFF_ID\uff1a\u5148\u5617\u8a66 deep link \u62c9\u56de LINE\n    if (JOIN_LIFF_ID){\n      const deep = buildDeepLink(JOIN_LIFF_ID, JOIN_URL);\n      const intent = buildAndroidIntent(JOIN_LIFF_ID, JOIN_URL);\n      const web = buildWebLink(JOIN_LIFF_ID, JOIN_URL);\n\n      \/\/ iOS\/Android \u5148\u8df3 line:\/\/ \uff0c\u5931\u6557\u518d\u88dc Android intent\uff0c\u6700\u5f8c\u518d\u9000\u56de web LIFF\n      location.href = deep;\n      setTimeout(function(){\n        if (\/Android\/i.test(navigator.userAgent)) {\n          location.href = intent;\n        } else {\n          location.href = web;\n        }\n      }, 800);\n      return;\n    }\n\n    \/\/ 3) \u6c92\u6709 JOIN_LIFF_ID\uff1a\u9000\u56de\u4e00\u822c\u700f\u89bd\u5668\u958b\u555f join\uff08\u6700\u5f8c\u624b\u6bb5\uff09\n    window.location.replace(JOIN_URL);\n  }\n\n  function fmtDate(s){\n    if (!s) return '';\n    const d = new Date(String(s).replace(' ', 'T'));\n    if (isNaN(d.getTime())) return s;\n    const pad = n=>String(n).padStart(2,'0');\n    return `${d.getFullYear()}\/${pad(d.getMonth()+1)}\/${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;\n  }\n  function fmtDiscount(type,val,max){\n    if (type === 'AMOUNT') return `\u6298\u62b5 $${Number(val).toFixed(0)}`;\n    let t = `\u6298\u62b5 ${Number(val)}%`; if (max) t += `\uff08\u4e0a\u9650 $${Number(max).toFixed(0)}\uff09`; return t;\n  }\n  function statusTag(s){\n    const map = { 'AVAILABLE':'#0a0', 'USED':'#777', 'EXPIRED':'#b00' };\n    const color = map[s] || '#333';\n    return `<span style=\"color:${color};font-weight:600;\">${s}<\/span>`;\n  }\n  function renderCoupons(list){\n    couponTbody.innerHTML = '';\n    list.forEach(c=>{\n      const tr = document.createElement('tr'); tr.style.borderBottom='1px solid #eee';\n      const btnCopy = `<button type=\"button\" data-code=\"${c.code}\" class=\"lml-copy-btn\" style=\"padding:4px 8px;\">\u8907\u88fd<\/button>`;\n      const link = new URL(\"https:\/\/book-directonline.com\/properties\/StayEasyInnDirect?locale=zh-TW\"); \/\/ \u7d55\u5c0d URL\n      link.searchParams.set(\"coupon\", c.code || '');\n      const btnBook = `<a href=\"${link.toString()}\" class=\"button\" style=\"padding:4px 8px;\">\u524d\u5f80\u8a02\u623f<\/a>`;\n      tr.innerHTML = `\n        <td style=\"padding:6px 8px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\">${c.code || ''}<\/td>\n        <td style=\"padding:6px 8px;\">${(c.name || c.campaign_key || '')}<\/td>\n        <td style=\"padding:6px 8px;\">${fmtDiscount(c.discount_type, c.discount_value, c.max_discount)}<\/td>\n        <td style=\"padding:6px 8px;\">${fmtDate(c.expires_at)}<\/td>\n        <td style=\"padding:6px 8px;\">${statusTag(c.status || 'AVAILABLE')}<\/td>\n        <td style=\"padding:6px 8px;\">${btnCopy} ${btnBook}<\/td>`;\n      couponTbody.appendChild(tr);\n    });\n    couponTbody.querySelectorAll('.lml-copy-btn').forEach(btn=>{\n      btn.addEventListener('click', async (e)=>{\n        const code = e.currentTarget.getAttribute('data-code');\n        try{ await navigator.clipboard.writeText(code); showNotice(`\u5df2\u8907\u88fd\u6298\u6263\u78bc\uff1a${code}`,'ok'); }\n        catch(_){ alert('\u8907\u88fd\u5931\u6557\uff0c\u8acb\u624b\u52d5\u9078\u53d6\u8907\u88fd\u3002'); }\n      });\n    });\n  }\n\n  \/\/ ===== \u5168\u6293\u4e00\u6b21 \u2192 \u524d\u7aef\u5feb\u53d6\uff08\u4fdd\u6301\u4f60\u539f\u672c\u7684\u9ad4\u9a57\uff09 =====\n  let COUPON_CACHE = [];\n  let COUPON_CACHE_LOADED = false;\n  async function ensureCouponCache(){\n    if (COUPON_CACHE_LOADED) return;\n    const CHUNK = 200;\n    const base = new URLSearchParams();\n    base.set('line_user_id', currentUserId);\n    base.set('status',''); base.set('sort','expiry'); base.set('order','asc'); base.set('per_page', CHUNK);\n    let p=1, total=0, all=[];\n    while(true){\n      const qs = new URLSearchParams(base); qs.set('page', p);\n      const resp = await fetch(`${COUPON_API}?${qs.toString()}`, {method:'GET', credentials:'same-origin'});\n      const data = await resp.json().catch(()=>null);\n      if (!resp.ok || !data || !data.ok) throw new Error('fetch coupons failed');\n      if (p===1) total = Number(data.total||0);\n      const list = Array.isArray(data.coupons)?data.coupons:[];\n      all = all.concat(list);\n      if (all.length >= total || list.length < CHUNK) break;\n      p++; if (p>50) break;\n    }\n    COUPON_CACHE = all; COUPON_CACHE_LOADED = true;\n  }\n  function filterByStatus(list){\n    const st = (statusFilter && statusFilter.value) ? String(statusFilter.value).toUpperCase() : '';\n    if (!st) return list.slice();\n    return list.filter(c => String(c.status||'').toUpperCase() === st);\n  }\n  function sortList(list){\n    const key = (sortSelect && sortSelect.value) ? sortSelect.value : 'expiry';\n    const arr = list.slice();\n    arr.sort((a,b)=>{\n      if (key === 'name')   return (a.name||'').localeCompare(b.name||'');\n      if (key === 'status') return (a.status||'').localeCompare(b.status||'');\n      const ax = a.expires_at||'', bx = b.expires_at||'';\n      if (!ax && !bx) return 0; if (!ax) return 1; if (!bx) return -1;\n      return ax.localeCompare(bx);\n    });\n    return arr;\n  }\n  function paginate(list, page, per){\n    const total = list.length, pages = Math.max(1, Math.ceil(total\/per));\n    const p = Math.min(Math.max(1,page||1), pages), start = (p-1)*per;\n    return { items: list.slice(start, start+per), p, pages, total };\n  }\n  async function fetchCoupons(page){\n    if (!currentUserId) return;\n    couponLoading.style.display='block'; couponEmpty.style.display='none';\n    couponTable.style.display='none'; pagWrap.style.display='none';\n    try{\n      await ensureCouponCache();\n      const filtered = filterByStatus(COUPON_CACHE);\n      const sorted   = sortList(filtered);\n      const {items, p, pages, total} = paginate(sorted, page||1, PER_PAGE);\n      renderCoupons(items); currentPage = p;\n      pageInfo.textContent = `\u7b2c ${p} \/ ${pages} \u9801\uff08\u5171 ${total} \u7b46\uff09`;\n      btnPrev.disabled = (p<=1); btnNext.disabled = (p>=pages);\n      couponTable.style.display = items.length?'table':'none';\n      couponEmpty.style.display = items.length?'none':'block';\n      pagWrap.style.display     = (pages>1)?'block':'none';\n    }catch(e){\n      console.error('[profile] coupons error', e);\n      couponEmpty.style.display='block';\n    }finally{ couponLoading.style.display='none'; }\n  }\n\n  function cameFromLiffAuth(){\n    const q = new URLSearchParams(location.search);\n    return q.has('code') || q.has('state') || q.has('liff.state') || q.has('liff_state') || q.has('liffClientId');\n  }\n  function clearAuthParams(){\n    try{\n      const q = new URLSearchParams(location.search);\n      ['code','state','liff.state','liff_state','liffClientId','liffRedirectUri'].forEach(k=>q.delete(k));\n      if (history.replaceState){\n        const clean = location.pathname + (q.toString()?('?'+q.toString()):'') + location.hash;\n        history.replaceState(null,'',clean);\n      }\n    }catch(_){}\n  }\n\n  async function initAndLoad(){\n    if (!LIFF_ID){ showError('LIFF_ID \u5c1a\u672a\u8a2d\u5b9a'); return; }\n    try{\n      await liff.init({ liffId: LIFF_ID });\n\n      if (typeof liff.isLoggedIn === 'function' && !liff.isLoggedIn()){\n        if (!cameFromLiffAuth()){\n          liff.login({ redirectUri: location.href });\n          return;\n        }\n        await new Promise(r=>setTimeout(r, 300));\n      }\n    }catch(e){\n      console.error('[profile] LIFF init error', e);\n      showError('LIFF \u521d\u59cb\u5316\u5931\u6557\uff0c\u8acb\u5728 LINE App \u4e2d\u958b\u555f\u6216\u7a0d\u5f8c\u518d\u8a66\u3002');\n      return;\n    }\n\n    \/\/ \u53d6\u5f97 LINE profile\n    let prof = null;\n    try{\n      prof = await liff.getProfile();\n    }catch(e){\n      console.error('[profile] getProfile error', e);\n      await goJoinSmart(); return;\n    }\n    if (!prof || !prof.userId){ await goJoinSmart(); return; }\n\n    \/\/ \u67e5\u6703\u54e1\n    try{\n      const url = ENDPOINT + '?line_user_id=' + encodeURIComponent(prof.userId);\n      const resp = await fetch(url, { method:'GET', credentials:'same-origin' });\n\n      if (resp.status === 404){ \/\/ \u672a\u8a3b\u518a \u279c \u7528\u667a\u6167\u8df3\u8f49\uff08\u76e1\u91cf\u7559\u5728 App \u5167\uff09\n        await goJoinSmart(); return;\n      }\n\n      const data = await resp.json().catch(()=>null);\n      if (!resp.ok || !data || !data.ok){\n        console.error('[profile] member get error', resp.status, data);\n        showError('\u8b80\u53d6\u6703\u54e1\u8cc7\u6599\u5931\u6557\uff08HTTP '+resp.status+'\uff09'); return;\n      }\n\n      const m = data.member;\n      document.getElementById('mp_line_user_id').innerText = m.line_user_id || '';\n      document.getElementById('mp_name').innerText = m.display_name || '';\n      document.getElementById('mp_phone').innerText = m.phone || '';\n      document.getElementById('mp_email').innerText = m.email || '';\n      document.getElementById('mp_nid').innerText = m.national_id || '';\n      document.getElementById('mp_birthday').innerText = m.birthday || '';\n      document.getElementById('mp_consent').innerText = (m.consent_marketing==1)?'\u5df2\u540c\u610f':'\u672a\u540c\u610f';\n      document.getElementById('mp_status').innerText = m.status || '';\n      document.getElementById('mp_created').innerText = m.created_at || '';\n      document.getElementById('mp_updated').innerText = m.updated_at || '';\n\n      \/\/ \u6298\u6263\u78bc\u9810\u8a2d\n      statusFilter.value = 'AVAILABLE';\n      sortSelect.value = 'expiry';\n      currentUserId = (m.line_user_id || prof.userId);\n      fetchCoupons(1);\n\n      loading.style.display='none';\n      profileBox.style.display='block';\n      showNotice('\u5df2\u8f09\u5165\u4f60\u7684\u6703\u54e1\u8cc7\u6599','ok');\n      clearAuthParams();\n\n    }catch(err){\n      console.error('[profile] fetch member error', err);\n      showError('\u9023\u7dda\u5931\u6557\uff0c\u8acb\u7a0d\u5f8c\u518d\u8a66');\n    }\n  }\n\n  \/\/ \u5206\u9801\u6309\u9215\n  window.lmlFetchCoupons = (p)=>{ try{ fetchCoupons(p||1); }catch(e){ console.error(e); } };\n  if (btnPrev) btnPrev.addEventListener('click', ()=>{ if (currentPage>1) fetchCoupons(currentPage-1); });\n  if (btnNext) btnNext.addEventListener('click', ()=>{ fetchCoupons(currentPage+1); });\n\n  initAndLoad();\n})();\n<\/script>\n\n<style>\n.lml-profile table th{ vertical-align:top; padding:6px 8px; color:#333; width:160px; }\n.lml-profile table td{ padding:6px 8px; color:#222; }\n<\/style>","protected":false},"excerpt":{"rendered":"\u6b63\u5728\u8f09\u5165\u6703\u54e1\u8cc7\u6599\u2026 \u6211\u7684\u6703\u54e1\u8cc7\u6599 LINE ID \u59d3\u540d \u624b\u6a5f Email \u8eab\u5206\u8b49 \u751f\u65e5 \u884c\u92b7\u540c\u610f \u72c0\u614b \u5efa\u7acb\u6642 [...]","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-1852","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/stayeasyinn.net\/ko\/wp-json\/wp\/v2\/pages\/1852","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/stayeasyinn.net\/ko\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/stayeasyinn.net\/ko\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/stayeasyinn.net\/ko\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/stayeasyinn.net\/ko\/wp-json\/wp\/v2\/comments?post=1852"}],"version-history":[{"count":3,"href":"https:\/\/stayeasyinn.net\/ko\/wp-json\/wp\/v2\/pages\/1852\/revisions"}],"predecessor-version":[{"id":1867,"href":"https:\/\/stayeasyinn.net\/ko\/wp-json\/wp\/v2\/pages\/1852\/revisions\/1867"}],"wp:attachment":[{"href":"https:\/\/stayeasyinn.net\/ko\/wp-json\/wp\/v2\/media?parent=1852"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}