From 237afddc805db058302cd937eb653907fbd74345 Mon Sep 17 00:00:00 2001 From: GoGradually Date: Tue, 23 Dec 2025 21:28:14 +0900 Subject: [PATCH] =?UTF-8?q?docs:=20Pinit=20=EC=95=8C=EB=A6=BC=20=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4=EC=97=90=20=EB=8C=80=ED=95=9C=20README=20?= =?UTF-8?q?=EB=AC=B8=EC=84=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 117 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..89c38c8 --- /dev/null +++ b/README.md @@ -0,0 +1,117 @@ +# Pinit Notification ๐Ÿ›Ž๏ธ + +> **ํ•€์ž‡ ์•Œ๋ฆผ ๋„๋ฉ”์ธ**์„ ์ „๋‹ดํ•˜๋Š” ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค. ์ผ์ •(Task) ์„œ๋น„์Šค์™€ ๋ถ„๋ฆฌ๋˜์–ด ์Šค์ผ€์ค„ ์ด๋ฒคํŠธ๋ฅผ ๋ฐ›์•„ ์‚ฌ์šฉ์ž์—๊ฒŒ ํ‘ธ์‹œ ์•Œ๋ฆผ์„ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค. + +--- + +## ๐ŸŽฏ Purpose + +### 1) ์•Œ๋ฆผ ๋ฐ”์šด๋””๋“œ ์ปจํ…์ŠคํŠธ ๋ถ„๋ฆฌ + +- ์ผ์ •/์ž‘์—… ๋„๋ฉ”์ธ(`task-service`)๊ณผ ๋ถ„๋ฆฌ๋œ **๋…๋ฆฝ ์„œ๋น„์Šค**๋กœ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค. +- ์Šค์ผ€์ค„ ๋ณ€๊ฒฝ์— ๋”ฐ๋ฅธ ์•Œ๋ฆผ ์ƒ์„ฑ/์ทจ์†Œ/๊ฐฑ์‹ ์„ ์ด๊ณณ์—์„œ๋งŒ ์ฑ…์ž„์ง€๋ฉฐ, **ID/ํ† ํฐ ์ˆ˜์ค€**์œผ๋กœ๋งŒ ๋‹ค๋ฅธ ์„œ๋น„์Šค์™€ ์—ฐ๊ฒฐํ•ฉ๋‹ˆ๋‹ค. + +### 2) ์ด๋ฒคํŠธ ๋“œ๋ฆฌ๋ธ & ๋ฉฑ๋“ฑ ์ฒ˜๋ฆฌ + +- RabbitMQ `task.schedule.direct` ์ต์Šค์ฒด์ธ์ง€์—์„œ ์ด๋ฒคํŠธ๋ฅผ ์ˆ˜์‹ ํ•ด ์•Œ๋ฆผ์„ ์˜ˆ์•ฝํ•ฉ๋‹ˆ๋‹ค. +- `idempotentKey` + ๋ณตํ•ฉ ์œ ๋‹ˆํฌ ํ‚ค(์†Œ์œ ์ž, ์Šค์ผ€์ค„)๋กœ **์ค‘๋ณต ์˜ˆ์•ฝ์„ ๋ฐฉ์ง€**ํ•˜๊ณ  ์žฌ์ฒ˜๋ฆฌ๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค. + +### 3) ์ฑ„๋„ ํ™•์žฅ์„ฑ์„ ๊ณ ๋ คํ•œ ํ‘ธ์‹œ ๊ธฐ๋ฐ˜ + +- ๊ธฐ๋ณธ ์ฑ„๋„์€ **FCM ํ‘ธ์‹œ**์ด๋ฉฐ, ์›น ํ‘ธ์‹œ์šฉ **VAPID ๊ณต๊ฐœํ‚ค**๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. +- ์•Œ๋ฆผ ๋ฐ์ดํ„ฐ(`UpcomingScheduleNotification`)๋Š” ์ฑ„๋„์— ์˜์กดํ•˜์ง€ ์•Š๋Š” ๊ตฌ์กฐ๋กœ ์„ค๊ณ„๋ผ ์ด๋ฉ”์ผ/SMS ๋“ฑ ํ™•์žฅ์ด ์šฉ์ดํ•ฉ๋‹ˆ๋‹ค. + +--- + +## ๐Ÿงฑ Architecture Overview + +``` +Task Service (schedule) --gRPC--> Notification (schedule basics ์กฐํšŒ) +Task Service --RabbitMQ(task.schedule.direct)--> Notification --MySQL--> ์•Œ๋ฆผ ์ €์žฅ + \--FCM--> ์‚ฌ์šฉ์ž ๋””๋ฐ”์ด์Šค +``` + +- gRPC: ์Šค์ผ€์ค„ ์ œ๋ชฉ/์‹œ์ž‘์‹œ๊ฐ์„ ์›๋ณธ ์„œ๋น„์Šค์—์„œ ์กฐํšŒํ•˜์—ฌ ์•Œ๋ฆผ ํŽ˜์ด๋กœ๋“œ๋ฅผ ์‹ ๋ขฐ์„ฑ ์žˆ๊ฒŒ ๊ตฌ์„ฑ. +- RabbitMQ: ์Šค์ผ€์ค„ ์ƒํƒœ ๋ณ€๊ฒฝ(์—…๋ฐ์ดํŠธ/์‚ญ์ œ/์‹œ์ž‘/์ทจ์†Œ/์™„๋ฃŒ) ์ด๋ฒคํŠธ๋ฅผ ๋น„๋™๊ธฐ๋กœ ์ฒ˜๋ฆฌ. +- Scheduler: 10๋ถ„ ์ฃผ๊ธฐ(`0 */10 * * * *`)๋กœ ๋งŒ๋ฃŒ๋œ ์•Œ๋ฆผ์„ ์ฐพ์•„ ํ‘ธ์‹œ ์ „์†ก ํ›„ ์ •๋ฆฌ. + +--- + +## ๐Ÿงฉ Domain Model + +- `UpcomingScheduleNotification` + - ํ•„๋“œ: ownerId, scheduleId, scheduleTitle, scheduleStartTime(ISO-8601 ๋ฌธ์ž์—ด), idempotentKey + - ์—ญํ• : ํŠน์ • ์‹œ์ (์‹œ์ž‘ ์‹œ๊ฐ„)์— ๋„๋‹ฌํ•˜๋ฉด ๋ฐœ์†กํ•ด์•ผ ํ•˜๋Š” ์•Œ๋ฆผ ์˜ˆ์•ฝ. `isDue` ๋กœ ๋งŒ๊ธฐ ํŒ๋‹จ. + +- `PushSubscription` + - ํ•„๋“œ: memberId, token (FCM) + - ์—ญํ• : ํšŒ์›๋ณ„ ํ‘ธ์‹œ ์ฑ„๋„ ์ •๋ณด. ๋งŒ๋ฃŒ/UNREGISTERED ๋ฐœ์ƒ ์‹œ ํ† ํฐ์„ ์ •๋ฆฌํ•ฉ๋‹ˆ๋‹ค. + +- `Notification` ์ธํ„ฐํŽ˜์ด์Šค + - `getData()` ๋กœ ์ฑ„๋„์— ๋…๋ฆฝ์ ์ธ ๋ฐ์ดํ„ฐ ๋งต์„ ์ œ๊ณตํ•ด ํ–ฅํ›„ ์ฑ„๋„ ํ™•์žฅ ์‹œ ์žฌ์‚ฌ์šฉ. + +--- + +## ๐Ÿงฐ Tech Stack + +- Java 21, Gradle, Spring Boot 3.5 (Web, Security, Data JPA, Scheduler, Actuator) +- ๋ฉ”์‹œ์ง•: Spring AMQP + RabbitMQ +- ํ†ต์‹ : gRPC ํด๋ผ์ด์–ธํŠธ(`ScheduleGrpcServiceBlockingStub`)๋กœ ์Šค์ผ€์ค„ ๊ธฐ๋ณธ ์ •๋ณด ์กฐํšŒ +- Push: Firebase Admin SDK, ์›น ํ‘ธ์‹œ VAPID ํ‚ค +- DB: MySQL (ํ…Œ์ŠคํŠธ์šฉ H2), JPA +- ๋ฌธ์„œํ™”: springdoc-openapi / Swagger UI + +--- + +## ๐Ÿ”‘ Main Features + +1) **์Šค์ผ€์ค„ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ** + - ๋ผ์šฐํŒ…ํ‚ค: `schedule.time.upcoming.updated`, `schedule.deleted`, `schedule.state.started|canceled|completed` + - ์ด๋ฒคํŠธ๋งˆ๋‹ค ์•Œ๋ฆผ ์˜ˆ์•ฝ์„ ์ƒ์„ฑ/๊ฐฑ์‹ /์‚ญ์ œํ•ด ์‹ค์ œ ์Šค์ผ€์ค„ ์ƒํƒœ์™€ ์•Œ๋ฆผ์„ ๋™๊ธฐํ™”. + +2) **์•Œ๋ฆผ ์˜ˆ์•ฝ/๋ฐœ์†ก** + - ์˜ˆ์•ฝ: `ScheduleNotificationService` ๊ฐ€ ์ด๋ฒคํŠธ๋ฅผ ๋ฐ›์•„ `UpcomingScheduleNotification` ์ƒ์„ฑ. + - ๋ฐœ์†ก: `NotificationDispatchScheduler` ๊ฐ€ ๋งŒ๊ธฐ ์•Œ๋ฆผ์„ ์ฐพ๊ณ , ํšŒ์›์˜ ๋ชจ๋“  ํ† ํฐ์— ์ „์†ก ํ›„ ์ผ๊ด„ ์‚ญ์ œ. + +3) **ํ‘ธ์‹œ ๊ตฌ๋… ๊ด€๋ฆฌ** + - API: `/push/subscribe`, `/push/unsubscribe` ๋กœ FCM ํ† ํฐ ๋“ฑ๋ก/์‚ญ์ œ. + - `/push/vapid` ๋กœ ์›น ํ‘ธ์‹œ ๊ณต๊ฐœํ‚ค ์ œ๊ณต. + - UNREGISTERED ์˜ค๋ฅ˜ ์‹œ ํ† ํฐ ์ž๋™ ์ •๋ฆฌ. + +4) **๋ณด์•ˆ/์ธ์ฆ ์—ฐ๋™** + - JWT ํ•„ํ„ฐ(`JwtAuthenticationFilter`, `MemberIdArgumentResolver`)๋กœ ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž๋งŒ ๊ตฌ๋…/๋ฐœ์†ก ๊ฒฝ๋กœ ์ ‘๊ทผ. + - CORS ํ—ˆ์šฉ ๋„๋ฉ”์ธ๊ณผ ํ‚ค ๊ฒฝ๋กœ๋ฅผ ํ”„๋กœํŒŒ์ผ๋ณ„(`application-{dev,prod}.yml`)๋กœ ๋ถ„๋ฆฌ ๊ด€๋ฆฌ. + +--- + +## โš™๏ธ Configuration & Run + +ํ•„์ˆ˜ ํ™˜๊ฒฝ ๋ณ€์ˆ˜ (์˜ˆ์‹œ, dev ๊ธฐ์ค€): + +- DB ์ ‘์†: `DB_HOST`, `DB_PORT`, `DB_USERNAME`, `DB_PASSWORD` +- RabbitMQ: `RABBITMQ_HOST`, `RABBITMQ_PORT`, `RABBITMQ_USERNAME`, `RABBITMQ_PASSWORD` +- gRPC Task: `task.grpc.host`, `task.grpc.port` (yml๋กœ ์ฃผ์ž…) +- Firebase ํ‚ค ๊ฒฝ๋กœ: `${HOME}/pinit/keys/pinit-firebase-key.json` +- JWT ๊ณต๊ฐœํ‚ค ๊ฒฝ๋กœ: `${HOME}/pinit/keys/jwt-public-key.pem` +- VAPID: `VAPID_PRIVATE_KEY` (๊ณต๊ฐœํ‚ค๋Š” `application.yml`์— ์ •์˜) + +๋กœ์ปฌ ์‹คํ–‰: + +```bash +./gradlew bootRun --args='--spring.profiles.active=dev' +``` + +Swagger UI: + +``` +http://localhost:8082/swagger-ui.html +``` + +--- + +## ๐Ÿงญ Design Principles + +- **๋А์Šจํ•œ ๊ฒฐํ•ฉ**: ์•Œ๋ฆผ ๋„๋ฉ”์ธ์€ ์ด๋ฒคํŠธ/ID/ํ† ํฐ ๋‹จ์œ„๋กœ๋งŒ ๋‹ค๋ฅธ ์„œ๋น„์Šค์™€ ์—ฐ๊ฒฐ, ๋‚ด๋ถ€ ๋ชจ๋ธ์„ ๊ณต์œ ํ•˜์ง€ ์•Š์Œ. +- **๋ฉฑ๋“ฑ & ์‹ ๋ขฐ์„ฑ**: `idempotentKey` ๊ธฐ๋ฐ˜ ์ค‘๋ณต ๋ฐฉ์ง€, Durable ํ์™€ ์ผ๊ด„ ์‚ญ์ œ๋กœ ์ผ๊ด€์„ฑ ์œ ์ง€. +- **๊ด€์ฐฐ์„ฑ/์šด์˜**: Actuator ํ—ฌ์Šค์ฒดํฌ, ์Šค์ผ€์ค„๋Ÿฌ ๋กœ๊ทธ, UNREGISTERED ํ† ํฐ ์ž๋™ ์ •๋ฆฌ๋กœ ์šด์˜ ๋ถ€๋‹ด ๊ฐ์†Œ. +- **ํ™•์žฅ ๊ฐ€๋Šฅ ์ฑ„๋„**: Notification ์ธํ„ฐํŽ˜์ด์Šค์™€ ๋ฐ์ดํ„ฐ๋งต ๊ตฌ์กฐ๋กœ ์ด๋ฉ”์ผ/SMS ๊ฐ™์€ ์ฑ„๋„ ์ถ”๊ฐ€ ์šฉ์ด.