PageSourceSearch

https://seodoon-church-admin.web.app/assets/AttendanceManagementPage-Cx-jtLCg.js

js seodoon-church-admin.web.app collected 2026-10-03 12:36:50 UTC 14,258 bytes, 1 lines download raw bytes

1import{j as e}from"./vendor-data-Z5doMMu-.js";import{r as t}from"./vendor-react-zNM0AU50.js";import{q as _,w as y,l as v,m as q,z as ae,y as le,k as ne,A as re,B as G,x as ie,I as ce}from"./vendor-firebase-Da9KY1q_.js";import{d as b}from"./index-D1K3lVhu.js";import{t as f}from"./Toast-DAGgZM11.js";import{J as L,c as T,i as D,U as de,bu as z,S as B,ab as oe,aa as me,ar as K,ah as xe,ai as ue,T as he}from"./vendor-ui-CW5bJ11U.js";import"./vendor-charts-08G1rM8P.js";const I="attendance";async function be(a){const x=new Date(`${a.date}T09:00:00`).toISOString(),c=a.date,u=`${c}_${a.serviceType}`,r=_(v(b,I),y("member_id","==",a.member_id),y("service_key","==",u));if(!(await q(r)).empty)throw new Error("해당 날짜에 이미 출석 기록이 있습니다.");return(await ae(v(b,I),{member_id:a.member_id,name:a.name,timestamp:x,serviceType:a.serviceType,method:"admin",date_key:c,service_key:u,auto_attendance:!1,verified:!0,created_at:re()})).id}async function ge(a){await le(ne(b,I,a))}function Se(){const[a,x]=t.useState([]),[c,u]=t.useState([]),[r,A]=t.useState({total:0,today:0,thisWeek:0,autoCheckIn:0,bgCheckIn:0,manualCheckIn:0}),[o,Q]=t.useState("all"),[h,U]=t.useState("all"),[N,H]=t.useState(!1),[J,g]=t.useState(!1),[w,S]=t.useState(""),[M,k]=t.useState([]),[d,C]=t.useState(null),[P,V]=t.useState("1부예배"),[F,X]=t.useState(new Date().toISOString().split("T")[0]),[E,R]=t.useState(!1),[$,p]=t.useState(null);t.useEffect(()=>{const s=_(v(b,"attendance"),ie("timestamp","desc"),G(200));return ce(s,i=>{const j=i.docs.map(l=>({id:l.id,...l.data()}));x(j),Y(j)},i=>{console.error("출석 데이터 로드 실패:",i)})},[]),t.useEffect(()=>{let s=a;o!=="all"&&(s=s.filter(n=>n.method===o)),h!=="all"&&(s=s.filter(n=>n.serviceType===h)),u(s)},[a,o,h]);const Y=s=>{const n=new Date,i=n.toISOString().split("T")[0],j=new Date(n.getTime()-7*24*60*60*1e3);A({total:s.length,today:s.filter(l=>{var W;return(W=l.timestamp)==null?void 0:W.startsWith(i)}).length,thisWeek:s.filter(l=>l.timestamp&&new Date(l.timestamp)>j).length,autoCheckIn:s.filter(l=>l.method==="geofence").length,bgCheckIn:s.filter(l=>l.method==="geofence_bg").length,manualCheckIn:s.filter(l=>l.method==="manual"||l.method==="admin").length})},Z=s=>s?new Date(s).toLocaleString("ko-KR",{month:"short",day:"numeric",hour:"2-digit",minute:"2-digit"}):"-",ee=s=>{const n={geofence:{className:"bg-blue-50 text-blue-700 border-blue-200",icon:e.jsx(z,{className:"w-3 h-3"}),label:"GPS 자동"},geofence_bg:{className:"bg-slate-50 text-slate-700 border-slate-200",icon:e.jsx(B,{className:"w-3 h-3"}),label:"백그라운드"},admin:{className:"bg-blue-50 text-blue-700 border-blue-200",icon:e.jsx(K,{className:"w-3 h-3"}),label:"관리자"},manual:{className:"bg-gray-50 text-gray-600 border-gray-200",label:"수동"},qr:{className:"bg-blue-50 text-blue-700 border-blue-200",label:"QR"}},i=n[s||""]||n.manual;return e.jsxs("span",{className:`inline-flex items-center gap-1 px-2.5 py-1 ${i.className} text-xs font-medium rounded-full border`,children:[i.icon,i.label]})},O=t.useCallback(async s=>{if(s.length<2){k([]);return}try{const n=await q(_(v(b,"members"),y("name",">=",s),y("name","<=",s+""),G(10)));k(n.docs.map(i=>({id:i.id,...i.data()})))}catch{k([])}},[]);t.useEffect(()=>{const s=setTimeout(()=>O(w),300);return()=>clearTimeout(s)},[w,O]);const se=async()=>{if(d){R(!0);try{await be({member_id:d.id,name:d.name,serviceType:P,date:F}),f.success("출석이 추가되었습니다."),g(!1),C(null),S("")}catch(s){f.error(s.message||"출석 추가 실패")}finally{R(!1)}}},te=async s=>{try{await ge(s),f.success("출석 기록이 삭제되었습니다."),p(null)}catch{f.error("삭제 실패")}};return e.jsxs("div",{className:"space-y-6 animate-fade-in",children:[e.jsx("div",{className:"relative overflow-hidden bg-white border border-gray-200 rounded-2xl p-6 shadow-sm",children:e.jsxs("div",{className:"relative flex items-center justify-between",children:[e.jsxs("div",{children:[e.jsxs("div",{className:"flex items-center gap-3 mb-2",children:[e.jsx("div",{className:"w-10 h-10 bg-blue-50 rounded-xl flex items-center justify-center",children:e.jsx(L,{className:"w-5 h-5 text-blue-600"})}),e.jsx("h1",{className:"text-2xl font-bold text-gray-900",children:"출석 관리"})]}),e.jsx("p",{className:"text-gray-500 text-sm",children:"GPS 자동 출석 및 수동 출석 현황을 실시간으로 확인하세요"})]}),e.jsxs("div",{className:"flex gap-3",children:[e.jsxs("div",{className:"bg-white border border-gray-200 rounded-xl px-4 py-3 min-w-[90px]",children:[e.jsxs("div",{className:"flex items-center gap-2 text-gray-500 text-xs mb-1",children:[e.jsx(T,{className:"w-3.5 h-3.5"}),e.jsx("span",{children:"오늘"})]}),e.jsxs("div",{className:"text-xl font-bold text-gray-900 tabular-nums",children:[r.today,e.jsx("span",{className:"text-sm font-medium ml-0.5",children:"명"})]})]}),e.jsxs("div",{className:"bg-white border border-gray-200 rounded-xl px-4 py-3 min-w-[90px]",children:[e.jsxs("div",{className:"flex items-center gap-2 text-gray-500 text-xs mb-1",children:[e.jsx(D,{className:"w-3.5 h-3.5"}),e.jsx("span",{children:"이번 주"})]}),e.jsxs("div",{className:"text-xl font-bold text-gray-900 tabular-nums",children:[r.thisWeek,e.jsx("span",{className:"text-sm font-medium ml-0.5",children:"명"})]})]})]})]})}),e.jsxs("div",{className:"grid grid-cols-2 md:grid-cols-6 gap-4",children:[e.jsx(m,{icon:de,label:"전체",value:r.total,color:"blue"}),e.jsx(m,{icon:T,label:"오늘",value:r.today,color:"blue",highlight:!0}),e.jsx(m,{icon:D,label:"이번 주",value:r.thisWeek,color:"blue"}),e.jsx(m,{icon:z,label:"GPS 자동",value:r.autoCheckIn,color:"blue"}),e.jsx(m,{icon:B,label:"백그라운드",value:r.bgCheckIn,color:"blue"}),e.jsx(m,{icon:L,label:"수동/관리자",value:r.manualCheckIn,color:"blue"})]}),e.jsxs("div",{className:"flex items-center justify-between",children:[e.jsxs("div",{className:"flex items-center gap-2",children:[e.jsxs("button",{onClick:()=>H(!N),className:`flex items-center gap-2 px-4 py-2 rounded-lg border text-sm font-medium transition-colors ${N?"bg-blue-50 border-blue-300 text-blue-700":"bg-white border-gray-200 text-gray-600 hover:bg-gray-50"}`,children:[e.jsx(oe,{className:"w-4 h-4"})," 필터"]}),N&&e.jsxs(e.Fragment,{children:[e.jsxs("select",{value:o,onChange:s=>Q(s.target.value),className:"px-3 py-2 rounded-lg border border-gray-200 text-sm bg-white",children:[e.jsx("option",{value:"all",children:"출석 방법 전체"}),e.jsx("option",{value:"geofence",children:"GPS 자동"}),e.jsx("option",{value:"geofence_bg",children:"백그라운드"}),e.jsx("option",{value:"manual",children:"수동"}),e.jsx("option",{value:"admin",children:"관리자"})]}),e.jsxs("select",{value:h,onChange:s=>U(s.target.value),className:"px-3 py-2 rounded-lg border border-gray-200 text-sm bg-white",children:[e.jsx("option",{value:"all",children:"예배 전체"}),e.jsx("option",{value:"1부예배",children:"1부예배"}),e.jsx("option",{value:"2부예배",children:"2부예배"}),e.jsx("option",{value:"3부예배",children:"3부예배"}),e.jsx("option",{value:"4부/청년예배",children:"4부/청년예배"}),e.jsx("option",{value:"수요예배",children:"수요예배"}),e.jsx("option",{value:"새벽기도",children:"새벽기도"})]})]})]}),e.jsxs("button",{onClick:()=>g(!0),className:"flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg text-sm font-medium hover:bg-blue-700 transition-colors",children:[e.jsx(me,{className:"w-4 h-4"})," 수동 출석 추가"]})]}),e.jsxs("div",{className:"bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden",children:[e.jsx("div",{className:"px-6 py-4 border-b border-gray-100 bg-slate-50",children:e.jsx("div",{className:"flex items-center justify-between",children:e.jsxs("div",{className:"flex items-center gap-3",children:[e.jsx("div",{className:"w-8 h-8 bg-blue-50 rounded-lg flex items-center justify-center",children:e.jsx(K,{className:"w-4 h-4 text-blue-600"})}),e.jsxs("div",{children:[e.jsx("h2",{className:"text-base font-bold text-gray-900",children:"출석 기록"}),e.jsxs("p",{className:"text-xs text-gray-500",children:[c.length,"건 표시",(o!=="all"||h!=="all")&&" (필터 적용됨)"]})]})]})})}),e.jsx("div",{className:"divide-y divide-gray-100",children:c.length===0?e.jsxs("div",{className:"px-6 py-16 text-center",children:[e.jsx("div",{className:"w-16 h-16 bg-gray-100 rounded-2xl flex items-center justify-center mx-auto mb-4",children:e.jsx(T,{className:"w-8 h-8 text-gray-400"})}),e.jsx("p",{className:"text-gray-600 font-medium",children:"출석 기록이 없습니다"}),e.jsx("p",{className:"text-sm text-gray-400 mt-1",children:"모바일 앱에서 출석 체크를 하면 여기에 표시됩니다"})]}):c.map(s=>e.jsx("div",{className:"px-6 py-4 hover:bg-slate-50 transition-all duration-200 group",children:e.jsxs("div",{className:"flex items-center justify-between",children:[e.jsxs("div",{className:"flex items-center gap-4",children:[e.jsx("div",{className:"w-10 h-10 bg-blue-600 rounded-xl flex items-center justify-center text-white font-bold text-sm shadow-sm",children:(s.name||s.user_name||"?").charAt(0)}),e.jsxs("div",{children:[e.jsxs("div",{className:"flex items-center gap-2 mb-1",children:[e.jsx("h3",{className:"text-sm font-semibold text-gray-900",children:s.name||s.user_name||"이름 없음"}),ee(s.method),s.serviceType&&e.jsx("span",{className:"px-2 py-0.5 bg-blue-50 text-blue-700 text-xs rounded-full font-medium",children:s.serviceType})]}),e.jsxs("div",{className:"flex items-center gap-3 text-xs text-gray-500",children:[e.jsxs("span",{className:"flex items-center gap-1",children:[e.jsx(D,{className:"w-3.5 h-3.5"}),Z(s.timestamp)]}),s.user_phone&&e.jsxs("span",{className:"flex items-center gap-1",children:[e.jsx(xe,{className:"w-3.5 h-3.5"}),s.user_phone]}),s.distance!=null&&e.jsxs("span",{className:"flex items-center gap-1 text-gray-500",children:[e.jsx(ue,{className:"w-3.5 h-3.5"}),Math.round(s.distance),"m"]}),s.parish&&e.jsxs("span",{className:"text-gray-400",children:[s.parish," ",s.district]})]})]})]}),e.jsx("button",{onClick:()=>
1p(s.id||null),className:"opacity-0 group-hover:opacity-100 p-2 text-gray-400 hover:text-red-500 hover:bg-red-50 rounded-lg transition-all",title:"삭제",children:e.jsx(he,{className:"w-4 h-4"})})]})},s.id))})]}),J&&e.jsx("div",{className:"fixed inset-0 bg-black/50 flex items-center justify-center z-50",onClick:()=>g(!1),children:e.jsxs("div",{className:"bg-white rounded-2xl p-6 w-full max-w-md shadow-2xl",onClick:s=>s.stopPropagation(),children:[e.jsx("h3",{className:"text-lg font-bold text-gray-900 mb-4",children:"수동 출석 추가"}),e.jsxs("div",{className:"space-y-4",children:[e.jsxs("div",{children:[e.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-1",children:"교인 검색"}),e.jsx("input",{type:"text",value:w,onChange:s=>{S(s.target.value),C(null)},placeholder:"이름으로 검색...",className:"w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500"}),M.length>0&&!d&&e.jsx("div",{className:"mt-1 border border-gray-200 rounded-lg max-h-40 overflow-y-auto",children:M.map(s=>e.jsxs("button",{onClick:()=>{C(s),S(s.name)},className:"w-full text-left px-3 py-2 text-sm hover:bg-blue-50 border-b last:border-0",children:[s.name," ",s.position&&e.jsxs("span",{className:"text-gray-400",children:["(",s.position,")"]})]},s.id))}),d&&e.jsxs("p",{className:"mt-1 text-sm text-blue-600",children:["✓ ",d.name," 선택됨"]})]}),e.jsxs("div",{children:[e.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-1",children:"예배 종류"}),e.jsxs("select",{value:P,onChange:s=>V(s.target.value),className:"w-full px-3 py-2 border border-gray-300 rounded-lg text-sm",children:[e.jsx("option",{value:"1부예배",children:"1부예배"}),e.jsx("option",{value:"2부예배",children:"2부예배"}),e.jsx("option",{value:"3부예배",children:"3부예배"}),e.jsx("option",{value:"4부/청년예배",children:"4부/청년예배"}),e.jsx("option",{value:"수요예배",children:"수요예배"}),e.jsx("option",{value:"새벽기도",children:"새벽기도"})]})]}),e.jsxs("div",{children:[e.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-1",children:"날짜"}),e.jsx("input",{type:"date",value:F,onChange:s=>X(s.target.value),className:"w-full px-3 py-2 border border-gray-300 rounded-lg text-sm"})]})]}),e.jsxs("div",{className:"flex justify-end gap-3 mt-6",children:[e.jsx("button",{onClick:()=>g(!1),className:"px-4 py-2 text-sm text-gray-600 hover:bg-gray-100 rounded-lg",children:"취소"}),e.jsx("button",{onClick:se,disabled:!d||E,className:"px-4 py-2 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50",children:E?"추가 중...":"출석 추가"})]})]})}),$&&e.jsx("div",{className:"fixed inset-0 bg-black/50 flex items-center justify-center z-50",onClick:()=>p(null),children:e.jsxs("div",{className:"bg-white rounded-2xl p-6 w-full max-w-sm shadow-2xl",onClick:s=>s.stopPropagation(),children:[e.jsx("h3",{className:"text-lg font-bold text-gray-900 mb-2",children:"출석 기록 삭제"}),e.jsx("p",{className:"text-sm text-gray-600 mb-6",children:"이 출석 기록을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다."}),e.jsxs("div",{className:"flex justify-end gap-3",children:[e.jsx("button",{onClick:()=>p(null),className:"px-4 py-2 text-sm text-gray-600 hover:bg-gray-100 rounded-lg",children:"취소"}),e.jsx("button",{onClick:()=>te($),className:"px-4 py-2 text-sm bg-red-600 text-white rounded-lg hover:bg-red-700",children:"삭제"})]})]})})]})}function m({icon:a,label:x,value:c,color:u,highlight:r}){return e.jsxs("div",{className:`bg-white rounded-xl p-4 border border-gray-200 shadow-sm transition-all ${r?"ring-2 ring-blue-200 ring-offset-2":""}`,children:[e.jsxs("div",{className:"flex items-center gap-2 mb-2",children:[e.jsx(a,{className:"w-4 h-4 text-blue-600"}),e.jsx("span",{className:"text-sm font-medium text-gray-500",children:x})]}),e.jsxs("div",{className:"text-2xl font-bold text-gray-900 tabular-nums",children:[c.toLocaleString(),e.jsx("span",{className:"text-sm font-medium ml-0.5",children:"명"})]})]})}export{Se as default};

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