💕
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 = `
`;
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 = ``;
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 += `
`;
});
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 = ``;
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 = ``;
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 => `
`;
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');
}
}
🔄
더 이상 프로필이 없습니다
${(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 ? `💜 최애 일치!` : ''}
${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.title}
${achievement.desc}
💕
새로운 매치!
${name}님과 매칭되었어요!
탭하여 닫기
${fieldNames[f]}
`).join('');
}
// ============================================
// Report & Block (v9)
// ============================================
function openReportModal(uid, name) {
const modal = document.createElement('div');
modal.className = 'report-modal';
modal.innerHTML = `
신고하기
${name}님을 신고합니다
📸
사진 인증
셀피를 찍어 본인 인증을 완료하세요.
인증된 프로필은 ✓ 뱃지가 표시됩니다.
인증된 프로필은 ✓ 뱃지가 표시됩니다.