๊ฐ์กฑ ๋จ์ ๋์งํธ ์ฌ์ฉ๋ ๊ด๋ฆฌ ์๋น์ค๋ฅผ ์ํ Spring Boot ๋ฐฑ์๋์ ๋๋ค. ๊ณ ๊ฐ/๊ด๋ฆฌ์ ์ธ์ฆ, ๊ฐ์กฑ ๋ฐ ์ ์ฑ ๊ด๋ฆฌ, ๋ฏธ์ -๋ณด์, ์ด์์ ๊ธฐ, ์ฌ์ฉ๋ ์ง๊ณ, ์๊ฐ ๋ฆฌ์บก, ์ด๋ฏธ์ง ์ ๋ก๋, ์๋ฆผ ์ด๋ฒคํธ ๋ฐํ์ ๋ด๋นํฉ๋๋ค.
- Language: Java 21
- Framework: Spring Boot 3.4
- DB: MySQL, Redis
- Messaging: Kafka
- Storage: Cloudflare R2(S3 ํธํ)
- Docs: Springdoc OpenAPI, Swagger UI
- Observability: Actuator, Prometheus, OTLP(OpenTelemetry)
src/main/java/com/project ๊ธฐ์ค ์ฃผ์ ๊ตฌ์กฐ์
๋๋ค.
com.project
โโ Application.java
โโ common
โ โโ api.response # ๊ณตํต API ์๋ต ํฌ๋งท
โ โโ auth # JWT, ์ธํฐ์
ํฐ, ์ธ์ฆ ์ปจํ
์คํธ, ๊ถํ AOP
โ โโ config # Web, Redis, JPA, Swagger, R2, ์๊ฐ ์ค์
โ โโ exception # ๊ณตํต ์์ธ, ์๋ฌ ์ฝ๋, ์ ์ญ ์์ธ ์ฒ๋ฆฌ
โ โโ util # ๊ณตํต ์ ํธ๋ฆฌํฐ
โโ domain
โโ admin # ๊ด๋ฆฌ์ ์ธ์ฆ, ๋์๋ณด๋
โโ appeal # ์ ์ฑ
์ด์์ ๊ธฐ, ๋๊ธ, ๊ธด๊ธ ์ฟผํฐ
โโ customer # ๊ณ ๊ฐ ์ธ์ฆ, ๋ง์ดํ์ด์ง, ๋ด ์ ๋ณด
โโ eventoutbox # ์๋ฆผ ์ด๋ฒคํธ outbox ์ ์ฅ ๋ฐ Kafka ๋ฐํ
โโ family # ๊ฐ์กฑ ์ ๋ณด, ๊ตฌ์ฑ์, ๊ด๋ฆฌ์์ฉ ๊ฐ์กฑ ๊ด๋ฆฌ
โโ mission # ๋ฏธ์
์์ฑ/์กฐํ/์์ฒญ/๋ก๊ทธ/์ด๋ ฅ
โโ policy # ์ ์ฑ
ํ
ํ๋ฆฟ, ๊ฐ์กฑ ์ ์ฑ
์กฐํ/์์
โโ recap # ์๊ฐ ๊ฐ์กฑ ๋ฆฌ์บก ์กฐํ
โโ reward # ๋ณด์ ํ
ํ๋ฆฟ, ๋ณด์ ์น์ธ/์ง๊ธ/์๋ น ์ด๋ ฅ
โโ upload # ๊ด๋ฆฌ์ ์ด๋ฏธ์ง ์
๋ก๋
โโ usagerecord # ๊ฐ์กฑ/๊ตฌ์ฑ์ ์ฌ์ฉ๋ ์ง๊ณ
๋๋ฉ์ธ ํจํค์ง๋ ๋์ฒด๋ก ์๋ ๋ ์ด์ด๋ฅผ ๋ฐ๋ฆ ๋๋ค.
controller: HTTP API ์ง์ ์ service: ๋น์ฆ๋์ค ๋ก์งrepository: DB ์กฐํ/์ ์ฅentity: JPA ์ํฐํฐdto: ์์ฒญ/์๋ต DTOmodel: ์๋น์ค ๋ฐํ ๋ชจ๋ธenums: ๋๋ฉ์ธ ์ํ๊ฐ
- ๊ณ ๊ฐ: ํ์๊ฐ์ , ๋ก๊ทธ์ธ, ํ ํฐ ์ฌ๋ฐ๊ธ, ๋ด ์ ๋ณด, ๋ง์ดํ์ด์ง ์กฐํ
- ๊ด๋ฆฌ์: ํ์๊ฐ์ , ๋ก๊ทธ์ธ, ํ ํฐ ์ฌ๋ฐ๊ธ, ๋ด ์ ๋ณด, ๋์๋ณด๋ ์กฐํ
- JWT ๊ธฐ๋ฐ ์ธ์ฆ์ด๋ฉฐ ๋๋ถ๋ถ์ API๋
LoginInterceptor์์ ํ ํฐ์ ๊ฒ์ฆํฉ๋๋ค. - ๊ด๋ฆฌ์ ์ ์ฉ API๋
@AdminOnly, ๋ณดํธ์ ์ ์ฉ API๋@OwnerOnly๋ก ์ ์ดํฉ๋๋ค.
flowchart TD
A[Client] --> B[Controller]
B --> C[Service]
C --> D{์ฌ์ฉ์ ์กด์ฌ ๋ฐ ๋น๋ฐ๋ฒํธ ์ผ์น}
D -- No --> E[ApplicationException]
D -- Yes --> F[์ญํ ์กฐํ]
F --> G[Access/Refresh Token ๋ฐ๊ธ]
G --> H[ApiResponse.success]
- ๋ณดํธ์๋ ๊ฐ์กฑ ์ด๋ฆ์ ๋ณ๊ฒฝํ๊ณ ๊ฐ์กฑ ๊ตฌ์ฑ์์ ์กฐํํ ์ ์์ต๋๋ค.
- ๋ณดํธ์๋ ๊ฐ์กฑ ๊ตฌ์ฑ์๋ณ ์ ์ฑ ์ ์์ ํ ์ ์์ต๋๋ค.
- ์ ์ฑ ๋ณ๊ฒฝ ์ DB ๋ฐ์๋ฟ ์๋๋ผ Redis ์ ์ฑ ๋๊ธฐํ์ ์๋ฆผ ๋ฐํ์ด ํจ๊ป ์ํ๋ฉ๋๋ค.
- ์๊ฐ ์ ํ ์ ์ฑ
์
CustomerQuota๋ฅผ ํจ๊ป ๊ฐฑ์ ํ๊ณ , ์๋ ์ฐจ๋จ ์ ์ฑ ์ ์ฐจ๋จ ์ํ๊น์ง ๋ฐ์ํฉ๋๋ค.
sequenceDiagram
participant Owner as Owner
participant Controller as FamilyPolicyController
participant Service as FamilyPolicyService
participant Assignment as PolicyAssignmentRepository
participant Redis as PolicyRedisService
participant Quota as CustomerQuotaRepository
participant Outbox as NotificationOutboxPublisher
Owner->>Controller: PATCH /families/policies
Controller->>Service: updateMemberPolicy(...)
Service->>Service: OWNER ๊ถํ ๊ฒ์ฆ
Service->>Assignment: ์ ์ฑ
ํ ๋น ์กฐํ
Assignment-->>Service: PolicyAssignment
Service->>Assignment: ์ ์ฑ
๋ณ๊ฒฝ ์ ์ฅ
Service->>Redis: Redis ์ ์ฑ
๋๊ธฐํ
alt MONTHLY_LIMIT
Service->>Quota: ์๊ฐ ํ๋ ๊ฐฑ์
else MANUAL_BLOCK
Service->>Quota: ์ฐจ๋จ/ํด์ ์ํ ๋ฐ์
else TIME_BLOCK / APP_BLOCK
Service->>Service: ์๋ฆผ ๋ฉ์์ง ์ค๋น
end
Service->>Outbox: ์๋ฆผ ์ด๋ฒคํธ ์ ์ฌ
Outbox-->>Controller: ์๋ฃ
Controller-->>Owner: ApiResponse.success
- ๋ณดํธ์๋ ๋ฏธ์ ์ ์์ฑํ๊ณ ์ทจ์ํ ์ ์์ต๋๋ค.
- ๋ฉค๋ฒ๋ ์์ ์๊ฒ ํ ๋น๋ ๋ฏธ์ ์ ๋ํด ์๋ฃ ์์ฒญ์ ์์ฑํ ์ ์์ต๋๋ค.
- ๋ณดํธ์๋ ์์ฒญ์ ์น์ธ/๊ฑฐ์ ํ ์ ์๊ณ , ์น์ธ ์
RewardGrant๊ฐ ๋ฐ๊ธ๋ฉ๋๋ค. - ๋ฏธ์ /๋ณด์ ํ๋ฆ์ ๋ก๊ทธ ์ ์ฅ๊ณผ ์๋ฆผ ๋ฐํ์ ํจ๊ป ์ํํฉ๋๋ค.
sequenceDiagram
participant Owner as Owner
participant Member as Member
participant Mission as MissionService
participant Reward as RewardService
participant MissionRepo as MissionItemRepository
participant RequestRepo as MissionRequestRepository
participant GrantRepo as RewardGrantRepository
participant LogRepo as MissionLogRepository
participant Outbox as NotificationOutboxPublisher
Owner->>Mission: ๋ฏธ์
์์ฑ ์์ฒญ
Mission->>Mission: ๋์ ๋ฉค๋ฒ/๊ฐ์กฑ ๊ฒ์ฆ
Mission->>MissionRepo: MissionItem ์ ์ฅ
Mission->>LogRepo: MISSION_CREATED ๋ก๊ทธ ์ ์ฅ
Mission->>Outbox: ๋ฏธ์
์์ฑ ์๋ฆผ ์ ์ฌ
Member->>Mission: ๋ฏธ์
์๋ฃ ์์ฒญ
Mission->>Mission: ๋์์/์ํ/์ค๋ณต ์์ฒญ ๊ฒ์ฆ
Mission->>RequestRepo: MissionRequest ์ ์ฅ
Mission->>LogRepo: MISSION_REQUESTED ๋ก๊ทธ ์ ์ฅ
Mission->>Outbox: ๋ณดํธ์ ์๋ฆผ ์ ์ฌ
Owner->>Reward: ๋ณด์ ์น์ธ/๊ฑฐ์
Reward->>RequestRepo: MissionRequest ์ ๊ธ ์กฐํ
Reward->>MissionRepo: MissionItem ์ ๊ธ ์กฐํ
alt ์น์ธ
Reward->>GrantRepo: RewardGrant ๋ฐ๊ธ
Reward->>LogRepo: MISSION_COMPLETED ๋ก๊ทธ ์ ์ฅ
Reward->>Outbox: ์น์ธ ์๋ฆผ ์ ์ฌ
else ๊ฑฐ์
Reward->>RequestRepo: rejectReason ์ ์ฅ
Reward->>Outbox: ๊ฑฐ์ ์๋ฆผ ์ ์ฌ
end
- ๋ฉค๋ฒ๋ ๋ณธ์ธ์๊ฒ ์ ์ฉ๋ ์ ์ฑ ์ ๋ํด ์ด์์ ๊ธฐ๋ฅผ ์์ฑํ ์ ์์ต๋๋ค.
- ๋ณดํธ์๋ ์ด์์ ๊ธฐ๋ฅผ ์น์ธ/๊ฑฐ์ ํ ์ ์์ผ๋ฉฐ ์น์ธ ์ ์ ์ฑ ์ ์ฉ๊ฐ์ด ์ฆ์ ๋ฐ์๋ฉ๋๋ค.
- ๋ฉค๋ฒ๋ ์ 1ํ ๊ธด๊ธ ์ฟผํฐ๋ฅผ ์์ฒญํ ์ ์๊ณ , ์น์ธ ์ฆ์ ๊ฐ์ธ ์๊ฐ ์ ํ๋์ด ์ฆ๊ฐํฉ๋๋ค.
sequenceDiagram
participant Member as Member
participant Owner as Owner
participant Controller as AppealController
participant Service as AppealService
participant AppealRepo as PolicyAppealRepository
participant AssignmentRepo as PolicyAssignmentRepository
participant QuotaRepo as CustomerQuotaRepository
participant Outbox as NotificationOutboxPublisher
Member->>Controller: ์ด์์ ๊ธฐ ์์ฑ
Controller->>Service: createAppeal(...)
Service->>Service: MEMBER ๊ถํ ๋ฐ ์์ ๊ฒ์ฆ
Service->>AssignmentRepo: PolicyAssignment ์กฐํ
Service->>AppealRepo: ์ค๋ณต PENDING ๊ฒ์ฌ
Service->>AppealRepo: PolicyAppeal ์ ์ฅ
Service->>Outbox: ๋ณดํธ์ ์๋ฆผ ์ ์ฌ
Owner->>Controller: ์ด์์ ๊ธฐ ์๋ต
Controller->>Service: respondAppeal(...)
Service->>AppealRepo: Appeal ์กฐํ
alt ์น์ธ
Service->>AssignmentRepo: ์ ์ฑ
rules/isActive ๋ฐ์
Service->>Outbox: ์น์ธ ์๋ฆผ ์ ์ฌ
else ๊ฑฐ์
Service->>AppealRepo: rejectReason ์ ์ฅ
Service->>Outbox: ๊ฑฐ์ ์๋ฆผ ์ ์ฌ
end
Member->>Controller: ๊ธด๊ธ ์ฟผํฐ ์์ฒญ
Controller->>Service: requestEmergencyQuota(...)
Service->>QuotaRepo: ์ด๋ฒ ๋ฌ CustomerQuota ์กฐํ
Service->>AppealRepo: EMERGENCY appeal ์ ์ฅ
Service->>QuotaRepo: ์๊ฐ ํ๋ ์ฆ๊ฐ
Service->>AssignmentRepo: MONTHLY_LIMIT ์ ์ฑ
๋๊ธฐํ
Service->>Outbox: ๋ณดํธ์ ์๋ฆผ ์ ์ฌ
- ๊ฐ์กฑ ํ์ฌ ์ด ์ฌ์ฉ๋, ๊ฐ์กฑ ๊ตฌ์ฑ์๋ณ ์๊ฐ ์ฌ์ฉ๋ ์์ฝ, ๋์๋ณด๋์ฉ ์์ธ ๋ถํฌ๋ฅผ ์ ๊ณตํฉ๋๋ค.
- ์๊ฐ ๋ฆฌ์บก์ DB์ ์ ์ฅ๋ ์ค๋ ์ท JSON์ ์ญ์ง๋ ฌํํด ์๋ตํฉ๋๋ค.
flowchart TD
A[Client] --> B{์กฐํ ๋์}
B -- ๊ฐ์กฑ ์ฌ์ฉ๋ --> C[FamilyQuota/๊ตฌ์ฑ์ ์ฌ์ฉ๋ ์กฐํ]
B -- ์๊ฐ ๋ฆฌ์บก --> D[FamilyRecapMonthly ์กฐํ]
D --> E[JSON Snapshot ์ญ์ง๋ ฌํ]
C --> F[์๋ต ๋ชจ๋ธ ์กฐํฉ]
E --> F
F --> G[ApiResponse.success]
- ๋ฏธ์ ์์ฑ, ๋ณด์ ์์ฒญ/์น์ธ, ์ ์ฑ ๋ณ๊ฒฝ, ์ด์์ ๊ธฐ ์์ฑ/์๋ต ๋ฑ ์ฃผ์ ์ด๋ฒคํธ๋ ๋ฐ๋ก Kafka๋ก๋ง ๋ณด๋ด์ง ์๊ณ Outbox ํ ์ด๋ธ์ ๋จผ์ ์ ์ฅํฉ๋๋ค.
- ํธ๋์ญ์
์ปค๋ฐ ์ดํ
PUBLISH_PENDING์ํ์ ์ด๋ฒคํธ๋ฅผ ๋ฐํํ๊ณ , ์ฑ๊ณต ์SENT๋ก ๋งํนํฉ๋๋ค. - ๋ฐํ ์คํจ ์ row๋ฅผ ์ ์งํ๋ฏ๋ก ์ฌ์๋ ์ ๋ต์ ๋ถ์ด๊ธฐ ์ข์ ๊ตฌ์กฐ์ ๋๋ค.
sequenceDiagram
participant Domain as Domain Service
participant Publisher as NotificationOutboxPublisher
participant OutboxRepo as EventOutboxRepository
participant Kafka as Kafka
participant Status as EventOutboxStatusService
Domain->>Publisher: enqueueAndPublishAfterCommit(payload)
Publisher->>OutboxRepo: outbox insert(PUBLISH_PENDING)
alt ํธ๋์ญ์
๋ด๋ถ
Publisher->>Publisher: afterCommit ํ
๋ฑ๋ก
Publisher->>Kafka: ์ปค๋ฐ ํ ๋ฐํ ์๋
else ํธ๋์ญ์
์ธ๋ถ
Publisher->>Kafka: ์ฆ์ ๋ฐํ ์๋
end
alt ๋ฐํ ์ฑ๊ณต
Publisher->>Status: markSent(outboxId)
else ๋ฐํ ์คํจ
Publisher->>Publisher: PUBLISH_PENDING ์ ์ง
end
- ์ ์ฑ
์์ ์ ๋จ์ํ
policy_assignment๋ง ๋ฐ๊พธ์ง ์์ต๋๋ค. FamilyPolicyServiceImpl์์ DB ์ ์ฅ ํ Redis ์ ์ฑ ์ ๋๊ธฐํํฉ๋๋ค.- ์ ์ฑ
ํ์
์ ๋ฐ๋ผ
customer_quota๊น์ง ํจ๊ป ๋ณ๊ฒฝํฉ๋๋ค. - ๋ง์ง๋ง์ผ๋ก ์ฌ์ฉ์์๊ฒ ์๋ฆผ ์ด๋ฒคํธ๋ฅผ ๋ฐํํฉ๋๋ค.
flowchart LR
A[์ ์ฑ
์์ ์์ฒญ] --> B[DB ์ ์ฅ]
B --> C[Redis ์ ์ฑ
๋๊ธฐํ]
C --> D[Quota ์ํ ๋ฐ์]
D --> E[์๋ฆผ Outbox ์ ์ฌ]
missions,missions/logs,missions/history,appeals,rewards/received๋ฑ์ cursor ๊ธฐ๋ฐ ํ์ด์ง๋ค์ด์ ์ ์ฌ์ฉํฉ๋๋ค.- ๋ณดํต
size + 1๊ฐ๋ฅผ ์กฐํํ ๋คhasNext์nextCursor๋ฅผ ๊ณ์ฐํฉ๋๋ค. - ์ปค์ ํ์ฑ ์คํจ ์ ๋๋ฉ์ธ๋ณ ์์ธ ์ฝ๋๋ก ์๋ตํฉ๋๋ค.
flowchart TD
A[Request cursor,size] --> B[์ปค์ ํ์ฑ]
B --> C[size + 1 ์กฐํ]
C --> D{pageSize ์ด๊ณผ ์ฌ๋ถ}
D -- Yes --> E[nextCursor ๊ณ์ฐ]
D -- No --> F[nextCursor null]
E --> G[hasNext=true]
F --> H[hasNext=false]
- ๋๋ฉ์ธ ํธ๋์ญ์ ๊ณผ ๋ฉ์์ง ๋ฐํ์ ์์์ฑ์ ๋์จํ๊ฒ ๋ง์ถ๊ธฐ ์ํ ๊ตฌ์กฐ์ ๋๋ค.
- ๋จผ์ Outbox์ ์ ์ฌํ๊ณ ์ปค๋ฐ ์ดํ ๋ฐํํฉ๋๋ค.
- ์ฌ์๋ ๊ฐ๋ฅ์ฑ์ ์ํด ์คํจ ์ ์ํ๋ฅผ ์ ์งํฉ๋๋ค.
FamilyRecapMonthly๋ ์์ผ๋ณ ์ฌ์ฉ๋, ํผํฌ ์ฌ์ฉ ์๊ฐ, ๋ฏธ์ /์ด์์ ๊ธฐ ์์ฝ ๋ฑ์ JSON ์ค๋ ์ท์ ๋ฌธ์์ด๋ก ์ ์ฅํฉ๋๋ค.RecapServiceImpl์์ ์์ฒญ ์์ ์ ์ญ์ง๋ ฌํํด ์๋ต ๋ชจ๋ธ๋ก ๋ณํํฉ๋๋ค.
flowchart LR
A[FamilyRecapMonthly] --> B[JSON ๋ฌธ์์ด ์ปฌ๋ผ]
B --> C[ObjectMapper.readValue]
C --> D[MonthlyRecap ์๋ต ๋ชจ๋ธ]
- ๊ณ ๊ฐ API๋
/customers,/families,/missions,/appeals,/rewards,/recaps๋ฑ์ ๋ถ์ฐ๋์ด ์์ต๋๋ค. - ๊ด๋ฆฌ์ API๋
/admin/**๊ฒฝ๋ก ์๋๋ก ๋ณ๋ ๋ถ๋ฆฌ๋์ด ์์ต๋๋ค. - ๊ถํ ์ฒดํฌ๋
@AdminOnly์@OwnerOnly๋ก ๋ช ํํ ๋ถ๋ฆฌ๋ฉ๋๋ค.
flowchart TD
A[Request] --> B{"๊ฒฝ๋ก/๊ถํ"}
B -- "/admin/**" --> C["@AdminOnly"]
B -- "๋ณดํธ์ ์ ์ฉ ๊ธฐ๋ฅ" --> D["@OwnerOnly"]
B -- "์ผ๋ฐ ๊ณ ๊ฐ ๊ธฐ๋ฅ" --> E["JWT + CustomerId Resolver"]
๋ชจ๋ ์๋ต์ ApiResponse<T>๋ก ๊ฐ์ธ์ง๋๋ค.
์ฑ๊ณต ์์:
{
"success": true,
"data": {
"id": 1
},
"timestamp": "2026-03-19T00:00:00Z"
}์คํจ ์์:
{
"success": false,
"error": {
"code": "MISSION_006",
"message": "์ด๋ฏธ ์ฒ๋ฆฌ ๋๊ธฐ ์ค์ธ ์์ฒญ์ด ์กด์ฌํฉ๋๋ค.",
"details": null
},
"timestamp": "2026-03-19T00:00:00Z"
}Request:
{
"phoneNumber": "01012345678",
"password": "1234"
}Response:
{
"success": true,
"data": {
"accessToken": "eyJhbGciOi...",
"refreshToken": "eyJhbGciOi...",
"role": "MEMBER"
},
"timestamp": "2026-03-19T00:00:00Z"
}Request:
GET /missions?cursor=120&size=20Response:
{
"success": true,
"data": {
"missions": [
{
"missionItemId": 101,
"requestId": 55,
"missionText": "์์ 30๋ถ ํ๊ธฐ",
"requestStatus": "PENDING",
"target": {
"customerId": 7,
"name": "๋ฏผ์"
},
"createdBy": {
"customerId": 1,
"name": "์๋ง"
},
"reward": {
"rewardId": 3,
"name": "์ ค๋ฆฌ 1๊ฐ",
"category": "SNACK",
"thumbnailUrl": "https://cdn.example.com/reward/jelly.png",
"templateId": 12
},
"createdAt": "2026-03-19T09:30:00"
}
],
"nextCursor": "101",
"hasNext": true
},
"timestamp": "2026-03-19T00:00:00Z"
}Request:
{
"updateInfo": {
"customerId": 7,
"type": "MONTHLY_LIMIT",
"rules": {
"limitBytes": 3221225472
},
"isActive": true
}
}Response:
{
"success": true,
"data": {
"customerId": 7,
"type": "MONTHLY_LIMIT",
"updated": true
},
"timestamp": "2026-03-19T00:00:00Z"
}Request:
{
"policyAssignmentId": 99,
"requestReason": "์์ ์ ์ถ ๋๋ฌธ์ ์ค๋๋ง ์ ํ์ ์ํํด์ฃผ์ธ์.",
"desiredRules": {
"startTime": "23:00",
"endTime": "07:00"
},
"policyActive": true
}Response:
{
"success": true,
"data": {
"appealId": 201,
"policyAssignmentId": 99,
"status": "PENDING",
"policyActive": true,
"desiredRules": {
"startTime": "23:00",
"endTime": "07:00"
},
"createdAt": "2026-03-19T10:15:00"
},
"timestamp": "2026-03-19T00:00:00Z"
}| Method | Path | ์ค๋ช | ๊ถํ |
|---|---|---|---|
| POST | /customers/login |
๊ณ ๊ฐ ๋ก๊ทธ์ธ | Public |
| POST | /customers/signup |
๊ณ ๊ฐ ํ์๊ฐ์ | Public |
| POST | /customers/refresh |
๊ณ ๊ฐ ํ ํฐ ์ฌ๋ฐ๊ธ | Public |
| POST | /customers/logout |
๊ณ ๊ฐ ๋ก๊ทธ์์ | Public |
| GET | /customers/me |
๊ณ ๊ฐ ๊ธฐ๋ณธ ์ ๋ณด ์กฐํ | Customer |
| GET | /customers/mypage |
๋ง์ดํ์ด์ง ์กฐํ | Customer |
| POST | /admin/login |
๊ด๋ฆฌ์ ๋ก๊ทธ์ธ | Public |
| POST | /admin/signup |
๊ด๋ฆฌ์ ํ์๊ฐ์ | Public |
| POST | /admin/refresh |
๊ด๋ฆฌ์ ํ ํฐ ์ฌ๋ฐ๊ธ | Public |
| POST | /admin/logout |
๊ด๋ฆฌ์ ๋ก๊ทธ์์ | Public |
| GET | /admin/me |
๊ด๋ฆฌ์ ๋ด ์ ๋ณด | Admin |
| GET | /admin/dashboard |
๊ด๋ฆฌ์ ๋์๋ณด๋ | Admin |
| Method | Path | ์ค๋ช | ๊ถํ |
|---|---|---|---|
| PUT | /families |
๊ฐ์กฑ ์ด๋ฆ ์์ | Owner |
| GET | /families/members |
๊ฐ์กฑ ๊ตฌ์ฑ์ ์กฐํ | Owner |
| GET | /families/usage/current |
ํ์ฌ ๊ฐ์กฑ ์ด ์ฌ์ฉ๋ ์กฐํ | Customer |
| GET | /families/usage/customers |
์๋ณ ๊ตฌ์ฑ์ ์ฌ์ฉ๋ ์์ฝ | Customer |
| GET | /families/usage/dashboard |
์๋ณ ๊ตฌ์ฑ์ ์ฌ์ฉ๋ ์์ธ | Customer |
| POST | /admin/families |
๊ฐ์กฑ ๊ฒ์ | Admin |
| GET | /admin/families/{familyId} |
๊ฐ์กฑ ์์ธ ์กฐํ | Admin |
| PATCH | /admin/families/{familyId} |
๊ฐ์กฑ ๊ตฌ์ฑ์ ์ญํ /ํ๋ ์์ | Admin |
| Method | Path | ์ค๋ช | ๊ถํ |
|---|---|---|---|
| GET | /policies/{policyId} |
์ ์ฑ ํ ํ๋ฆฟ ์์ธ ์กฐํ | Admin |
| GET | /policies |
์ ์ฑ ํ ํ๋ฆฟ ๋ชฉ๋ก ์กฐํ | Admin |
| POST | /policies |
์ ์ฑ ํ ํ๋ฆฟ ์์ฑ | Admin |
| PUT | /policies/{policyId} |
์ ์ฑ ํ ํ๋ฆฟ ์์ | Admin |
| DELETE | /policies/{policyId} |
์ ์ฑ ํ ํ๋ฆฟ ์ญ์ | Admin |
| GET | /families/policies |
๊ฐ์กฑ ๊ตฌ์ฑ์๋ณ ์ ์ฑ ์กฐํ | Customer |
| PATCH | /families/policies |
ํน์ ๊ตฌ์ฑ์ ์ ์ฑ ์์ | Owner |
| Method | Path | ์ค๋ช | ๊ถํ |
|---|---|---|---|
| GET | /missions |
๋ฏธ์ ๋ชฉ๋ก ์กฐํ | Customer |
| GET | /missions/logs |
๋ฏธ์ ๋ก๊ทธ ์กฐํ | Customer |
| GET | /missions/history |
๋ฏธ์ ์์ฒญ ์ด๋ ฅ ์กฐํ | Customer |
| POST | /missions |
๋ฏธ์ ์์ฑ | Owner |
| DELETE | /missions/{missionId} |
๋ฏธ์ ์ทจ์ | Owner |
| POST | /missions/{missionId}/request |
๋ฏธ์ ์๋ฃ ์์ฒญ | Customer |
| GET | /rewards/templates |
๋ณด์ ํ ํ๋ฆฟ ๋ชฉ๋ก ์กฐํ | Owner |
| PUT | /rewards/requests/{requestId}/respond |
๋ณด์ ์์ฒญ ์น์ธ/๊ฑฐ์ | Owner |
| GET | /rewards/received |
๋ด ์๋ น ๋ณด์ ์ด๋ ฅ ์กฐํ | Customer |
| GET | /admin/rewards/templates |
๋ณด์ ํ ํ๋ฆฟ ๋ชฉ๋ก ์กฐํ | Admin |
| GET | /admin/rewards/templates/{id} |
๋ณด์ ํ ํ๋ฆฟ ์์ธ ์กฐํ | Admin |
| POST | /admin/rewards/templates |
๋ณด์ ํ ํ๋ฆฟ ์์ฑ | Admin |
| PUT | /admin/rewards/templates/{id} |
๋ณด์ ํ ํ๋ฆฟ ์์ | Admin |
| DELETE | /admin/rewards/templates/{id} |
๋ณด์ ํ ํ๋ฆฟ ์ญ์ | Admin |
| GET | /admin/rewards/grants |
๋ณด์ ์ง๊ธ ์ด๋ ฅ ์กฐํ | Admin |
| Method | Path | ์ค๋ช | ๊ถํ |
|---|---|---|---|
| GET | /appeals/policies |
์ด์์ ๊ธฐ ๊ฐ๋ฅ ์ ์ฑ ๋ชฉ๋ก ์กฐํ | Customer |
| GET | /appeals |
์ด์์ ๊ธฐ ๋ชฉ๋ก ์กฐํ | Customer |
| GET | /appeals/{appealId} |
์ด์์ ๊ธฐ ์์ธ ์กฐํ | Customer |
| POST | /appeals |
์ด์์ ๊ธฐ ์์ฑ | Customer |
| PATCH | /appeals/{appealId}/respond |
์ด์์ ๊ธฐ ์น์ธ/๊ฑฐ์ | Owner |
| POST | /appeals/{appealId}/comments |
์ด์์ ๊ธฐ ๋๊ธ ์์ฑ | Customer |
| PATCH | /appeals/{appealId}/cancel |
์ด์์ ๊ธฐ ์ทจ์ | Customer |
| POST | /appeals/emergency |
๊ธด๊ธ ์ฟผํฐ ์์ฒญ | Customer |
| GET | /recaps/monthly |
์๊ฐ ๊ฐ์กฑ ๋ฆฌ์บก ์กฐํ | Customer |
| POST | /uploads/images |
์ด๋ฏธ์ง ์ ๋ก๋ | Admin |
LoginInterceptor๊ฐ ๋๋ถ๋ถ์ ์์ฒญ์์Authorizationํค๋๋ฅผ ์ถ์ถํฉ๋๋ค.JwtTokenUtil์ด ํ ํฐ ์ ํจ์ฑ, ๋ง๋ฃ, ํ์ ์ ๊ฒ์ฆํฉ๋๋ค.CustomerArgumentResolver,AdminArgumentResolver๊ฐ ์ปจํธ๋กค๋ฌ ํ๋ผ๋ฏธํฐ์ ์ฌ์ฉ์ ์๋ณ์๋ฅผ ์ฃผ์ ํฉ๋๋ค.
@AdminOnly: ๊ด๋ฆฌ์ ํ ํฐ๋ง ํ์ฉ@OwnerOnly: ๊ฐ์กฑ ๋ด ๋ณดํธ์ ์ญํ ๋ง ํ์ฉ- ์๋น์ค ๋ ์ด์ด์์๋
AuthContext์ ๊ฐ์กฑ ์์์ ๋ค์ ๊ฒ์ฆํด ๋๋ฉ์ธ ๋ฌด๊ฒฐ์ฑ์ ๋ณด์ฅํฉ๋๋ค.
์ ์ญ ์์ธ ์ฒ๋ฆฌ๋ ExceptionAdvice์์ ๋ด๋นํฉ๋๋ค.
- ๋๋ฉ์ธ ์์ธ๋
ApplicationException extends BaseException์ผ๋ก ๋์ง๋๋ค. - ๊ฐ ๋๋ฉ์ธ์
BaseErrorCode๊ตฌํ enum์ ํตํด HTTP ์ํ, ์ปค์คํ ์ฝ๋, ๋ฉ์์ง๋ฅผ ์ ์ํฉ๋๋ค. - ์ฒ๋ฆฌ๋์ง ์์ ์์ธ๋
GLOBAL_001๋ก ํต์ผํด ๋ฐํํฉ๋๋ค.
flowchart TD
A[Controller/Service] --> B{์์ธ ๋ฐ์}
B -- ApplicationException --> C[ExceptionAdvice.handleBaseException]
B -- ๊ธฐํ Exception --> D[ExceptionAdvice.handleUnhandledException]
C --> E[BaseErrorCode์์ status/code/message ์ถ์ถ]
D --> F[GLOBAL_001 INTERNAL_SERVER_ERROR]
E --> G[ApiResponse.fail]
F --> G
๋ํ ์๋ฌ ์ฝ๋ ์์:
| ๋๋ฉ์ธ | ์ฝ๋ | HTTP Status | ์๋ฏธ |
|---|---|---|---|
| Global | GLOBAL_001 |
500 | ์ฒ๋ฆฌ๋์ง ์์ ์๋ฒ ์ค๋ฅ |
| Global | GLOBAL_002 |
400 | ์๋ชป๋ ์ ๋ ฅ๊ฐ |
| Global | GLOBAL_003 |
401 | ์ ํจํ์ง ์์ ํ ํฐ |
| Mission | MISSION_006 |
409 | ์ค๋ณต ๋ฏธ์ ์์ฒญ |
| Mission | MISSION_010 |
400 | ์๋ชป๋ ์ปค์ |
| Mission | MISSION_012 |
400 | ๋ณด์ ๊ฑฐ์ ์ฌ์ ๋๋ฝ |
| Appeal | APPEAL_003 |
403 | ์ด์์ ๊ธฐ ์ ๊ทผ ๊ถํ ์์ |
| Appeal | APPEAL_011 |
409 | ์ค๋ณต PENDING ์ด์์ ๊ธฐ |
| Upload | UPLOAD_002 |
400 | ํ์ฉ๋์ง ์์ MIME ํ์ |
| Upload | UPLOAD_004 |
500 | ์ ๋ก๋ ์คํจ |
์์ธ ERD ๋ฌธ์๋ src/main/resources/db/migration/docs/ERD.md์ ์๊ณ , README์๋ ๋๋ฉ์ธ ๊ด์ ์์ ํต์ฌ ๊ด๊ณ๋ง ์์ฝํฉ๋๋ค.
- ์ฌ์ฉ์/๊ฐ์กฑ:
customer,admin,family,family_member - ์ ์ฑ
/์ฟผํฐ:
policy,policy_assignment,customer_quota,family_quota - ๋ฏธ์
/๋ณด์:
mission_item,mission_request,mission_log,reward,reward_template,reward_grant - ์ด์์ ๊ธฐ:
policy_appeal,policy_appeal_comment - ์ง๊ณ/๋ฆฌ์บก:
usage_record,family_recap_monthly,family_recap_weekly - ์ด๋ฒคํธ:
event_outbox์ฑ๊ฒฉ์usage_event_outbox์ ์ ํ๋ฆฌ์ผ์ด์ ์ notification outbox ํ ์ด๋ธ
erDiagram
FAMILY ||--o{ FAMILY_MEMBER : has
CUSTOMER ||--o{ FAMILY_MEMBER : joins
POLICY ||--o{ POLICY_ASSIGNMENT : template
CUSTOMER ||--o{ POLICY_ASSIGNMENT : target
FAMILY ||--o{ POLICY_ASSIGNMENT : scope
CUSTOMER ||--o{ CUSTOMER_QUOTA : owns
FAMILY ||--o{ FAMILY_QUOTA : owns
CUSTOMER ||--o{ MISSION_ITEM : target_or_creator
FAMILY ||--o{ MISSION_ITEM : scope
MISSION_ITEM ||--o{ MISSION_REQUEST : has
MISSION_ITEM ||--o{ MISSION_LOG : has
REWARD_TEMPLATE ||--o{ REWARD : snapshot_of
REWARD ||--|| MISSION_ITEM : rewards
REWARD ||--o{ REWARD_GRANT : issued_as
POLICY_ASSIGNMENT ||--o{ POLICY_APPEAL : appealed
POLICY_APPEAL ||--o{ POLICY_APPEAL_COMMENT : comments
FAMILY ||--o{ FAMILY_RECAP_MONTHLY : monthly
| ๋๋ฉ์ธ | ์ฃผ์ ํ ์ด๋ธ | ์ค๋ช |
|---|---|---|
| ์ธ์ฆ/์ฌ์ฉ์ | customer, admin, family_member |
๋ก๊ทธ์ธ ์ฃผ์ฒด์ ๊ฐ์กฑ ๋ด ์ญํ ๊ด๋ฆฌ |
| ์ ์ฑ | policy, policy_assignment, customer_quota |
ํ ํ๋ฆฟ๊ณผ ์ค์ ์ ์ฉ ์ํ๋ฅผ ๋ถ๋ฆฌ |
| ๋ฏธ์ | mission_item, mission_request, mission_log |
ํ ๋น, ์์ฒญ, ๊ฐ์ฌ ๋ก๊ทธ๋ฅผ ๋ถ๋ฆฌ |
| ๋ณด์ | reward_template, reward, reward_grant |
ํ ํ๋ฆฟ๊ณผ ๋ฐ๊ธ ๋ณด์ ์ค๋ ์ท ๋ถ๋ฆฌ |
| ์ด์์ ๊ธฐ | policy_appeal, policy_appeal_comment |
๋ณธ๋ฌธ๊ณผ ๋๊ธ์ ๋ถ๋ฆฌ |
| ๋ฆฌ์บก | family_recap_monthly |
์๊ฐ ์ค๋ ์ท์ JSON ํฌํจ ํํ๋ก ์ ์ฅ |
src/main/resources/application.yml ๊ธฐ์ค ์ฃผ์ ํ๊ฒฝ ๋ณ์์
๋๋ค.
DATABASE_URL,DATABASE_USER,DATABASE_PASSWORDJWT_SECRET_KEY,JWT_ACCESS_TOKEN_EXPIRES_IN,JWT_REFRESH_TOKEN_EXPIRES_INREDIS_HOST,REDIS_PORT,REDIS_PASSWORDKAFKA_BOOTSTRAP_SERVERSR2_ENDPOINT,R2_ACCESS_KEY,R2_SECRET_KEY,R2_BUCKET,R2_CDN_BASE_URLFRONTEND_URLOTEL_EXPORTER_OTLP_ENDPOINT
./gradlew bootRunWindows:
.\gradlew.bat bootRunํ ์คํธ:
./gradlew testSwagger UI:
http://localhost:8080/swagger-ui.html
OpenAPI:
http://localhost:8080/v3/api-docs
- Flyway๋ฅผ ์ฌ์ฉํด ์คํค๋ง๋ฅผ ๊ด๋ฆฌํฉ๋๋ค.
- ๋ง์ด๊ทธ๋ ์ด์
ํ์ผ์
src/main/resources/db/migration์ ์์ต๋๋ค. - ERD ๋ฌธ์๋
src/main/resources/db/migration/docs/ERD.md์ ์์ต๋๋ค.
- ์๋น์ค ๋จ์ ํ
์คํธ์ ์ผ๋ถ ํตํฉ ํ
์คํธ๊ฐ
src/test/java์ ๊ตฌ์ฑ๋์ด ์์ต๋๋ค. - ๋ํ์ ์ผ๋ก
mission,reward,appeal,family,policy,recap,upload๊ด๋ จ ํ ์คํธ๊ฐ ์กด์ฌํฉ๋๋ค.