diff --git a/README.md b/README.md new file mode 100644 index 0000000..f3dcdd1 --- /dev/null +++ b/README.md @@ -0,0 +1,209 @@ +
+ +# ๐Ÿงน LinClean API + +**๋งํฌ ์ €์žฅ ยท AI ๋ถ„์„ ๋ชจ๋ฐ”์ผ ์„œ๋น„์Šค์˜ ๋ฐฑ์—”๋“œ REST API ์„œ๋ฒ„** + +[![Java](https://img.shields.io/badge/Java-21-007396?style=flat-square&logo=openjdk&logoColor=white)](https://openjdk.org/projects/jdk/21/) +[![Spring Boot](https://img.shields.io/badge/Spring%20Boot-3.5.13-6DB33F?style=flat-square&logo=springboot&logoColor=white)](https://spring.io/projects/spring-boot) +[![PostgreSQL](https://img.shields.io/badge/PostgreSQL-336791?style=flat-square&logo=postgresql&logoColor=white)](https://www.postgresql.org/) +[![Redis](https://img.shields.io/badge/Redis%20(Valkey)-DC382D?style=flat-square&logo=redis&logoColor=white)](https://valkey.io/) +[![Gradle](https://img.shields.io/badge/Gradle-8.14.4-02303A?style=flat-square&logo=gradle&logoColor=white)](https://gradle.org/) +[![Docker](https://img.shields.io/badge/Docker-2496ED?style=flat-square&logo=docker&logoColor=white)](https://www.docker.com/) + +[![CI](https://github.com/403-Forb2dden/LinClean-BE-spring/actions/workflows/CI.yml/badge.svg)](https://github.com/403-Forb2dden/LinClean-BE-spring/actions/workflows/CI.yml) +[![CD](https://github.com/403-Forb2dden/LinClean-BE-spring/actions/workflows/CD.yml/badge.svg)](https://github.com/403-Forb2dden/LinClean-BE-spring/actions/workflows/CD.yml) + +
+ +--- + +## ๐Ÿ“– ํ”„๋กœ์ ํŠธ ์†Œ๊ฐœ + +**LinClean**์€ ์‚ฌ์šฉ์ž๊ฐ€ ์ €์žฅํ•œ ๋งํฌ(URL)๋ฅผ ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„๋กœ ๊ด€๋ฆฌํ•˜๊ณ , AI ๋ถ„์„ ์—”์ง„์„ ํ†ตํ•ด ๋งํฌ๋ฅผ ๋ถ„์„ยท๋ถ„๋ฅ˜ํ•ด ์ฃผ๋Š” ๋ชจ๋ฐ”์ผ ์„œ๋น„์Šค์ž…๋‹ˆ๋‹ค. + +์ด ๋ ˆํฌ์ง€ํ† ๋ฆฌ(`linclean-api`)๋Š” ๊ทธ์ค‘ **๋ฐฑ์—”๋“œ REST API ์„œ๋ฒ„**๋กœ, ๋ชจ๋ฐ”์ผ ์•ฑ(Expo)์˜ ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•˜๊ณ  ํšŒ์›ยท๋งํฌยท๋ถ„์„ยท์•ฝ๊ด€ยท๊ณต์ง€ ๋„๋ฉ”์ธ์„ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค. ์ธ์ฆ์€ **Clerk(OAuth2 JWT)** ๋กœ ์œ„์ž„ํ•˜๊ณ , ๋งํฌ ๋ถ„์„์€ ๋ณ„๋„์˜ **FastAPI ๋ถ„์„ ์—”์ง„**๊ณผ ๋น„๋™๊ธฐ๋กœ ์—ฐ๋™ํ•ฉ๋‹ˆ๋‹ค. + +## โœจ ์ฃผ์š” ๊ธฐ๋Šฅ + +- **๐Ÿ”— ๋งํฌ ๊ด€๋ฆฌ** โ€” ๋งํฌ ์ €์žฅ/์กฐํšŒ/์‚ญ์ œ, ๋ถ๋งˆํฌยท์นดํ…Œ๊ณ ๋ฆฌยท์ œ๋ชฉ ์ˆ˜์ •, ์ค‘๋ณต ํ™•์ธ +- **๐Ÿ—‚๏ธ ์นดํ…Œ๊ณ ๋ฆฌ ๊ด€๋ฆฌ** โ€” ์นดํ…Œ๊ณ ๋ฆฌ ์ƒ์„ฑ/์กฐํšŒ/์ˆ˜์ •/์‚ญ์ œ +- **๐Ÿค– AI ๋ถ„์„** โ€” ๋ถ„์„ ์š”์ฒญ, ๋ถ„์„ ๊ฒฐ๊ณผ ์กฐํšŒ, ๋ถ„์„ ํ†ต๊ณ„ ์ง‘๊ณ„ (FastAPI ๋ถ„์„ ์—”์ง„๊ณผ ๋น„๋™๊ธฐ ์—ฐ๋™ + ๋‚ด๋ถ€ ์ฝœ๋ฐฑ ์ˆ˜์‹ ) +- **๐Ÿ‘ค ํšŒ์› ๊ด€๋ฆฌ** โ€” Clerk ๊ธฐ๋ฐ˜ ํšŒ์› ๋™๊ธฐํ™”, ํšŒ์› ํƒˆํ‡ด ์‹œ *Soft Delete โ†’ ๋ณด์กด ๊ธฐ๊ฐ„ ๊ฒฝ๊ณผ ํ›„ Hard Delete* ์Šค์ผ€์ค„๋ง +- **๐Ÿ“œ ์ด์šฉ์•ฝ๊ด€ / ๐Ÿ“ข ๊ณต์ง€์‚ฌํ•ญ** โ€” ์•ฝ๊ด€ ํƒ€์ž…๋ณ„ ์กฐํšŒ, ๊ณต์ง€ ๋ชฉ๋กยท์ƒ์„ธ ์กฐํšŒ +- **๐Ÿ” ์ธ์ฆยท๋ณด์•ˆ** โ€” Clerk OAuth2 JWT ๊ฒ€์ฆ, ๋‚ด๋ถ€ ํ†ต์‹ ์šฉ API Key ํ•„ํ„ฐ(`InternalApiKeyFilter`) +- **๐Ÿ“Š ๋กœ๊น…ยท๋ชจ๋‹ˆํ„ฐ๋ง** โ€” Logback + Logstash JSON ํŒŒ์ผ ๋กœ๊น…, ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ **Discord ์›นํ›… ์•Œ๋ฆผ**, Actuator ํ—ฌ์Šค ์ฒดํฌ + +## ๐Ÿ› ๏ธ ๊ธฐ์ˆ  ์Šคํƒ + +| ๊ตฌ๋ถ„ | ๊ธฐ์ˆ  | +| --- | --- | +| **Language** | Java 21 (LTS) | +| **Framework** | Spring Boot 3.5.13, Spring MVC (Spring Web), Spring Security | +| **HTTP Client** | Spring WebClient (์™ธ๋ถ€ ๋ถ„์„ ์—”์ง„ยทClerk API ํ˜ธ์ถœ์šฉ) | +| **Auth** | OAuth2 Resource Server (JWT) ยท Clerk | +| **Database** | PostgreSQL, Spring Data JPA (Hibernate), Flyway (๋งˆ์ด๊ทธ๋ ˆ์ด์…˜) | +| **Cache** | Redis (Valkey ํ˜ธํ™˜), Spring Data Redis | +| **API Docs** | SpringDoc OpenAPI 2.8.6 (Swagger UI) | +| **Logging** | Logback + Logstash Logback Encoder (JSON), Discord Webhook | +| **Build** | Gradle 8.14.4 | +| **Test** | JUnit 5, Spring Boot Test, Testcontainers (PostgreSQL), OkHttp MockWebServer | +| **Infra** | Docker (๋ฉ€ํ‹ฐ์Šคํ…Œ์ด์ง€ ๋นŒ๋“œ), GitHub Actions (CI/CD), AWS EC2 + SSM | + +## ๐Ÿ—๏ธ ์‹œ์Šคํ…œ ์•„ํ‚คํ…์ฒ˜ + +```mermaid +flowchart LR + App["๐Ÿ“ฑ ๋ชจ๋ฐ”์ผ ์•ฑ
(Expo)"] + + subgraph LinClean API["๐Ÿงน LinClean API (Spring Boot)"] + direction TB + Sec["Security
(Clerk JWT ๊ฒ€์ฆ / Internal API Key)"] + Domain["Domain Layer
analysis ยท link ยท member
notice ยท terms"] + Sec --> Domain + end + + PG[("๐Ÿ˜ PostgreSQL")] + Redis[("๐Ÿ”ด Redis
(Valkey)")] + Clerk["๐Ÿ” Clerk
(JWKS / ์ธ์ฆ)"] + Engine["๐Ÿค– FastAPI
๋ถ„์„ ์—”์ง„"] + + App -->|REST API + JWT| LinClean + Domain --> PG + Domain --> Redis + Sec -.JWKS ๊ฒ€์ฆ.-> Clerk + Domain -.๋ถ„์„ ์š”์ฒญ (๋น„๋™๊ธฐ).-> Engine + Engine -.๋ถ„์„ ๊ฒฐ๊ณผ ์ฝœ๋ฐฑ.-> Sec +``` + +## ๐Ÿ“‚ ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ + +``` +src/main/java/com/linclean +โ”œโ”€โ”€ LincleanApiApplication.java # ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ง„์ž…์  +โ”œโ”€โ”€ domain # ๋น„์ฆˆ๋‹ˆ์Šค ๋„๋ฉ”์ธ (controllerยทserviceยทentityยทdtoยทrepository) +โ”‚ โ”œโ”€โ”€ analysis # ๋งํฌ ๋ถ„์„ ์š”์ฒญยท๊ฒฐ๊ณผยทํ†ต๊ณ„ +โ”‚ โ”œโ”€โ”€ link # ์ €์žฅ ๋งํฌ & ์นดํ…Œ๊ณ ๋ฆฌ +โ”‚ โ”œโ”€โ”€ member # ํšŒ์› ๊ด€๋ฆฌยทํƒˆํ‡ด ์Šค์ผ€์ค„๋ง +โ”‚ โ”œโ”€โ”€ notice # ๊ณต์ง€์‚ฌํ•ญ +โ”‚ โ”œโ”€โ”€ terms # ์ด์šฉ์•ฝ๊ด€ +โ”‚ โ”œโ”€โ”€ device # ๊ธฐ๊ธฐ ์ •๋ณด +โ”‚ โ””โ”€โ”€ notification # ์•Œ๋ฆผ +โ”œโ”€โ”€ security # ์ธ์ฆ ์ฃผ์ฒด(MemberPrincipal) / ์ธ์ฆ ์—”๋“œํฌ์ธํŠธ +โ””โ”€โ”€ global # ๊ณตํ†ต ๊ธฐ๋Šฅ + โ”œโ”€โ”€ config # SecurityยทRedisยทSwaggerยทAsyncยทWebClient ๋“ฑ ์„ค์ • + โ”œโ”€โ”€ entity # BaseEntity (Auditing) + โ”œโ”€โ”€ web # ๊ณตํ†ต ์‘๋‹ต ํฌ๋งท(ApiResponse) + โ”œโ”€โ”€ exception # ์ „์—ญ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ(GlobalExceptionHandlerยทErrorCode) + โ”œโ”€โ”€ log # JSON ๋กœ๊ทธ / Discord ์›นํ›… Appender + โ””โ”€โ”€ security # InternalApiKeyFilter (๋‚ด๋ถ€ ํ†ต์‹  ์ธ์ฆ) + +src/main/resources +โ”œโ”€โ”€ application.yml # ๋ฉ”์ธ ์„ค์ • (+ dev / prod / test ํ”„๋กœํ•„) +โ”œโ”€โ”€ logback-spring.xml # ๋กœ๊น… ์„ค์ • +โ””โ”€โ”€ db/migration # Flyway ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์Šคํฌ๋ฆฝํŠธ +``` + +## ๐Ÿš€ ์‹œ์ž‘ํ•˜๊ธฐ + +### ์‚ฌ์ „ ์š”๊ตฌ์‚ฌํ•ญ + +- **JDK 21** +- **PostgreSQL**, **Redis(Valkey)** โ€” ๋กœ์ปฌ ์„ค์น˜ ๋˜๋Š” Docker +- **Clerk** ์ธ์ฆ ํ‚ค, **FastAPI ๋ถ„์„ ์—”์ง„** ์—”๋“œํฌ์ธํŠธ (์™ธ๋ถ€ ์—ฐ๋™) + +### ํ™˜๊ฒฝ ๋ณ€์ˆ˜ + +๋ฃจํŠธ์— `.env` ํŒŒ์ผ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค (`application.yml`์ด `optional:file:.env`๋กœ ๋กœ๋“œ). + +| ํ‚ค | ์„ค๋ช… | +| --- | --- | +| `SPRING_PROFILES_ACTIVE` | ํ™œ์„ฑ ํ”„๋กœํ•„ (`dev` / `prod` / `test`) | +| `API_PORT` | ์„œ๋ฒ„ ํฌํŠธ | +| `POSTGRES_HOST` ยท `POSTGRES_PORT` ยท `POSTGRES_DATABASE` ยท `POSTGRES_USER` ยท `POSTGRES_PASSWORD` | PostgreSQL ์—ฐ๊ฒฐ ์ •๋ณด | +| `REDIS_HOST` ยท `REDIS_PORT` ยท `REDIS_PASSWORD` | Redis(Valkey) ์—ฐ๊ฒฐ ์ •๋ณด | +| `CLERK_JWKS_URI` ยท `CLERK_ISSUER` ยท `CLERK_SECRET_KEY` | Clerk OAuth2 JWT ์„ค์ • | +| `ANALYSIS_ENGINE_URL` | FastAPI ๋ถ„์„ ์—”์ง„ Base URL | +| `ANALYSIS_ENGINE_CONNECT_TIMEOUT_MS` ยท `ANALYSIS_ENGINE_READ_TIMEOUT_S` | ๋ถ„์„ ์—”์ง„ ํ†ต์‹  ํƒ€์ž„์•„์›ƒ (๊ธฐ๋ณธ 3000ms / 5s) | +| `INTERNAL_API_KEY` | ๋‚ด๋ถ€ ํ†ต์‹ (๋ถ„์„ ์—”์ง„ โ†” Spring) ์ธ์ฆ ํ‚ค | +| `MEMBER_WITHDRAWAL_RETENTION_DAYS` | ํƒˆํ‡ด ํšŒ์› ๋ณด์กด ๊ธฐ๊ฐ„ (๊ธฐ๋ณธ 30์ผ) | +| `MEMBER_WITHDRAWAL_HARD_DELETE_CRON` | Hard Delete ์Šค์ผ€์ค„ cron (๊ธฐ๋ณธ ๋งค์ผ 03:00) | +| `DISCORD_WEBHOOK_URL` | ์—๋Ÿฌ ๋กœ๊ทธ Discord ์•Œ๋ฆผ ์›นํ›… URL | + +### ๋นŒ๋“œ & ์‹คํ–‰ + +```bash +# ๋นŒ๋“œ +./gradlew build + +# ๋กœ์ปฌ ์‹คํ–‰ +./gradlew bootRun + +# ๋˜๋Š” JAR ์ง์ ‘ ์‹คํ–‰ +java -jar build/libs/linclean-api-0.0.1-SNAPSHOT.jar +``` + +### Docker ์‹คํ–‰ + +```bash +docker build -t linclean-api . +docker run -p 8080:8080 --env-file .env linclean-api +``` + +### Spring ํ”„๋กœํ•„ + +| ํ”„๋กœํ•„ | ์šฉ๋„ | +| --- | --- | +| `dev` | ๋กœ์ปฌ ๊ฐœ๋ฐœ (SQL ๋กœ๊น…ยทSwagger ํ™œ์„ฑ, ๊ธฐ๋ณธ๊ฐ’) | +| `prod` | ์šด์˜ (Graceful shutdown ๋“ฑ) | +| `test` | ํ…Œ์ŠคํŠธ (Testcontainers ๊ธฐ๋ฐ˜) | + +## ๐Ÿ“‘ API ๋ฌธ์„œ + +์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์‹คํ–‰ ํ›„ Swagger UI์—์„œ ์ „์ฒด API ๋ช…์„ธ๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. + +- **Swagger UI**: `http://localhost:{API_PORT}/swagger-ui.html` +- **OpenAPI Docs**: `http://localhost:{API_PORT}/v3/api-docs` + +### ์ฃผ์š” ์—”๋“œํฌ์ธํŠธ + +| ๋„๋ฉ”์ธ | Method & Path | ์„ค๋ช… | +| --- | --- | --- | +| ์ธ์ฆ | `GET /api/v1/auth/me` | ๋‚ด ์ธ์ฆ ์ •๋ณด ์กฐํšŒ | +| ๋ถ„์„ | `POST /api/v1/analyses` | ๋งํฌ ๋ถ„์„ ์š”์ฒญ | +| ๋ถ„์„ | `GET /api/v1/analyses/statistics` | ๋ถ„์„ ํ†ต๊ณ„ ์กฐํšŒ | +| ๋ถ„์„ | `GET /api/v1/analyses/{analysisId}` | ๋ถ„์„ ๊ฒฐ๊ณผ ์กฐํšŒ | +| ๋งํฌ | `POSTยทGET /api/v1/saved-links` | ๋งํฌ ์ €์žฅ / ๋ชฉ๋ก ์กฐํšŒ | +| ๋งํฌ | `GET /api/v1/saved-links/check` | ๋งํฌ ์ค‘๋ณต ํ™•์ธ | +| ๋งํฌ | `PATCH /api/v1/saved-links/{id}/{bookmark\|category\|title}` | ๋ถ๋งˆํฌยท์นดํ…Œ๊ณ ๋ฆฌยท์ œ๋ชฉ ์ˆ˜์ • | +| ๋งํฌ | `DELETE /api/v1/saved-links/{id}` | ๋งํฌ ์‚ญ์ œ | +| ์นดํ…Œ๊ณ ๋ฆฌ | `POSTยทGET /api/v1/categories` | ์นดํ…Œ๊ณ ๋ฆฌ ์ƒ์„ฑ / ๋ชฉ๋ก ์กฐํšŒ | +| ์นดํ…Œ๊ณ ๋ฆฌ | `PATCHยทDELETE /api/v1/categories/{id}` | ์นดํ…Œ๊ณ ๋ฆฌ ์ˆ˜์ • / ์‚ญ์ œ | +| ํšŒ์› | `DELETE /api/v1/members/me` | ํšŒ์› ํƒˆํ‡ด | +| ์•ฝ๊ด€ | `GET /api/v1/terms/{type}` | ์•ฝ๊ด€ ํƒ€์ž…๋ณ„ ์กฐํšŒ | +| ๊ณต์ง€ | `GET /api/v1/notices` ยท `GET /api/v1/notices/{id}` | ๊ณต์ง€ ๋ชฉ๋ก / ์ƒ์„ธ ์กฐํšŒ | +| ํ—ฌ์Šค | `GET /actuator/health` | ํ—ฌ์Šค ์ฒดํฌ | + +> `POST /internal/analysis-result` ๋“ฑ `/internal/**` ๊ฒฝ๋กœ๋Š” ๋ถ„์„ ์—”์ง„๊ณผ์˜ ๋‚ด๋ถ€ ํ†ต์‹  ์ „์šฉ์œผ๋กœ, `INTERNAL_API_KEY`๋กœ ๋ณดํ˜ธ๋ฉ๋‹ˆ๋‹ค. + +## ๐Ÿงช ํ…Œ์ŠคํŠธ + +```bash +./gradlew test +``` + +> ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ๋Š” **Testcontainers(PostgreSQL)** ๋ฅผ ์‚ฌ์šฉํ•˜๋ฏ€๋กœ ์‹คํ–‰ ํ™˜๊ฒฝ์— **Docker**๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. + +## ๐Ÿ”„ CI/CD + +GitHub Actions๋กœ ๋นŒ๋“œยท๋ฐฐํฌ๋ฅผ ์ž๋™ํ™”ํ•ฉ๋‹ˆ๋‹ค. + +- **CI** (`.github/workflows/CI.yml`) โ€” `dev`/`main` push ๋ฐ `dev` ๋Œ€์ƒ PR์—์„œ Gradle ํ…Œ์ŠคํŠธ ์‹คํ–‰ +- **CD** (`.github/workflows/CD.yml`) โ€” `main` CI ์„ฑ๊ณต ์‹œ Docker ์ด๋ฏธ์ง€๋ฅผ ๋นŒ๋“œํ•ด Docker Hub์— ํ‘ธ์‹œํ•˜๊ณ , **AWS SSM**์œผ๋กœ EC2์—์„œ `docker compose`๋ฅผ ํ†ตํ•ด ๋ฌด์ค‘๋‹จ ๋ฐฐํฌ + +> ๋ฐฐํฌ ์•„ํ‚คํ…์ฒ˜ ์ƒ์„ธ๋Š” [`docs/๋ฐฐํฌ.md`](docs/๋ฐฐํฌ.md)๋ฅผ ์ฐธ๊ณ ํ•˜์„ธ์š”. + +## ๐Ÿ“ ์ปจ๋ฒค์…˜ + +- **์ปค๋ฐ‹ ์ปจ๋ฒค์…˜**: [`docs/commit.md`](docs/commit.md) ์ฐธ๊ณ  +- **๋ธŒ๋žœ์น˜ ์ „๋žต**: ๊ธฐ๋Šฅ ๋ธŒ๋žœ์น˜ โ†’ `dev`(PR base) โ†’ `main`(๋ฐฐํฌ)