PageSourceSearch

https://seodoon-church-admin.web.app/assets/BackupManagementPage-CwVXUyAs.js

js seodoon-church-admin.web.app collected 2026-10-03 12:36:46 UTC 18,677 bytes, 1 lines download raw bytes

1import{j as e}from"./vendor-data-Z5doMMu-.js";import{r as m}from"./vendor-react-zNM0AU50.js";import{a7 as n,a0 as $,i as E,bq as Q,j as V,L as _,R as W,bm as X,br as P,ad as Z,f as ee,aL as te,bd as se,U as ae,D as le,T as ne}from"./vendor-ui-CW5bJ11U.js";import{d as C,h as y,g as re,o as ce,u as oe}from"./index-D1K3lVhu.js";import{k as T,K as ie,j as de,q as ue,B as me,x as xe,l as he,m as pe,T as L}from"./vendor-firebase-Da9KY1q_.js";import"./vendor-charts-08G1rM8P.js";const j=re(ce,"us-central1"),be="backup_history",A="system/backup_settings",h={async startFullBackup(){return(await y(j,"backupFullDatabase",{timeout:6e5})()).data},async startSelectiveBackup(t){return(await y(j,"backupCollections",{timeout:6e5})({collections:t,includeChurchSub:!0})).data},async startAuthBackup(){return(await y(j,"backupAuth",{timeout:3e5})()).data},async getBackupHistory(t=50){const a=ue(he(C,be),xe("created_at","desc"),me(t));return(await pe(a)).docs.map(c=>{const l=c.data();return{id:c.id,type:l.type,status:l.status,output_uri:l.output_uri,download_urls:l.download_urls,collections:l.collections,document_count:l.document_count,file_size_bytes:l.file_size_bytes,error_message:l.error_message,created_by:l.created_by,created_by_name:l.created_by_name,created_at:l.created_at instanceof L?l.created_at.toDate().toISOString():l.created_at||"",completed_at:l.completed_at instanceof L?l.completed_at.toDate().toISOString():l.completed_at}})},async getDownloadUrl(t){return(await y(j,"getBackupDownloadUrl")({filePath:t})).data.url},async deleteBackup(t){await y(j,"deleteBackup")({backupId:t})},async getBackupSettings(){const t=T(C,A),a=await de(t);return a.exists()?a.data():{auto_backup_enabled:!1,schedule_time:"03:00",retention_days:30,include_auth:!1}},async saveBackupSettings(t,a){const r=T(C,A);await ie(r,{...t,updated_by:a,updated_at:new Date().toISOString()},{merge:!0})}},ge={firestore_native:"Firestore 전체 백업",json_selective:"JSON 선택 백업",auth:"사용자 계정 백업",scheduled:"자동 스케줄 백업"},fe={pending:"대기 중",running:"진행 중",completed:"완료",failed:"실패"},ye={pending:{bg:"bg-gray-100",text:"text-gray-600",dot:"bg-gray-400"},running:{bg:"bg-blue-100",text:"text-blue-700",dot:"bg-blue-500"},completed:{bg:"bg-green-100",text:"text-green-700",dot:"bg-green-500"},failed:{bg:"bg-red-100",text:"text-red-700",dot:"bg-red-500"}},je=[{label:"교인/사용자",collections:["users","members","newMembers"],sensitive:!0},{label:"재정",collections:["offerings","expenses","budgets","accounts"],sensitive:!0},{label:"목양/상담",collections:["visit_records","pastoral_counseling","prayer_requests"],sensitive:!0},{label:"교역자실",collections:["staff_reports","staff_report_comments","staff_report_templates","staff_schedules","staff_announcements","staff_posts","staff_post_comments","staff_notifications","staff_approvals"]},{label:"콘텐츠/소통",collections:["communications","posts","bulletins","notices","banners","albums","qt_contents","qt_daily"]},{label:"출석/일정",collections:["attendance","calendar","events"]},{label:"당회",collections:["council_posts","resource_library"]},{label:"청년부",collections:["youth_members","youthMembers","youth_homes","youth_posts","youth_events","youth_challenges","youth_attendance"]},{label:"교육부서 (중등/고등/초등)",collections:["departments"]},{label:"투게더",collections:["togetherMembers","togetherPosts","togetherGroups","togetherEvents"]},{label:"남선교회/여전도회",collections:["menPosts","menEvents","womenPosts","womenEvents","group_schedules","group_boards","group_prayers"]},{label:"문화강좌/아카데미",collections:["cultureSemesters","cultureCourses","cultureEnrollments","academyPrograms","academyEnrollments"]},{label:"시스템/설정",collections:["church_settings","app_settings","system","fcm_tokens","notification_subscriptions","notification_settings","devices","error_reports","accessLogs"]}];function Te(){const{user:t}=oe(),[a,r]=m.useState("history"),[c,l]=m.useState([]),[x,N]=m.useState({auto_backup_enabled:!1,schedule_time:"03:00",retention_days:30,include_auth:!1}),[S,w]=m.useState(!1),[d,f]=m.useState(!0),[p,b]=m.useState(null),[i,v]=m.useState([]),[F,O]=m.useState({}),g=m.useCallback(async()=>{try{f(!0);const s=await h.getBackupHistory();l(s)}catch(s){console.error("백업 이력 조회 실패:",s),n.error("백업 이력을 불러올 수 없습니다.")}finally{f(!1)}},[]),D=m.useCallback(async()=>{try{const s=await h.getBackupSettings();N(s)}catch(s){console.error("설정 조회 실패:",s)}},[]);m.useEffect(()=>{g(),D()},[g,D]);const G=async()=>
1{if(!p&&confirm("전체 데이터베이스를 백업합니다. 몇 분이 소요될 수 있습니다. 진행하시겠습니까?"))try{b("full"),n.loading("전체 백업 진행 중...",{id:"backup"});const s=await h.startFullBackup();n.success(`전체 백업 완료! ${s.totalDocs.toLocaleString()}개 문서, ${k(s.totalSize)}`,{id:"backup",duration:5e3}),g()}catch(s){n.error(`백업 실패: ${s.message}`,{id:"backup"})}finally{b(null)}},M=async()=>{if(!p){if(i.length===0){n.error("백업할 컬렉션을 선택해주세요.");return}if(confirm(`${i.length}개 컬렉션을 백업합니다. 진행하시겠습니까?`))try{b("selective"),n.loading("선택 백업 진행 중...",{id:"backup"});const s=await h.startSelectiveBackup(i);n.success(`선택 백업 완료! ${s.totalDocs.toLocaleString()}개 문서, ${k(s.totalSize)}`,{id:"backup",duration:5e3}),v([]),g()}catch(s){n.error(`백업 실패: ${s.message}`,{id:"backup"})}finally{b(null)}}},z=async()=>{if(!p&&confirm("사용자 계정 정보를 백업합니다. 진행하시겠습니까?"))try{b("auth"),n.loading("사용자 계정 백업 진행 중...",{id:"backup"});const s=await h.startAuthBackup();n.success(`계정 백업 완료! ${s.userCount}명, ${k(s.totalSize)}`,{id:"backup",duration:5e3}),g()}catch(s){n.error(`백업 실패: ${s.message}`,{id:"backup"})}finally{b(null)}},H=async s=>{if(!s.output_uri){n.error("다운로드 경로가 없습니다.");return}try{n.loading("다운로드 URL 생성 중...",{id:"download"});const o=`${s.output_uri}_metadata.json`,u=await h.getDownloadUrl(o);window.open(u,"_blank"),n.success("다운로드가 시작됩니다.",{id:"download"})}catch{try{if(s.collections&&s.collections.length>0){const o=s.collections[0],u=`${s.output_uri}${o}.json`,B=await h.getDownloadUrl(u);window.open(B,"_blank"),n.success("다운로드가 시작됩니다.",{id:"download"})}else n.error("다운로드할 파일을 찾을 수 없습니다.",{id:"download"})}catch(o){n.error(`다운로드 실패: ${o.message}`,{id:"download"})}}},K=async s=>{if(confirm(`이 백업을 삭제하시겠습니까? (${U(s.created_at)})`))try{await h.deleteBackup(s.id),n.success("백업이 삭제되었습니다."),g()}catch(o){n.error(`삭제 실패: ${o.message}`)}},R=async()=>{if(t!=null&&t.uid)try{w(!0),await h.saveBackupSettings(x,t.uid),n.success("백업 설정이 저장되었습니다.")}catch(s){n.error(`설정 저장 실패: ${s.message}`)}finally{w(!1)}},q=s=>{O(o=>({...o,[s]:!o[s]}))},I=s=>{v(o=>o.includes(s)?o.filter(u=>u!==s):[...o,s])},Y=s=>{const o=s.every(u=>i.includes(u));v(o?u=>u.filter(B=>!s.includes(B)):u=>[...new Set([...u,...s])])},J=[{key:"history",label:"백업 현황",icon:E},{key:"manual",label:"수동 백업",icon:Q},{key:"settings",label:"자동 백업 설정",icon:V}];return e.jsxs("div",{className:"max-w-6xl mx-auto px-4 py-6",children:[e.jsxs("div",{className:"flex items-center gap-3 mb-6",children:[e.jsx("div",{className:"w-10 h-10 bg-blue-50 rounded-xl flex items-center justify-center",children:e.jsx($,{className:"w-5 h-5 text-blue-600"})}),e.jsxs("div",{children:[e.jsx("h1",{className:"text-xl font-bold text-gray-900",children:"데이터 백업"}),e.jsx("p",{className:"text-sm text-gray-500",children:"전체 플랫폼 데이터를 안전하게 백업하고 관리합니다."})]})]}),e.jsx("div",{className:"flex border-b border-gray-200 mb-6",children:J.map(s=>e.jsxs("button",{onClick:()=>r(s.key),className:`flex items-center gap-2 px-4 py-3 text-sm font-medium border-b-2 transition-colors ${a===s.key?"border-blue-500 text-blue-600":"border-transparent text-gray-500 hover:text-gray-700"}`,children:[e.jsx(s.icon,{className:"w-4 h-4"}),s.label]},s.key))}),a==="history"&&e.jsx(_e,{history:c,loading:d,onRefresh:g,onDownload:H,onDelete:K}),a==="manual"&&e.jsx(we,{backupRunning:p,selectedCollections:i,expandedGroups:F,onFullBackup:G,onSelectiveBackup:M,onAuthBackup:z,onToggleGroup:q,onToggleCollection:I,onToggleGroupCollections:Y}),a==="settings"&&e.jsx(ve,{settings:x,loading:S,onSettingsChange:N,onSave:R})]})}function _e({history:t,loading:a,onRefresh:r,onDownload:c,onDelete:l}){return a?e.jsx("div",{className:"flex items-center justify-center py-20",children:e.jsx(_,{className:"w-8 h-8 text-blue-500 animate-spin"})}):e.jsxs("div",{children:[e.jsxs("div",{className:"flex justify-between items-center mb-4",children:[e.jsxs("p",{className:"text-sm text-gray-500",children:["총 ",t.length,"개의 백업 기록"]}),e.jsxs("button",{onClick:r,className:"flex items-center gap-1.5 px-3 py-1.5 text-sm text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors",children:[e.jsx(W,{className:"w-4 h-4"}),"새로고침"]})]}),t.length===0?e.jsxs("div",{className:"text-center py-20 bg-gray-50 rounded-xl",children:[e.jsx($,{className:"w-12 h-12 text-gray-300 mx-auto mb-3"}),e.jsx("p",{className:"text-gray-500",children:"아직 백업 기록이 없습니다."}),e.jsx("p",{className:"text-sm text-gray-400 mt-1",children:"'수동 백업' 탭에서 첫 백업을 시작하세요."})]}):e.jsx("div",{className:"space-y-3",children:t.map(x=>e.jsx(Ne,{record:x,onDownload:()=>c(x),onDelete:()=>l(x)},x.id))})]})}function Ne({record:t,onDownload:a,onDelete:r}){const c=ye[t.status];return e.jsx("div",{className:"bg-white border border-gray-200 rounded-xl p-4 hover:shadow-sm transition-shadow",children:e.jsxs("div",{className:"flex items-start justify-between",children:[e.jsxs("div",{className:"flex-1",children:[e.jsxs("div",{className:"flex items-center gap-2 mb-1",children:[e.jsxs("span",{className:`inline-flex items-center gap-1.5 px-2 py-0.5 text-xs font-medium rounded-full ${c.bg} ${c.text}`,children:[e.jsx("span",{className:`w-1.5 h-1.5 rounded-full ${c.dot}`}),fe[t.status]]}),e.jsx("span",{className:"text-xs text-gray-400",children:ge[t.type]})]}),e.jsx("p",{className:"text-sm font-medium text-gray-900",children:U(t.created_at)}),e.jsxs("div",{className:"flex items-center gap-4 mt-1.5 text-xs text-gray-500",children:[t.document_count!==void 0&&e.jsxs("span",{className:"flex items-center gap-1",children:[e.jsx(P,{className:"w-3.5 h-3.5"}
1),t.document_count.toLocaleString(),"개 문서"]}),t.file_size_bytes!==void 0&&e.jsxs("span",{className:"flex items-center gap-1",children:[e.jsx(se,{className:"w-3.5 h-3.5"}),k(t.file_size_bytes)]}),t.created_by==="system"?e.jsxs("span",{className:"flex items-center gap-1",children:[e.jsx(E,{className:"w-3.5 h-3.5"}),"자동 백업"]}):e.jsxs("span",{className:"flex items-center gap-1",children:[e.jsx(ae,{className:"w-3.5 h-3.5"}),"수동 백업"]})]}),t.error_message&&e.jsx("p",{className:"mt-2 text-xs text-red-600 bg-red-50 p-2 rounded-lg",children:t.error_message})]}),e.jsxs("div",{className:"flex items-center gap-2 ml-4",children:[t.status==="completed"&&t.output_uri&&e.jsx("button",{onClick:a,className:"p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors",title:"다운로드",children:e.jsx(le,{className:"w-4 h-4"})}),e.jsx("button",{onClick:r,className:"p-2 text-red-500 hover:bg-red-50 rounded-lg transition-colors",title:"삭제",children:e.jsx(ne,{className:"w-4 h-4"})})]})]})})}function we({backupRunning:t,selectedCollections:a,expandedGroups:r,onFullBackup:c,onSelectiveBackup:l,onAuthBackup:x,onToggleGroup:N,onToggleCollection:S,onToggleGroupCollections:w}){return e.jsxs("div",{className:"space-y-6",children:[e.jsxs("div",{className:"grid grid-cols-1 md:grid-cols-3 gap-4",children:[e.jsxs("button",{onClick:c,disabled:!!t,className:"flex items-center gap-3 p-4 bg-blue-600 text-white rounded-xl hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-all shadow-sm",children:[t==="full"?e.jsx(_,{className:"w-8 h-8 animate-spin"}):e.jsx($,{className:"w-8 h-8"}),e.jsxs("div",{className:"text-left",children:[e.jsx("p",{className:"font-bold",children:"전체 백업"}),e.jsx("p",{className:"text-xs text-blue-100",children:"모든 Firestore 데이터"})]})]}),e.jsxs("button",{onClick:x,disabled:!!t,className:"flex items-center gap-3 p-4 bg-blue-600 text-white rounded-xl hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-all shadow-sm",children:[t==="auth"?e.jsx(_,{className:"w-8 h-8 animate-spin"}):e.jsx(X,{className:"w-8 h-8"}),e.jsxs("div",{className:"text-left",children:[e.jsx("p",{className:"font-bold",children:"계정 백업"}),e.jsx("p",{className:"text-xs text-blue-100",children:"Firebase Auth 사용자"})]})]}),e.jsxs("button",{onClick:l,disabled:!!t||a.length===0,className:"flex items-center gap-3 p-4 bg-blue-600 text-white rounded-xl hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-all shadow-sm",children:[t==="selective"?e.jsx(_,{className:"w-8 h-8 animate-spin"}):e.jsx(P,{className:"w-8 h-8"}),e.jsxs("div",{className:"text-left",children:[e.jsx("p",{className:"font-bold",children:"선택 백업"}),e.jsx("p",{className:"text-xs text-blue-100",children:a.length>0?`${a.length}개 컬렉션 선택됨`:"아래에서 컬렉션 선택"})]})]})]}),e.jsxs("div",{className:"bg-white border border-gray-200 rounded-xl",children:[e.jsxs("div",{className:"p-4 border-b border-gray-100",children:[e.jsx("h3",{className:"font-bold text-gray-900",children:"컬렉션 선택 (선택 백업용)"}),e.jsx("p",{className:"text-xs text-gray-500 mt-1",children:"백업할 데이터 그룹을 선택하세요. 민감 데이터는 별도 표시됩니다."})]}),e.jsx("div",{className:"divide-y divide-gray-100",children:je.map(d=>{const f=r[d.label],p=d.collections.filter(i=>a.includes(i)).length,b=p===d.collections.length;return e.jsxs("div",{children:[e.jsxs("div",{className:"flex items-center justify-between p-3 hover:bg-gray-50 cursor-pointer",onClick:()=>N(d.label),children:[e.jsxs("div",{className:"flex items-center gap-3",children:[e.jsx("input",{type:"checkbox",checked:b,onChange:i=>{i.stopPropagation(),w(d.collections)},className:"w-4 h-4 text-blue-500 rounded border-gray-300"}),e.jsx("span",{className:"text-sm font-medium text-gray-700",children:d.label}),d.sensitive&&e.jsx("span",{className:"px-1.5 py-0.5 text-[10px] font-medium bg-amber-100 text-amber-700 rounded",children:"민감"}),p>0&&e.jsxs("span",{className:"px-1.5 py-0.5 text-[10px] font-medium bg-blue-100 text-blue-700 rounded",children:[p,"/",d.collections.length]})]}),f?e.jsx(Z,{className:"w-4 h-4 text-gray-400"}):e.jsx(ee,{className:"w-4 h-4 text-gray-400"})]}),f&&e.jsx("div",{className:"px-4 pb-3 ml-7 space-y-1",children:d.collections.map(i=>e.jsxs("label",{className:"flex items-center gap-2 px-2 py-1.5 hover:bg-gray-50 rounded cursor-pointer",children:[e.jsx("input",{type:"checkbox",checked:a.includes(i),onChange:()=>S(i),className:"w-3.5 h-3.5 text-blue-500 rounded border-gray-300"}),e.jsx("code",{className:"text-xs text-gray-600 bg-gray-100 px-1.5 py-0.5 rounded",children:i})]},i))})]},d.label)})})]})]})}function ve({settings:t,loading:a,onSettingsChange:r,onSave:c}
1){return e.jsx("div",{className:"max-w-xl",children:e.jsxs("div",{className:"bg-white border border-gray-200 rounded-xl p-6 space-y-6",children:[e.jsxs("div",{children:[e.jsx("h3",{className:"font-bold text-gray-900 mb-1",children:"자동 백업 설정"}),e.jsx("p",{className:"text-xs text-gray-500",children:"매일 자동으로 주요 데이터를 백업합니다."})]}),e.jsxs("div",{className:"flex items-center justify-between",children:[e.jsxs("div",{children:[e.jsx("p",{className:"text-sm font-medium text-gray-700",children:"자동 백업"}),e.jsx("p",{className:"text-xs text-gray-500",children:"매일 새벽 3시에 자동 실행"})]}),e.jsx("button",{onClick:()=>r({...t,auto_backup_enabled:!t.auto_backup_enabled}),className:`relative w-11 h-6 rounded-full transition-colors ${t.auto_backup_enabled?"bg-blue-500":"bg-gray-300"}`,children:e.jsx("span",{className:`absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full shadow transition-transform ${t.auto_backup_enabled?"translate-x-5":""}`})})]}),e.jsxs("div",{children:[e.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-2",children:"보관 기간"}),e.jsxs("select",{value:t.retention_days,onChange:l=>r({...t,retention_days:Number(l.target.value)}),className:"w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500",children:[e.jsx("option",{value:7,children:"7일"}),e.jsx("option",{value:30,children:"30일"}),e.jsx("option",{value:90,children:"90일"}),e.jsx("option",{value:0,children:"영구 보관"})]}),e.jsx("p",{className:"text-xs text-gray-500 mt-1",children:t.retention_days>0?`${t.retention_days}일이 지난 자동 백업은 자동으로 삭제됩니다.`:"자동 백업이 삭제되지 않습니다. 수동으로 관리해주세요."})]}),e.jsxs("div",{className:"flex items-center justify-between",children:[e.jsxs("div",{children:[e.jsx("p",{className:"text-sm font-medium text-gray-700",children:"계정 정보 포함"}),e.jsx("p",{className:"text-xs text-gray-500",children:"자동 백업에 Auth 데이터 포함"})]}),e.jsx("button",{onClick:()=>r({...t,include_auth:!t.include_auth}),className:`relative w-11 h-6 rounded-full transition-colors ${t.include_auth?"bg-blue-500":"bg-gray-300"}`,children:e.jsx("span",{className:`absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full shadow transition-transform ${t.include_auth?"translate-x-5":""}`})})]}),e.jsxs("button",{onClick:c,disabled:a,className:"w-full flex items-center justify-center gap-2 px-4 py-2.5 bg-blue-500 text-white text-sm font-medium rounded-lg hover:bg-blue-600 disabled:opacity-50 transition-colors",children:[a?e.jsx(_,{className:"w-4 h-4 animate-spin"}):e.jsx(te,{className:"w-4 h-4"}),"설정 저장"]})]})})}function k(t){if(t===0)return"0 B";const a=1024,r=["B","KB","MB","GB"],c=Math.floor(Math.log(t)/Math.log(a));return`${parseFloat((t/Math.pow(a,c)).toFixed(1))} ${r[c]}`}function U(t){if(!t)return"-";try{return new Date(t).toLocaleString("ko-KR",{year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit"})}catch{return t}}export{Te as BackupManagementPage};

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.