# راهنمای اتصالِ فرانت‌اند به این بک‌اند این پوشه برای تیمِ فرانت (`MathApp_frontend`) است. `api.ts` یک کلاینتِ تایپ‌دار است که از روی OpenAPI واقعیِ همین سرور نوشته شده — نامِ فیلدها حدس نیست. ```bash cp docs/frontend/api.ts <پروژه‌ی-فرانت>/src/lib/api.ts ``` ```ts import { createClient } from './lib/api'; export const api = createClient({ baseUrl: import.meta.env.VITE_API_URL ?? '/api' }); ``` ## نگاشتِ صفحاتِ فعلیِ فرانت به API فرانت الان (`src/app/App.tsx`) خروجیِ Figma Make است و هیچ فراخوانیِ APIای ندارد؛ همه‌ی داده‌ها هاردکد است. این جدول می‌گوید هر صفحه به چه چیزی وصل شود. | صفحه‌ی فرانت | فراخوانی | نکته | |---|---|---| | `SplashScreen` | `api.auth.isSignedIn()` | «قبلاً حساب دارم» → صفحه‌ی ورود | | — (**وجود ندارد، باید ساخته شود**) | `auth.requestOtp` → `auth.login` | پایین ببینید | | `OnboardingScreen` | — | ایستا | | `ProfileSetupScreen` | `profile.update({name})` + `profile.setGrade(id)` | `GRADES` هاردکد را با `api.grades()` عوض کنید | | `HomeScreen` | `api.home()` + `api.summary()` | بدونِ آرگومان، پایه‌ی خودِ کاربر ملاک است | | `SkillMapScreen` | `api.home()` → `units[].nodes[]` | `status` = `locked / unlocked / in_progress / completed` | | `LessonScreen` | `lesson.intro(id)` + `lesson.questions(id)` | پایین ببینید | | `QuestionScreen` | `lesson.answer(qid, optId)` → `lesson.complete(id)` | `mascotMood` حالتِ رُبو را می‌دهد | | `AskAIScreen` | `ai.askStream({userQuestion, lessonId}, onDelta)` | فقط `lessonId` کافی‌ست | | `SubscriptionScreen` | `subscription.plans()` + `subscription.purchase(planId)` | قیمت‌های هاردکد را بردارید | ## چهار چیزی که الان با فرانت جور نیست **۱. صفحه‌ی ورود وجود ندارد.** فرانت فقط اسم و پایه می‌پرسد، ولی احرازِ هویتِ ما OTP با شماره‌ی موبایل است. دو راه: - **مهمان** (سریع‌ترین): `api.auth.guest(deviceId)` — بدونِ شماره، کاربر بلافاصله وارد می‌شود و بعداً می‌تواند شماره‌اش را وصل کند. - **کامل**: یک صفحه‌ی «شماره + کد» بین Splash و ProfileSetup. در حالتِ توسعه (`DJANGO_DEBUG=1`) پاسخِ `requestOtp` فیلدِ `devOtp` دارد، پس برای تست لازم نیست پیامک واقعی برسد. **برای تستِ مسیرِ کامل روی سرورِ واقعی، شماره‌ی تست بگیرید.** روی `mathapi.tadrisino2.ir` سرویسِ پیامک هنوز تنظیم نشده، پس `requestOtp` برای یک شماره‌ی معمولی خطای ۵۰۳ می‌دهد. بک‌اند چند شماره‌ی تست با کدِ ثابت دارد (`TEST_LOGIN_PHONES` در `.env` سرور): برای آن شماره‌ها پیامکی ارسال نمی‌شود و همان کد همیشه جواب می‌دهد، دقیقاً مثل مسیرِ واقعی. یک جفت «شماره + کد» برای خودتان بخواهید — ساختنش کارِ یک خط ویرایشِ `.env` و یک ری‌استارت است. > کدِ شماره‌ی تست در پاسخِ API برنمی‌گردد. اگر `devOtp` خالی آمد، یعنی همین > مسیر فعال است، نه اینکه چیزی خراب باشد. **یک حسابِ تستِ آماده هم روی پروداکشن هست** که از قبل داده دارد، تا برای دیدنِ صفحه‌های اعلان و لیدربرد مجبور نباشید منتظرِ تولیدِ داده‌ی واقعی بمانید: ```bash curl -X POST https://mathapi.tadrisino2.ir/api/auth/session \ -H 'Content-Type: application/json' \ -d '{"type":"guest","deviceId":"frontend-test-device"}' ``` - کاربرِ شماره‌ی ۶، با نامِ «حساب تست فرانت». هیچ پیامکی و هیچ توکنی که دست به دست شود در کار نیست — همان `deviceId` کافی‌ست. - `GET /api/notifications` روی این حساب چند اعلانِ واقعی دارد. **پاسخ در کلیدِ `items` است**، نه `results`. - `GET /api/leaderboard?period=weekly` و `?period=all_time` هر کدام ۶ ردیف می‌دهند، با **ترتیبِ عمداً متفاوت** بینِ دو بازه، تا بشود دید پارامترِ `period` واقعاً اثر می‌کند. - این داده دستی روی دیتابیسِ پروداکشن ساخته شده و در گیت نیست؛ پیش از باز شدنِ سرویس روی کاربرانِ واقعی باید پاک شود (`docs/operations.md`). ⚠️ این حساب **مشترک** است. هر کسی که همین `deviceId` را بفرستد وارد همان حساب می‌شود، پس رویش چیزی که مالِ خودتان باشد نگذارید. برای کارِ روزمره یک `deviceId` مخصوصِ خودتان بسازید — هر رشته‌ای قبول است. **۲. فهرستِ پایه‌ها از `api.grades()` می‌آید** (انجام شده). ترتیب همان ترتیبِ کلاس‌هاست و `id` هر پایه برابرِ شماره‌اش است: `gradeId: 8` یعنی پایه‌ی هشتم. فیلدِ `number` هم همین عدد را صریح می‌دهد. پیش از ۲۰۲۶-۰۹-۲۳ idها ترتیبِ واردشدنِ کتاب‌ها بودند (هشتم=۵)؛ اپ چون همیشه `gradeId` را از سرور می‌گیرد، تغییری لازم نداشت. **۳. صفحه‌ی درس، ویدیو دارد.** `LessonScreen` فهرستی از `{type: "video" | "quiz"}` نشان می‌دهد. بک‌اند هنوز مفهومِ ویدیو ندارد — `lessons/{id}/intro` فقط متن و حالتِ ماسکوت می‌دهد. فعلاً فقط `quiz` را نشان دهید؛ اگر ویدیو واقعاً لازم است بگویید تا در مدل اضافه کنیم. **۴. «سطح» در آمار.** `HomeScreen` سه عدد نشان می‌دهد: روز پیاپی، امتیاز، سطح. دوتای اول را داریم (`streak.current`، `xp`)؛ «سطح» را نداریم. یا از روی `xp` سمتِ فرانت حساب کنید، یا بگویید تا در `/api/user/summary` اضافه کنیم. ## چیزهایی که سمتِ بک‌اند برایتان درست شد - **CORS** اضافه شد. origin خود را در `CORS_ALLOWED_ORIGINS` بدهید (در حالتِ توسعه پورت‌های ۳۰۰۰ و ۵۱۷۳ پیش‌فرض بازند). - **`PATCH /api/user/profile`** اضافه شد — قبلاً پروفایل فقط خواندنی بود و هیچ راهی برای ثبتِ اسم نبود. - **`GET /api/home` بدونِ پارامتر** حالا پایه‌ی ذخیره‌شده‌ی کاربر را می‌خواند. قبلاً اگر `?grade=` نمی‌فرستادید نقشه خالی برمی‌گشت. - **`POST /api/ai/ask` با فقط `lessonId`** حالا خودش می‌فهمد آن درس مالِ کدام پایه و فصلِ کتاب است. قبلاً باید `subject` + `grade` + `lessonNumber` را خودتان می‌فرستادید وگرنه پاسخ بدونِ استناد به کتاب تولید می‌شد. ## چند قاعده که وقت‌تان را می‌خرد **`Idempotency-Key` بفرستید** روی هر چیزی که پول/امتیاز/قلب جابه‌جا می‌کند (`answer`, `complete`, `purchase`). یک UUID که تا موفق شدنِ درخواست ثابت بماند. بدونِ آن، قطعیِ شبکه + دکمه‌ی دوباره = دوبار کم شدنِ قلب. **۴۰۱ یعنی توکن منقضی شده، نه اینکه کاربر بیرون است.** کلاینت خودش یک‌بار `refresh` می‌زند و درخواست را تکرار می‌کند؛ فقط اگر آن هم شکست خورد کاربر را به صفحه‌ی ورود ببرید. **۴۲۹ و ۵۰۳ را جدا نشان دهید.** ۴۲۹ یعنی سقفِ روزانه‌ی سوال از دستیار پر شده (الان ۱۵ تا) یا throttle؛ پیامش «فردا برگرد» است. ۵۰۳ یعنی سرویسِ AI موقتاً قطع است؛ پیامش «چند لحظه دیگر دوباره». `ApiError.isRateLimited` و `.isProviderDown` همین را می‌دهند. **پرداخت را در مرورگر باز کنید.** `purchase` یک `paymentUrl` می‌دهد که به سرورِ خودمان اشاره می‌کند، نه به زرین‌پال. اپ نباید مستقیم با درگاه حرف بزند. **امتیاز موقعِ اتمامِ درس در مجموع می‌نشیند، نه سرِ هر سوال.** پاسخِ `answer` فیلدِ `xpEarned` دارد ولی آن ارزشِ همان سوال است؛ `user/summary` تا وقتی `complete` صدا نخورد تکان نمی‌خورد. در UI می‌توانید خوش‌بینانه جمع بزنید، ولی عددِ معتبر بعد از `complete` می‌آید. **`complete` دو فیلدِ اجباری دارد:** `correctCount` و `totalQuestions`. سرور `correctCount` را با تعدادِ واقعیِ سوال‌های آن درس سقف می‌زند، پس عددِ متورم امتیاز نمی‌سازد. **`usedChunks` خالی یعنی پاسخ به کتاب مستند نیست.** اگر خواستید، در UI تفکیکش کنید («بر اساسِ کتابِ درسی» در برابر «پاسخِ عمومی»). ## اجرای محلی ```bash # بک‌اند DJANGO_DEBUG=1 python manage.py runserver # 127.0.0.1:8000 # فرانت VITE_API_URL=http://127.0.0.1:8000/api npm run dev ``` مستنداتِ زنده‌ی همه‌ی endpointها: `http://127.0.0.1:8000/api/docs/`