로그인
회원가입

다시 오신 걸 환영합니다

아이디·비밀번호로 로그인하세요

새 모임 만들기
기존 모임 참여

새 모임 만들기

본 이름은 필수, 상위 단체는 선택입니다
만든 사람이 자동으로 회장이 됩니다

다른 계정으로 로그인
⭐ 총무
it sb.from('agenda').upsert(row); } async function pushNotification(n){ const row = { id:n.id, group_id:activeGroupId, type:n.type, title:n.title, body:n.body||null, agenda_id:n.agendaId||null, for_user:n.forUser||null, read:n.read||false, created_at: n.createdAt ? new Date(n.createdAt).toISOString() : new Date().toISOString() }; return await sb.from('notifications').upsert(row); } async function pushApplicant(a){ const row = { id:a.id, group_id:activeGroupId, name:a.name, phone:a.phone||null, message:a.message||null, status:a.status||'pending', reject_reason:a.rejectReason||null }; return await sb.from('applicants').upsert(row); } async function pushAsset(a){ const row = { id:a.id, group_id:activeGroupId, icon:a.icon, name:a.name, category:a.category||null, status:a.status||'available', rented_by:a.rentedBy||null, rented_from:a.rentedFrom||null, rented_until:a.rentedUntil||null }; return await sb.from('assets').upsert(row); } async function pushRental(r){ return await sb.from('rentals').upsert({ id:r.id, group_id:activeGroupId, asset_id:r.assetId, user_id:r.userId, from_date:r.from, until_date:r.until, memo:r.memo||null, returned_at:r.returnedAt||null }); } async function pushTransfer(t){ return await sb.from('transfers').upsert({ id:t.id, group_id:activeGroupId, role:t.role, from_user:t.fromUser, to_user:t.toUser, reason:t.reason||null, started_at:t.startedAt, status:t.status, completed_at:t.completedAt||null, snapshot:t.snapshot||null }); } async function pushProfileSelf(){ // v3: role은 memberships 테이블로 이동 — profiles에는 name/phone만 업데이트 await sb.from('profiles').update({ name: D.me.name, phone: (D.members.find(m=>m.id===D.me.id)||{}).phone || null }).eq('id', currentSession.user.id); // 직책(role)은 활성 그룹의 memberships에 반영 if(activeGroupId && D.me.role){ await sb.from('memberships').update({ role: D.me.role }) .eq('user_id', currentSession.user.id) .eq('group_id', activeGroupId); } } async function pushRules(){ const r = D.rules.current; await sb.from('rules').upsert({ group_id:activeGroupId, current_title:r.title, effective_date:r.date||null, current_articles:r.articles||0, pdf_data:r.pdfData, pdf_name:r.pdfName, history:D.rules.history }, { onConflict:'group_id' }); } // ════════════════════════════════════════ // 시작 진입점 // ════════════════════════════════════════ applyFontSize(); $('welcome').classList.add('hide'); window.addEventListener('DOMContentLoaded', async ()=>{ if(!initSupabase()){ setTimeout(()=>location.reload(), 500); return; } // 모든 페이지에 당겨서 새로고침 부착 attachPullToRefresh(); // 무작위 예시 placeholder 적용 (개인정보 노출 방지) applyRandomExamples(); const { data: sess } = await sb.auth.getSession(); if(sess.session){ currentSession = sess.session; await afterAuth(); } else { showAuth('auth-screen'); } // 인증 변경 감지 (다른 탭에서 로그아웃 등) sb.auth.onAuthStateChange((event, session)=>{ if(event==='SIGNED_OUT'){ currentSession = null; currentProfile = null; if(realtimeChannel) sb.removeChannel(realtimeChannel); showAuth('auth-screen'); } }); }); // PWA Service Worker (옵션 — 설치 가능하게) if('serviceWorker' in navigator){ // 단일 파일 PWA를 위해 minimal SW 등록은 v2에서 } document.createElement('div'); ind.className = 'ptr-indicator'; ind.textContent = '↓ 당겨서 새로고침'; page.insertBefore(ind, page.firstChild); } st.indicator = ind; return st; } function attachPullToRefresh(){ const pages = document.querySelectorAll('.page'); pages.forEach(page => { const start = (e) => { const t = e.touches ? e.touches[0] : e; if(page.scrollTop > 0) return; // 위에 있을 때만 const st = _ensureIndicator(page); if(st.busy) return; st.startY = t.clientY; st.pulling = false; st.indicator.style.transform = 'translateY(-100%)'; st.indicator.style.opacity = '0'; }; const move = (e) => { const st = _ptrState.get(page); if(!st || st.startY === null) return; const t = e.touches ? e.touches[0] : e; const dy = t.clientY - st.startY; if(dy <= 0){ st.indicator.style.transform = 'translateY(-100%)'; return; } st.pulling = true; const max = 80; const visible = Math.min(dy * 0.6, max); st.indicator.style.transform = `translateY(${visible - 100}%)`; st.indicator.style.opacity = String(Math.min(1, visible/40)); if(visible >= 40 && st.indicator.textContent === '↓ 당겨서 새로고침'){ st.indicator.textContent = '↑ 놓으면 새로고침'; } else if(visible < 40 && st.indicator.textContent === '↑ 놓으면 새로고침'){ st.indicator.textContent = '↓ 당겨서 새로고침'; } }; const finish = async (e) => { const st = _ptrState.get(page); if(!st || st.startY === null) return; const tEnd = e.changedTouches ? e.changedTouches[0] : e; const dy = (tEnd ? tEnd.clientY : 0) - st.startY; st.startY = null; if(dy >= 70 && !st.busy){ st.busy = true; st.indicator.textContent = '⟳ 새로고침 중...'; st.indicator.style.transform = 'translateY(0%)'; try{ if(activeGroupId){ await loadGroupData(); refreshActivePage(); } } catch(_){} st.busy = false; st.indicator.textContent = '✓ 완료'; setTimeout(()=>{ st.indicator.style.transform = 'translateY(-100%)'; st.indicator.style.opacity = '0'; setTimeout(()=>{ st.indicator.textContent = '↓ 당겨서 새로고침'; }, 300); }, 600); } else { st.indicator.style.transform = 'translateY(-100%)'; st.indicator.style.opacity = '0'; } }; page.addEventListener('touchstart', start, { passive:true }); page.addEventListener('touchmove', move, { passive:true }); page.addEventListener('touchend', finish, { passive:true }); page.addEventListener('touchcancel', finish, { passive:true }); }); } // saveData()는 더 이상 동기 저장 안 함. Supabase가 진짜 저장소. function saveData(){ // v2: Supabase가 진실원본. 호출은 호환용. } function loadData(){ return JSON.parse(JSON.stringify(DEFAULT_DATA)); } // ════════════════════════════════════════ // Supabase 헬퍼: 각 엔티티 push // ════════════════════════════════════════ async function pushTx(tx){ const row = { id:tx.id, group_id:activeGroupId, type:tx.type, amount:tx.amount, date:tx.date, title:tx.title, category:tx.category||null, memo:tx.memo||null, by_user:tx.by||currentSession.user.id, attaches:tx.attaches||[], member_id: tx.memberId || null, match_status: tx.matchStatus || 'unmatched' }; return await sb.from('transactions').upsert(row); } async function deleteTx(id){ return await sb.from('transactions').delete().eq('id',id); } async function pushMeeting(m){ const row = { id:m.id, group_id:activeGroupId, title:m.title, date:m.date||null, time:m.time||null, place:m.place||null, memo:m.memo||null, attendance:m.attendance||{}, album:m.album||[] }; return await sb.from('meetings').upsert(row); } async function pushAgenda(a){ const row = { id:a.id, group_id:activeGroupId, title:a.title, detail:a.detail||null, kind:a.kind||'normal', target_user:a.target||null, by_user:a.by, end_date:a.endDate, votes:a.votes||{yes:0,no:0}, voters:a.voters||{}, total:a.total, status:a.status||'open' }; return await sb.from('agenda').upsert(row); } async function pushNotification(n){ const row = { id:n.id, group_id:activeGroupId, type:n.type, title:n.title, body:n.body||null, agenda_id:n.agendaId||null, for_user:n.forUser||null, read:n.read||false, created_at: n.createdAt ? new Date(n.createdAt).toISOString() : new Date().toISOString() }; return await sb.from('notifications').upsert(row); } async function pushApplicant(a){ const row = { id:a.id, group_id:activeGroupId, name:a.name, phone:a.phone||null, message:a.message||null, status:a.status||'pending', reject_reason:a.rejectReason||null }; return await sb.from('applicants').upsert(row); } async function pushAsset(a){ const row = { id:a.id, group_id:activeGroupId, icon:a.icon, name:a.name, category:a.category||null, status:a.status||'available', rented_by:a.rentedBy||null, rented_from:a.rentedFrom||null, rented_until:a.rentedUntil||null }; return await sb.from('assets').upsert(row); } async function pushRental(r){ return await sb.from('rentals').upsert({ id:r.id, group_id:activeGroupId, asset_id:r.assetId, user_id:r.userId, from_date:r.from, until_date:r.until, memo:r.memo||null, returned_at:r.returnedAt||null }); } async function pushTransfer(t){ return await sb.from('transfers').upsert({ id:t.id, group_id:activeGroupId, role:t.role, from_user:t.fromUser, to_user:t.toUser, reason:t.reason||null, started_at:t.startedAt, status:t.status, completed_at:t.completedAt||null, snapshot:t.snapshot||null }); } async function pushProfileSelf(){ // v3: role은 memberships 테이블로 이동 — profiles에는 name/phone만 업데이트 await sb.from('profiles').update({ name: D.me.name, phone: (D.members.find(m=>m.id===D.me.id)||{}).phone || null }).eq('id', currentSession.user.id); // 직책(role)은 활성 그룹의 memberships에 반영 if(activeGroupId && D.me.role){ await sb.from('memberships').update({ role: D.me.role }) .eq('user_id', currentSession.user.id) .eq('group_id', activeGroupId); } } async function pushRules(){ const r = D.rules.current; await sb.from('rules').upsert({ group_id:activeGroupId, current_title:r.title, effective_date:r.date||null, current_articles:r.articles||0, pdf_data:r.pdfData, pdf_name:r.pdfName, history:D.rules.history }, { onConflict:'group_id' }); } // ════════════════════════════════════════ // 시작 진입점 // ════════════════════════════════════════ applyFontSize(); $('welcome').classList.add('hide'); window.addEventListener('DOMContentLoaded', async ()=>{ if(!initSupabase()){ setTimeout(()=>location.reload(), 500); return; } // 모든 페이지에 당겨서 새로고침 부착 attachPullToRefresh(); // 무작위 예시 placeholder 적용 (개인정보 노출 방지) applyRandomExamples(); const { data: sess } = await sb.auth.getSession(); if(sess.session){ currentSession = sess.session; await afterAuth(); } else { showAuth('auth-screen'); } // 인증 변경 감지 (다른 탭에서 로그아웃 등) sb.auth.onAuthStateChange((event, session)=>{ if(event==='SIGNED_OUT'){ currentSession = null; currentProfile = null; if(realtimeChannel) sb.removeChannel(realtimeChannel); showAuth('auth-screen'); } }); }); // PWA Service Worker (옵션 — 설치 가능하게) if('serviceWorker' in navigator){ // 단일 파일 PWA를 위해 minimal SW 등록은 v2에서 } 션 — 설치 가능하게) if('serviceWorker' in navigator){ // 단일 파일 PWA를 위해 minimal SW 등록은 v2에서 }