diff --git a/.gitignore b/.gitignore index e280487..52dcb91 100644 --- a/.gitignore +++ b/.gitignore @@ -57,4 +57,6 @@ application-local.properties frontend/node_modules/ frontend/dist/ frontend/.env.local -frontend/.env.*.local \ No newline at end of file +frontend/.env.*.local +### Docker ### +.docker/ diff --git a/DEVELOPMENT_SETUP.md b/DEVELOPMENT_SETUP.md new file mode 100644 index 0000000..7c9d028 --- /dev/null +++ b/DEVELOPMENT_SETUP.md @@ -0,0 +1,326 @@ +# 개발 환경 설정 가이드 + +이 가이드는 윈도우 데스크톱과 맥북에서 프로젝트를 설정하는 방법을 설명합니다. + +--- + +## 📋 목차 + +1. [Docker 설치](#1-docker-설치) +2. [프로젝트 클론](#2-프로젝트-클론) +3. [로컬 개발 환경 시작](#3-로컬-개발-환경-시작) +4. [백엔드 실행](#4-백엔드-실행) +5. [프론트엔드 실행](#5-프론트엔드-실행) +6. [프로덕션 배포](#6-프로덕션-배포) +7. [FAQ](#7-faq) + +--- + +## 1. Docker 설치 + +### 윈도우 (데스크톱) + +1. **Docker Desktop 다운로드** + - 공식 사이트: https://www.docker.com/products/docker-desktop/ + - "Download for Windows" 클릭 + +2. **설치** + - 다운로드한 `Docker Desktop Installer.exe` 실행 + - WSL 2 백엔드 사용 옵션 체크 (권장) + - 설치 완료 후 재부팅 + +3. **설치 확인** + ```bash + # PowerShell 또는 CMD에서 실행 + docker --version + docker-compose --version + ``` + +### 맥북 (MacOS) + +1. **Docker Desktop 다운로드** + - 공식 사이트: https://www.docker.com/products/docker-desktop/ + - Apple Silicon (M1/M2/M3): "Download for Mac - Apple Chip" + - Intel: "Download for Mac - Intel Chip" + +2. **설치** + - 다운로드한 `.dmg` 파일 실행 + - Docker 아이콘을 Applications 폴더로 드래그 + - Docker Desktop 실행 + +3. **설치 확인** + ```bash + # 터미널에서 실행 + docker --version + docker-compose --version + ``` + +--- + +## 2. 프로젝트 클론 + +```bash +# GitHub에서 프로젝트 클론 +git clone https://github.com/YOUR_USERNAME/community-platform.git +cd community-platform +``` + +--- + +## 3. 로컬 개발 환경 시작 + +### 3-1. Docker로 PostgreSQL 시작 + +```bash +# 프로젝트 루트에서 실행 +docker-compose up -d +``` + +**명령어 설명:** +- `up`: 컨테이너 시작 +- `-d`: 백그라운드 실행 (detached mode) + +**확인:** +```bash +# 컨테이너 상태 확인 +docker-compose ps + +# 로그 확인 +docker-compose logs postgres + +# PostgreSQL 연결 테스트 +docker exec -it community-platform-db psql -U postgres -d community +# 성공하면 postgres=# 프롬프트가 나타남 +# \q 로 종료 +``` + +### 3-2. Docker 중지 및 재시작 + +```bash +# 중지 (데이터 보존) +docker-compose stop + +# 재시작 +docker-compose start + +# 완전히 제거 (데이터 삭제) +docker-compose down -v + +# 다시 시작 +docker-compose up -d +``` + +--- + +## 4. 백엔드 실행 + +### 4-1. Gradle 빌드 및 의존성 설치 + +```bash +# 윈도우 +gradlew.bat build + +# 맥/리눅스 +./gradlew build +``` + +### 4-2. 로컬 개발 모드로 실행 + +```bash +# 윈도우 +gradlew.bat bootRun --args='--spring.profiles.active=dev' + +# 맥/리눅스 +./gradlew bootRun --args='--spring.profiles.active=dev' +``` + +**또는 IDE에서 실행:** + +**IntelliJ IDEA:** +1. `CommunityApplication.java` 우클릭 +2. "Modify Run Configuration..." +3. VM options에 `-Dspring.profiles.active=dev` 추가 +4. Run + +**VS Code:** +1. `.vscode/launch.json` 생성 +```json +{ + "version": "0.2.0", + "configurations": [ + { + "type": "java", + "name": "Community Platform (Dev)", + "request": "launch", + "mainClass": "com.example.community.CommunityApplication", + "vmArgs": "-Dspring.profiles.active=dev" + } + ] +} +``` + +### 4-3. 백엔드 동작 확인 + +브라우저에서: +- 헬스체크: http://localhost:8080/actuator/health +- API 테스트: http://localhost:8080/api-test.html + +--- + +## 5. 프론트엔드 실행 + +```bash +cd frontend + +# 의존성 설치 (처음 한 번만) +npm install + +# 개발 서버 시작 +npm run dev +``` + +브라우저에서 http://localhost:5173 접속 + +--- + +## 6. 프로덕션 배포 + +### 6-1. Supabase 환경변수 설정 + +**방법 1: 환경변수 사용 (권장)** + +```bash +# 윈도우 (PowerShell) +$env:SPRING_PROFILES_ACTIVE="prod" +$env:DB_URL="jdbc:postgresql://YOUR-REGION.pooler.supabase.com:5432/postgres" +$env:DB_USERNAME="postgres.YOUR-PROJECT-ID" +$env:DB_PASSWORD="YOUR-DATABASE-PASSWORD" +$env:JWT_SECRET="YOUR-BASE64-ENCODED-SECRET" + +# 맥/리눅스 +export SPRING_PROFILES_ACTIVE=prod +export DB_URL="jdbc:postgresql://YOUR-REGION.pooler.supabase.com:5432/postgres" +export DB_USERNAME="postgres.YOUR-PROJECT-ID" +export DB_PASSWORD="YOUR-DATABASE-PASSWORD" +export JWT_SECRET="YOUR-BASE64-ENCODED-SECRET" +``` + +**방법 2: application-prod.properties 직접 수정 (비권장)** + +`src/main/resources/application-prod.properties`에서 직접 값 입력 +(주의: Git에 커밋하지 말 것!) + +### 6-2. 프로덕션 모드로 실행 + +```bash +# 윈도우 +gradlew.bat bootRun --args='--spring.profiles.active=prod' + +# 맥/리눅스 +./gradlew bootRun --args='--spring.profiles.active=prod' +``` + +--- + +## 7. FAQ + +### Q1. "docker-compose: command not found" 에러 + +**A:** Docker Desktop이 실행 중인지 확인하세요. +- 윈도우: 작업 표시줄에서 Docker 아이콘 확인 +- 맥: 메뉴바에서 Docker 아이콘 확인 + +### Q2. 포트 5432가 이미 사용 중이라는 에러 + +**A:** 로컬에 PostgreSQL이 이미 설치되어 있을 수 있습니다. + +**해결 방법 1: 로컬 PostgreSQL 중지** +```bash +# 윈도우 +services.msc 실행 → PostgreSQL 서비스 중지 + +# 맥 +brew services stop postgresql +``` + +**해결 방법 2: Docker 포트 변경** +`docker-compose.yml`에서: +```yaml +ports: + - "5433:5432" # 5432 → 5433으로 변경 +``` + +그리고 `application-dev.properties`에서: +```properties +spring.datasource.url=jdbc:postgresql://localhost:5433/community +``` + +### Q3. Flyway 마이그레이션 에러 + +**A:** DB를 완전히 초기화하고 다시 시작: +```bash +docker-compose down -v +docker-compose up -d +# 애플리케이션 재시작 +``` + +### Q4. 윈도우와 맥북에서 DB 데이터 공유하고 싶어요 + +**A:** Docker 볼륨은 로컬 저장이므로 공유되지 않습니다. + +**옵션 1: Git으로 스키마만 공유 (권장)** +- Flyway 마이그레이션 파일은 Git에 커밋됨 +- 각 환경에서 자동으로 동일한 스키마 생성 + +**옵션 2: 개발용 Supabase 사용** +- 무료 Supabase 프로젝트 생성 +- 두 환경에서 동일한 Supabase 접속 + +### Q5. JWT Secret은 어떻게 생성하나요? + +```bash +# 리눅스/맥 +echo -n "my-super-secret-jwt-key-12345" | base64 + +# 윈도우 (PowerShell) +[Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes("my-super-secret-jwt-key-12345")) +``` + +### Q6. 프로파일을 지정하지 않으면? + +**A:** 기본값은 `dev`입니다. (`application.properties`에서 설정) + +--- + +## 🎯 빠른 시작 체크리스트 + +### 처음 시작할 때 + +- [ ] Docker Desktop 설치 및 실행 +- [ ] 프로젝트 클론 +- [ ] `docker-compose up -d` 실행 +- [ ] 백엔드 실행 (`./gradlew bootRun`) +- [ ] 프론트엔드 실행 (`cd frontend && npm install && npm run dev`) +- [ ] http://localhost:5173 접속 확인 + +### 매일 개발할 때 + +- [ ] Docker Desktop 실행 +- [ ] `docker-compose start` (중지했었다면) +- [ ] 백엔드 실행 +- [ ] 프론트엔드 실행 + +### 작업 끝날 때 + +- [ ] 코드 커밋 및 푸시 +- [ ] `docker-compose stop` (선택사항, 계속 켜둬도 됨) + +--- + +## 📞 문제 발생 시 + +1. Docker 컨테이너 로그 확인: `docker-compose logs -f` +2. 백엔드 로그 확인 (콘솔 출력) +3. 프론트엔드 브라우저 콘솔 확인 (F12) + +궁금한 점이 있으면 이슈를 등록해주세요! diff --git a/QUICK_START.md b/QUICK_START.md new file mode 100644 index 0000000..9de4877 --- /dev/null +++ b/QUICK_START.md @@ -0,0 +1,133 @@ +# ⚡ 빠른 시작 가이드 + +## 5분 안에 개발 환경 구축하기 + +### 전제 조건 +- ✅ Docker Desktop 설치 완료 +- ✅ Java 17+ 설치 +- ✅ Node.js 18+ 설치 + +> 자세한 설치 방법은 [DEVELOPMENT_SETUP.md](./DEVELOPMENT_SETUP.md) 참고 + +--- + +## 1️⃣ 프로젝트 클론 + +```bash +git clone https://github.com/YOUR_USERNAME/community-platform.git +cd community-platform +``` + +--- + +## 2️⃣ Docker로 DB 시작 + +```bash +docker-compose up -d +``` + +**확인:** +```bash +docker-compose ps +# community-platform-db가 "Up" 상태여야 함 +``` + +--- + +## 3️⃣ 백엔드 실행 + +**터미널 1번:** + +```bash +# 윈도우 +gradlew.bat bootRun + +# 맥/리눅스 +./gradlew bootRun +``` + +**확인:** http://localhost:8080/actuator/health + +```json +{"status":"UP"} +``` + +--- + +## 4️⃣ 프론트엔드 실행 + +**터미널 2번:** + +```bash +cd frontend +npm install +npm run dev +``` + +**확인:** http://localhost:5173 + +--- + +## ✅ 완료! + +이제 개발을 시작할 수 있습니다! + +- 회원가입: http://localhost:5173/register +- 로그인: http://localhost:5173/login + +--- + +## 🛑 작업 종료 + +```bash +# 프론트엔드 중지: Ctrl + C +# 백엔드 중지: Ctrl + C + +# Docker 중지 (선택사항) +docker-compose stop +``` + +--- + +## 🔄 다음 날 시작하기 + +```bash +# Docker가 중지되어 있다면 +docker-compose start + +# 백엔드 실행 +./gradlew bootRun + +# 프론트엔드 실행 +cd frontend && npm run dev +``` + +--- + +## ❓ 문제 발생 시 + +### 포트 충돌 (5432 already in use) + +```bash +# 포트 사용 중인 프로세스 확인 +# 윈도우 +netstat -ano | findstr :5432 + +# 맥/리눅스 +lsof -i :5432 + +# 해결: 로컬 PostgreSQL 중지하거나 Docker 포트 변경 +``` + +### Flyway 마이그레이션 실패 + +```bash +# DB 초기화 +docker-compose down -v +docker-compose up -d +# 백엔드 재시작 +``` + +### 더 자세한 도움말 + +[DEVELOPMENT_SETUP.md](./DEVELOPMENT_SETUP.md)의 FAQ 섹션을 확인하세요! diff --git a/build.gradle b/build.gradle index 90e0134..25a4bcc 100644 --- a/build.gradle +++ b/build.gradle @@ -33,6 +33,10 @@ dependencies { // SUPABASE - PostgreSQL 추가 runtimeOnly 'org.postgresql:postgresql' + // Flyway DB Migration + implementation 'org.flywaydb:flyway-core' + implementation 'org.flywaydb:flyway-database-postgresql' + // JWT implementation 'io.jsonwebtoken:jjwt-api:0.12.3' runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.3' diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..025d721 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,26 @@ +version: '3.8' + +services: + postgres: + image: postgres:15-alpine + container_name: community-platform-db + ports: + - "5432:5432" + environment: + POSTGRES_DB: community + POSTGRES_USER: postgres + POSTGRES_PASSWORD: localpass123 + POSTGRES_INITDB_ARGS: "-E UTF8 --locale=C" + TZ: Asia/Seoul + volumes: + - postgres_data:/var/lib/postgresql/data + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 10s + timeout: 5s + retries: 5 + +volumes: + postgres_data: + driver: local diff --git a/src/main/java/com/example/community/config/SecurityConfig.java b/src/main/java/com/example/community/config/SecurityConfig.java index 1e3bfd7..4f3ad96 100644 --- a/src/main/java/com/example/community/config/SecurityConfig.java +++ b/src/main/java/com/example/community/config/SecurityConfig.java @@ -38,6 +38,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { // Url 접근 권한 설정 .authorizeHttpRequests(authz -> authz + // Actuator 헬스체크 허용 (로드밸런서용) + .requestMatchers("/actuator/**").permitAll() .requestMatchers("/api/health/**").permitAll() // API 엔드포인트 허용 (회원가입, 로그인 등) diff --git a/src/main/java/com/example/community/entity/Post.java b/src/main/java/com/example/community/entity/Post.java new file mode 100644 index 0000000..53672df --- /dev/null +++ b/src/main/java/com/example/community/entity/Post.java @@ -0,0 +1,58 @@ +package com.example.community.entity; + +import jakarta.persistence.*; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; +import java.time.LocalDateTime; + +@Entity +@Table(name = "posts") +public class Post { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, length = 200) + private String title; + + @Column(nullable = false, columnDefinition = "TEXT") + private String content; + + @Column(nullable = false) + private Long authorId; + + @Column(nullable = false) + private Integer viewCount = 0; + + @CreationTimestamp + @Column(updatable = false) + private LocalDateTime createdAt; + + @UpdateTimestamp + private LocalDateTime updatedAt; + + // 기본 생성자 + protected Post() {} + + // 생성자 + public Post(String title, String content, Long authorId) { + this.title = title; + this.content = content; + this.authorId = authorId; + } + + // Getters + public Long getId() { return id; } + public String getTitle() { return title; } + public String getContent() { return content; } + public Long getAuthorId() { return authorId; } + public Integer getViewCount() { return viewCount; } + public LocalDateTime getCreatedAt() { return createdAt; } + public LocalDateTime getUpdatedAt() { return updatedAt; } + + // Setters (업데이트용) + public void setTitle(String title) { this.title = title; } + public void setContent(String content) { this.content = content; } + public void incrementViewCount() { this.viewCount++; } +} diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties new file mode 100644 index 0000000..f6752da --- /dev/null +++ b/src/main/resources/application-dev.properties @@ -0,0 +1,30 @@ +# 로컬 개발 환경 설정 (Docker PostgreSQL) +# 사용법: --spring.profiles.active=dev + +# Local Docker PostgreSQL +spring.datasource.url=jdbc:postgresql://localhost:5432/community +spring.datasource.username=postgres +spring.datasource.password=localpass123 +spring.datasource.driver-class-name=org.postgresql.Driver + +# JPA 설정 +spring.jpa.hibernate.ddl-auto=validate +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect +spring.jpa.show-sql=true +spring.jpa.properties.hibernate.format_sql=true + +# Flyway 마이그레이션 +spring.flyway.enabled=true +spring.flyway.baseline-on-migrate=true +spring.flyway.locations=classpath:db/migration + +# JWT 설정 (개발용 - 짧은 만료시간) +jwt.secret=ZGV2LXNlY3JldC1rZXktZm9yLWxvY2FsLWRldmVsb3BtZW50LTEyMzQ1Njc4OTA= +jwt.access-token-validity-ms=3600000 +jwt.refresh-token-validity-ms=604800000 + +# 로그 레벨 +logging.level.com.example.community=DEBUG +logging.level.org.springframework.web=DEBUG +logging.level.org.hibernate.SQL=DEBUG +logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE diff --git a/src/main/resources/application-prod.properties b/src/main/resources/application-prod.properties new file mode 100644 index 0000000..777281e --- /dev/null +++ b/src/main/resources/application-prod.properties @@ -0,0 +1,33 @@ +# 프로덕션 환경 설정 (Supabase PostgreSQL) +# 사용법: --spring.profiles.active=prod + +# Supabase PostgreSQL (환경변수 사용) +spring.datasource.url=${DB_URL} +spring.datasource.username=${DB_USERNAME} +spring.datasource.password=${DB_PASSWORD} +spring.datasource.driver-class-name=org.postgresql.Driver + +# Connection Pool 설정 (프로덕션) +spring.datasource.hikari.maximum-pool-size=10 +spring.datasource.hikari.minimum-idle=5 +spring.datasource.hikari.connection-timeout=30000 + +# JPA 설정 +spring.jpa.hibernate.ddl-auto=validate +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect +spring.jpa.show-sql=false + +# Flyway 마이그레이션 +spring.flyway.enabled=true +spring.flyway.baseline-on-migrate=true +spring.flyway.locations=classpath:db/migration + +# JWT 설정 (프로덕션 - 환경변수 사용) +jwt.secret=${JWT_SECRET} +jwt.access-token-validity-ms=900000 +jwt.refresh-token-validity-ms=604800000 + +# 로그 레벨 (프로덕션) +logging.level.com.example.community=INFO +logging.level.org.springframework.web=WARN +logging.level.org.hibernate.SQL=WARN diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 9212aef..5ed1b26 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,33 +1,12 @@ spring.application.name=community-platform -# Import additional properties file -spring.config.import=optional:classpath:application-local.properties - -# Supabase PostgreSQL (Session Pooler) -# NOTE: Actual values are in application-local.properties (not committed to Git) -spring.datasource.url=${DB_URL:jdbc:postgresql://localhost:5432/community} -spring.datasource.username=${DB_USERNAME:postgres} -spring.datasource.password=${DB_PASSWORD:changeme} -spring.datasource.driver-class-name=org.postgresql.Driver - -# JPA 설정 (인메모리 DB용) -spring.jpa.hibernate.ddl-auto=update -spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.show-sql=true - -# H2 Console 활성화 (개발용) - 사용하지 않음 -spring.h2.console.enabled=true -spring.h2.console.path=/h2-console +# 기본 프로파일 (dev: 로컬 Docker, prod: Supabase) +spring.profiles.active=${SPRING_PROFILES_ACTIVE:dev} # Actuator 헬스체크 엔드포인트 management.endpoints.web.exposure.include=health,info management.endpoints.health.show-details=always -# 로그 레벨 (디버깅용) -logging.level.com.example.community=DEBUG -logging.level.org.springframework.web=INFO -logging.level.org.hibernate.SQL=DEBUG - # HTTP 인코딩 설정 (한글 깨짐 방지) server.servlet.encoding.charset=UTF-8 server.servlet.encoding.enabled=true @@ -35,8 +14,4 @@ server.servlet.encoding.force=true spring.http.encoding.charset=UTF-8 spring.http.encoding.enabled=true -# JWT 설정 -# NOTE: Actual secret is in application-local.properties (not committed to Git) -jwt.secret=${JWT_SECRET:ZGVmYXVsdC1kZXYtc2VjcmV0LXBsZWFzZS1jaGFuZ2UtaW4tcHJvZHVjdGlvbg==} -jwt.access-token-validity-ms=900000 -jwt.refresh-token-validity-ms=604800000 \ No newline at end of file +# NOTE: DB, JWT 설정은 application-dev.properties 또는 application-prod.properties에 있습니다. \ No newline at end of file diff --git a/src/main/resources/db/migration/V1__create_users_table.sql b/src/main/resources/db/migration/V1__create_users_table.sql new file mode 100644 index 0000000..90df02b --- /dev/null +++ b/src/main/resources/db/migration/V1__create_users_table.sql @@ -0,0 +1,18 @@ +-- 사용자 테이블 생성 +CREATE TABLE users ( + id BIGSERIAL PRIMARY KEY, + email VARCHAR(100) UNIQUE NOT NULL, + nickname VARCHAR(20) NOT NULL, + password VARCHAR(255) NOT NULL, + profile_image VARCHAR(500), + bio VARCHAR(500), + github_url VARCHAR(100), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- 이메일 검색을 위한 인덱스 +CREATE INDEX idx_users_email ON users(email); + +-- 생성일 검색을 위한 인덱스 +CREATE INDEX idx_users_created_at ON users(created_at); diff --git a/src/main/resources/db/migration/V2__create_refresh_tokens_table.sql b/src/main/resources/db/migration/V2__create_refresh_tokens_table.sql new file mode 100644 index 0000000..80fbf51 --- /dev/null +++ b/src/main/resources/db/migration/V2__create_refresh_tokens_table.sql @@ -0,0 +1,17 @@ +-- RefreshToken 테이블 생성 +CREATE TABLE refresh_tokens ( + id BIGSERIAL PRIMARY KEY, + token VARCHAR(500) UNIQUE NOT NULL, + user_id BIGINT NOT NULL, + expiry_date TIMESTAMP NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- 토큰 검색을 위한 인덱스 +CREATE INDEX idx_refresh_tokens_token ON refresh_tokens(token); + +-- 사용자별 토큰 조회를 위한 인덱스 +CREATE INDEX idx_refresh_tokens_user_id ON refresh_tokens(user_id); + +-- 만료된 토큰 정리를 위한 인덱스 +CREATE INDEX idx_refresh_tokens_expiry_date ON refresh_tokens(expiry_date); diff --git a/src/main/resources/db/migration/V3__create_posts_table.sql b/src/main/resources/db/migration/V3__create_posts_table.sql new file mode 100644 index 0000000..60704ca --- /dev/null +++ b/src/main/resources/db/migration/V3__create_posts_table.sql @@ -0,0 +1,30 @@ +-- 게시판 테이블 생성 +CREATE TABLE posts ( + id BIGSERIAL PRIMARY KEY, + title VARCHAR(200) NOT NULL, + content TEXT NOT NULL, + author_id BIGINT NOT NULL, + view_count INT NOT NULL DEFAULT 0, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT fk_posts_author FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE CASCADE +); + +-- 인덱스 +CREATE INDEX idx_posts_author_id ON posts(author_id); +CREATE INDEX idx_posts_created_at ON posts(created_at DESC); +CREATE INDEX idx_posts_title ON posts(title); + +-- updated_at 자동 업데이트 트리거 +CREATE OR REPLACE FUNCTION update_posts_updated_at() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER trigger_posts_updated_at + BEFORE UPDATE ON posts + FOR EACH ROW + EXECUTE FUNCTION update_posts_updated_at();