Futbol Veritabanı
8대 리그 클럽·선수·통계—Transfermarkt 데이터
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
פרויקט JoinUp: אפליקציה למציאת משחקי ספורט, ניהול קבוצות וצ'אט בזמן אמת. קרא את זה לפני כל שינוי קוד! • ❌ לא להשתמש ב-npm install – להשתמש ב-Expo בלבד • ❌ לא לעשות db push – להשתמש ב-migrations בלבד • ❌ לא להשתמש ב-FlatList מקונן (Nested) – להשתמש ב-.map() בתוך View רגיל • ✅ כל שינוי ב-DB דרך prisma migrate dev • ✅ כל Branch עבודה: Dev Socket Events – מה השרת שולח ולאן 🐛 [FIXED] Socket מתנתק בכל ניווט / לא עובד בזמן אמתREADME 전문 보기 ↗
פרויקט JoinUp: אפליקציה למציאת משחקי ספורט, ניהול קבוצות וצ'אט בזמן אמת.
JoinUpApp/
├── mobile_app/ # אפליקציית Expo (React Native)
├── next_app/ # אתר Next.js
└── server/ # שרת Express.js + Socket.io + Prisma
קרא את זה לפני כל שינוי קוד!
npm install – להשתמש ב-Expo בלבדdb push – להשתמש ב-migrations בלבדFlatList מקונן (Nested) – להשתמש ב-.map() בתוך View רגילprisma migrate devDev_layout.tsx → AuthGuard → SocketManager.connect(token) [פעם אחת בלבד]
↓
ChatContext.tsx → SocketManager.emit('setup', { id: user.id }) [על כל connect]
↓
ChatContext.tsx → SocketManager.emit('joinChats', [chatIds]) [אחרי שהצ'אטים נטענו]
↓
useChatLogic.ts → SocketManager.emit('joinRoom', roomId) [בכניסה לצ'אט ספציפי]
| Event | שולח לאיפה | מי מאזין |
|---|---|---|
message | io.to(roomId) | useChatLogic + ChatContext |
message:received | io.to(roomId) | alias ל-frontend |
chat:sync | io.to(userId) (חדר אישי) | ChatContext.updateChatList |
typing:start / typing:stop | io.to(roomId) | useChatLogic + ChatContext |
notification | io.to(userId) | NotificationContext |
| Event | מתי |
|---|---|
setup | מיד אחרי connect, ובכל reconnect – מצרף את המשתמש לחדר האישי שלו |
joinRoom | בכניסה לצ'אט ספציפי |
joinChats | אחרי טעינת רשימת הצ'אטים |
message | שליחת הודעה |
typing | הקלדה |
leaveRoom | יציאה מצ'אט |
תסמינים:
Connected: xxx מופיע מספר פעמים ברצףMissed emit: "setup". Socket disconnected.אבחנה:
ב-_layout.tsx, לפונקציה AuthGuard היה useEffect עם return () => SocketManager.disconnect(). ב-React, הפונקציה הזו (cleanup) רצה בכל פעם שה-dependencies משתנים – לא רק כשהקומפוננטה נהרסת לגמרי. כל ניווט בין מסכים גרם ל-Socket להתנתק ולהתחבר מחדש, מה שיצר לופ של connect/disconnect.
בנוסף, ה-setup event (שמצרף את המשתמש לחדר האישי שלו בשרת) נשלח לפני שה-Socket סיים להתחבר, ולכן הלך לאיבוד.
הפתרון:
// _layout.tsx → AuthGuard
// ❌ לפני התיקון – disconnect על כל re-render!
useEffect(() => {
if (isSignedIn) {
SocketManager.connect(token);
}
return () => SocketManager.disconnect(); // ← הבעיה!
}, [isLoaded, isSignedIn]);
// ✅ אחרי התיקון – disconnect רק על sign-out אמיתי
useEffect(() => {
if (!isLoaded) return;
if (isSignedIn) {
getToken().then(token => {
if (token) SocketManager.connect(token);
});
} else {
SocketManager.disconnect(); // רק כשמתנתקים מהמערכת
}
// אין return/cleanup! ה-Socket חייב לשרוד re-renders!
}, [isLoaded, isSignedIn]);
// ChatContext.tsx – שולח setup על כל connect ו-reconnect
useEffect(() => {
const sendSetup = () => {
if (user?.id) SocketManager.emit('setup', { id: user.id });
};
sendSetup(); // גם אם כבר מחובר
const unsub = SocketManager.on('connect', sendSetup); // גם על reconnect
return () => unsub();
}, [user?.id]);
קבצים רלוונטיים:
mobile_app/app/_layout.tsx – AuthGuardmobile_app/src/context/ChatContext.tsx – setup + joinChatsmobile_app/src/services/socketManager.ts – singleton socketתסמינים: רשימת הצ'אטים לא מתרעננת כשמגיעה הודעה, גם כשה-Socket מקבל אותה
אבחנה: extraData של ה-FlatList הכיל רק typingStatus. כשהודעה מגיעה ו-chats[] מתעדכן, ה-FlatList לא זיהה שינוי כי ה-reference של המערך לא בהכרח השתנה.
הפתרון:
// chats.tsx – extraData עם signal משולב
const extraData = useMemo(() => ({
typing: typingStatus,
// signal שמשתנה כשיש הודעה חדשה או שינוי בסדר
msgKey: filteredChats.map(c =>
`${c.id}:${c.lastMessage?.createdAt ?? ''}:${c.unreadCount}`
).join('|'),
}), [typingStatus, filteredChats]);
<FlatList extraData={extraData} ... />
transports: ['websocket'] ב-Expo Goתסמינים: xhr poll error בלוגים, Socket לא מתחבר בכלל
אבחנה: כשמגדירים transports: ['websocket'] בלקוח אבל השרת מוגדר גם הוא עם transports: ['websocket'] בלבד – זה תקין. אבל אם הלקוח מוגדר בלי הגבלת transport, הוא מנסה polling קודם, והשרת שדורש websocket בלבד מחזיר שגיאה.
הכלל: שני הצדדים (client + server) חייבים להיות מסונכרנים על transports: ['websocket'], או ששניהם מאפשרים polling.
הגדרה נוכחית:
// socketManager.ts (mobile)
socket = io(API_BASE, {
path: '/api/socket',
transports: ['websocket'], // ← חייב להתאים לשרת
auth: { token },
});
// server/index.js
const io = new Server(server, {
path: '/api/socket',
transports: ['websocket'], // ← שרת מקבל websocket בלבד
});
קובץ ה-source of truth: mobile_app/src/utils/sports.ts
כל מסך מובייל חייב לייבא את SPORT_MAPPING מהקובץ הזה. אין להגדיר שמות ספורט ישירות בקומפוננטות.
import { SPORT_MAPPING, getSportName, getSportKey } from '@/utils/sports';
| סביבה | כתובת |
|---|---|
| שרת Production | https://joinup-api.duckdns.org |
| שרת Dev (Coolify) | https://joinup-dev-api.duckdns.org — ראו docs/dev-environment.md |
| Web Production | Vercel |
| Mobile Dev | Expo Go + .env עם EXPO_PUBLIC_API_URL |
אפשר לשלוח תיקוני JS לאפליקציה בלי לעבור דרך החנות:
cd mobile_app
npm run update:production -- --message "תיאור קצר של התיקון"
eas update ישירות: eas update לא קורא את ה-env של פרופילי ה-build ב-eas.json. בלי הסקריפט, העדכון ייקח ערכים מקובץ .env מקומי של המפתח, או ייצא בלי כתובת שרת, בלי מפתח Clerk ובלי Sentry. הסקריפט scripts/eas-update.mjs:
EXPO_PUBLIC_* מקומי (מ-.env או מהטרמינל) להיכנס לבאנדל;eas-cli;--allow-dirty).runtimeVersion הוא fingerprint: עדכון מגיע רק לבילדים עם אותו קוד native. שינוי בתלות native, ב-plugin או בשדות native של app.json דורש בילד חדש לחנות, ו-OTA לא יגיע לבילדים הישנים. זה בכוונה, כדי שעדכון לא יקריס בילד שלא מתאים לו.
eas.json, .easignore, .gitignore וה-npm scripts לא נכנסים ל-fingerprint (ראו fingerprint.config.js). לכן שינוי כתובת שרת ב-eas.json כן יכול להגיע לבילדים קיימים ב-OTA. מצד שני, שינוי native ב-eas.json (למשל buildType) לא מחייב בילד חדש לבד, אז צריך לזכור לשחרר בילד.google-services.json נשמר תמיד עם סופי שורה LF (.gitattributes), כדי שבילד מ-Windows ועדכון מ-Linux/CI יחשבו אותו fingerprint.SENTRY_AUTH_TOKEN מוגדר בסביבה המקומית, הסקריפט מעלה את ה-source maps של העדכון ל-Sentry. ב-EAS הטוקן זמין רק לשרתי ה-build, אז לעדכון מקומי צריך להגדיר אותו בטרמינל.npm run update:preview -- --message "..." שולח לבילדים של פרופיל preview.development ו-preview מצביעים על סביבת ה-Dev (https://joinup-dev-api.duckdns.org + מפתח Clerk מסוג pk_test_); רק production מצביע על הפרודקשן (pk_live_). הסקריפט מסרב לפרסם production אם כתובת ה-API אינה של הפרודקשן או שהמפתח אינו pk_live_, ומסרב לפרסם preview/development עם מפתח pk_live_ או עם כתובת הפרודקשן. בילד שכבר מותקן שומר את הערכים שנאפו בו עד שיקבל עדכון בערוץ שלו או שיותקן בילד חדש.