feat: Initial commit for Full-Stack Monorepo Starter Template with CockroachDB

This commit is contained in:
Flook 2025-10-26 18:11:54 +07:00
commit 05a332ed8b
61 changed files with 4255 additions and 0 deletions

46
.gitignore vendored Normal file
View File

@ -0,0 +1,46 @@
# =======================================================
# 1. PYTHON / DJANGO BACKEND
# =======================================================
# Virtual Environments (ไม่ว่าจะชื่อ venv, env, หรือ .venv)
*venv/
*.env
env/
.venv/
# Python Cache
__pycache__/
*.pyc
*.pyd
*.pyo
# Django & Database Files
/backend/*.sqlite3
/backend/staticfiles/
/backend/media/
/backend/local_settings.py
# =======================================================
# 2. NODE.JS / REACT FRONTEND (Web & Mobile)
# =======================================================
/web/node_modules/
/mobile/node_modules/
/web/dist/ # Build output for web (Vite)
/mobile/.expo/ # Expo cache files
/mobile/web-build/ # Expo web build output
/mobile/android/ # Native build folder
/mobile/ios/ # Native build folder
# IDE Files (IntelliJ / VS Code)
.idea/ # IntelliJ/WebStorm specific folder
.vscode/
# =======================================================
# 3. DOCKER / INFRASTRUCTURE
# =======================================================
# ห้าม Commit Volumes และ Secrets
# Docker Volumes ถูกสร้างขึ้นในโฟลเดอร์นี้
/infra/volumes/
*.log
# Secrets (ถ้าใช้ไฟล์ .env ในอนาคต)
.env

8
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

9
.idea/cremation-monorepo.iml generated Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

6
.idea/misc.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="temurin-21 (2)" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

8
.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/cremation-monorepo.iml" filepath="$PROJECT_DIR$/.idea/cremation-monorepo.iml" />
</modules>
</component>
</project>

7
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$/mobile" vcs="Git" />
</component>
</project>

74
README.md Normal file
View File

@ -0,0 +1,74 @@
# MONOREPO STARTER TEMPLATE
## ภาพรวม
โปรเจกต์นี้เป็น Monorepo แบบ Full-Stack ที่มีโครงสร้างพร้อมใช้งานสำหรับการพัฒนาและทดสอบ:
1. **Backend (Django/DRF):** โครงสร้าง Modular Monolith พร้อมระบบ Authentication ด้วย Djoser/JWT
2. **ฐานข้อมูล (HA):** CockroachDB Cluster 3 Node (เข้ากันได้กับ PostgreSQL)
3. **Frontend (Web):** React + Vite + Tailwind CSS / DaisyUI และ Mobile ผ่าน Expo
4. **Automation:** Docker Compose พร้อมตั้งค่า Database อัตโนมัติเมื่อเริ่มต้น
## การเริ่มต้นใช้งาน (Run Local)
1. **Clone the Repository:**
```bash
git clone [Your Gitea URL]
cd cremation-monorepo
```
2. **รัน Infrastructure & Backend:**
ไปที่โฟลเดอร์ infra และสตาร์ทบริการทั้งหมด คำสั่งนี้จะ build backend และตั้งค่า CockroachDB Cluster อัตโนมัติ
```bash
cd infra
docker compose up --build -d
```
3. **เข้าถึง Admin UI:**
ตรวจสอบว่า Backend และฐานข้อมูลพร้อมใช้งาน
- Django Admin: http://localhost:8000/admin/
- CockroachDB Status: http://localhost:8080/
Username: admin
Password: Str0ngp@ssword123-
- การเข้าถึงและตรวจสอบ API Endpoints อื่น ๆ:
http://localhost:8000/api/v1/auth/jwt/create/
http://localhost:8000/api/v1/auth/users/
http://localhost:8000/api/v1/auth/users/reset_password/
http://localhost:8000/api/v1/auth/users/me/
4. **รัน Frontend (Web):**
```bash
cd ../web
npm install
npm run dev
```
5. **รัน Frontend (Mobile):**
- ติดตั้ง Expo Go บนมือถือ
- สแกน QR Code โดยมือถือและเครื่อง Notebook ต้องอยู่ใน Lan วงเดียวกัน
```bash
cd ../mobile
npm expo start
```
## การหยุดชั่วคราวและรันต่อ
ถ้าต้องการหยุดพัฒนาโดยไม่ลบฐานข้อมูล สามารถรัน:
```bash
cd ../infra
docker compose down
```
เมื่อต้องการกลับมาพัฒนาต่อ ให้รัน:
```bash
cd ../infra
docker compose up -d
```
คำสั่ง docker compose down -v จะลบ Volume และฐานข้อมูลทั้งหมด ใช้เฉพาะตอนต้องการเริ่มต้นฐานข้อมูลใหม่
คำเตือนสำคัญ ตรวจสอบให้แน่ใจว่าได้กำหนด AUTH_USER_MODEL ถูกต้องใน settings.py ก่อนรัน migrate ครั้งแรกเสมอ เพื่อหลีกเลี่ยงปัญหา InconsistentMigrationHistory ในภายหลัง

27
backend/Dockerfile Normal file
View File

@ -0,0 +1,27 @@
# backend/Dockerfile
# 1. BASE IMAGE
FROM python:3.11-slim
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1 # เพื่อให้ Log แสดงผลทันที
# 2. WORK DIRECTORY
WORKDIR /app
# 3. DEPENDENCIES: Copy และติดตั้ง (ใช้ประโยชน์จาก Docker Layer Caching)
COPY requirements.txt /app/
RUN pip install --no-cache-dir -r requirements.txt
# 4. ENTRYPOINT SCRIPT: Copy และกำหนดสิทธิ์รัน (ใช้สำหรับ Startup Automation)
COPY docker-entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
# 5. CODE: คัดลอกโค้ดโปรเจกต์ที่เหลือทั้งหมดมาไว้ใน /app
COPY . /app/
# 6. EXPOSE:
EXPOSE 8000
# 7. ENTRYPOINT/CMD: กำหนด Entrypoint หลัก
ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]

View File

13
backend/accounts/admin.py Normal file
View File

@ -0,0 +1,13 @@
# backend/accounts/admin.py
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .models import CustomUser
# ใช้ UserAdmin เพื่อให้ได้ UI ที่ครบถ้วนสำหรับการจัดการผู้ใช้
class CustomUserAdmin(UserAdmin):
# Fieldsets หรือ list_display (ถ้าต้องการปรับแต่ง UI)
pass
# ลงทะเบียน CustomUser ด้วย CustomUserAdmin
admin.site.register(CustomUser, CustomUserAdmin)

6
backend/accounts/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class AccountsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'accounts'

View File

@ -0,0 +1,45 @@
# Generated by Django 5.2.7 on 2025-10-26 10:02
import django.contrib.auth.models
import django.contrib.auth.validators
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
migrations.CreateModel(
name='CustomUser',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('phone_number', models.CharField(blank=True, max_length=20, null=True, unique=True)),
('email', models.EmailField(max_length=254, unique=True)),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
],
options={
'verbose_name': 'user',
'verbose_name_plural': 'users',
'abstract': False,
},
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
]

View File

View File

@ -0,0 +1,20 @@
from django.db import models
# Create your models here.
from django.contrib.auth.models import AbstractUser
class CustomUser(AbstractUser):
# เพิ่มฟิลด์ที่ต้องการ เช่น phone_number
phone_number = models.CharField(max_length=20, blank=True, null=True, unique=True)
# ตัวอย่าง:
# is_customer = models.BooleanField(default=False)
# ถ้าต้องการให้ email เป็น Unique (ไม่ซ้ำกัน)
email = models.EmailField(unique=True)
# Field ที่ใช้สำหรับการล็อกอิน (ถ้าไม่ใช่ username)
# USERNAME_FIELD = 'email'
# REQUIRED_FIELDS = ['username'] # ถ้าเปลี่ยน USERNAME_FIELD ต้องกำหนด REQUIRED_FIELDS
# ไม่ต้องใส่ pass เพราะมันสืบทอดมาจาก AbstractUser
pass

View File

@ -0,0 +1,18 @@
# accounts/serializers.py
from djoser.serializers import UserCreateSerializer as BaseUserCreateSerializer
from rest_framework import serializers
from .models import CustomUser
class UserCreateSerializer(BaseUserCreateSerializer):
# Serializer สำหรับการลงทะเบียน (Djoser จะใช้ตัวนี้)
class Meta(BaseUserCreateSerializer.Meta):
model = CustomUser
fields = ('id', 'username', 'email', 'phone_number', 'password') # เพิ่ม phone_number
class UserSerializer(serializers.ModelSerializer):
# Serializer สำหรับการดึงข้อมูล (ใช้แสดงข้อมูลผู้ใช้ปัจจุบัน)
class Meta:
model = CustomUser
fields = ('id', 'username', 'email', 'phone_number', 'first_name', 'last_name')
read_only_fields = ('id', 'username')

View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

View File

View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class CremationApiConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'cremation_api'

View File

@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

View File

View File

@ -0,0 +1,16 @@
"""
ASGI config for cremation_backend project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'cremation_backend.settings')
application = get_asgi_application()

View File

@ -0,0 +1,193 @@
"""
Django settings for cremation_backend project.
Generated by 'django-admin startproject' using Django 5.2.7.
For more information on this file, see
https://docs.djangoproject.com/en/5.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.2/ref/settings/
"""
from pathlib import Path
import os
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-&f#$_k8e766il)kxagku-545x7*ji&%rqx!(&9k@t7qp)+9)t@'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
# Application definition
DJANGO_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]
THIRD_PARTY_APPS = [
'django_bolt',
'rest_framework',
'corsheaders',
'djoser',
'rest_framework_simplejwt'
]
LOCAL_APPS = [
'cremation_api',
'accounts',
'user_profile',
'permissions',
]
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware', # สำคัญมากสำหรับ Frontend
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
# ตั้งค่า CORS: อนุญาตให้ Frontend เข้าถึง API ได้ (ใน Dev Mode)
CORS_ALLOWED_ORIGINS = [
"http://localhost:5173", # Vite/React Default Port
"http://127.0.0.1:5173",
]
CORS_ALLOW_ALL_ORIGINS = True # ควรเป็น False ใน Production
ROOT_URLCONF = 'cremation_backend.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'cremation_backend.wsgi.application'
# Database
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
# backend/cremation_backend/settings.py
DATABASES = {
'default': {
'ENGINE': 'django_cockroachdb',
'NAME': os.environ.get('DB_NAME', 'my_db'),
'HOST': os.environ.get('DB_HOST', 'cockroach-1'),
'PORT': os.environ.get('DB_PORT', '26257'),
'USER': os.environ.get('DB_USER', 'root'),
'PASSWORD': os.environ.get('DB_PASSWORD', ''),
# อยู่ระดับเดียวกับ 'ENGINE' และ 'OPTIONS'
'support_check_by_version': False,
'OPTIONS': {
# ใช้ 'options' เพื่อส่งค่า config ไปยัง CockroachDB/PostgreSQL
'options': '-c default_transaction_read_only=off'
},
'ATOMIC_REQUESTS': False,
},
}
# Password validation
# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/5.2/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.2/howto/static-files/
STATIC_URL = 'static/'
# Default primary key field type
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# 1. กำหนด Custom User Model
AUTH_USER_MODEL = 'accounts.CustomUser' # ต้องชี้ไปที่ Model ใน App accounts/models.py
# 2. ตั้งค่า REST Framework
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
# ใช้ JWT เป็นวิธีการยืนยันตัวตนหลัก
'rest_framework_simplejwt.authentication.JWTAuthentication',
),
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
)
}
# 3. ตั้งค่า DJOSER (เพื่อจัดการ Auth Endpoints)
DJOSER = {
# ใช้งาน JWT โดยตรง
'USER_ID_FIELD': 'id', # ใช้ ID ของ User Model
'PASSWORD_RESET_CONFIRM_URL': '#/password/reset/confirm/{uid}/{token}', # URL สำหรับ Frontend
'USERNAME_RESET_CONFIRM_URL': '#/username/reset/confirm/{uid}/{token}',
'ACTIVATION_URL': '#/activate/{uid}/{token}', # หากต้องการยืนยันอีเมล
'SERIALIZERS': {
'user_create': 'accounts.serializers.UserCreateSerializer', # จะสร้างในขั้นตอนถัดไป
'user': 'accounts.serializers.UserSerializer', # ใช้ UserSerializer เดิม
'current_user': 'accounts.serializers.UserSerializer',
},
'TOKEN_MODEL': None, # ไม่ใช้ TokenAuth แบบเก่า
'PERMISSIONS': {
'user_create': ['rest_framework.permissions.AllowAny'],
}
}

View File

@ -0,0 +1,33 @@
"""
URL configuration for cremation_backend project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/5.2/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include
from rest_framework.routers import DefaultRouter
# 1. กำหนดตัวแปร router ก่อนใช้งาน
router = DefaultRouter()
urlpatterns = [
path('admin/', admin.site.urls),
# 1. Endpoints สำหรับการยืนยันตัวตน (Login, Logout, Register)
path('api/v1/auth/', include('djoser.urls')), # /users/ (Register/Update/Me), /users/set_password
path('api/v1/auth/', include('djoser.urls.jwt')), # /jwt/create (Login), /jwt/refresh (Refresh Token)
# 2. รวม Router API
path('api/v1/', include(router.urls)),
]

View File

@ -0,0 +1,16 @@
"""
WSGI config for cremation_backend project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'cremation_backend.settings')
application = get_wsgi_application()

View File

@ -0,0 +1,51 @@
#!/bin/sh
echo "Waiting for CockroachDB cluster to be ready..."
# ใช้ Python แทน client cockroach
until python - <<END
import psycopg
import os
try:
conn = psycopg.connect(
host=os.environ.get("DB_HOST","cockroach-1"),
port=os.environ.get("DB_PORT",26257),
dbname="defaultdb",
user=os.environ.get("DB_USER","root"),
password=os.environ.get("DB_PASSWORD","")
)
conn.close()
except:
exit(1)
END
do
echo "CockroachDB is unavailable - sleeping"
sleep 1
done
echo "CockroachDB is ready."
# Create DB
python - <<END
import psycopg
import os
conn = psycopg.connect(
host=os.environ.get("DB_HOST","cockroach-1"),
port=os.environ.get("DB_PORT",26257),
dbname="defaultdb",
user=os.environ.get("DB_USER","root"),
password=os.environ.get("DB_PASSWORD","")
)
conn.autocommit = True
cur = conn.cursor()
cur.execute(f"CREATE DATABASE IF NOT EXISTS {os.environ.get('DB_NAME','my_db')};")
cur.close()
conn.close()
END
# Migrate & Superuser
python manage.py migrate --noinput
python manage.py shell -c "from django.contrib.auth import get_user_model; User=get_user_model(); User.objects.filter(username='admin').exists() or User.objects.create_superuser('admin','admin@softwarecraft.tech','Str0ngp@ssword123-')"
echo "Starting Django server..."
exec python manage.py runserver 0.0.0.0:8000

22
backend/manage.py Normal file
View File

@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'cremation_backend.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

View File

View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class PermissionsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'permissions'

View File

@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

9
backend/requirements.txt Normal file
View File

@ -0,0 +1,9 @@
Django
djangorestframework
django-cors-headers
django-bolt
django-cockroachdb
psycopg[binary]
typing_extensions
djoser # สำหรับ endpoints: /users/ (Register), /token/login (Login), /password/reset
djangorestframework-simplejwt # สำหรับสร้าง JWT (JSON Web Tokens)

View File

View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class UserProfileConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'user_profile'

View File

@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

71
infra/docker-compose.yml Normal file
View File

@ -0,0 +1,71 @@
# infra/docker-compose.yml
version: '3.8'
services:
# Node 1: Primary Node (Bootstrap)
cockroach-1:
image: cockroachdb/cockroach:latest
container_name: cockroach-1
command: start --insecure --host=cockroach-1 --listen-addr=cockroach-1:26257 --advertise-addr=cockroach-1:26257 --join=cockroach-1:26257,cockroach-2:26257,cockroach-3:26257
restart: always
volumes:
- cockroach1:/cockroach/data
ports:
- "26257:26257" # Default SQL Port
- "8080:8080" # Web UI Port (สำหรับดูสถานะ Cluster)
# Node 2: Joining Node
cockroach-2:
image: cockroachdb/cockroach:latest
container_name: cockroach-2
command: start --insecure --host=cockroach-2 --listen-addr=cockroach-2:26257 --advertise-addr=cockroach-2:26257 --join=cockroach-1:26257,cockroach-2:26257,cockroach-3:26257
restart: always
volumes:
- cockroach2:/cockroach/data
# Node 3: Joining Node
cockroach-3:
image: cockroachdb/cockroach:latest
container_name: cockroach-3
command: start --insecure --host=cockroach-3 --listen-addr=cockroach-3:26257 --advertise-addr=cockroach-3:26257 --join=cockroach-1:26257,cockroach-2:26257,cockroach-3:26257
restart: always
volumes:
- cockroach3:/cockroach/data
# 4. Init Cluster (ตั้งค่าครั้งแรก)
init-cluster:
image: cockroachdb/cockroach:latest
container_name: init-cluster
command: init --insecure --host=cockroach-1:26257
depends_on:
- cockroach-1
- cockroach-2
- cockroach-3
# ตั้งค่าให้ Container ตายหลังจากรัน init เสร็จ (ไม่รันซ้ำ)
restart: "no"
# Backend/API (DRF)
backend:
build:
context: ../backend
dockerfile: Dockerfile
volumes:
- ../backend:/app
ports:
- "8000:8000"
# เปลี่ยนให้พึ่งพา Node ฐานข้อมูลโดยตรง (เพราะ entrypoint script จะจัดการการรอ)
depends_on:
- cockroach-1
- cockroach-2
- cockroach-3
environment:
DB_HOST: cockroach-1
DB_PORT: 26257
DB_NAME: my_db
DB_USER: root
DB_PASSWORD: ''
volumes:
cockroach1:
cockroach2:
cockroach3:

1
mobile Submodule

@ -0,0 +1 @@
Subproject commit b1f5edec5dd804cff3932e1f79b7a90f52dec459

24
web/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

16
web/README.md Normal file
View File

@ -0,0 +1,16 @@
# React + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
## Expanding the ESLint configuration
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.

29
web/eslint.config.js Normal file
View File

@ -0,0 +1,29 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{js,jsx}'],
extends: [
js.configs.recommended,
reactHooks.configs['recommended-latest'],
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaVersion: 'latest',
ecmaFeatures: { jsx: true },
sourceType: 'module',
},
},
rules: {
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
},
},
])

13
web/index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en" data-theme="corporate">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>web</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

3343
web/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

30
web/package.json Normal file
View File

@ -0,0 +1,30 @@
{
"name": "web",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@tailwindcss/vite": "^4.1.16",
"daisyui": "^5.3.9",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"tailwindcss": "^4.1.16"
},
"devDependencies": {
"@eslint/js": "^9.36.0",
"@types/react": "^19.1.16",
"@types/react-dom": "^19.1.9",
"@vitejs/plugin-react": "^5.0.4",
"eslint": "^9.36.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.22",
"globals": "^16.4.0",
"vite": "^7.1.7"
}
}

1
web/public/vite.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

11
web/src/App.jsx Normal file
View File

@ -0,0 +1,11 @@
function App() {
return (
<>
<h1 className={"text-lg"}>ทดสอบนำเขาฟอนตในโปรเจกต Daisy UI</h1>
<button className="btn btn-neutral">Neutral</button>
</>
)
}
export default App

1
web/src/assets/react.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

4
web/src/index.css Normal file
View File

@ -0,0 +1,4 @@
@import "tailwindcss";
@plugin "daisyui"{
themes: light --default, dark --prefersdark, corporate;
}

11
web/src/main.jsx Normal file
View File

@ -0,0 +1,11 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
import './styles.css'
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
)

5
web/src/styles.css Normal file
View File

@ -0,0 +1,5 @@
@import url("https://fonts.googleapis.com/css2?family=Noto%20Sans%20Thai:wght@400;500;600;700;800;900&display=swap");
body {
font-family: 'Noto Sans Thai', sans-serif;
}

8
web/vite.config.js Normal file
View File

@ -0,0 +1,8 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite';
// https://vite.dev/config/
export default defineConfig({
plugins: [react(), tailwindcss()],
})