Seoul Mate

v7

K-Culture를 사랑하는
사람들을 위한 착한 데이팅

또는

계속 진행하면 이용약관개인정보 처리방침에 동의합니다

기본 정보

프로필 기본 정보를 입력해주세요

사진 & K-문화

최소 1장의 사진을 업로드해주세요

📷 메인 사진
사진 2
사진 3

연애 목적 & 언어

마지막 단계입니다!

K-문화 퀴즈
1 / 5
Seoul Mate
오늘 25 / 25
선택한 사람
온라인
프로필
홍기동
🎂 24세
🌍 🇰🇷 한국
🌡️ 37.2°C
프로필 완성도
0%
0
좋아요
0
매치
0
˃FcFcFcࠢ&WB6V7FFb673'&fR6V7F#Fb673'&fR6V7FFFR# ^;CFcFbC'&fT&6V7F"7GS'FFs'&6w&VCf"&r6&B&&FW"&FW3f"&FW2BfB6SGƖRֆVvCS#FcFcFcFcFcࠢFb673&b#'WGF673&b֗FV7FfR"6Ɩ6'7vF6F"vF66fW"r#Fb673&b֖6#)NFcFb673&b&V#Ϋ*Fc'WGF'WGF673&b֗FV"6Ɩ6'7vF6F"vVvRr#Fb673&b֖6# *FcFb673&b&V#ɫNxFc'WGF'WGF673&b֗FV"6Ɩ6'7vF6F"vF6W2r#Fb673&b֖6# )SFcFb673&b&V#zN˙FcFb673&b&FvR"C&F6W4&FvR"7GS&F7S#Fc'WGF'WGF673&b֗FV"6Ɩ6'7vF6F"v6Br#Fb673&b֖6# (FcFb673&b&V#NSFcFb673&b&FvR"C&6D&FvR"7GS&F7S#Fc'WGF'WGF673&b֗FV"6Ɩ6'7vF6F"w&fRr#Fb673&b֖6# CFcFb673&b&V#HNXCFc'WGFFcFcࠣ67&B7&3&GG3wwrw7FF26f&V&6V2rf&V&6R6B2#67&C67&B7&3&GG3wwrw7FF26f&V&6V2rf&V&6RWF6B2#67&C67&B7&3&GG3wwrw7FF26f&V&6V2rf&V&6Rf&W7F&R6B2#67&C67&B7&3&GG3wwrw7FF26f&V&6V2rf&V&6R7F&vR6B2#67&C67&C4TTDRcrТf&V&6R@Цf&V&6RFƗTW$AIzaSyB1KRBLS6TDR9GCNoHJKo3_KoQ66m2hEO8", authDomain: "nocap-seoulmate.firebaseapp.com", projectId: "nocap-seoulmate", storageBucket: "nocap-seoulmate.firebasestorage.app", messagingSenderId: "1095126172029", appId: "1:1095126172029:web:7f8c68b484dec25a27cc14" }); const auth = firebase.auth(); const db = firebase.firestore(); const storage = firebase.storage(); // ============================================ // App State // ============================================ let currentUser = null; let currentProfile = null; let selectedProfiles = []; let currentCardIndex = 0; let currentPhotoIndex = 0; let swipeCount = 0; let currentChatUser = null; let currentConversationId = null; let activeFilters = {}; let currentLoungeCategory = 'all'; let newPostCategory = 'lang'; let translateMode = false; let quizAnswers = []; let quizCurrentQuestion = 0; const TOTAL_STEPS = 3; const TIER_LIMITS = { guest: { swipesPerDay: 25, superLikesPerDay: 1, boostsPerWeek: 0 }, verified: { swipesPerDay: Infinity, superLikesPerDay: 3, boostsPerWeek: 1 }, plus: { swipesPerDay: Infinity, superLikesPerDay: Infinity, boostsPerWeek: 3 } }; const NATIONALITIES = { KR: '🇰🇷 한국', JP: '🇯🇵 일본', US: '🇺🇸 미국', GB: '🇬🇧 영국', AU: '🇦🇺 호주', CA: '🇨🇦 캐나다', FR: '🇫🇷 프랑스', DE: '🇩🇪 독일', BR: '🇧🇷 브라질', PH: '🇵🇭 필리핀', TH: '🇹🇭 태국', VN: '🇻🇳 베트남', ID: '🇮🇩 인도네시아', MX: '🇲🇽 멕시코', IN: '🇮🇳 인도', CN: '🇨🇳 중국', TW: '🇹🇼 대만', MY: '🇲🇾 말레이시아', SG: '🇸🇬 싱가포르', OTHER: '기타' }; const LANGUAGES = ['한국어','日本語','English','中文','Español','Français','Deutsch','Português','Tiếng Việt','ภาษาไทย','Bahasa Indonesia','Bahasa Melayu','Tagalog','हिन्दी']; const RELATIONSHIP_GOALS = ['진지한 연애','캐주얼','친구 먼저','결혼 상대','언어 교환','K-Culture 친구']; const CULTURE_LEVELS = ['입문','초급','중급','고급','마스터']; const QUIZ_QUESTIONS = [ { q: 'BTS의 데뷔 연도는?', opts: ['2012','2013','2014','2015'], ans: 1 }, { q: "드라마 '이태원 클라쓰'의 원작은?", opts: ['웹툰','소설','영화','라디오'], ans: 0 }, { q: "'사랑해요'를 일본어로?", opts: ['愛してる','好きです','ありがとう','すみません'], ans: 0 }, { q: "김치에 들어가지 않는 것은?", opts: ['고추장','배추','마늘','젓갈'], ans: 0 }, { q: 'BLACKPINK 멤버가 아닌 사람은?', opts: ['제니','리사','지수','화사'], ans: 3 } ]; // Onboarding state let onboardingStep = 1; let onboardingData = { name: '', age: null, gender: '', nationality: '', photos: [], kpopBias: '', cultureLevel: 0, relationshipGoals: [], languages: [], mannerTemp: 36.5 }; // Initialize document.addEventListener('DOMContentLoaded', () => { auth.onAuthStateChanged(user => { if (user) { currentUser = user; loadUserProfile(); } else { showScreen('authScreen'); } }); // Apply saved theme if (localStorage.getItem('sm_theme') === 'light') { document.body.classList.add('light-mode'); const btn = document.getElementById('themeBtn'); if (btn) btn.textContent = '☀️'; } }); // ============================================ // Auth Functions // ============================================ function togglePhoneAuth() { const s = document.getElementById('phoneAuthSection'); s.style.display = s.style.display === 'none' ? 'block' : 'none'; } function toggleEmailAuth() { const s = document.getElementById('emailAuthSection'); s.style.display = s.style.display === 'none' ? 'block' : 'none'; } function signInWithGoogle() { const provider = new firebase.auth.GoogleAuthProvider(); auth.signInWithPopup(provider) .then(result => { currentUser = result.user; startOnboarding(); }) .catch(e => showToast('로그인 실패: ' + e.message, 'error')); } function signInWithPhone() { const country = document.getElementById('countryCode').value; const phone = document.getElementById('phoneInput').value.trim(); if (!phone) return showToast('전화번호를 입력해주세요', 'error'); const appVerifier = new firebase.auth.RecaptchaVerifier('authScreen'); auth.signInWithPhoneNumber(country + phone, appVerifier) .then(confirmationResult => { window.confirmationResult = confirmationResult; document.getElementById('verifySection').style.display = 'block'; showToast('인증 코드가 전송되었습니다', 'success'); }) .catch(e => showToast('전송 실패: ' + e.message, 'error')); } function confirmVerificationCode() { const code = document.getElementById('verifyCode').value.trim(); if (!code) return showToast('인증 코드를 입력해주세요', 'error'); window.confirmationResult.confirm(code) .then(result => { currentUser = result.user; startOnboarding(); }) .catch(e => showToast('인증 실패: ' + e.message, 'error')); } function sendEmailLink() { const email = document.getElementById('emailInput').value.trim(); if (!email) return showToast('이메일을 입력해주세요', 'error'); auth.sendSignInLinkToEmail(email, { url: window.location.href, handleCodeInApp: true }) .then(() => { localStorage.setItem('emailForSignIn', email); showToast('이메일로 로그인 링크를 보냈습니다!', 'success'); }) .catch(e => showToast('전송 실패: ' + e.message, 'error')); } // Check for email sign-in link if (firebase.auth.isSignInWithEmailLink(window.location.href)) { let email = localStorage.getItem('emailForSignIn'); if (!email) { const emailSection = document.getElementById('emailAuthSection'); if (emailSection) emailSection.style.display = 'block'; const emailInput = document.getElementById('emailInput'); if (emailInput) emailInput.focus(); } else { auth.signInWithEmailLink(email, window.location.href) .then(() => localStorage.removeItem('emailForSignIn')) .catch(e => showToast('로그인 실패: ' + e.message, 'error')); } } // ============================================ // Onboarding // ============================================ function startOnboarding() { onboardingStep = 1; onboardingData = { name: '', age: null, gender: '', nationality: '', photos: [], kpopBias: '', cultureLevel: 0, relationshipGoals: [], languages: [], mannerTemp: 36.5 }; renderProgressBar(); showStep(1); showScreen('onboardingScreen'); } function renderProgressBar() { const bar = document.getElementById('progressBar'); bar.innerHTML = ''; for (let i = 1; i <= TOTAL_STEPS; i++) { const dot = document.createElement('div'); dot.className = 'progress-dot' + (i <= onboardingStep ? ' active' : ''); bar.appendChild(dot); } } function showStep(step) { document.querySelectorAll('.onboarding-step').forEach(s => s.classList.remove('active')); document.getElementById('step' + step).classList.add('active'); const btn = document.getElementById('nextBtn'); if (step === 1) { btn.textContent = '다음'; } else if (step === TOTAL_STEPS) { btn.textContent = '시작하기! 🎉'; } else { btn.textContent = '다음'; } renderProgressBar(); } function validateCurrentStep() { if (onboardingStep === 1) { const name = document.getElementById('nameInput').value.trim(); const age = parseInt(document.getElementById('ageInput').value); const gender = document.getElementById('genderInput').value; const nationality = document.getElementById('nationalityInput').value; if (!name) return showToast('이름을 입력해주세요', 'error'), false; if (!age || age < 18 || age > 100) return showToast('18-100세 사이의 나이를 입력해주세요', 'error'), false; if (!gender) return showToast('성별을 선택해주세요', 'error'), false; if (!nationality) return showToast('국적을 선택해주세요', 'error'), false; onboardingData.name = name; onboardingData.age = age; onboardingData.gender = gender; onboardingData.nationality = nationality; return true; } else if (onboardingStep === 2) { if (onboardingData.photos.length === 0) return showToast('최소 1장의 사진을 업로드해주세요', 'error'), false; onboardingData.kpopBias = document.getElementById('kpopInput').value.trim(); if (onboardingData.cultureLevel === 0) return showToast('문화 레벨을 선택해주세요', 'error'), false; return true; } else if (onboardingStep === 3) { if (onboardingData.relationshipGoals.length === 0) return showToast('연애 목적을 선택해주세요', 'error'), false; if (onboardingData.languages.length === 0) return showToast('언어를 선택해주세요', 'error'), false; return true; } return true; } function nextStep() { if (!validateCurrentStep()) return; if (onboardingStep < TOTAL_STEPS) { onboardingStep++; showStep(onboardingStep); if (onboardingStep === 2) renderPhotoStep(); if (onboardingStep === 3) renderStep3(); } else { completeOnboarding(); } } function previousStep() { if (onboardingStep > 1) { onboardingStep--; showStep(onboardingStep); } } function skipOnboarding() { if (confirm('스킵하면 나중에 프로필을 완성할 수 있습니다. 계속하시겠습니까?')) { completeOnboarding(); } } function renderPhotoStep() { // Photos are already in HTML } function renderStep3() { const rgGrid = document.getElementById('relationshipGoalsGrid'); rgGrid.innerHTML = ''; RELATIONSHIP_GOALS.forEach(g => { const chip = document.createElement('button'); chip.className = 'chip'; chip.textContent = g; chip.onclick = () => { chip.classList.toggle('selected'); onboardingData.relationshipGoals = Array.from(rgGrid.querySelectorAll('.chip.selected')).map(c => c.textContent); }; rgGrid.appendChild(chip); }); const langGrid = document.getElementById('languagesGrid'); langGrid.innerHTML = ''; LANGUAGES.forEach(l => { const chip = document.createElement('button'); chip.className = 'chip'; chip.textContent = l; chip.onclick = () => { chip.classList.toggle('selected'); onboardingData.languages = Array.from(langGrid.querySelectorAll('.chip.selected')).map(c => c.textContent); }; langGrid.appendChild(chip); }); } function handlePhotoUpload(index) { const input = document.getElementById('photoInput' + index); const file = input.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (e) => { onboardingData.photos[index] = e.target.result; const slots = document.querySelectorAll('.photo-slot'); slots[index].innerHTML = ``; slots[index].classList.add('filled'); }; reader.readAsDataURL(file); } function removePhoto(index) { onboardingData.photos.splice(index, 1); const slots = document.querySelectorAll('.photo-slot'); slots[index].innerHTML = '사진 ' + (index + 1) + ''; slots[index].classList.remove('filled'); } // Quiz function startQuiz() { quizCurrentQuestion = 0; quizAnswers = []; document.getElementById('quizModal').classList.add('active'); renderQuizQuestion(); } function renderQuizQuestion() { if (quizCurrentQuestion >= QUIZ_QUESTIONS.length) { showQuizResult(); return; } const q = QUIZ_QUESTIONS[quizCurrentQuestion]; document.getElementById('quizQuestion').textContent = q.q; document.getElementById('quizCurrentQuestion').textContent = quizCurrentQuestion + 1; document.getElementById('quizTotalQuestions').textContent = QUIZ_QUESTIONS.length; const opts = document.getElementById('quizOptions'); opts.innerHTML = ''; q.opts.forEach((opt, i) => { const btn = document.createElement('button'); btn.className = 'quiz-option'; btn.textContent = opt; btn.onclick = () => selectQuizAnswer(i); opts.appendChild(btn); }); } function selectQuizAnswer(index) { const opts = document.querySelectorAll('.quiz-option'); opts.forEach(o => o.classList.remove('selected')); opts[index].classList.add('selected'); window.selectedQuizAnswer = index; } function submitQuizAnswer() { if (window.selectedQuizAnswer === undefined) return showToast('답변을 선택해주세요', 'error'); const correct = window.selectedQuizAnswer === QUIZ_QUESTIONS[quizCurrentQuestion].ans; if (correct) quizAnswers.push(1); else quizAnswers.push(0); const opts = document.querySelectorAll('.quiz-option'); opts[QUIZ_QUESTIONS[quizCurrentQuestion].ans].classList.add('correct'); if (!correct) opts[window.selectedQuizAnswer].classList.add('incorrect'); setTimeout(() => { quizCurrentQuestion++; window.selectedQuizAnswer = undefined; renderQuizQuestion(); }, 1000); } function showQuizResult() { const score = quizAnswers.filter(a => a === 1).length; const levels = ['입문','초급','중급','고급','마스터']; const levelIndex = Math.min(score, 4); onboardingData.cultureLevel = levelIndex; const badges = ['🌱','📚','🎬','🎤','👑']; document.getElementById('quizBadgeEmoji').textContent = badges[levelIndex]; document.getElementById('quizLevelText').textContent = levels[levelIndex] + ' (Lv.' + (levelIndex + 1) + ')'; document.getElementById('quizScoreText').textContent = score + ' / ' + QUIZ_QUESTIONS.length + ' 정답'; document.getElementById('quizBody').style.display = 'none'; document.getElementById('quizResultSection').style.display = 'block'; document.getElementById('quizResult').textContent = '✓ ' + levels[levelIndex] + ' 레벨로 측정됨'; } function closeQuiz() { document.getElementById('quizModal').classList.remove('active'); } // v9: Client-side image compression (saves Firebase Storage bandwidth cost) function compressImage(dataUrl, maxWidth = 800, quality = 0.7) { return new Promise((resolve) => { const img = new Image(); img.onload = () => { const canvas = document.createElement('canvas'); let w = img.width, h = img.height; if (w > maxWidth) { h = (maxWidth / w) * h; w = maxWidth; } canvas.width = w; canvas.height = h; const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0, w, h); resolve(canvas.toDataURL('image/jpeg', quality)); }; img.src = dataUrl; }); } async function completeOnboarding() { try { // Upload photos with client-side compression (v9) const uploadedUrls = []; for (let i = 0; i < onboardingData.photos.length; i++) { const photo = onboardingData.photos[i]; if (photo.startsWith('data:')) { const compressed = await compressImage(photo, 800, 0.7); const res = await fetch(compressed); const blob = await res.blob(); const ref = storage.ref(`sm_photos/${currentUser.uid}/${Date.now()}_${i}.jpg`); await ref.put(blob); const url = await ref.getDownloadURL(); uploadedUrls.push(url); } else { uploadedUrls.push(photo); } } // Save to Firestore await db.collection('sm_users').doc(currentUser.uid).set({ uid: currentUser.uid, email: currentUser.email, name: onboardingData.name, age: onboardingData.age, gender: onboardingData.gender, nationality: onboardingData.nationality, photos: uploadedUrls, kpopBias: onboardingData.kpopBias, cultureLevel: onboardingData.cultureLevel, relationshipGoals: onboardingData.relationshipGoals, languages: onboardingData.languages, mannerTemp: onboardingData.mannerTemp, verified: false, createdAt: new Date(), lastActive: new Date(), stats: { likes: 0, matches: 0, visits: 0 } }); currentProfile = await db.collection('sm_users').doc(currentUser.uid).get(); showScreen('mainApp'); loadDiscoverProfiles(); } catch (e) { showToast('프로필 저장 실패: ' + e.message, 'error'); } } // ============================================ // Main App - Screen Management // ============================================ function showScreen(id) { document.querySelectorAll('.screen').forEach(s => s.classList.remove('active')); document.getElementById(id).classList.add('active'); } function switchTab(tab) { document.querySelectorAll('.app-tab').forEach(t => t.classList.remove('active')); document.getElementById(tab + 'Tab').classList.add('active'); document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active')); event.target.closest('.nav-item').classList.add('active'); if (tab === 'chat' && currentChatUser) loadChat(); if (tab === 'lounge') renderDailyChallenge(); if (tab === 'profile') updateProfileTab(); } function loadUserProfile() { db.collection('sm_users').doc(currentUser.uid).get().then(doc => { if (doc.exists) { currentProfile = doc.data(); showScreen('mainApp'); loadDiscoverProfiles(); updateProfileTab(); } else { startOnboarding(); } }).catch(e => showToast('프로필 로드 실패: ' + e.message, 'error')); // Update lastActive db.collection('sm_users').doc(currentUser.uid).update({ lastActive: new Date() }).catch(() => {}); // v10: Initialize streak and check achievements updateStreak(); checkAchievements(); } // ============================================ // Discover Tab (v9: pagination + active filter + blocked filter) // ============================================ let lastProfileDoc = null; let blockedUsers = JSON.parse(localStorage.getItem('sm_blocked') || '[]'); const PROFILES_PER_PAGE = 20; function loadDiscoverProfiles(append = false) { if (!append) { lastProfileDoc = null; selectedProfiles = []; currentCardIndex = 0; } if (!append) { const container = document.getElementById('cardContainer'); if (container) container.innerHTML = '
'; } const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000); let query = db.collection('sm_users') .where('lastActive', '>', sevenDaysAgo) .orderBy('lastActive', 'desc') .limit(PROFILES_PER_PAGE); if (lastProfileDoc) query = query.startAfter(lastProfileDoc); query.get().then(snap => { if (snap.docs.length > 0) lastProfileDoc = snap.docs[snap.docs.length - 1]; const newProfiles = snap.docs .map(d => d.data()) .filter(p => p.uid !== currentUser.uid && !blockedUsers.includes(p.uid)) .map(p => ({ ...p, _score: calculateCompatibility(p) })) .sort((a, b) => b._score - a._score); selectedProfiles = append ? selectedProfiles.concat(newProfiles) : newProfiles; if (!append) currentCardIndex = 0; renderCard(); }).catch(e => console.error(e)); } function loadMoreProfiles() { loadDiscoverProfiles(true); } function calculateCompatibility(profile) { let score = 0; // Language match — most important if (currentProfile.languages && profile.languages) { const common = currentProfile.languages.filter(l => profile.languages.includes(l)); score += common.length * 10; } // Nationality cross-match (Korean+foreign pairs are ideal) const myNat = currentProfile.nationality; const theirNat = profile.nationality; if (myNat && theirNat) { if (myNat !== theirNat) score += 8; // cross-cultural = bonus else score += 3; } // Relationship goals if (currentProfile.relationshipGoals && profile.relationshipGoals) { const common = currentProfile.relationshipGoals.filter(g => profile.relationshipGoals.includes(g)); score += common.length * 8; } // K-pop bias match — strong interest signal (+15 if exact match) const myBias = (currentProfile.kpopBias || '').toLowerCase().trim(); const theirBias = (profile.kpopBias || '').toLowerCase().trim(); if (myBias && theirBias) { if (myBias === theirBias) { score += 15; profile._sharedBias = true; } else { // Partial match (same group, different member) const myParts = myBias.split(/[\s,\/]+/); const theirParts = theirBias.split(/[\s,\/]+/); const overlap = myParts.filter(w => w.length > 1 && theirParts.includes(w)); if (overlap.length > 0) score += 7; } } // Culture level compatibility (+5 if within 1 level) if (currentProfile.cultureLevel !== undefined && profile.cultureLevel !== undefined) { const diff = Math.abs(currentProfile.cultureLevel - profile.cultureLevel); if (diff === 0) score += 5; else if (diff === 1) score += 3; } return score + Math.random() * 5; } // v9: Online status helper function getOnlineStatus(lastActive) { if (!lastActive) return { cls: 'off', label: '' }; const ts = lastActive.toDate ? lastActive.toDate() : new Date(lastActive); const diff = Date.now() - ts.getTime(); if (diff < 15 * 60 * 1000) return { cls: 'on', label: '온라인' }; if (diff < 60 * 60 * 1000) return { cls: 'away', label: `${Math.round(diff/60000)}분 전` }; if (diff < 24 * 60 * 60 * 1000) return { cls: 'away', label: `${Math.round(diff/3600000)}시간 전` }; return { cls: 'off', label: `${Math.round(diff/86400000)}일 전` }; } function renderCard() { const container = document.getElementById('cardContainer'); if (currentCardIndex >= selectedProfiles.length) { container.innerHTML = `
🔄
더 이상 프로필이 없습니다
`; return; } const p = selectedProfiles[currentCardIndex]; const mannerColor = p.mannerTemp < 36.5 ? 'manner-cold' : p.mannerTemp < 42 ? 'manner-warm' : p.mannerTemp < 46 ? 'manner-hot' : 'manner-fire'; const online = getOnlineStatus(p.lastActive); container.innerHTML = `
${p.name}
${(currentPhotoIndex + 1)} / ${p.photos ? p.photos.length : 1}
${p.name}, ${p.age} ${online.label}
${p.nationality ? NATIONALITIES[p.nationality] || p.nationality : ''}
🌡️ ${p.mannerTemp.toFixed(1)}°C
${p.verified ? `✓ 인증됨` : ''} ${p.cultureLevel !== undefined ? `📚 ${CULTURE_LEVELS[p.cultureLevel] || '입문'}` : ''} ${p.kpopBias ? `🎤 ${p.kpopBias}` : ''} ${p._sharedBias ? `💜 최애 일치!` : ''}
`; enableCardSwipe(); } function likeCard() { const p = selectedProfiles[currentCardIndex]; showCardOverlay('like'); db.collection('sm_likes').add({ from: currentUser.uid, to: p.uid, createdAt: new Date() }); // Increase manner temp db.collection('sm_users').doc(p.uid).update({ mannerTemp: firebase.firestore.FieldValue.increment(0.5) }).catch(() => {}); // v10: Track stats & check achievements userStats.likes++; checkAchievements(); // v10: Check for mutual match db.collection('sm_likes').where('from', '==', p.uid).where('to', '==', currentUser.uid).get().then(snap => { if (!snap.empty) { showMatchPopup(p.name); // Create conversation const convId = [currentUser.uid, p.uid].sort().join('_'); db.collection('sm_conversations').doc(convId).set({ users: [currentUser.uid, p.uid], createdAt: new Date(), lastMessage: '매칭되었어요! 인사를 나눠보세요 💕', lastTimestamp: new Date() }, { merge: true }); } }).catch(() => {}); nextCard(); } function passCard() { showCardOverlay('pass'); nextCard(); } function nextCard() { currentCardIndex++; currentPhotoIndex = 0; setTimeout(() => renderCard(), 300); } function showCardOverlay(type) { const overlay = document.getElementById('cardOverlay'); overlay.className = 'profile-overlay ' + type; overlay.innerHTML = `
${type === 'like' ? '❤️' : '✕'}
`; setTimeout(() => overlay.classList.remove('like', 'pass'), 300); } // Card swipe gesture function enableCardSwipe() { const card = document.getElementById('currentCard'); if (!card) return; let startX = 0, currentX = 0; card.addEventListener('touchstart', (e) => { startX = e.touches[0].clientX; }); card.addEventListener('touchmove', (e) => { currentX = e.touches[0].clientX; const diff = currentX - startX; card.style.transform = `translateX(${diff * 0.5}px) rotate(${diff * 0.1}deg)`; }); card.addEventListener('touchend', () => { const diff = currentX - startX; if (Math.abs(diff) > 100) { if (diff > 0) likeCard(); else passCard(); } else { card.style.transform = 'translateX(0) rotate(0deg)'; } }); } // ============================================ // Chat Tab // ============================================ function loadChat() { if (!currentChatUser) return; db.collection('sm_conversations').doc(currentConversationId).collection('messages') .orderBy('timestamp', 'asc').onSnapshot(snap => { const messages = document.getElementById('chatMessages'); messages.innerHTML = ''; snap.docs.forEach((doc, i) => { const msg = doc.data(); const isMine = msg.from === currentUser.uid; const time = msg.timestamp ? new Date(msg.timestamp.toDate ? msg.timestamp.toDate() : msg.timestamp).toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'}) : ''; messages.innerHTML += `
${msg.text} ${!isMine && translateMode ? `` : ''}
${time}${isMine ? `${msg.read ? ' ✓✓' : ' ✓'}` : ''}
`; }); messages.scrollTop = messages.scrollHeight; // Mark messages as read snap.docs.forEach(doc => { const msg = doc.data(); if (msg.from !== currentUser.uid && !msg.read) { doc.ref.update({ read: true }).catch(() => {}); } }); }); } function sendMessage() { const input = document.getElementById('chatInput'); const text = input.value.trim(); if (!text || !currentConversationId) return; db.collection('sm_conversations').doc(currentConversationId).collection('messages').add({ from: currentUser.uid, to: currentChatUser.uid, text: text, timestamp: new Date(), read: false }); // Update conversation last message db.collection('sm_conversations').doc(currentConversationId).update({ lastMessage: text, lastTimestamp: new Date() }).catch(() => {}); // v10: Track message stats userStats.messages++; checkAchievements(); input.value = ''; input.style.height = 'auto'; } function toggleTranslate() { translateMode = !translateMode; document.getElementById('translateBtn').style.opacity = translateMode ? '1' : '0.5'; const statusBar = document.getElementById('translateStatus'); const toggleBtn = document.getElementById('translateToggleBtn'); if (statusBar) { statusBar.style.display = translateMode ? 'flex' : 'none'; if (toggleBtn) toggleBtn.className = 'ts-toggle' + (translateMode ? '' : ' off'); } if (translateMode) loadChat(); // re-render messages with translate buttons } function translateMessage(text, btn) { btn.textContent = '…'; btn.disabled = true; const target = currentProfile.nationality === 'KR' ? 'en' : 'ko'; const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=${target}&dt=t&q=${encodeURIComponent(text)}`; fetch(url).then(r => r.json()).then(data => { if (data && data[0] && data[0][0]) { const translated = data[0].map(x => x[0]).join(''); const parent = btn.closest('.message-bubble'); if (parent) { let el = parent.querySelector('.translated-text'); if (!el) { el = document.createElement('div'); el.className = 'translated-text'; parent.appendChild(el); } el.innerHTML = `번역 ↓
${translated}`; } btn.style.display = 'none'; } }).catch(() => { btn.textContent = '翻'; btn.disabled = false; }); } // ============================================ // Daily Challenge (Korean Word of the Day) // ============================================ // v10: Achievement & Streak System const ACHIEVEMENTS = [ { id: 'first_like', icon: '💖', title: '첫 좋아요', desc: '첫 번째 좋아요를 보냈어요!', condition: s => s.likes >= 1 }, { id: 'social_5', icon: '🦋', title: '소셜 비터플라이', desc: '5명에게 좋아요를 보냈어요!', condition: s => s.likes >= 5 }, { id: 'chatter', icon: '💬', title: '수다쟁이', desc: '첫 메시지를 보냈어요!', condition: s => s.messages >= 1 }, { id: 'streak_3', icon: '🔥', title: '3일 연속', desc: '3일 연속으로 접속했어요!', condition: s => s.streak >= 3 }, { id: 'streak_7', icon: '⚡', title: '일주일 연속', desc: '7일 연속 접속 덬성!', condition: s => s.streak >= 7 }, { id: 'verified', icon: '✅', title: '인증 완료', desc: '사진 인증을 완료했어요!', condition: s => s.verified }, { id: 'culture_master', icon: '🏆', title: '문화 덬인', desc: 'K-문화 퀴즈 만점!', condition: s => s.quizPerfect }, { id: 'popular', icon: '🌟', title: '인기스타', desc: '좋아요 10개 이상 받았어요!', condition: s => s.likesReceived >= 10 } ]; let userStats = { likes: 0, messages: 0, streak: 0, verified: false, quizPerfect: false, likesReceived: 0 }; let earnedAchievements = JSON.parse(localStorage.getItem('sm_achievements') || '[]'); function updateStreak() { const today = new Date().toDateString(); const lastVisit = localStorage.getItem('sm_last_visit'); let streak = parseInt(localStorage.getItem('sm_streak') || '0'); if (lastVisit !== today) { const yesterday = new Date(Date.now() - 86400000).toDateString(); streak = (lastVisit === yesterday) ? streak + 1 : 1; localStorage.setItem('sm_streak', streak); localStorage.setItem('sm_last_visit', today); } userStats.streak = streak; return streak; } function checkAchievements() { ACHIEVEMENTS.forEach(a => { if (!earnedAchievements.includes(a.id) && a.condition(userStats)) { earnedAchievements.push(a.id); localStorage.setItem('sm_achievements', JSON.stringify(earnedAchievements)); showAchievementToast(a); } }); } function showAchievementToast(achievement) { const el = document.createElement('div'); el.className = 'achievement-toast'; el.innerHTML = `
${achievement.icon}
${achievement.title}
${achievement.desc}
`; document.body.appendChild(el); setTimeout(() => el.remove(), 4000); } function showMatchPopup(name) { const popup = document.createElement('div'); popup.className = 'match-popup'; popup.onclick = () => popup.remove(); popup.innerHTML = `
💕
새로운 매치!
${name}님과 매칭되었어요!
탭하여 닫기
`; document.body.appendChild(popup); setTimeout(() => popup.remove(), 5000); } // ============================================ const DAILY_WORDS = [ { word: '설레다', roman: 'seol-le-da', meaning: 'To feel one\'s heart flutter with excitement' }, { word: '눈치', roman: 'nun-chi', meaning: 'Social awareness / reading the room' }, { word: '정 (情)', roman: 'jeong', meaning: 'Deep emotional bond that grows over time' }, { word: '오가거리다', roman: 'o-geul-geo-ri-da', meaning: 'To feel secondhand embarrassment (cringe)' }, { word: '밀당', roman: 'mil-dang', meaning: 'Push and pull dynamic in a relationship' }, { word: '썸', roman: 'sseom', meaning: 'The "something" stage before officially dating' }, { word: '빠지다', roman: 'ppa-ji-da', meaning: 'To fall for someone / to be hooked' }, { word: '두근두근', roman: 'du-geun-du-geun', meaning: 'Heart pounding (onomatopoeia for excitement)' }, { word: '인연', roman: 'in-yeon', meaning: 'Fate / destined connection between people' }, { word: '사랑스럽다', roman: 'sa-rang-seu-reob-da', meaning: 'To be lovable / adorable' } ]; function renderDailyChallenge() { const idx = new Date().getDate() % DAILY_WORDS.length; const w = DAILY_WORDS[idx]; const el = document.getElementById('dailyChallengeCard'); if (!el) return; document.getElementById('challengeWord').textContent = w.word; document.getElementById('challengeRoman').textContent = w.roman; document.getElementById('challengeMeaning').textContent = w.meaning; el.style.display = 'block'; } function shareChallengeToLounge() { const idx = new Date().getDate() % DAILY_WORDS.length; const w = DAILY_WORDS[idx]; showToast(`"${w.word}" 챌린지를 라운지에 공유했습니다! 🎯`, 'success'); } function listenChallenge() { const idx = new Date().getDate() % DAILY_WORDS.length; const w = DAILY_WORDS[idx]; if ('speechSynthesis' in window) { const u = new SpeechSynthesisUtterance(w.word); u.lang = 'ko-KR'; u.rate = 0.8; window.speechSynthesis.speak(u); showToast('🔊 발음 재생 중...', 'info'); } else { showToast('이 기기에서는 음성을 지원하지 않습니다', 'error'); } } // ============================================ // Profile Tab // ============================================ function updateProfileTab() { if (!currentProfile) return; document.getElementById('profileNameLarge').textContent = currentProfile.name || '사용자'; document.getElementById('profileAge').textContent = (currentProfile.age || 0) + '세'; document.getElementById('profileNationality').textContent = NATIONALITIES[currentProfile.nationality] || currentProfile.nationality || ''; document.getElementById('profileMannerTemp').textContent = '🌡️ ' + (currentProfile.mannerTemp || 36.5).toFixed(1) + '°C'; // v10: Show streak const streak = updateStreak(); const streakEl = document.getElementById('streakBadge'); if (streakEl) streakEl.innerHTML = streak > 1 ? `🔥 ${streak}일 연속` : ''; checkAchievements(); // Verified badge const verifiedBadge = document.getElementById('verifiedBadgeLarge'); const verifyBtn = document.getElementById('verifyProfileBtn'); if (verifiedBadge) verifiedBadge.style.display = currentProfile.verified ? 'inline-flex' : 'none'; if (verifyBtn) verifyBtn.style.display = currentProfile.verified ? 'none' : 'block'; if (currentProfile.photos && currentProfile.photos[0]) { document.getElementById('profileAvatarImg').src = currentProfile.photos[0]; } // Stats document.getElementById('statLikes').textContent = currentProfile.stats?.likes || 0; document.getElementById('statMatches').textContent = currentProfile.stats?.matches || 0; document.getElementById('statVisits').textContent = currentProfile.stats?.visits || 0; // Profile completion updateProfileCompletion(); } function updateProfileCompletion() { const fields = ['name', 'age', 'gender', 'nationality', 'photos', 'kpopBias', 'cultureLevel', 'relationshipGoals', 'languages']; let completed = 0; const incomplete = []; fields.forEach(f => { const value = currentProfile[f]; const hasValue = value && (Array.isArray(value) ? value.length > 0 : true); if (hasValue) completed++; else incomplete.push(f); }); const percent = Math.round((completed / fields.length) * 100); document.getElementById('completionPercent').textContent = percent + '%'; document.getElementById('completionFill').style.width = percent + '%'; const fieldNames = { name: '이름', age: '나이', gender: '성별', nationality: '국적', photos: '사진', kpopBias: 'K-POP 최애', cultureLevel: '문화레벨', relationshipGoals: '연애목적', languages: '언어' }; const list = document.getElementById('incompleteFieldsList'); list.innerHTML = incomplete.map(f => `
${fieldNames[f]}
`).join(''); } // ============================================ // Report & Block (v9) // ============================================ function openReportModal(uid, name) { const modal = document.createElement('div'); modal.className = 'report-modal'; modal.innerHTML = `
신고하기
${name}님을 신고합니다
`; document.body.appendChild(modal); } function submitReport(uid, reason, btn) { db.collection('sm_reports').add({ reporter: currentUser.uid, reported: uid, reason: reason, createdAt: new Date() }).then(() => { btn.closest('.report-modal').remove(); showToast('신고가 접수되었습니다. 검토 후 조치하겠습니다.', 'success'); passCard(); }).catch(() => showToast('신고 실패. 다시 시도해주세요.', 'error')); } function blockUser(uid, name, btn) { blockedUsers.push(uid); localStorage.setItem('sm_blocked', JSON.stringify(blockedUsers)); btn.closest('.report-modal').remove(); showToast(`${name}님을 차단했습니다.`, 'info'); // Remove blocked user from current stack selectedProfiles = selectedProfiles.filter(p => p.uid !== uid); renderCard(); } // ============================================ // Utility Functions // ============================================ function showToast(message, type = 'info') { const toast = document.createElement('div'); toast.style.cssText = ` position: fixed; bottom: 20px; left: 20px; right: 20px; padding: 12px 16px; border-radius: var(--radius-md); background: ${type === 'error' ? 'var(--error)' : type === 'success' ? 'var(--success)' : 'var(--info)'}; color: white; font-size: 13px; z-index: 1000; animation: slideInUp 0.3s ease; `; toast.textContent = message; document.body.appendChild(toast); setTimeout(() => toast.remove(), 3000); } function toggleTheme() { const isLight = document.body.classList.toggle('light-mode'); document.getElementById('themeBtn').textContent = isLight ? '☀️' : '🌙'; localStorage.setItem('sm_theme', isLight ? 'light' : 'dark'); } function openSettings() { showToast('설정은 개발 중입니다', 'info'); } function openFilterModal() { showToast('필터는 개발 중입니다', 'info'); } function openNewPostModal() { showToast('새 게시글 기능은 개발 중입니다', 'info'); } function openProfileEditModal() { showToast('프로필 수정은 개발 중입니다', 'info'); } // ============================================ // Photo Verification // ============================================ function startPhotoVerification() { // Create a modal for photo verification const modal = document.createElement('div'); modal.style.cssText = `position:fixed;inset:0;background:rgba(0,0,0,0.85);z-index:2000;display:flex;align-items:center;justify-content:center;padding:20px;`; modal.innerHTML = `
📸
사진 인증
셀피를 찍어 본인 인증을 완료하세요.
인증된 프로필은 ✓ 뱃지가 표시됩니다.
`; document.body.appendChild(modal); } function processSelfie(input) { const file = input.files[0]; if (!file) return; input.closest('[style*=fixed]').remove(); showToast('인증 처리 중...', 'info'); // Simulate AI verification (instant approval for demo) setTimeout(() => { db.collection('sm_users').doc(currentUser.uid).update({ verified: true }) .then(() => { currentProfile.verified = true; updateProfileTab(); showToast('✓ 사진 인증이 완료되었습니다!', 'success'); }).catch(() => showToast('인증 실패. 다시 시도해주세요.', 'error')); }, 1500); } function setLoungeCategory(cat) { currentLoungeCategory = cat; document.querySelectorAll('#loungeTab .chip').forEach(c => c.classList.remove('selected')); event.target.classList.add('selected'); const challengeCard = document.getElementById('dailyChallengeCard'); if (cat === 'challenge') { if (challengeCard) challengeCard.style.display = 'block'; } else if (cat === 'all') { if (challengeCard) challengeCard.style.display = 'block'; } else { if (challengeCard) challengeCard.style.display = 'none'; showToast('라운지 j능은 개발 중입니다', 'info'); } }