import React, { useState, useEffect, useMemo, useRef } from 'react'; import { initializeApp } from 'firebase/app'; import { getAuth, signInAnonymously, onAuthStateChanged, signInWithCustomToken, signOut, GoogleAuthProvider, signInWithPopup, createUserWithEmailAndPassword, signInWithEmailAndPassword, updateProfile, updatePassword, RecaptchaVerifier, signInWithPhoneNumber } from 'firebase/auth'; import { getFirestore, doc, setDoc, updateDoc, onSnapshot, collection, addDoc, serverTimestamp, getDoc } from 'firebase/firestore'; import { MapPin, Utensils, Coffee, Beer, Music, Scissors, Sparkles, User, Ticket, MessageCircle, ChevronRight, X, Star, Map as MapIcon, RotateCcw, Home, PlusCircle, Heart, Send, Store, Clock, QrCode, LogOut, Mail, Lock, Phone, Plus, MessageSquare, Edit, HelpCircle, Bell, Gift, ChevronLeft, CreditCard, AlertCircle, GraduationCap, Image as ImageIcon, Users, CheckCircle, ChevronDown, ChevronUp, PawPrint, Plane, Navigation, Siren, // For Report Smartphone, // For Phone Auth Camera, // For Image Upload Car, // For Grab Shield, // For Admin Briefcase, // For Boss Calendar // For DOB } from 'lucide-react'; // --- Firebase Configuration --- const firebaseConfig = JSON.parse(__firebase_config); const app = initializeApp(firebaseConfig); const auth = getAuth(app); const db = getFirestore(app); const appId = typeof __app_id !== 'undefined' ? __app_id : 'default-app-id'; // --- Global Constants --- const fontStyle = { fontFamily: "'Pretendard', 'Apple SD Gothic Neo', 'Noto Sans KR', sans-serif" }; const GOOGLE_MAPS_API_KEY = ""; // TODO: 나중에 여기에 실제 Google Maps API Key를 넣으세요. const LANGUAGES = [ { code: 'ko', flag: '🇰🇷' }, { code: 'en', flag: '🇺🇸' }, { code: 'vn', flag: '🇻🇳' } ]; // Updated Categories with Pet and Travel const CATEGORIES_DATA = [ { id: 'food', Icon: Utensils }, { id: 'cafe', Icon: Coffee }, { id: 'pub', Icon: Beer }, { id: 'club', Icon: Music }, { id: 'massage', Icon: Sparkles }, { id: 'barber', Icon: Scissors }, { id: 'beauty', Icon: User }, { id: 'shopping', Icon: Store }, { id: 'pet', Icon: PawPrint }, { id: 'travel', Icon: Plane }, ]; const CITIES = [ { id: 'hcm', name: '호치민 (HCM)', icon: '🏙️', lat: 10.7769, lng: 106.7009 }, { id: 'hanoi', name: '하노이', icon: '🏯', lat: 21.0285, lng: 105.8542 }, { id: 'danang', name: '다낭', icon: '🏖️', lat: 16.0544, lng: 108.2022 }, { id: 'nhatrang', name: '나트랑', icon: '🌴', lat: 12.2388, lng: 109.1967 }, ]; // 레슨용 지역 상수 (달랏 포함) const LESSON_LOCATIONS = [ { id: 'hcm', name: '호치민' }, { id: 'hanoi', name: '하노이' }, { id: 'danang', name: '다낭' }, { id: 'nhatrang', name: '나트랑' }, { id: 'dalat', name: '달랏' }, ]; // Updated MOCK_BUSINESSES with REAL Coordinates for Google Maps const MOCK_BUSINESSES = [ { id: 'b1', name: 'Pho Quynh', category: 'food', city: 'hcm', rating: 4.8, reviewCount: 128, image: 'https://images.unsplash.com/photo-1582878826629-29b7ad1cdc43?auto=format&fit=crop&q=80&w=800', desc: { ko: '부이비엔 근처 가장 유명한 24시간 쌀국수 맛집.', en: 'Famous 24h Pho restaurant near Bui Vien walking street.', vn: 'Quán phở 24h nổi tiếng nhất gần phố đi bộ Bùi Viện.' }, hours: '10:00 ~ 02:00', phone: '028 3836 8515', address: '323 Phạm Ngũ Lão, Phường Phạm Ngũ Lão, Quận 1, Hồ Chí Minh', coupon: { ko: '음료 1잔 무료', en: 'Free Drink', vn: 'Miễn phí 1 đồ uống' }, couponDesc: { ko: '직원에게 이 화면을 보여주세요.', en: 'Show this screen to the staff.', vn: 'Vui lòng đưa màn hình này cho nhân viên.' }, lat: 10.7676, lng: 106.6935 // Real coordinates }, { id: 'b2', name: 'Bun Cha 145', category: 'food', city: 'hcm', rating: 4.5, reviewCount: 856, image: 'https://images.unsplash.com/photo-1513558161293-cdaf765ed2fd?auto=format&fit=crop&q=80&w=800', desc: { ko: '부이비엔 분짜 맛집', en: 'Bun Cha 145', vn: 'Bun Cha Ngon' }, hours: '12:30 ~ 20:00', phone: '028 3837 3474', address: '145 Bùi Viện, Phường Phạm Ngũ Lão, Quận 1, Hồ Chí Minh', coupon: { ko: '10% 할인', en: '10% Discount', vn: 'Giảm giá 10%' }, couponDesc: { ko: '메인 메뉴 주문 시', en: 'Main dish order', vn: 'Khi gọi món chính' }, lat: 10.7671, lng: 106.6943 }, { id: 'b3', name: 'Pizza 4P\'s', category: 'food', city: 'hcm', image: 'https://images.unsplash.com/photo-1574071318508-1cdbab80d002?auto=format&fit=crop&q=80&w=800', desc: { ko: '화덕 피자', en: 'Pizza', vn: 'Pizza' }, hours: '11:00~22:00', phone: '1900 6043', address: '8/15 Le Thanh Ton', coupon: { ko: '디저트 증정', en: 'Free Dessert', vn: 'Tặng tráng miệng' }, couponDesc: { ko: '설명', en: 'Desc', vn: 'Mô tả' }, lat: 10.7797, lng: 106.7045 }, { id: 'b4', name: 'Cong Cafe', category: 'cafe', city: 'danang', image: 'https://images.unsplash.com/photo-1509042239860-f550ce710b93?auto=format&fit=crop&q=80&w=800', desc: { ko: '코코넛 커피', en: 'Coconut Coffee', vn: 'Cà phê cốt dừa' }, hours: '07:00~23:00', phone: '0236 6553', address: '98 Bach Dang', coupon: { ko: '사이즈 업', en: 'Size up', vn: 'Up size' }, couponDesc: { ko: '설명', en: 'Desc', vn: 'Mô tả' }, lat: 16.0688, lng: 108.2238 }, { id: 'b5', name: 'Miu Miu Spa', category: 'massage', city: 'hcm', image: 'https://images.unsplash.com/photo-1600334089648-b0d9d3028eb2?auto=format&fit=crop&q=80&w=800', desc: { ko: '마사지', en: 'Massage', vn: 'Massage' }, hours: '09:30~23:00', phone: '028 6659', address: '4 Chu Manh Trinh', coupon: { ko: '20% 할인', en: '20% DC', vn: 'Giảm 20%' }, couponDesc: { ko: '설명', en: 'Desc', vn: 'Mô tả' }, lat: 10.7810, lng: 106.7040 }, ]; const MOCK_POSTS = [ { id: 'p1', author: '여행자123', city: 'hcm', title: '호치민 1군 맛집 추천 좀 해주세요!', content: '부모님 모시고 가는데 깔끔한 곳 찾습니다.', likes: 12, isLiked: false, comments: [], timestamp: '방금 전' } ]; // Updated Mock Winners to support Multi-language const MOCK_WINNERS = { ko: ["🔥 [호치민] user82**님 'Pizza 4P's' 무료 피자 당첨!"], en: ["🔥 [HCMC] user82** won 'Free Pizza' at 'Pizza 4P's'!"], vn: ["🔥 [TP.HCM] user82** đã trúng 'Pizza miễn phí' tại 'Pizza 4P's'!"] }; const COUNTRY_CODES = [ { code: '+84', name: '베트남', flag: '🇻🇳' }, { code: '+82', name: '대한민국', flag: '🇰🇷' }, { code: '+1', name: '미국', flag: '🇺🇸' }, ]; const SLOT_OPTIONS = [ { count: 1, price: 2, label: '1개 확장' }, { count: 3, price: 3, label: '3개 확장' }, { count: 5, price: 5, label: '5개 확장' }, { count: 10, price: 8, label: '10개 확장' }, ]; const RESIDENCE_OPTIONS = [ { id: 'hanoi', label: '하노이' }, { id: 'danang', label: '다낭' }, { id: 'nhatrang', label: '나트랑' }, { id: 'dalat', label: '달랏' }, { id: 'hcm', label: '호치민' }, { id: 'custom', label: '직접 입력' } ]; const TRANSLATIONS = { ko: { home: '룰렛', map: '지도', coupon: '내 쿠폰함', community: '커뮤니티', mypage: '마이', login_needed: '로그인이 필요해요', login_btn: '로그인 / 회원가입', login: '로그인', signup: '회원가입', id_placeholder: '아이디', pw_placeholder: '비밀번호', email_placeholder: '이메일 (연락용)', phone_placeholder: '핸드폰 번호', spin_left: '오늘 남은 기회', daily_user: '오늘 {n}명이 룰렛으로 결정했어요!', lineup: '이달의 라인업', click_info: '클릭하여 업체 정보 보기', boss_only: '사장님 전용', store_inquiry: '입점 신청하기', inquiry_desc: '100만 동으로 우리 가게를\nDealBox 룰렛에 올리세요!', apply: '입점 신청하기', spin_msg_login: '로그인하고 룰렛 돌리기', spin_msg_no_chance: '스핀 기회가 없나요? 광고보고 충전하기 (+1)', slot_add: '슬롯 추가', my_slot: 'MY 쿠폰 슬롯', spin_free: '무제한', spin_no: '기회없음', coupon_validity: '* 쿠폰은 발급 후 24시간 동안 유효합니다.', use_coupon: '사용하기', save_coupon: '쿠폰 담기', available: '사용가능', hours_left: '시간 남음', congrats: '🎉 당첨을 축하합니다!', saved_msg: '쿠폰함에 저장하여 매장 방문 시 사용하세요!', boss_check: '사장님 확인 완료', boss_check_title: '쿠폰 사용 확인 (사장님 보여주기)', write_title: '글쓰기', write_placeholder_title: '제목', write_placeholder_content: '내용 입력', register: '등록하기', comments: '댓글', likes: '공감', city_select: '지역 선택', traveler_talk: '트래블러 톡', talk_placeholder: '정보 공유하기...', cat_food: '맛집', cat_cafe: '카페', cat_pub: '펍/바', cat_club: '클럽', cat_massage: '마사지', cat_barber: '이발소', cat_beauty: '뷰티', cat_shopping: '쇼핑', cat_pet: '애완동물', cat_travel: '여행사', tag_food: '오늘 뭐 먹지?', tag_cafe: '분위기 좋은 카페', tag_pub: '한잔 할까?', tag_club: '오늘 밤 파티?', tag_massage: '피로 풀기', tag_barber: '깔끔하게 변신', tag_beauty: '더 예뻐지기', tag_shopping: '기념품 & 쇼핑', tag_pet: '댕댕이와 함께', tag_travel: '여행 떠나볼까?', city_hcm: '호치민', city_hanoi: '하노이', city_danang: '다낭', city_nhatrang: '나트랑', menu_edit: '내 정보 수정', menu_inquiry: '신고/문의 센터', menu_notice: '공지사항', menu_event: '이벤트', menu_lesson: '개인레슨', logout: '로그아웃', lesson_write: '레슨 등록하기', lesson_price: '시간당 가격 (VND)', lesson_contact: '연락처', lesson_apply: '신청하기', lesson_applicants: '신청자', lesson_apply_title: '레슨 신청', lesson_apply_btn: '신청 완료', lesson_apply_msg_placeholder: '선생님께 남길 메시지', lesson_applicants_view: '신청자 명단', google_fail: '구글 로그인 실패', login_fail: '로그인 실패', signup_success: '회원가입 성공', map_view: '구글 지도로 보기', review: '리뷰', ticker_format: "🎉 [{city}] {user}님 '{shop}' {coupon} 당첨!", business_hours: '영업시간:', address_label: '주소:', phone_label: '전화:', map_guide: '지도를 움직여 업체를 확인하세요', map_key_missing: '지도를 불러오지 못했습니다. API 키를 확인해주세요.', grab_go: 'Grab으로 이동', report: '신고하기', report_title: '신고하기', report_reason: '신고 사유', report_btn: '신고 접수', push_setting: '알림 설정', admin_page: '관리자 페이지', boss_page: '사장님 페이지', zalo_login: 'Zalo로 로그인', phone_verify: '인증', name_placeholder: '이름', dob_placeholder: '생년월일 (YYYY-MM-DD)', residence_label: '거주지역' }, en: { home: 'Roulette', map: 'Map', coupon: 'My Coupons', community: 'Community', mypage: 'My', login_needed: 'Login Required', login_btn: 'Login / Signup', login: 'Login', signup: 'Signup', id_placeholder: 'ID', pw_placeholder: 'Password', email_placeholder: 'Email', phone_placeholder: 'Phone Number', spin_left: 'Spins Left', daily_user: '{n} users decided with Roulette today!', lineup: 'Lineup of the Month', click_info: 'Click for info', boss_only: 'Partners', store_inquiry: 'Join DealBox', inquiry_desc: 'Promote your shop on DealBox\nfor 1M VND!', apply: 'Apply to Join', spin_msg_login: 'Login to Spin', spin_msg_no_chance: 'No spins left? Watch Ad to recharge (+1)', slot_add: 'Add Slots', my_slot: 'My Slots', spin_free: 'Free', spin_no: '0', coupon_validity: '* Coupon valid for 24 hours after issuance.', use_coupon: 'Use Coupon', save_coupon: 'Save', available: 'Available', hours_left: 'h left', congrats: '🎉 Congratulations!', saved_msg: 'Saved to your wallet!', boss_check: 'Verified by Staff', boss_check_title: 'Use Coupon (Show Staff)', write_title: 'Write Post', write_placeholder_title: 'Title', write_placeholder_content: 'Content', register: 'Post', comments: 'Comments', likes: 'Likes', city_select: 'Select City', traveler_talk: 'Traveler Talk', talk_placeholder: 'Share info...', cat_food: 'Food', cat_cafe: 'Cafe', cat_pub: 'Pub/Bar', cat_club: 'Club', cat_massage: 'Massage', cat_barber: 'Barber', cat_beauty: 'Beauty', cat_shopping: 'Shopping', cat_pet: 'Pet', cat_travel: 'Travel', tag_food: 'Hungry?', tag_cafe: 'Nice Vibes', tag_pub: 'Drink?', tag_club: 'Party Tonight?', tag_massage: 'Relax', tag_barber: 'Haircut', tag_beauty: 'Beauty', tag_shopping: 'Shopping', tag_pet: 'With Pets', tag_travel: 'Let\'s go!', city_hcm: 'HCMC', city_hanoi: 'Hanoi', city_danang: 'Danang', city_nhatrang: 'Nha Trang', menu_edit: 'Edit Profile', menu_inquiry: 'Report/Support', menu_notice: 'Notices', menu_event: 'Events', menu_lesson: 'Lessons', logout: 'Logout', lesson_write: 'Post Lesson', lesson_price: 'Price/Hr (VND)', lesson_contact: 'Contact', lesson_apply: 'Apply', lesson_applicants: 'Applicants', lesson_apply_title: 'Apply for Lesson', lesson_apply_btn: 'Submit', lesson_apply_msg_placeholder: 'Message to Tutor', lesson_applicants_view: 'Applicant List', google_fail: 'Google Login Failed', login_fail: 'Login Failed', signup_success: 'Signup Success', map_view: 'Open in Google Maps', review: 'Reviews', ticker_format: "🎉 [{city}] {user} won '{coupon}' at '{shop}'!", business_hours: 'Hours:', address_label: 'Address:', phone_label: 'Phone:', map_guide: 'Move map to explore', map_key_missing: 'Map unavailable. Check API Key.', grab_go: 'Open Grab', report: 'Report', report_title: 'Report', report_reason: 'Reason', report_btn: 'Submit', push_setting: 'Push Notifications', admin_page: 'Admin Page', boss_page: 'Partner Page', zalo_login: 'Login with Zalo', phone_verify: 'Verify', name_placeholder: 'Name', dob_placeholder: 'DOB (YYYY-MM-DD)', residence_label: 'Residence' }, vn: { home: 'Vòng quay', map: 'Bản đồ', coupon: 'Kho Coupon', community: 'Cộng đồng', mypage: 'Tôi', login_needed: 'Cần đăng nhập', login_btn: 'Đăng nhập / Đăng ký', login: 'Đăng nhập', signup: 'Đăng ký', id_placeholder: 'Tài khoản', pw_placeholder: 'Mật khẩu', email_placeholder: 'Email', phone_placeholder: 'Số điện thoại', spin_left: 'Lượt quay', daily_user: '{n} người đã quay hôm nay!', lineup: 'Địa điểm nổi bật', click_info: 'Xem chi tiết', boss_only: 'Đối tác', store_inquiry: 'Hợp tác', inquiry_desc: 'Quảng bá cửa hàng trên DealBox\nchỉ với 1tr VNĐ!', apply: 'Đăng ký ngay', spin_msg_login: 'Đăng nhập để quay', spin_msg_no_chance: 'Hết lượt? Xem QC để thêm lượt (+1)', slot_add: 'Thêm ô', my_slot: 'Ô chứa Coupon', spin_free: 'Miễn phí', spin_no: 'Hết', coupon_validity: '* Coupon có hiệu lực trong 24h.', use_coupon: 'Sử dụng', save_coupon: 'Lưu', available: 'Có sẵn', hours_left: 'giờ', congrats: '🎉 Chúc mừng!', saved_msg: 'Đã lưu vào ví!', boss_check: 'Xác nhận', boss_check_title: 'Dùng Coupon (Đưa nhân viên)', write_title: 'Viết bài', write_placeholder_title: 'Tiêu đề', write_placeholder_content: 'Nội dung', register: 'Đăng', comments: 'Bình luận', likes: 'Thích', city_select: 'Chọn TP', traveler_talk: 'Chat Du lịch', talk_placeholder: 'Chia sẻ...', cat_food: 'Ăn uống', cat_cafe: 'Cafe', cat_pub: 'Pub/Bar', cat_club: 'Club', cat_massage: 'Massage', cat_barber: 'Hớt tóc', cat_beauty: 'Làm đẹp', cat_shopping: 'Mua sắm', cat_pet: 'Thú cưng', cat_travel: 'Du lịch', tag_food: 'Đói chưa?', tag_cafe: 'Cafe đẹp', tag_pub: 'Đi nhậu?', tag_club: 'Quẩy nào', tag_massage: 'Thư giãn', tag_barber: 'Cắt tóc', tag_beauty: 'Làm đẹp', tag_shopping: 'Mua sắm', tag_pet: 'Với thú cưng', tag_travel: 'Đi du lịch nào', city_hcm: 'TP.HCM', city_hanoi: 'Hà Nội', city_danang: 'Đà Nẵng', city_nhatrang: 'Nha Trang', menu_edit: 'Sửa hồ sơ', menu_inquiry: 'Hỗ trợ', menu_notice: 'Thông báo', menu_event: 'Sự kiện', menu_lesson: 'Gia sư', logout: 'Đăng xuất', lesson_write: 'Đăng bài', lesson_price: 'Giá/giờ (VND)', lesson_contact: 'Liên hệ', lesson_apply: 'Đăng ký', lesson_applicants: 'Người đăng ký', lesson_apply_title: 'Đăng ký học', lesson_apply_btn: 'Gửi', lesson_apply_msg_placeholder: 'Tin nhắn cho giáo viên', lesson_applicants_view: 'Danh sách đăng ký', google_fail: 'Lỗi đăng nhập Google', login_fail: 'Lỗi đăng nhập', signup_success: 'Đăng ký thành công', map_view: 'Mở Google Maps', review: 'Đánh giá', ticker_format: "🎉 [{city}] {user} đã trúng '{coupon}' tại '{shop}'!", business_hours: 'Giờ mở cửa:', address_label: 'Địa chỉ:', phone_label: 'Điện thoại:', map_guide: 'Di chuyển bản đồ để khám phá', map_key_missing: 'Không thể tải bản đồ. Kiểm tra API Key.', grab_go: 'Mở Grab', report: 'Báo cáo', report_title: 'Báo cáo', report_reason: 'Lý do', report_btn: 'Gửi', push_setting: 'Thông báo', admin_page: 'Trang Quản trị', boss_page: 'Trang Đối tác', zalo_login: 'Đăng nhập Zalo', phone_verify: 'Xác thực', name_placeholder: 'Tên', dob_placeholder: 'Ngày sinh (YYYY-MM-DD)', residence_label: 'Nơi ở' } }; // --- Confetti Component --- const Confetti = () => { // ... (Same as before) const particles = Array.from({ length: 50 }).map((_, i) => { const style = { left: '50%', top: '50%', '--x': `${(Math.random() - 0.5) * 600}px`, '--y': `${(Math.random() - 0.5) * 600}px`, '--rotate': `${Math.random() * 360}deg`, backgroundColor: ['#EC4899', '#8B5CF6', '#3B82F6', '#F59E0B', '#10B981'][Math.floor(Math.random() * 5)], animationDelay: `${Math.random() * 0.2}s` }; return
; }); return (
{particles}
); }; // --- Helper Components --- const Modal = ({ isOpen, onClose, children, title, hideClose = false, bgColor="bg-[#1f2937]" }) => { if (!isOpen) return null; return (
{!hideClose && } {title &&
{title}
}
{children}
); }; const BusinessCard = ({ data, onClick, lang = 'ko' }) => { const currentDesc = typeof data.desc === 'string' ? data.desc : (data.desc[lang] || data.desc['ko']); const currentCoupon = typeof data.coupon === 'string' ? data.coupon : (data.coupon[lang] || data.coupon['ko']); return (
onClick(data)} className="bg-[#1f2937] rounded-xl p-3 flex items-center gap-4 mb-3 border border-gray-800 hover:border-pink-500/50 transition-all cursor-pointer active:scale-95 touch-manipulation"> {data.name}

{data.name}

{currentCoupon}

); }; const RouletteWheel = ({ items, onSpinComplete, canSpin, t }) => { const [spinning, setSpinning] = useState(false); const [rotation, setRotation] = useState(0); const handleSpin = () => { if (!canSpin || spinning) return; setSpinning(true); const newRotation = rotation + 1800 + Math.random() * 360; setRotation(newRotation); setTimeout(() => { setSpinning(false); const degrees = newRotation % 360; const sliceAngle = 360 / items.length; const index = Math.floor(((360 - degrees) % 360) / sliceAngle); onSpinComplete(items[index]); }, 3000); }; return (
{/* SVG content omitted for brevity as it's same as before */} {items.map((_, i) => { const sliceAngle = 360 / items.length; const x1 = 100 + 100 * Math.cos(Math.PI * (i * sliceAngle) / 180); const y1 = 100 + 100 * Math.sin(Math.PI * (i * sliceAngle) / 180); const x2 = 100 + 100 * Math.cos(Math.PI * ((i + 1) * sliceAngle) / 180); const y2 = 100 + 100 * Math.sin(Math.PI * ((i + 1) * sliceAngle) / 180); return ( ); })} {items.map((item, i) => { const sliceAngle = 360 / items.length; const textAngle = (i * sliceAngle) + sliceAngle / 2; const tx = 100 + 65 * Math.cos(Math.PI * textAngle / 180); const ty = 100 + 65 * Math.sin(Math.PI * textAngle / 180); return ( {item.image ? ( <> ) : ( )} {item.name} ); })}
); }; // Real-time Winner Ticker Component const WinnerTicker = ({ lang }) => { const [winners, setWinners] = useState([]); useEffect(() => { const winnersRef = collection(db, 'artifacts', appId, 'public', 'data', 'winners'); const unsubscribe = onSnapshot(winnersRef, (snapshot) => { let newWinners = snapshot.docs.map(doc => doc.data()); newWinners.sort((a, b) => { const tA = a.timestamp?.seconds || 0; const tB = b.timestamp?.seconds || 0; return tB - tA; }); newWinners = newWinners.slice(0, 10); if (newWinners.length > 0) setWinners(newWinners); else setWinners([]); }, (error) => console.log("Ticker Error:", error)); return () => unsubscribe(); }, []); const formatWinner = (item) => { if (typeof item === 'string') return item; const currentLang = TRANSLATIONS[lang] ? lang : 'ko'; const format = TRANSLATIONS[currentLang].ticker_format; const city = TRANSLATIONS[currentLang][`city_${item.cityId}`] || item.cityId; const couponName = (typeof item.coupon === 'object') ? (item.coupon[currentLang] || item.coupon['ko']) : item.coupon; return format .replace('{city}', city) .replace('{user}', item.userName) .replace('{shop}', item.businessName) .replace('{coupon}', couponName); }; let displayList = []; if (winners.length > 0) { displayList = winners.map(formatWinner); } else { displayList = MOCK_WINNERS[lang] || MOCK_WINNERS['ko']; } let animatedList = []; if (displayList.length > 0) { animatedList = [...displayList]; while (animatedList.length < 20) { animatedList = [...animatedList, ...displayList]; } } const duration = Math.max(animatedList.length * 2, 20); return (
{animatedList.map((text, i) => ({text}))}
); }; export default function DealBoxApp() { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); const [selectedCity, setSelectedCity] = useState('hcm'); const [selectedCategory, setSelectedCategory] = useState('food'); const [activeTab, setActiveTab] = useState('home'); const [viewMode, setViewMode] = useState('roulette'); const [lang, setLang] = useState('ko'); const [showLangMenu, setShowLangMenu] = useState(false); const [myPageView, setMyPageView] = useState('main'); const [showCityModal, setShowCityModal] = useState(false); const [selectedBusiness, setSelectedBusiness] = useState(null); const [spinResult, setSpinResult] = useState(null); const [isAdWatching, setIsAdWatching] = useState(false); const [showConfetti, setShowConfetti] = useState(false); const [showChat, setShowChat] = useState(false); const [chatMessages, setChatMessages] = useState([]); const [chatInput, setChatInput] = useState(''); const [showInquiryModal, setShowInquiryModal] = useState(false); const [showWriteModal, setShowWriteModal] = useState(false); const [showAuthModal, setShowAuthModal] = useState(false); const [showSlotShop, setShowSlotShop] = useState(false); const [focusedBusiness, setFocusedBusiness] = useState(null); // New Feature States const [showReportModal, setShowReportModal] = useState(false); const [reportTarget, setReportTarget] = useState(null); const [reportReason, setReportReason] = useState(''); const [pushEnabled, setPushEnabled] = useState(false); const [deviceId, setDeviceId] = useState(''); const [userRole, setUserRole] = useState('user'); const [postImagePreview, setPostImagePreview] = useState(null); // Map Logic const mapRef = useRef(null); const mapInstanceRef = useRef(null); const markersRef = useRef([]); // Lesson States const [showLessonWriteModal, setShowLessonWriteModal] = useState(false); const [showLessonApplyModal, setShowLessonApplyModal] = useState(false); const [showApplicantListModal, setShowApplicantListModal] = useState(false); const [selectedLesson, setSelectedLesson] = useState(null); const [lessons, setLessons] = useState([]); // Lesson Forms const [lessonForm, setLessonForm] = useState({ title: '', category: 'academic', city: 'hanoi', price: '', desc: '' }); const [lessonApplyForm, setLessonApplyForm] = useState({ name: '', region: 'hanoi', phone: '', messengerId: '' }); const [lessonApplicants, setLessonApplicants] = useState([]); const [applicantCount, setApplicantCount] = useState(0); const [posts, setPosts] = useState(MOCK_POSTS); const [expandedPostId, setExpandedPostId] = useState(null); const [dailyParticipants, setDailyParticipants] = useState(0); const [selectedCoupon, setSelectedCoupon] = useState(null); const [authMode, setAuthMode] = useState('login'); const [inquiryForm, setInquiryForm] = useState({ storeName: '', category: 'food', city: 'hcm', discountInfo: '', contact: '', representativeName: '', googleMapUrl: '' }); const [postForm, setPostForm] = useState({ title: '', content: '' }); const [commentText, setCommentText] = useState(''); const [loginId, setLoginId] = useState(''); const [loginPassword, setLoginPassword] = useState(''); // Updated Signup Form const [signupForm, setSignupForm] = useState({ id: '', password: '', email: '', name: '', dob: '', residence: 'hanoi', residenceCustom: '', countryCode: '+84', phone: '' }); const [changePw, setChangePw] = useState(''); const [myInquiry, setMyInquiry] = useState(''); const [userData, setUserData] = useState({ spinsLeft: 3, maxCoupons: 3, lastSpinDate: null, savedCoupons: [], usedCoupons: [] }); const [savedBusinesses, setSavedBusinesses] = useState([]); const fileInputRef = useRef(null); // Category Scroll Logic const scrollRef = React.useRef(null); const [isDragging, setIsDragging] = useState(false); const [startX, setStartX] = useState(0); const [scrollLeft, setScrollLeft] = useState(0); const onMouseDown = (e) => { setIsDragging(true); setStartX(e.pageX - scrollRef.current.offsetLeft); setScrollLeft(scrollRef.current.scrollLeft); }; const onMouseLeave = () => { setIsDragging(false); }; const onMouseUp = () => { setIsDragging(false); }; const onMouseMove = (e) => { if (!isDragging) return; e.preventDefault(); const x = e.pageX - scrollRef.current.offsetLeft; const walk = (x - startX) * 2; scrollRef.current.scrollLeft = scrollLeft - walk; }; const t = (key) => (TRANSLATIONS[lang] ? TRANSLATIONS[lang][key] : TRANSLATIONS['ko'][key]) || ''; const categories = useMemo(() => CATEGORIES_DATA.map(cat => ({ ...cat, name: t(`cat_${cat.id}`), tagline: t(`tag_${cat.id}`) })), [lang]); const currentBusinesses = useMemo(() => MOCK_BUSINESSES.filter(b => b.city === selectedCity && b.category === selectedCategory), [selectedCity, selectedCategory]); const rouletteItems = useMemo(() => currentBusinesses.length > 0 ? currentBusinesses.slice(0, 6) : [{name: '업체 없음', id: 'none'}], [currentBusinesses]); const filteredPosts = useMemo(() => posts.filter(p => p.city === selectedCity), [posts, selectedCity]); // --- Handlers --- const handleCitySelect = (cityId) => { setSelectedCity(cityId); setShowCityModal(false); }; const handleSpinComplete = async (winner) => { if (!user || winner.id === 'none') return; if (userData.savedCoupons?.includes(winner.id)) { alert("이미 쿠폰을 받은 업체입니다."); return; } if (userData.usedCoupons?.includes(winner.id)) { alert("이미 쿠폰을 사용한 업체입니다."); return; } await updateDoc(doc(db, 'artifacts', appId, 'users', user.uid, 'profile', 'data'), { spinsLeft: userData.spinsLeft - 1 }); const maskedName = (user.displayName || 'User').substring(0, 2) + '**'; await addDoc(collection(db, 'artifacts', appId, 'public', 'data', 'winners'), { cityId: selectedCity, userName: maskedName, businessName: winner.name, coupon: winner.coupon, timestamp: serverTimestamp() }); setSpinResult(winner); setShowConfetti(true); setTimeout(() => setShowConfetti(false), 2500); }; const handleSaveCoupon = async (businessId) => { if (!user) return; if (userData.savedCoupons.length >= userData.maxCoupons) { alert("슬롯이 가득 찼습니다!"); return; } const userDocRef = doc(db, 'artifacts', appId, 'users', user.uid, 'profile', 'data'); if (!userData.savedCoupons.includes(businessId) && !userData.usedCoupons?.includes(businessId)) { await updateDoc(userDocRef, { savedCoupons: [...userData.savedCoupons, businessId] }); alert("쿠폰이 저장되었습니다."); } else { alert("이미 저장된 쿠폰입니다."); } }; const handleUseCoupon = (business) => setSelectedCoupon(business); const handleUseCouponConfirm = async () => { if (!user || !selectedCoupon) return; const newSaved = userData.savedCoupons.filter(id => id !== selectedCoupon.id); const newUsed = [...(userData.usedCoupons || []), selectedCoupon.id]; await updateDoc(doc(db, 'artifacts', appId, 'users', user.uid, 'profile', 'data'), { savedCoupons: newSaved, usedCoupons: newUsed }); alert("쿠폰 사용 완료!"); setSelectedCoupon(null); }; const handleBuySlot = async (option) => { if (!user || user.isAnonymous) { setShowAuthModal(true); return; } if (userData.maxCoupons + option.count > 15) { alert("최대 슬롯 제한."); return; } if (confirm(`${option.label} ($${option.price}) 결제하시겠습니까?`)) { await updateDoc(doc(db, 'artifacts', appId, 'users', user.uid, 'profile', 'data'), { maxCoupons: userData.maxCoupons + option.count }); alert("슬롯 확장 완료!"); setShowSlotShop(false); } }; const handleWatchAd = async () => { setIsAdWatching(true); setTimeout(async () => { setIsAdWatching(false); if (!user) return; await updateDoc(doc(db, 'artifacts', appId, 'users', user.uid, 'profile', 'data'), { spinsLeft: (userData.spinsLeft || 0) + 1 }); alert("스핀 기회가 충전되었습니다!"); }, 2000); }; const handleSendChat = async () => { if (!chatInput.trim() || !user) return; await addDoc(collection(db, 'artifacts', appId, 'public', 'data', `chat_${selectedCity}`), { text: chatInput, userId: user.uid, userName: user.displayName || 'Traveler', timestamp: serverTimestamp() }); setChatInput(''); }; const handleGoogleLogin = async () => { try { await signInWithPopup(auth, new GoogleAuthProvider()); setShowAuthModal(false); } catch (error) { alert(t('google_fail') + ": " + error.message); } }; const handleIdLogin = async () => { try { await signInWithEmailAndPassword(auth, `${loginId}@dealbox.vn`, loginPassword); setShowAuthModal(false); } catch (error) { alert(t('login_fail')); } }; const handlePhoneLogin = () => { alert("휴대폰 인증은 실제 배포 환경에서 Firebase Phone Auth를 통해 작동합니다.\n(인증 완료 시뮬레이션)"); }; const handleZaloLogin = () => { alert("Zalo 로그인은 실제 배포 시 Zalo SDK 연동이 필요합니다.\n(로그인 성공 시뮬레이션)"); }; const handleSignup = async () => { // Check Required Fields if (!signupForm.id || !signupForm.email || !signupForm.password || !signupForm.name || !signupForm.dob || !signupForm.phone) return alert("모든 정보를 입력해주세요."); try { const cred = await createUserWithEmailAndPassword(auth, `${signupForm.id}@dealbox.vn`, signupForm.password); await updateProfile(cred.user, { displayName: signupForm.id }); // Using ID as DisplayName for now // Determine Residence const finalResidence = signupForm.residence === 'custom' ? signupForm.residenceCustom : signupForm.residence; await setDoc(doc(db, 'artifacts', appId, 'users', cred.user.uid, 'profile', 'data'), { id: signupForm.id, name: signupForm.name, dob: signupForm.dob, contactEmail: signupForm.email, phone: `${signupForm.countryCode} ${signupForm.phone}`, residence: finalResidence, spinsLeft: 3, maxCoupons: 3, savedCoupons: [], usedCoupons: [], lastSpinDate: new Date().toISOString().split('T')[0], role: 'user', deviceId: deviceId }); alert(t('signup_success')); setShowAuthModal(false); } catch (error) { alert("회원가입 실패: " + error.message); } }; const handleLogout = async () => { try { await signOut(auth); alert("로그아웃 완료"); } catch (e) {} }; const handleLike = (postId) => { setPosts(posts.map(p => { if (p.id === postId) return { ...p, likes: (p.isLiked ? p.likes - 1 : p.likes + 1), isLiked: !p.isLiked }; return p; })); }; const handleAddComment = (postId) => { if (!commentText.trim()) return; setPosts(posts.map(p => { if (p.id === postId) return { ...p, comments: [...p.comments, { id: Date.now(), user: '나', text: commentText }] }; return p; })); setCommentText(''); }; const handleSubmitPost = () => { if (!postForm.title || !postForm.content) return; setPosts([{ id: Date.now(), author: user.displayName || '나', city: selectedCity, title: postForm.title, content: postForm.content, likes: 0, comments: [], timestamp: '방금 전', imageUrl: postImagePreview }, ...posts]); setShowWriteModal(false); setPostForm({ title: '', content: '' }); setPostImagePreview(null); }; const handleSubmitInquiry = () => { if (!inquiryForm.storeName || !inquiryForm.discountInfo || !inquiryForm.contact || !inquiryForm.representativeName) { alert("필수 항목을 모두 입력해주세요."); return; } alert("승인 대기중입니다."); setShowInquiryModal(false); setInquiryForm({ storeName: '', category: 'food', city: 'hcm', discountInfo: '', contact: '', representativeName: '', googleMapUrl: '' }); }; const handleChangePassword = async () => { if(!changePw) return; try { await updatePassword(user, changePw); alert("비밀번호가 변경되었습니다."); setChangePw(''); } catch(e) { alert("변경 실패: " + e.message); } }; const handleSubmitMyInquiry = async () => { if(!myInquiry) return; // Store report to Firestore with detailed info for Admin await addDoc(collection(db, 'artifacts', appId, 'public', 'data', 'reports'), { type: 'inquiry_1on1', content: myInquiry, userId: user.uid, userName: user.displayName, status: 'pending', // For Admin to track timestamp: serverTimestamp() }); alert("문의/신고가 접수되었습니다."); setMyInquiry(''); }; const handleWritePost = () => { if(!user || user.isAnonymous) { alert("회원만 글을 쓸 수 있습니다."); setShowAuthModal(true); return; } setShowWriteModal(true); }; const handleWriteLesson = () => { if(!user || user.isAnonymous) { alert("회원만 등록할 수 있습니다."); setShowAuthModal(true); return; } setShowLessonWriteModal(true); }; const handleSubmitLesson = async () => { if (!lessonForm.title || !lessonForm.price || !lessonForm.desc) { alert("모든 항목을 입력해주세요."); return; } await addDoc(collection(db, 'artifacts', appId, 'public', 'data', 'lessons'), { ...lessonForm, authorId: user.uid, tutor: user.displayName || 'Teacher', timestamp: serverTimestamp() }); setShowLessonWriteModal(false); setLessonForm({ title: '', category: 'academic', city: 'hanoi', price: '', desc: '' }); alert("레슨이 등록되었습니다."); }; const handleApplyLesson = (lesson) => { if(!user || user.isAnonymous) { setShowAuthModal(true); return; } setSelectedLesson(lesson); setShowLessonApplyModal(true); }; const handleShowApplicants = (lesson) => { setSelectedLesson(lesson); setShowApplicantListModal(true); }; const handleSubmitApplication = async () => { if (!lessonApplyForm.name || !lessonApplyForm.phone || !lessonApplyForm.messengerId) { alert("필수 정보를 입력해주세요."); return; } await addDoc(collection(db, 'artifacts', appId, 'public', 'data', `lesson_applicants_${selectedLesson.id}`), { ...lessonApplyForm, applicantId: user.uid, timestamp: serverTimestamp() }); alert("신청이 완료되었습니다."); setShowLessonApplyModal(false); setLessonApplyForm({ name: '', region: 'hanoi', phone: '', messengerId: '' }); }; // --- New Features Handlers --- const handleReport = (post) => { setReportTarget(post); setShowReportModal(true); }; const submitReport = async () => { if(!reportReason) return alert("사유를 입력해주세요"); // Store Report with 'pending' status for Admin await addDoc(collection(db, 'artifacts', appId, 'public', 'data', 'reports'), { type: 'post_report', targetId: reportTarget.id, reason: reportReason, reporterId: user.uid, status: 'pending', timestamp: serverTimestamp() }); alert("신고가 접수되었습니다."); setShowReportModal(false); setReportReason(''); }; const handlePushToggle = () => { if (!("Notification" in window)) { alert("이 브라우저는 알림을 지원하지 않습니다."); return; } if (Notification.permission === "granted") { setPushEnabled(!pushEnabled); alert("알림 설정이 변경되었습니다."); } else if (Notification.permission !== "denied") { Notification.requestPermission().then((permission) => { if (permission === "granted") { setPushEnabled(true); alert("알림이 켜졌습니다."); } }); } else { alert("알림 권한이 차단되어 있습니다. 브라우저 설정에서 허용해주세요."); } }; const handleImageUpload = (e) => { const file = e.target.files[0]; if (file) { const reader = new FileReader(); reader.onloadend = () => { setPostImagePreview(reader.result); }; reader.readAsDataURL(file); } }; const openGrab = () => { let grabUrl = "https://grab.onelink.me/2695613898?pid=website&c=footer_logo&is_retargeting=true"; // If selected business has coordinates, try to use them (Note: Deep linking to specific coords in universal link varies by OS) // This is a standard universal link fallback to store if app not present if (selectedBusiness && selectedBusiness.lat && selectedBusiness.lng) { // Attempt to use a google maps link that Grab might intercept or user can choose app // But standard Grab deep link is 'grab://open?screenType=BOOKING' // Here we use the universal link as requested for general compatibility } if (confirm("Grab 앱을 여시겠습니까? (설치되어 있지 않으면 스토어로 이동합니다)")) { window.open(grabUrl, '_blank'); } }; // --- Logic Hooks --- useEffect(() => { // Generate Device ID if not exists let storedDeviceId = localStorage.getItem('dealbox_device_id'); if (!storedDeviceId) { storedDeviceId = 'device_' + Math.random().toString(36).substr(2, 9); localStorage.setItem('dealbox_device_id', storedDeviceId); } setDeviceId(storedDeviceId); const initAuth = async () => { if (typeof __initial_auth_token !== 'undefined' && __initial_auth_token) await signInWithCustomToken(auth, __initial_auth_token); else await signInAnonymously(auth); }; initAuth(); onAuthStateChanged(auth, (u) => { setUser(u); setLoading(false); }); const today = new Date().toLocaleDateString(); const storedDate = localStorage.getItem('dealbox_date'); let count = parseInt(localStorage.getItem('dealbox_count')); if (storedDate !== today || !count) { count = Math.floor(Math.random() * (1000 - 50 + 1)) + 50; localStorage.setItem('dealbox_date', today); localStorage.setItem('dealbox_count', count); } setDailyParticipants(count); }, []); // ... (Existing Hooks for data fetching kept as is) useEffect(() => { if (!user) return; const lessonRef = collection(db, 'artifacts', appId, 'public', 'data', 'lessons'); const unsubscribe = onSnapshot(lessonRef, (snapshot) => { const lessonList = snapshot.docs.map(d => ({ id: d.id, ...d.data() })); lessonList.sort((a, b) => (b.timestamp?.seconds || 0) - (a.timestamp?.seconds || 0)); setLessons(lessonList); }); return () => unsubscribe(); }, [user]); useEffect(() => { if (!selectedLesson || !user) return; const applyRef = collection(db, 'artifacts', appId, 'public', 'data', `lesson_applicants_${selectedLesson.id}`); const unsubscribe = onSnapshot(applyRef, (snapshot) => { setApplicantCount(snapshot.size); setLessonApplicants(snapshot.docs.map(d => ({ id: d.id, ...d.data() }))); }); return () => unsubscribe(); }, [selectedLesson, user]); useEffect(() => { if (!user) return; const userDocRef = doc(db, 'artifacts', appId, 'users', user.uid, 'profile', 'data'); const unsubscribe = onSnapshot(userDocRef, (docSnap) => { if (docSnap.exists()) { const data = docSnap.data(); if (data.role) setUserRole(data.role); const today = new Date().toISOString().split('T')[0]; if (data.lastSpinDate !== today) { updateDoc(userDocRef, { spinsLeft: 3, lastSpinDate: today }); setUserData({ ...data, spinsLeft: 3, lastSpinDate: today }); } else { setUserData(data); } } else { setDoc(userDocRef, { spinsLeft: 3, maxCoupons: 3, lastSpinDate: new Date().toISOString().split('T')[0], savedCoupons: [], usedCoupons: [], role: 'user', deviceId: deviceId }); setUserData({ spinsLeft: 3, maxCoupons: 3, lastSpinDate: new Date().toISOString().split('T')[0], savedCoupons: [], usedCoupons: [] }); } }); return () => unsubscribe(); }, [user, deviceId]); useEffect(() => { if (userData.savedCoupons?.length > 0) { setSavedBusinesses(MOCK_BUSINESSES.filter(b => userData.savedCoupons.includes(b.id))); } else { setSavedBusinesses([]); } }, [userData.savedCoupons]); useEffect(() => { if (!user || !showChat) return; const chatRef = collection(db, 'artifacts', appId, 'public', 'data', `chat_${selectedCity}`); const unsubscribe = onSnapshot(chatRef, (snapshot) => { let msgs = snapshot.docs.map(d => ({ id: d.id, ...d.data() })); msgs.sort((a, b) => (b.timestamp?.seconds || 0) - (a.timestamp?.seconds || 0)); msgs = msgs.slice(0, 50); setChatMessages(msgs.reverse()); }); return () => unsubscribe(); }, [selectedCity, showChat, user]); // Google Maps Hooks useEffect(() => { if (!window.google && viewMode === 'map') { const script = document.createElement('script'); script.src = `https://maps.googleapis.com/maps/api/js?key=${GOOGLE_MAPS_API_KEY}&callback=initMap&libraries=places`; script.async = true; script.defer = true; window.initMap = () => { console.log("Google Maps loaded"); }; document.head.appendChild(script); } }, [viewMode]); useEffect(() => { if (viewMode === 'map' && mapRef.current && window.google && !mapInstanceRef.current) { const cityInfo = CITIES.find(c => c.id === selectedCity); const center = { lat: cityInfo?.lat || 10.7769, lng: cityInfo?.lng || 106.7009 }; mapInstanceRef.current = new window.google.maps.Map(mapRef.current, { center: center, zoom: 14, disableDefaultUI: true, zoomControl: true }); } }, [viewMode, selectedCity]); useEffect(() => { if (!mapInstanceRef.current || !window.google) return; markersRef.current.forEach(marker => marker.setMap(null)); markersRef.current = []; currentBusinesses.forEach(biz => { const marker = new window.google.maps.Marker({ position: { lat: biz.lat, lng: biz.lng }, map: mapInstanceRef.current, title: biz.name, icon: { url: "http://maps.google.com/mapfiles/ms/icons/pink-dot.png" } }); marker.addListener("click", () => { setSelectedBusiness(biz); mapInstanceRef.current.panTo(marker.getPosition()); mapInstanceRef.current.setZoom(16); }); markersRef.current.push(marker); }); if (!focusedBusiness) { const cityInfo = CITIES.find(c => c.id === selectedCity); if (cityInfo) { mapInstanceRef.current.panTo({ lat: cityInfo.lat, lng: cityInfo.lng }); mapInstanceRef.current.setZoom(14); } } }, [currentBusinesses, selectedCity, focusedBusiness]); useEffect(() => { if(focusedBusiness && mapInstanceRef.current) { mapInstanceRef.current.panTo({ lat: focusedBusiness.lat, lng: focusedBusiness.lng }); mapInstanceRef.current.setZoom(16); } }, [focusedBusiness]); // ... (Renderers stay mostly same, added new UI elements in return) ... // For brevity, only showing changed parts in Renderers within the full file context const renderMap = () => { return (
{!window.google && (

{t('map_key_missing')}

)}
{currentBusinesses.map((biz, i) => (
setFocusedBusiness(biz)} className={`min-w-[280px] bg-[#1f2937] rounded-xl p-3 border transition-all cursor-pointer shadow-xl ${focusedBusiness?.id === biz.id ? 'border-pink-500 ring-2 ring-pink-500/50' : 'border-gray-700'}`}>
{biz.name}

{biz.name}

{typeof biz.coupon === 'string' ? biz.coupon : (biz.coupon[lang] || biz.coupon['ko'])}

))}
{t('map_guide')}
); }; const renderHome = () => (
{t('my_slot')}
{Array.from({ length: 10 }).map((_, i) => ( i < userData.maxCoupons && (
) ))}
{categories.map(cat => ())}
{viewMode === 'roulette' ? (

{t(`city_${selectedCity}`)} • {categories.find(c => c.id === selectedCategory)?.name}

{categories.find(c => c.id === selectedCategory)?.tagline}

0 && rouletteItems[0].id !== 'none'} spinCount={userData.spinsLeft} onSpinComplete={handleSpinComplete} lang={lang} t={t} />
{!user || user.isAnonymous ? : userData.spinsLeft <= 0 ? :

{t('spin_left')}: {userData.spinsLeft}회

}

{t('daily_user').replace('{n}', dailyParticipants)}

) : ( renderMap() )}

{t('lineup')}

{t('click_info')}
{currentBusinesses.length > 0 ? currentBusinesses.map(biz => ) :
등록된 업체가 없습니다.
}
setShowInquiryModal(true)} className="mt-6 p-5 rounded-xl bg-[#1f2937] border border-gray-700 text-center relative overflow-hidden cursor-pointer active:scale-98 transition-transform">
{t('boss_only')}

🛍️ {t('store_inquiry')}

{t('inquiry_desc')}

); // ... CouponBox, Community, MyPage (with added Report Button in MyPage) ... const renderCommunity = () => (

{t('community')}

📍 {t(`city_${selectedCity}`)}
{filteredPosts.length > 0 ? filteredPosts.map(post => (
setExpandedPostId(expandedPostId === post.id ? null : post.id)}> {t(`city_${post.city}`)}

{post.title}

{post.likes} {post.comments.length} {expandedPostId === post.id ? : }
{expandedPostId === post.id && (

{post.content}

{post.imageUrl && Post} {post.timestamp}
{post.comments.length > 0 ? ( post.comments.map(c => (
{c.user}{c.text}
)) ) :

첫 댓글을 남겨보세요!

}
setCommentText(e.target.value)} placeholder={t('comments')} className="flex-1 bg-gray-700 border-none rounded-lg px-3 py-2 text-xs text-white focus:ring-1 focus:ring-pink-500 outline-none" />
)}
)) :
이 지역에 작성된 글이 없습니다.
}
); const renderCouponBox = () => (

{t('coupon')}

{userData.savedCoupons?.length || 0} / {userData.maxCoupons} 사용중
{savedBusinesses.length > 0 ? savedBusinesses.map(biz => (

{biz.name}

{t(`city_${biz.city}`)}
{t('available')}
{typeof biz.coupon === 'string' ? biz.coupon : (biz.coupon[lang] || biz.coupon['ko'])}
23{t('hours_left')}
)) :

저장된 쿠폰이 없습니다.

}
); const renderMyPage = () => { const subPageHeader = (title) => (

{title}

); if (myPageView === 'edit') return (
{subPageHeader(t('menu_edit'))}
setChangePw(e.target.value)}/>
); // Updated Inquiry/Report Center if (myPageView === 'inquiry') return (
{subPageHeader(t('menu_inquiry'))}

신고/문의 하기

서비스 이용 중 불편한 점이나 신고할 내용이 있다면 관리자에게 직접 보내주세요.