Gisheri

Gisheri ხელნაკეთ ქვის სამაჯურებსა და კულონებს ყიდის, და მთელი პროდუქტი ორენოვანია კონტენტის მოდელის დონეზეც კი: ყოველი პროდუქტი, კოლექცია და ზოდიაქოს ნიშანი ქართულ ტექსტს ინგლისურის გვერდით ინახავს, ნაგულისხმევი ენა კი ქართულია. ყველაფერი თავიდან ბოლომდე ჩვენ ავაგეთ — Django 5 + Ninja API JWT ავთენტიფიკაციით, აუდიტის ჟურნალითა და როლებით დაცული ადმინისტრატორის ნაწილით, და მასზე დაშენებული React 19 ონლაინ-მაღაზია და კონსოლი.

წელი: 2026 · კატეგორია: ელ-კომერცია

ჩვენი როლი

Full-stack. დავაპროექტეთ მონაცემთა მოდელი, ფასდაკლებებისა და შეკვეთების პაიპლაინი და კატალოგის ორფენიანი i18n, და სრულად ავაგეთ ფრონტენდი.

პრობლემა

ხელნაკეთი ქვის სამაჯურების სახელოსნოს კატალოგი ელცხრილში ინახებოდა, შეკვეთების წიგნი კი — ტელეფონში. ახალი დიზაინები Instagram-ზე ცხადდებოდა, ფასებს მომხმარებლებს მეხსიერებიდან უწერდნენ, ხოლო რამდენი ცალი დარჩა, ის იყო, რაც ტელეფონზე მოპასუხეს ახსოვდა. არაფერი იწერდა, რა იყიდა დაბრუნებულმა მომხმარებელმა ადრე, ამიტომ მაღაზია ყოველ მუდმივ მომხმარებელს უცნობივით ხვდებოდა.

მაღაზიის მომხმარებლები არც პროდუქტის სახელით ყიდულობენ. ისინი მოდიან კითხვით, რა შეეფერება მორიელს, ან ეძებენ რამეს შფოთიანი მეგობრის დასამშვიდებლად, ან საჩუქარს, რომელსაც მნიშვნელობა აქვს — კითხვებით, რომლებსაც სახელებისა და ფასების სია ვერ უპასუხებს. ამასთანავე, ისინი ქართულად კითხულობენ, ამიტომ ყოველი ნაკეთობა ორჯერ, ორივე ენაზე უნდა აღწეროს მისმა შემქმნელმა, სანამ ის რომელიმე ტიპის ვიზიტორზე გაიყიდება.

მიდგომა

კატალოგი თავად პროდუქტია, ამიტომ კატალოგი ისაა, რასაც ადმინი სრულად ფლობს. ნაკეთობას აქვს თავისი ქვები, დანიშნულებები, ზოდიაქოს ნიშნები და ფასი, და ყოველი ეკრანი, რომელიც მას ყიდის — მაღაზიის ფასეტები, კოლექციების გვერდები, თორმეტი ზოდიაქოს გვერდი, ქვიზი — ამ ველებზე გაშვებული მოთხოვნაა და არა ვინმეს მიერ ხელით შედგენილი სია. ზოდიაქოს ნიშნისთვის ქვის ან კოლექციისთვის სამაჯურის დამატება კონსოლში ერთი ფორმის შევსებაა, ხოლო კოლექციების, ზოდიაქოს გვერდების, ქვიზისა და საიტის პარამეტრების შემთხვევაში მაღაზიის ქეშის ინვალიდაცია იმავე სესიაში ხდება, ასე რომ, ცვლილება დეპლოის გარეშე ჩანს.

ყოველი თარგმნადი ველი სვეტების წყვილია და არა თარგმანის ფაილი. პროდუქტი ქართულ სახელს ინგლისურის გვერდით ინახავს, რადგან თარგმანი, რომელიც მაღაზიამ თავად უნდა დაწეროს, მონაცემია და იქ ეკუთვნის, სადაც მონაცემებია; i18next-ში მხოლოდ ინტერფეისის უცვლელი ელემენტები რჩება. განმსაზღვრელი ერთ დოკუმენტირებულ თანმიმდევრობას მისდევს — ქართული სვეტი, შემდეგ i18n გასაღები საწყისი კონტენტისთვის, ბოლო სარეზერვო ვარიანტად კი კანონიკური ინგლისური — ასე რომ, ნაკეთობა, რომლის თარგმნაც ბოლომდე არავის დაუსრულებია, ცარიელის ნაცვლად წაკითხვად ტექსტს აჩვენებს.

შეკვეთის პაიპლაინი განზრახ მცირეა: არც მიწოდების კალკულატორი, არც საგადასახადო ძრავა, ერთი ფასდაკლების კოდი. სამაგიეროდ, ის ფრთხილად აკეთებს ზუსტად იმ ნაწილს, რომლის შეცდომაც ფული ჯდება. კოდი შეკვეთის შექმნისას ჩანაწერის დაბლოკვის პირობებში ხელახლა მოწმდება, კალათიდან მიღებულ მონაცემებზე დაყრდნობის ნაცვლად, ამიტომ წამის წინ მოქმედ კოდზე მაინც შეიძლება უარი ითქვას, ხოლო გამოყენების ლიმიტი პარალელური მოთხოვნებით (race condition) ვერ გადაილახება; ყოველი პოზიცია იმ სახელის, სურათისა და ფასის სნეპშოტს იღებს, რომლითაც გაიყიდა; ხოლო ადმინის ყოველი ხელი, რომელიც შეკვეთას ან მომხმარებელს ეხება, უკან აუდიტის ჩანაწერს ტოვებს.

მთავარი მომენტები

  • ნაგულისხმევი ენა ქართულია, და კატალოგში თარგმანი ორ ფენად განისაზღვრება: ბაზის სვეტი თითოეული თარგმნადი ველისთვის (ყველაფრისთვის, რასაც ადმინი წერს) და i18n გასაღები აპლიკაციასთან ერთად მიწოდებული კონტენტისთვის; ბოლო სარეზერვო ვარიანტად კი კანონიკური ინგლისური რჩება.
  • ადმინის კონსოლი იმავე SPA-ში სრულფასოვანი მეორე ინტერფეისია — თვრამეტი გვერდი, როლებით დაცული როგორც მარშრუტის, ისე API-ის დონეზე, რომლებიც ერთ დალაგებად ცხრილს, თარიღების დიაპაზონის ერთ ჰუკსა და აქტივობის ერთ ლენტს იზიარებენ.
  • ფასდაკლებები შეკვეთის შექმნისას ატომურად ხელახლა მოწმდება და არა მხოლოდ კალათაში: კოდი, რომელმაც შემოწმება გაიარა, წამის შემდეგ მაინც შეიძლება უარყოფილი იყოს, ხოლო გამოყენების მთვლელს მხოლოდ შეკვეთის შექმნის გზა ცვლის.
  • შეკვეთის პოზიციები სნეპშოტებია — სახელი, სურათი და ერთეულის ფასი გაყიდვის მომენტში იყინება, ამიტომ ფასის შემდგომი რედაქტირება ისტორიას არასოდეს გადააწერს, ხოლო ადმინის პოზიციების რედაქტორი ჯამებს ფასდაკლების ხელახალი გამოთვლის გარეშე ითვლის.
  • ადმინის ყოველი ცვლილება შეკვეთაში — და ყოველი როლის ან აქტივაციის ცვლილება მომხმარებელზე — აუდიტის ჩანაწერს წერს, და ორივე გვერდი საკუთარ ლენტს მისგან აგებს: ვინ რა შეცვალა, როდის, რისგან რაში.
  • ქვიზი მოქმედ კატალოგს ზოდიაქოს ნიშნის, განწყობისა და განზრახვის მიხედვით აფასებს და არა წინასწარ შედგენილი პასუხების გასაღებით, ხოლო კითხვების ოთხივე ბანკს ადმინი არედაქტირებს.
  • SEO მონაცემებია: პარამეტრების სინგლტონი ნაგულისხმევ მნიშვნელობებს აწვდის, გვერდის მისამართის (path) მიხედვით გადაფარვის ცხრილი კი მათ ჩაანაცვლებს — ორივე კონსოლიდან რედაქტირდება და არა დეპლოით.

არქიტექტურა

  • ონლაინ-მაღაზია: React 19 TanStack Query-ით კატალოგის ოთხი წაკითხვის მოთხოვნისთვის და ჩვეულებრივი კომპონენტის მდგომარეობით ყველგან, სადაც მდგომარეობა ნამდვილად ლოკალურია. მაღაზიის ფილტრები — დანიშნულება, ნიშანი, სქესი, ფასი, დალაგება, გვერდი — URL-ში ინახება, ამიტომ გაფილტრული ხედი ბმულია, ხოლო ყოველი ფასეტის რაოდენობა მიმდინარე ნაკრებზე ითვლება, ასე რომ, მკვდარი ფილტრი თვალსაჩინოდ მკვდარია.
  • ადმინის კონსოლი: თვრამეტი მოთხოვნისას ჩასატვირთი (lazy) გვერდი როლზე დაფუძნებული წვდომის ერთი ბარიერის უკან, რომლებიც ერთ დალაგებად, გვერდებად დაყოფილ ცხრილს, თარიღების დიაპაზონის ერთ ჰუკსა და აქტივობის ერთ ლენტს იზიარებენ. ეს მაღაზიის ბანდლშივე ჩადგმული მეორე ინტერფეისია, ამიტომ გაფორმებისას გაკეთებული შეკვეთა პირველივე წაკითხვისას უკვე შეკვეთების წიგნშია.
  • კატალოგის i18n: ორი ფენა და დოკუმენტირებული განსაზღვრის თანმიმდევრობა: თარგმნილი ბაზის სვეტი, i18n გასაღები, შემდეგ კანონიკური ინგლისური. აქტიური ენა i18next-ის ინსტანციიდან იკითხება და props-ით არ გადაეცემა, ამიტომ პროდუქტის სახელის გამომსახველ კომპონენტს ამ ყველაფრის ცოდნა არ სჭირდება.
  • შეკვეთები და ფასდაკლებები: შექმნის ერთი გზა ემსახურება მომხმარებლის შეკვეთის გაფორმებასაც და პერსონალის სატელეფონო შეკვეთებსაც. ის კოდს `SELECT FOR UPDATE`-ის ქვეშ ხელახლა ამოწმებს და გამოყენების ლიმიტს იქვე აღასრულებს და არა კალათაში, ყოველ პოზიციას იმ ფასით აფიქსირებს, რომლითაც გაიყიდა, და ადმინის პოზიციების რედაქტორს ჯამების გადათვლის საშუალებას აძლევს ფასდაკლების ხელახალი გამოთვლის გარეშე — ასე რომ, შესწორებული შეკვეთა ინარჩუნებს იმ პირობებს, რომლითაც გაფორმდა.
  • კონტენტი და SEO: პარამეტრების სინგლტონი ინახავს ჰირო-ბლოკის ტექსტს, ბანერს, რჩეულ კოლექციებს და SEO-ს ნაგულისხმევ მნიშვნელობებს, ხოლო გვერდის მისამართის (path) მიხედვით გადაფარვის ცხრილი მას გადაფარავს. ორივე კონსოლიდან რედაქტირდება, ამიტომ მთავარი გვერდი და მეტა-ტეგები მონაცემებია და არა დეპლოი.
  • ბექენდი: შვიდი Django აპლიკაცია და 12 მოდელი Postgres-ზე, django-ninja-ს როუტერებით, რომლებიც ორჯერ არის მიმაგრებული — საჯარო და ადმინის როუტერი თითოეულ აპლიკაციაზე — JWT django-ninja-jwt-დან, camelCase გადაცემისას და თანხები ათწილადის სტრიქონებად სერიალიზებული, ასე რომ, არცერთი ფასი float-ში არ გადის არც სერვერზე და არც გადაცემისას.
  • დეპლოი: Docker Compose: Postgres, Django, Vite, კომიტირებული მიგრაციებით. ერთი ბრძანება რეპოზიტორიის კლონირებიდან საწყისი მონაცემებით შევსებულ მაღაზიამდე, რომელშიც უკვე შეიძლება შესვლა.

ციფრებში

API ენდფოინთები: 68 · Django მოდელები: 12 · ფრონტენდის კოდის სტრიქონები: 18k · ენები: 2

შედეგები

  • კატალოგი ელცხრილიდან გადმოვიდა: პროდუქტის, კოლექციის ან ზოდიაქოს აღწერის დამატება ადმინის ქმედება გახდა და არა დეპლოი.
  • ქართული აღარ არის მეორეხარისხოვანი — მაღაზია საკუთარ ბაზარზე მშობლიურ ენაზე იკითხება და ინგლისურზე მხოლოდ იქ გადადის, სადაც ქართული ჯერ არავის დაუწერია.
  • სატელეფონო და ვებ-შეკვეთები ერთი და იგივე ობიექტი გახდა, ერთი და იმავე ფასდაკლების წესებით, სნეპშოტებითა და აუდიტის ჟურნალით.
  • ფასდაკლების კოდი ლიმიტზე მეტჯერ ვერ გამოიყენება მაშინაც, როცა ორი ადამიანი შეკვეთას ერთდროულად აფორმებს, რადგან ლიმიტი იქ მოწმდება, სადაც შეკვეთა იწერება, და არა იქ, სადაც კოდი ითავაზება.
  • პერსონალს ნებისმიერი მომხმარებლისთვის შეუძლია ნახოს, რამდენი დახარჯა და ბოლოს როდის შეუკვეთა, ისე, რომ თვალწინ არსებული შეკვეთა არ დატოვოს.

რა გამოვიყენეთ

  • ბექენდი: Python 3.12, Django 5.2, Django Ninja 1.3, django-ninja-jwt 5.3, Pydantic (გადაცემის სქემები camelCase ფორმატში), django-cors-headers, Pillow
  • მონაცემები: PostgreSQL 17, psycopg 3 (binary), კომიტირებული მიგრაციები, JSONField ტეგების მასივებისა და ქვიზის ბანკებისთვის, DecimalField ყველა თანხისთვის
  • ფრონტენდი: React 19, TypeScript 5.8, Vite 5.4, Tailwind CSS 3.4, Radix-ზე აგებული shadcn/ui, TanStack Query 5, React Router 6.30, React Hook Form 7 + Zod 3, i18next 25 (KA/EN), framer-motion 12, fuse.js 7, sonner
  • ინსტრუმენტები და ინფრასტრუქტურა: Docker Compose, Bun 1, pytest + pytest-django, Vitest 3 + Testing Library, ruff, mypy strict + django-stubs, ESLint 9, OpenAPI მისამართზე /api/docs

დემო

რეალური ფრონტენდი, რომლის Django ბექენდიც ბრაუზერშივე მომუშავე სიმულირებული API-ით არის ჩანაცვლებული: საწყისი მონაცემები ბანდლშია ჩაშენებული და მეხსიერებიდან მიეწოდება იმავე დაყოვნებითა და შეცდომის იმავე პასუხებით, რასაც API დააბრუნებდა. ყოველი ჩაწერა მუშაობს — გააფორმეთ შეკვეთა მომხმარებლის რანგში და ის კონსოლში უკვე გელოდებათ, აუდიტის ჟურნალის ჩათვლით — და არაფერი ინახება, ამიტომ გვერდის გადატვირთვა ხელუხლებელ მაღაზიას აბრუნებს. კუთხეში არსებული ბანერი სისტემაში შეგიყვანთ როგორც მომხმარებელს, ისე ადმინისტრატორს — რეალური შესვლის ფორმით და არა მისი გვერდის ავლით. იმ მაღაზიისგან განსხვავებით, საიდანაც ის არის გადმოტანილი, დემო ინგლისურად იხსნება; ჰედერში არსებული გადამრთველი მთელ დემოს, კონსოლის ჩათვლით, ისევ ქართულზე აბრუნებს. არანაირი ქსელური მოთხოვნა არ იგზავნება — არც API, არც შრიფტები, არც ანალიტიკა — ამიტომ ის ოფლაინშიც და iframe-შიც მუშაობს.