diff --git a/README.md b/README.md new file mode 100644 index 0000000..82b05f1 --- /dev/null +++ b/README.md @@ -0,0 +1,40 @@ +# ๐Ÿ› ๏ธ Spring Boot Blog API Server + +## ๐Ÿš€ Project Overview +๊ธฐ์กด **Jekyll(GitHub Pages)** ๋กœ ์šด์˜ํ•˜๋˜ ์ •์  ๋ธ”๋กœ๊ทธ์˜ ํ•œ๊ณ„๋ฅผ ๊ทน๋ณตํ•˜๊ณ , ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ๋ธ”๋กœ๊ทธ ์‹œ์Šคํ…œ์„ ๊ตฌ์ถ•ํ•˜๊ธฐ ์œ„ํ•ด ๊ฐœ๋ฐœํ•œ **Spring Boot ๊ธฐ๋ฐ˜์˜ REST API ์„œ๋ฒ„**์ž…๋‹ˆ๋‹ค. + +์ •์  ์‚ฌ์ดํŠธ์—์„œ๋Š” ๊ตฌํ˜„ํ•˜๊ธฐ ์–ด๋ ค์› ๋˜ **๋™์  ํฌ์ŠคํŒ… ๊ด€๋ฆฌ, ๊ด€๋ฆฌ์ž ๋Œ€์‹œ๋ณด๋“œ(Admin), ๋Œ“๊ธ€ ์‹œ์Šคํ…œ** ๋“ฑ์˜ ๊ธฐ๋Šฅ์„ ์ง์ ‘ ๊ตฌํ˜„ํ•˜์—ฌ ๋ธ”๋กœ๊ทธ ์šด์˜์˜ ํšจ์œจ์„ฑ์„ ๋†’์˜€์Šต๋‹ˆ๋‹ค. **RESTful ์›์น™**์— ์ž…๊ฐํ•œ API ์„ค๊ณ„์™€ **Spring Security**๋ฅผ ํ™œ์šฉํ•œ ๋ณด์•ˆ/์ธ์ฆ ๋กœ์ง์„ ์ ์šฉํ•˜์—ฌ ๋ฐฑ์—”๋“œ ์•„ํ‚คํ…์ฒ˜ ์—ญ๋Ÿ‰์„ ๊ฐ•ํ™”ํ•˜๋Š” ๋ฐ ์ค‘์ ์„ ๋‘์—ˆ์Šต๋‹ˆ๋‹ค. + +### ๐ŸŽฏ Key Objectives +* **Migration**: ์ •์  ํŒŒ์ผ(Jekyll) ์˜์กด์„ฑ์„ ์ œ๊ฑฐํ•˜๊ณ  DB ๊ธฐ๋ฐ˜์˜ ๋™์  ์‹œ์Šคํ…œ์œผ๋กœ ์ „ํ™˜ +* **Feature Expansion**: ์›น ์—๋””ํ„ฐ, ๊ฒŒ์‹œ๊ธ€ ๊ด€๋ฆฌ, ๋ฐฉ๋ฌธ์ž ํ†ต๊ณ„ ๋“ฑ ๊ด€๋ฆฌ์ž(Admin) ๊ธฐ๋Šฅ ์ถ”๊ฐ€ +* **Architecture**: ํ”„๋ก ํŠธ์—”๋“œ์™€ ๋ฐฑ์—”๋“œ๋ฅผ ๋ถ„๋ฆฌํ•œ REST API ์„œ๋ฒ„ ๊ตฌ์ถ• + +### ๐Ÿ› ๏ธ Tech Stack +* **Java 21, Spring Boot 3.5.9** +* **Spring Security, JWT** (Auth) +* **JPA (Hibernate), QueryDSL** (ORM) +* **PostgreSQL, Redis** (Cache/Session) +* **Markdown Parser** (Content) + +--- + +## ๐Ÿ”Œ API Reference + +**Base URL**: `http://localhost:8080` +**Auth**: `Authorization: Bearer {Access_Token}` + +### 1. ๊ณตํ†ต ์‘๋‹ต ๊ทœ๊ฒฉ (Common Response) +๋ชจ๋“  API ์‘๋‹ต์€ ์•„๋ž˜ ํ‘œ์ค€ ํฌ๋งท์„ ๋”ฐ๋ฆ…๋‹ˆ๋‹ค. + +```json +{ + "code": "SUCCESS", // ๊ฒฐ๊ณผ ์ฝ”๋“œ (SUCCESS ๋˜๋Š” ERROR_CODE) + "message": "์š”์ฒญ ์„ฑ๊ณต", // ์‘๋‹ต ๋ฉ”์‹œ์ง€ + "data": { ... } // ์‹ค์ œ ๋ฐ์ดํ„ฐ (์—†์„ ๊ฒฝ์šฐ null) +} +``` + +## ๊ด€๋ จ ๋ฌธ์„œ +- [**ENDPOINT**](https://affine.wypark.me/workspace/f85df0c4-a315-4166-94a8-6558cdafff1d/p13joh_beKNJ6R-I9KrFW) +- [**API ๋ช…์„ธ์„œ**](https://affine.wypark.me/workspace/f85df0c4-a315-4166-94a8-6558cdafff1d/7fRNXK9utxG4mcekWivGK) \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index b975943..e0b3b82 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,9 +1,10 @@ plugins { kotlin("jvm") version "1.9.25" kotlin("plugin.spring") version "1.9.25" + kotlin("plugin.jpa") version "1.9.25" + kotlin("kapt") version "1.9.25" id("org.springframework.boot") version "3.5.9" id("io.spring.dependency-management") version "1.1.7" - kotlin("plugin.jpa") version "1.9.25" } group = "me.wypark" @@ -12,7 +13,7 @@ description = "blog-backend" java { toolchain { - languageVersion = JavaLanguageVersion.of(21) + languageVersion = JavaLanguageVersion.of(21) // Java 21 ๊ตฟ! } } @@ -20,22 +21,43 @@ repositories { mavenCentral() } +val springCloudAwsVersion = "3.2.1" + dependencies { + // 1. Standard Spring Boot Starters implementation("org.springframework.boot:spring-boot-starter-data-jpa") implementation("org.springframework.boot:spring-boot-starter-security") implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.boot:spring-boot-starter-data-redis") + implementation("org.springframework.boot:spring-boot-starter-validation") + + // 2. Kotlin Modules implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.jetbrains.kotlin:kotlin-reflect") - developmentOnly("org.springframework.boot:spring-boot-devtools") + + // 3. Database Drivers runtimeOnly("org.postgresql:postgresql") + + // 4. AWS S3 / MinIO + implementation("io.awspring.cloud:spring-cloud-aws-starter-s3:$springCloudAwsVersion") + + // 5. QueryDSL + implementation("com.querydsl:querydsl-jpa:5.1.0:jakarta") + kapt("com.querydsl:querydsl-apt:5.1.0:jakarta") + kapt("jakarta.persistence:jakarta.persistence-api") + kapt("jakarta.annotation:jakarta.annotation-api") + + // 6. JWT (0.12.x) + implementation("io.jsonwebtoken:jjwt-api:0.12.3") + runtimeOnly("io.jsonwebtoken:jjwt-impl:0.12.3") + runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.12.3") + + // 7. Test & Dev + developmentOnly("org.springframework.boot:spring-boot-devtools") testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") testImplementation("org.springframework.security:spring-security-test") testRuntimeOnly("org.junit.platform:junit-platform-launcher") - implementation("io.jsonwebtoken:jjwt-api:0.12.3") - runtimeOnly("io.jsonwebtoken:jjwt-impl:0.12.3") - runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.12.3") } kotlin { @@ -52,4 +74,4 @@ allOpen { tasks.withType { useJUnitPlatform() -} +} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 852b47c..a9d0b66 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,15 +1,24 @@ services: - # ๋ฐฑ์—”๋“œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ + # 1. ๋ฐฑ์—”๋“œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ blog-api: build: . container_name: blog-api restart: always + ports: + - "8080:8080" environment: + # Database - SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/blog_db - SPRING_DATASOURCE_USERNAME=wypark - SPRING_DATASOURCE_PASSWORD=your_password + # Redis - SPRING_DATA_REDIS_HOST=redis - SPRING_DATA_REDIS_PORT=6379 + # AWS S3 / MinIO (Docker ๋‚ด๋ถ€ ํ†ต์‹ ์šฉ) + - CLOUD_AWS_CREDENTIALS_ACCESS_KEY=admin + - CLOUD_AWS_CREDENTIALS_SECRET_KEY=password + - CLOUD_AWS_REGION_STATIC=ap-northeast-2 + - CLOUD_AWS_S3_ENDPOINT=http://minio:9000 depends_on: db: condition: service_healthy @@ -18,11 +27,13 @@ services: networks: - blog-net - # ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค (PostgreSQL 17 ์ถ”์ฒœ) + # 2. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค (PostgreSQL) db: image: postgres:17-alpine container_name: blog-db restart: always + ports: + - "5432:5432" environment: POSTGRES_USER: wypark POSTGRES_PASSWORD: your_password @@ -36,7 +47,7 @@ services: networks: - blog-net - # ์บ์‹œ ์„œ๋ฒ„ (Redis 7) + # 3. ์บ์‹œ ์„œ๋ฒ„ (Redis) redis: image: redis:7-alpine container_name: blog-redis @@ -48,6 +59,22 @@ services: networks: - blog-net + # 4. ์˜ค๋ธŒ์ ํŠธ ์Šคํ† ๋ฆฌ์ง€ (MinIO) + minio: + image: minio/minio + container_name: minio + ports: + - "9000:9000" # API ํ†ต์‹  + - "9001:9001" # ์›น ์ฝ˜์†” + environment: + MINIO_ROOT_USER: "admin" + MINIO_ROOT_PASSWORD: "password" + command: server /data --console-address ":9001" + volumes: + - ./minio_data:/data + networks: + - blog-net + networks: blog-net: driver: bridge \ No newline at end of file diff --git a/src/main/kotlin/me/wypark/blogbackend/domain/auth/RefreshTokenRepository.kt b/src/main/kotlin/me/wypark/blogbackend/domain/auth/RefreshTokenRepository.kt index 6820f27..39b6b02 100644 --- a/src/main/kotlin/me/wypark/blogbackend/domain/auth/RefreshTokenRepository.kt +++ b/src/main/kotlin/me/wypark/blogbackend/domain/auth/RefreshTokenRepository.kt @@ -11,7 +11,6 @@ class RefreshTokenRepository( @Value("\${jwt.refresh-token-validity}") private val refreshTokenValidity: Long ) { // ์ €์žฅ (Key: Email, Value: RefreshToken) - // RTR ํ•ต์‹ฌ: ์‚ฌ์šฉ์ž๊ฐ€ ๋กœ๊ทธ์ธ์„ ์ƒˆ๋กœ ํ•˜๊ฑฐ๋‚˜ ํ† ํฐ์„ ์žฌ๋ฐœ๊ธ‰ ๋ฐ›์„ ๋•Œ๋งˆ๋‹ค ๋ฎ์–ด์”Œ์›€ fun save(email: String, refreshToken: String) { redisTemplate.opsForValue().set( "RT:$email", diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 4debd48..7e8f51e 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -1,19 +1,49 @@ spring: + application: + name: blog-api + + # 1. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค์ • datasource: + # Docker ๋‚ด๋ถ€ ํ†ต์‹ ์šฉ url: jdbc:postgresql://db:5432/blog_db - username: wypark - password: your_password + username: ${SPRING_DATASOURCE_USERNAME:wypark} + password: ${SPRING_DATASOURCE_PASSWORD:your_password} + driver-class-name: org.postgresql.Driver + + # 2. JPA ์„ค์ • jpa: hibernate: ddl-auto: update properties: hibernate: - format_sql: true + format_sql: true # ์ฟผ๋ฆฌ ์ค„๋ฐ”๊ฟˆ + show_sql: true # ์ฟผ๋ฆฌ ์ถœ๋ ฅ + highlight_sql: true # ์ฟผ๋ฆฌ ์ƒ‰์ƒ ๊ฐ•์กฐ (๊ฐ€๋…์„ฑ UP) + open-in-view: false # OSIV ์ข…๋ฃŒ (DB ์ปค๋„ฅ์…˜ ์ตœ์ ํ™”) + + # 3. Redis ์„ค์ • data: redis: - host: redis + host: redis # Docker ์„œ๋น„์Šค๋ช… port: 6379 + +# 4. AWS S3 / MinIO ์„ค์ • (์•„๊นŒ ์ด์•ผ๊ธฐํ•œ ๋ถ€๋ถ„) +cloud: + aws: + s3: + bucket: my-blog-bucket # MinIO ์ฝ˜์†”์—์„œ ๋ฏธ๋ฆฌ ์ƒ์„ฑํ•ด์•ผ ํ•จ + endpoint: http://minio:9000 # Docker ๋‚ด๋ถ€ ํ†ต์‹ ์šฉ + credentials: + access-key: admin + secret-key: password + region: + static: ap-northeast-2 + stack: + auto: false + +# 5. JWT ์„ค์ • jwt: - secret: "v3ryS3cr3tK3yF0rMyB10gPr0j3ctM4k3ItL0ng3rTh4n32Byt3s!!" # 32๋ฐ”์ดํŠธ ์ด์ƒ ํ•„์ˆ˜ - access-token-validity: 1800000 # 30๋ถ„ (ms) - refresh-token-validity: 604800000 # 7์ผ (ms) \ No newline at end of file + # ๋ณด์•ˆ ๊ฒฝ๊ณ : ์‹ค์ œ ๋ฐฐํฌ ์‹œ์—๋Š” ์ ˆ๋Œ€ ์ฝ”๋“œ์— ๋น„๋ฐ€ํ‚ค๋ฅผ ๋‚จ๊ธฐ์ง€ ๋งˆ์„ธ์š”. ํ™˜๊ฒฝ๋ณ€์ˆ˜๋กœ ์ฃผ์ž…๋ฐ›์œผ์„ธ์š”. + secret: ${JWT_SECRET:v3ryS3cr3tK3yF0rMyB10gPr0j3ctM4k3ItL0ng3rTh4n32Byt3s!!} + access-token-validity: 1800000 # 30๋ถ„ + refresh-token-validity: 604800000 # 7์ผ \ No newline at end of file