mirror of
https://github.com/cna-bootcamp/lifesub.git
synced 2026-06-12 20:49:09 +00:00
release
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
서비스명|구독추천
|
||||
마이크로서비스 이름|SubRecommend
|
||||
유저스토리 ID|RSS-005
|
||||
유저스토리 제목|추천 구독 카테고리
|
||||
Controller 이름|RecommendController
|
||||
API 목적|사용자의 지출 패턴을 분석하여 추천 구독 카테고리 제공
|
||||
API Method|GET
|
||||
API 그룹 Path|/api/recommend
|
||||
API Path|/categories
|
||||
Path <변수유형> <변수명>|
|
||||
Query Key|userId
|
||||
Query <변수유형> <변수명>|String userId
|
||||
Request DTO 이름|
|
||||
Request DTO 배열 여부|
|
||||
Request DTO 구조|
|
||||
Response DTO 이름|RecommendCategoryDTO
|
||||
Response DTO 배열 여부|No
|
||||
Response DTO 구조|String categoryName; LocalDate baseDate; String spendingCategory; Long totalSpending
|
||||
@@ -0,0 +1,37 @@
|
||||
!theme mono
|
||||
title Recommendation Service - 데이터 모델
|
||||
|
||||
' Style configurations
|
||||
skinparam linetype ortho
|
||||
hide circle
|
||||
|
||||
entity "Spending_History" as spending {
|
||||
* id: bigint <<PK>>
|
||||
--
|
||||
userId: varchar(50)
|
||||
category: varchar(50)
|
||||
amount: decimal(15,2)
|
||||
spendingDate: date
|
||||
createdAt: timestamp
|
||||
}
|
||||
|
||||
entity "Recommended_Categories" as recommend {
|
||||
* id: bigint <<PK>>
|
||||
--
|
||||
spendingCategory: varchar(50)
|
||||
recommendCategory: varchar(50)
|
||||
createdAt: timestamp
|
||||
}
|
||||
|
||||
note right of spending
|
||||
사용자별 지출 이력 관리
|
||||
- 금액
|
||||
- 카테고리
|
||||
- 지출일자
|
||||
end note
|
||||
|
||||
note right of recommend
|
||||
지출-구독 카테고리 매핑
|
||||
- 지출 카테고리
|
||||
- 추천 구독 카테고리
|
||||
end note
|
||||
@@ -0,0 +1,54 @@
|
||||
!theme mono
|
||||
title 구독추천 서비스 - 내부 시퀀스 다이어그램
|
||||
|
||||
actor Client
|
||||
participant "추천 컨트롤러\n(RecommendController)" as Controller
|
||||
participant "추천 서비스\n(RecommendService)" as Service
|
||||
participant "지출분석 서비스\n(SpendingAnalyzer)" as Analyzer
|
||||
database "구독추천 DB" as RecommendDB
|
||||
|
||||
' 지출 기반 추천 카테고리 조회
|
||||
Client -> Controller: GET /api/recommend/categories\n[지출 기반 추천 카테고리 조회]
|
||||
activate Controller
|
||||
|
||||
Controller -> Service: getRecommendedCategory(userId)
|
||||
activate Service
|
||||
|
||||
' 사용자 지출 패턴 분석
|
||||
Service -> Analyzer: analyzeSpending(spendings)
|
||||
activate Analyzer
|
||||
Analyzer -> RecommendDB: findSpendingsByUserIdAndDateAfter(userId, startDate)
|
||||
RecommendDB --> Analyzer: List<SpendingEntity>
|
||||
Analyzer -> Analyzer: calculateTotalByCategory()
|
||||
Analyzer -> Analyzer: findTopCategory()
|
||||
Analyzer --> Service: SpendingCategory
|
||||
deactivate Analyzer
|
||||
|
||||
' 추천 카테고리 매핑
|
||||
Service -> RecommendDB: findBySpendingCategory(topSpendingCategory)
|
||||
RecommendDB --> Service: RecommendedCategory
|
||||
|
||||
' 추천 결과 반환
|
||||
Service --> Controller: RecommendCategoryDTO
|
||||
Controller --> Client: HTTP Response\n(recommendCategory, spendingCategory, baseDate)
|
||||
|
||||
deactivate Service
|
||||
deactivate Controller
|
||||
|
||||
note right of Controller
|
||||
1. 요청 파라미터 검증
|
||||
2. 서비스 계층 호출
|
||||
3. 응답 변환
|
||||
end note
|
||||
|
||||
note right of Service
|
||||
1. 지출 패턴 분석 요청
|
||||
2. 추천 카테고리 매핑
|
||||
3. 추천 결과 생성
|
||||
end note
|
||||
|
||||
note right of Analyzer
|
||||
1. 최근 1개월 지출 데이터 조회
|
||||
2. 카테고리별 지출 합산
|
||||
3. 최고 지출 카테고리 도출
|
||||
end note
|
||||
@@ -0,0 +1,81 @@
|
||||
!theme mono
|
||||
title Recommendation Service - Class Diagram
|
||||
|
||||
package "com.unicorn.lifesub.recommend" {
|
||||
package "domain" {
|
||||
class SpendingCategory {
|
||||
-category: String
|
||||
-totalAmount: Long
|
||||
}
|
||||
|
||||
class RecommendedCategory {
|
||||
-spendingCategory: String
|
||||
-recommendCategory: String
|
||||
-baseDate: LocalDate
|
||||
}
|
||||
}
|
||||
|
||||
package "service" {
|
||||
interface RecommendService {
|
||||
+getRecommendedCategory(userId: String): RecommendCategoryDTO
|
||||
}
|
||||
|
||||
class RecommendServiceImpl {
|
||||
-recommendRepository: RecommendRepository
|
||||
-spendingRepository: SpendingRepository
|
||||
-spendingAnalyzer: SpendingAnalyzer
|
||||
+getRecommendedCategory(userId: String): RecommendCategoryDTO
|
||||
}
|
||||
|
||||
class SpendingAnalyzer {
|
||||
+analyzeSpending(spendings: List<SpendingEntity>): SpendingCategory
|
||||
-calculateTotalByCategory(spendings: List<SpendingEntity>): Map<String, Long>
|
||||
-findTopCategory(totals: Map<String, Long>): SpendingCategory
|
||||
}
|
||||
}
|
||||
|
||||
package "controller" {
|
||||
class RecommendController {
|
||||
-recommendService: RecommendService
|
||||
+getRecommendedCategory(userId: String): ResponseEntity<ApiResponse<RecommendCategoryDTO>>
|
||||
}
|
||||
}
|
||||
|
||||
package "dto" {
|
||||
class RecommendCategoryDTO {
|
||||
-categoryName: String
|
||||
-baseDate: LocalDate
|
||||
-spendingCategory: String
|
||||
-totalSpending: Long
|
||||
}
|
||||
}
|
||||
|
||||
package "repository" {
|
||||
package "jpa" {
|
||||
interface SpendingRepository {
|
||||
+findSpendingsByUserIdAndDateAfter(userId: String, startDate: LocalDate): List<SpendingEntity>
|
||||
}
|
||||
|
||||
interface RecommendRepository {
|
||||
+findBySpendingCategory(category: String): Optional<RecommendedCategoryEntity>
|
||||
}
|
||||
}
|
||||
|
||||
package "entity" {
|
||||
class SpendingEntity {
|
||||
-id: Long
|
||||
-userId: String
|
||||
-category: String
|
||||
-amount: Long
|
||||
-spendingDate: LocalDate
|
||||
}
|
||||
|
||||
class RecommendedCategoryEntity {
|
||||
-id: Long
|
||||
-spendingCategory: String
|
||||
-recommendCategory: String
|
||||
+toDomain(): RecommendedCategory
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
!theme mono
|
||||
|
||||
title 구독관리 서비스 - 논리 아키텍처
|
||||
|
||||
' Components
|
||||
package "클라이언트 계층" {
|
||||
[모바일/웹 앱] as App
|
||||
}
|
||||
|
||||
package "회원 서비스" {
|
||||
[회원 컨트롤러] as MemberController
|
||||
[회원 서비스] as MemberService
|
||||
[JWT 토큰 제공자] as JwtTokenProvider
|
||||
database "회원 DB" as MemberDB
|
||||
|
||||
note right of MemberService
|
||||
1. 로그인/로그아웃 처리
|
||||
2. JWT 토큰 생성/검증
|
||||
end note
|
||||
}
|
||||
|
||||
package "마이구독 서비스" {
|
||||
[마이구독 컨트롤러] as MySubController
|
||||
[카테고리 컨트롤러] as CategoryController
|
||||
[서비스 컨트롤러] as ServiceController
|
||||
[마이구독 서비스] as MySubService
|
||||
database "마이구독 DB" as MySubDB {
|
||||
[사용자별 구독 정보]
|
||||
[구독 서비스 정보]
|
||||
[카테고리 정보]
|
||||
}
|
||||
|
||||
note right of MySubService
|
||||
4. 총 구독료 계산
|
||||
5. 나의 구독 목록 관리
|
||||
8. 구독 상세 정보 제공
|
||||
9. 구독 신청 처리
|
||||
10. 구독 취소 처리
|
||||
11. 구독 카테고리 관리
|
||||
12. 카테고리별 구독 서비스 제공
|
||||
end note
|
||||
}
|
||||
|
||||
package "구독추천 서비스" {
|
||||
[구독추천 컨트롤러] as RecommendController
|
||||
[구독추천 서비스] as RecommendService
|
||||
[지출분석 서비스] as SpendingAnalyzer
|
||||
database "구독추천 DB" as RecommendDB {
|
||||
[지출 이력]
|
||||
[추천 카테고리]
|
||||
}
|
||||
|
||||
note right of RecommendService
|
||||
6. 지출 카테고리 기반 구독 추천
|
||||
- 사용자의 지출 패턴 분석
|
||||
- 최적 구독 카테고리 추천
|
||||
end note
|
||||
}
|
||||
|
||||
' Relationships
|
||||
App --> MemberController
|
||||
App --> MySubController
|
||||
App --> CategoryController
|
||||
App --> ServiceController
|
||||
App --> RecommendController
|
||||
|
||||
MemberController --> MemberService
|
||||
MemberService --> JwtTokenProvider
|
||||
MemberService --> MemberDB
|
||||
|
||||
MySubController --> MySubService
|
||||
CategoryController --> MySubService
|
||||
ServiceController --> MySubService
|
||||
MySubService --> MySubDB
|
||||
|
||||
RecommendController --> RecommendService
|
||||
RecommendService --> SpendingAnalyzer
|
||||
RecommendService --> RecommendDB
|
||||
@@ -0,0 +1,18 @@
|
||||
서비스명|마이구독|마이구독|마이구독|마이구독|마이구독|마이구독|마이구독
|
||||
마이크로서비스 이름|MySubscription|MySubscription|MySubscription|MySubscription|MySubscription|MySubscription|MySubscription
|
||||
유저스토리 ID|MSS-005|MSS-010|MSS-020|MSS-025|MSS-030|MSS-035|MSS-040
|
||||
유저스토리 제목|총 구독료 표시|나의 구독 목록|구독상세|구독하기|구독취소|구독 카테고리 표시|구독서비스 목록
|
||||
Controller 이름|MySubController|MySubController|ServiceController|ServiceController|ServiceController|CategoryController|CategoryController
|
||||
API 목적|총 구독료 조회|구독 목록 조회|구독 상세 조회|구독 신청|구독 취소|전체 카테고리 목록 조회|카테고리별 서비스 목록 조회
|
||||
API Method|GET|GET|GET|POST|DELETE|GET|GET
|
||||
API 그룹 Path|/api/mysub|/api/mysub|/api/mysub/services|/api/mysub/services|/api/mysub/services|/api/mysub|/api/mysub
|
||||
API Path|/total-fee|/list|/{subscriptionId}|/{subscriptionId}/subscribe|/{subscriptionId}|/categories|/services
|
||||
Path <변수유형> <변수명>|||Long subscriptionId|Long subscriptionId|Long subscriptionId||
|
||||
Query Key|userId|userId||userId|||categoryId
|
||||
Query <변수유형> <변수명>|String userId|String userId||String userId|||String categoryId
|
||||
Request DTO 이름|||||||
|
||||
Request DTO 배열 여부|||||||
|
||||
Request DTO 구조|||||||
|
||||
Response DTO 이름|TotalFeeResponse|MySubResponse|SubDetailResponse||null|CategoryResponse|ServiceListResponse
|
||||
Response DTO 배열 여부|No|Yes|No||No|Yes|Yes
|
||||
Response DTO 구조|Long totalFee; String feeLevel|String serviceName; String logoUrl|String serviceName; String logoUrl; String category; String description; int price; int maxSharedUsers||null|String categoryId; String categoryName|String serviceId; String serviceName; String description; int price; String logoUrl
|
||||
@@ -0,0 +1,52 @@
|
||||
!theme mono
|
||||
title MySubscription Service - 데이터 모델
|
||||
|
||||
' Style configurations
|
||||
skinparam linetype ortho
|
||||
hide circle
|
||||
|
||||
entity "My_Subscriptions" as my_subs {
|
||||
* id: bigint <<PK>>
|
||||
--
|
||||
userId: varchar(50)
|
||||
subscriptionId: bigint <<FK>>
|
||||
createdAt: timestamp
|
||||
updatedAt: timestamp
|
||||
}
|
||||
|
||||
entity "Subscriptions" as subs {
|
||||
* id: bigint <<PK>>
|
||||
--
|
||||
name: varchar(100)
|
||||
description: text
|
||||
category: varchar(50)
|
||||
price: decimal(15,2)
|
||||
maxSharedUsers: integer
|
||||
logoUrl: varchar(255)
|
||||
createdAt: timestamp
|
||||
updatedAt: timestamp
|
||||
}
|
||||
|
||||
entity "Categories" as categories {
|
||||
* categoryId: varchar(50) <<PK>>
|
||||
--
|
||||
name: varchar(100)
|
||||
createdAt: timestamp
|
||||
updatedAt: timestamp
|
||||
}
|
||||
|
||||
' Relationships
|
||||
my_subs }o--|| subs : subscriptionId
|
||||
subs }o--|| categories : category-categoryId
|
||||
|
||||
note right of my_subs
|
||||
사용자별 구독 정보 관리
|
||||
end note
|
||||
|
||||
note right of subs
|
||||
구독 서비스 정보 관리
|
||||
end note
|
||||
|
||||
note right of categories
|
||||
구독 카테고리 관리
|
||||
end note
|
||||
@@ -0,0 +1,101 @@
|
||||
!theme mono
|
||||
title 마이구독 서비스 - 내부 시퀀스 다이어그램
|
||||
|
||||
actor Client
|
||||
participant "마이구독 컨트롤러\n(MySubController)" as MySubController
|
||||
participant "서비스 컨트롤러\n(ServiceController)" as ServiceController
|
||||
participant "카테고리 컨트롤러\n(CategoryController)" as CategoryController
|
||||
participant "마이구독 서비스\n(MySubService)" as Service
|
||||
database "마이구독 DB" as MySubDB
|
||||
database "구독서비스 DB" as SubServiceDB
|
||||
|
||||
' 총 구독료 조회
|
||||
Client -> MySubController: GET /api/mysub/total-fee\n[총 구독료 조회]
|
||||
activate MySubController
|
||||
MySubController -> Service: getTotalFee(userId)
|
||||
activate Service
|
||||
Service -> MySubDB: findMySubscriptions(userId)
|
||||
MySubDB --> Service: List<MySubscription>
|
||||
Service -> SubServiceDB: findSubscriptionsByIds(subIds)
|
||||
SubServiceDB --> Service: List<Subscription>
|
||||
Service --> MySubController: TotalFeeResponse
|
||||
MySubController --> Client: HTTP Response\n(total fee, level)
|
||||
deactivate Service
|
||||
deactivate MySubController
|
||||
|
||||
' 나의 구독 목록 조회
|
||||
Client -> MySubController: GET /api/mysub/list\n[구독 목록 조회]
|
||||
activate MySubController
|
||||
MySubController -> Service: getMySubscriptions(userId)
|
||||
activate Service
|
||||
Service -> MySubDB: findMySubscriptions(userId)
|
||||
MySubDB --> Service: List<MySubscription>
|
||||
Service --> MySubController: List<MySubResponse>
|
||||
MySubController --> Client: HTTP Response\n(subscription list)
|
||||
deactivate Service
|
||||
deactivate MySubController
|
||||
|
||||
' 구독 상세 조회
|
||||
Client -> ServiceController: GET /api/mysub/services/{subscriptionId}\n[구독 상세 조회]
|
||||
activate ServiceController
|
||||
ServiceController -> Service: getSubscriptionDetail(subscriptionId)
|
||||
activate Service
|
||||
Service -> SubServiceDB: findById(subscriptionId)
|
||||
SubServiceDB --> Service: Subscription
|
||||
Service --> ServiceController: SubDetailResponse
|
||||
ServiceController --> Client: HTTP Response\n(subscription detail)
|
||||
deactivate Service
|
||||
deactivate ServiceController
|
||||
|
||||
' 구독 신청
|
||||
Client -> ServiceController: POST /api/mysub/services/{subscriptionId}/subscribe\n[구독 신청]
|
||||
activate ServiceController
|
||||
ServiceController -> Service: subscribe(subscriptionId, userId)
|
||||
activate Service
|
||||
Service -> SubServiceDB: findById(subscriptionId)
|
||||
Service -> MySubDB: save(mySubscription)
|
||||
Service --> ServiceController: void
|
||||
ServiceController --> Client: HTTP Response\n(success)
|
||||
deactivate Service
|
||||
deactivate ServiceController
|
||||
|
||||
' 구독 취소
|
||||
Client -> ServiceController: DELETE /api/mysub/services/{subscriptionId}\n[구독 취소]
|
||||
activate ServiceController
|
||||
ServiceController -> Service: cancel(subscriptionId)
|
||||
activate Service
|
||||
Service -> MySubDB: delete(subscriptionId)
|
||||
Service --> ServiceController: void
|
||||
ServiceController --> Client: HTTP Response\n(success)
|
||||
deactivate Service
|
||||
deactivate ServiceController
|
||||
|
||||
' 카테고리 목록 조회
|
||||
Client -> CategoryController: GET /api/mysub/categories\n[카테고리 목록 조회]
|
||||
activate CategoryController
|
||||
CategoryController -> Service: getAllCategories()
|
||||
activate Service
|
||||
Service -> SubServiceDB: findAllCategories()
|
||||
SubServiceDB --> Service: List<Category>
|
||||
Service --> CategoryController: List<CategoryResponse>
|
||||
CategoryController --> Client: HTTP Response\n(category list)
|
||||
deactivate Service
|
||||
deactivate CategoryController
|
||||
|
||||
' 카테고리별 서비스 목록 조회
|
||||
Client -> CategoryController: GET /api/mysub/services\n[카테고리별 서비스 목록 조회]
|
||||
activate CategoryController
|
||||
CategoryController -> Service: getServicesByCategory(categoryId)
|
||||
activate Service
|
||||
Service -> SubServiceDB: findByCategory(categoryId)
|
||||
SubServiceDB --> Service: List<Subscription>
|
||||
Service --> CategoryController: List<ServiceListResponse>
|
||||
CategoryController --> Client: HTTP Response\n(service list)
|
||||
deactivate Service
|
||||
deactivate CategoryController
|
||||
|
||||
note right of Service
|
||||
1. 비즈니스 로직 처리
|
||||
2. 구독 관리
|
||||
3. 카테고리 관리
|
||||
end note
|
||||
@@ -0,0 +1,260 @@
|
||||
!theme mono
|
||||
title MySubscription Service - Clean Architecture Class Diagram
|
||||
|
||||
' BIZ Layer
|
||||
package "com.unicorn.lifesub.mysub.biz" {
|
||||
package "usecase" {
|
||||
package "in" {
|
||||
interface TotalFeeUseCase {
|
||||
+getTotalFee(userId: String): TotalFeeResponse
|
||||
}
|
||||
|
||||
interface MySubscriptionsUseCase {
|
||||
+getMySubscriptions(userId: String): List<MySubResponse>
|
||||
}
|
||||
|
||||
interface SubscriptionDetailUseCase {
|
||||
+getSubscriptionDetail(subscriptionId: Long): SubDetailResponse
|
||||
}
|
||||
|
||||
interface SubscribeUseCase {
|
||||
+subscribe(subscriptionId: Long, userId: String): void
|
||||
}
|
||||
|
||||
interface CancelSubscriptionUseCase {
|
||||
+cancel(subscriptionId: Long): void
|
||||
}
|
||||
|
||||
interface CategoryUseCase {
|
||||
+getAllCategories(): List<CategoryResponse>
|
||||
+getServicesByCategory(categoryId: String): List<ServiceListResponse>
|
||||
}
|
||||
}
|
||||
|
||||
package "out" {
|
||||
interface MySubscriptionReader {
|
||||
+findByUserId(userId: String): List<MySubscription>
|
||||
+findById(id: Long): Optional<MySubscription>
|
||||
}
|
||||
|
||||
interface MySubscriptionWriter {
|
||||
+save(userId: String, subscriptionId: Long): MySubscription
|
||||
+delete(id: Long): void
|
||||
}
|
||||
|
||||
interface SubscriptionReader {
|
||||
+findById(id: Long): Optional<Subscription>
|
||||
+findByCategory(category: String): List<Subscription>
|
||||
+findAllCategories(): List<Category>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
package "domain" {
|
||||
class MySubscription {
|
||||
-id: Long
|
||||
-userId: String
|
||||
-subscription: Subscription
|
||||
+getPrice(): int
|
||||
}
|
||||
|
||||
class Subscription {
|
||||
-id: Long
|
||||
-name: String
|
||||
-description: String
|
||||
-category: String
|
||||
-price: int
|
||||
-maxSharedUsers: int
|
||||
-logoUrl: String
|
||||
}
|
||||
|
||||
class Category {
|
||||
-categoryId: String
|
||||
-name: String
|
||||
}
|
||||
}
|
||||
|
||||
package "service" {
|
||||
class MySubscriptionService {
|
||||
-mySubscriptionReader: MySubscriptionReader
|
||||
-mySubscriptionWriter: MySubscriptionWriter
|
||||
-subscriptionReader: SubscriptionReader
|
||||
-collectorThreshold: long
|
||||
-addictThreshold: long
|
||||
+getTotalFee(userId: String): TotalFeeResponse
|
||||
+getMySubscriptions(userId: String): List<MySubResponse>
|
||||
+getSubscriptionDetail(subscriptionId: Long): SubDetailResponse
|
||||
+subscribe(subscriptionId: Long, userId: String): void
|
||||
+cancel(subscriptionId: Long): void
|
||||
+getAllCategories(): List<CategoryResponse>
|
||||
+getServicesByCategory(categoryId: String): List<ServiceListResponse>
|
||||
-calculateFeeLevel(totalFee: long): String
|
||||
}
|
||||
|
||||
enum FeeLevel {
|
||||
LIKFER
|
||||
COLLECTOR
|
||||
ADDICT
|
||||
}
|
||||
}
|
||||
|
||||
package "dto" {
|
||||
class TotalFeeResponse {
|
||||
-totalFee: Long
|
||||
-feeLevel: String
|
||||
}
|
||||
|
||||
class MySubResponse {
|
||||
-id: Long
|
||||
-serviceName: String
|
||||
-logoUrl: String
|
||||
}
|
||||
|
||||
class SubDetailResponse {
|
||||
-serviceName: String
|
||||
-logoUrl: String
|
||||
-category: String
|
||||
-description: String
|
||||
-price: int
|
||||
-maxSharedUsers: int
|
||||
}
|
||||
|
||||
class CategoryResponse {
|
||||
-categoryId: String
|
||||
-categoryName: String
|
||||
}
|
||||
|
||||
class ServiceListResponse {
|
||||
-serviceId: String
|
||||
-serviceName: String
|
||||
-description: String
|
||||
-price: int
|
||||
-logoUrl: String
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
' INFRA Layer
|
||||
package "com.unicorn.lifesub.mysub.infra" {
|
||||
package "controller" {
|
||||
class MySubController {
|
||||
-totalFeeUseCase: TotalFeeUseCase
|
||||
-mySubscriptionsUseCase: MySubscriptionsUseCase
|
||||
+getTotalFee(userId: String): ResponseEntity<ApiResponse<TotalFeeResponse>>
|
||||
+getMySubscriptions(userId: String): ResponseEntity<ApiResponse<List<MySubResponse>>>
|
||||
}
|
||||
|
||||
class ServiceController {
|
||||
-subscriptionDetailUseCase: SubscriptionDetailUseCase
|
||||
-subscribeUseCase: SubscribeUseCase
|
||||
-cancelSubscriptionUseCase: CancelSubscriptionUseCase
|
||||
+getSubscriptionDetail(subscriptionId: Long): ResponseEntity<ApiResponse<SubDetailResponse>>
|
||||
+subscribe(subscriptionId: Long, userId: String): ResponseEntity<ApiResponse<Void>>
|
||||
+cancel(subscriptionId: Long): ResponseEntity<ApiResponse<Void>>
|
||||
}
|
||||
|
||||
class CategoryController {
|
||||
-categoryUseCase: CategoryUseCase
|
||||
+getAllCategories(): ResponseEntity<ApiResponse<List<CategoryResponse>>>
|
||||
+getServicesByCategory(categoryId: String): ResponseEntity<ApiResponse<List<ServiceListResponse>>>
|
||||
}
|
||||
}
|
||||
|
||||
package "gateway" {
|
||||
class MySubscriptionGateway implements MySubscriptionReader, MySubscriptionWriter {
|
||||
-mySubscriptionRepository: MySubscriptionJpaRepository
|
||||
-subscriptionRepository: SubscriptionJpaRepository
|
||||
}
|
||||
|
||||
class SubscriptionGateway implements SubscriptionReader {
|
||||
-subscriptionRepository: SubscriptionJpaRepository
|
||||
-categoryRepository: CategoryJpaRepository
|
||||
}
|
||||
|
||||
package "repository" {
|
||||
interface MySubscriptionJpaRepository {
|
||||
+findByUserId(userId: String): List<MySubscriptionEntity>
|
||||
+findBySubscription_Id(subscriptionId: Long): Optional<MySubscriptionEntity>
|
||||
}
|
||||
|
||||
interface SubscriptionJpaRepository {
|
||||
+findById(id: Long): Optional<SubscriptionEntity>
|
||||
+findByCategory(category: String): List<SubscriptionEntity>
|
||||
}
|
||||
|
||||
interface CategoryJpaRepository {
|
||||
+findAll(): List<CategoryEntity>
|
||||
}
|
||||
}
|
||||
|
||||
package "entity" {
|
||||
class MySubscriptionEntity {
|
||||
-id: Long
|
||||
-userId: String
|
||||
-subscription: SubscriptionEntity
|
||||
+toDomain(): MySubscription
|
||||
}
|
||||
|
||||
class SubscriptionEntity {
|
||||
-id: Long
|
||||
-name: String
|
||||
-description: String
|
||||
-category: String
|
||||
-price: int
|
||||
-maxSharedUsers: int
|
||||
-logoUrl: String
|
||||
+toDomain(): Subscription
|
||||
}
|
||||
|
||||
class CategoryEntity {
|
||||
-categoryId: String
|
||||
-name: String
|
||||
+toDomain(): Category
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
package "config" {
|
||||
class SecurityConfig {
|
||||
-jwtTokenProvider: JwtTokenProvider
|
||||
+securityFilterChain(http: HttpSecurity): SecurityFilterChain
|
||||
+corsConfigurationSource(): CorsConfigurationSource
|
||||
}
|
||||
|
||||
class SwaggerConfig {
|
||||
+openAPI(): OpenAPI
|
||||
}
|
||||
|
||||
class DataLoader {
|
||||
-categoryRepository: CategoryJpaRepository
|
||||
-subscriptionRepository: SubscriptionJpaRepository
|
||||
+run(): void
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
' Relationships
|
||||
MySubscriptionService ..|> TotalFeeUseCase
|
||||
MySubscriptionService ..|> MySubscriptionsUseCase
|
||||
MySubscriptionService ..|> SubscriptionDetailUseCase
|
||||
MySubscriptionService ..|> SubscribeUseCase
|
||||
MySubscriptionService ..|> CancelSubscriptionUseCase
|
||||
MySubscriptionService ..|> CategoryUseCase
|
||||
|
||||
MySubController --> TotalFeeUseCase
|
||||
MySubController --> MySubscriptionsUseCase
|
||||
ServiceController --> SubscriptionDetailUseCase
|
||||
ServiceController --> SubscribeUseCase
|
||||
ServiceController --> CancelSubscriptionUseCase
|
||||
CategoryController --> CategoryUseCase
|
||||
|
||||
MySubscriptionGateway --> MySubscriptionJpaRepository
|
||||
MySubscriptionGateway --> SubscriptionJpaRepository
|
||||
SubscriptionGateway --> SubscriptionJpaRepository
|
||||
SubscriptionGateway --> CategoryJpaRepository
|
||||
|
||||
MySubscriptionEntity ..> MySubscription : converts to
|
||||
SubscriptionEntity ..> Subscription : converts to
|
||||
CategoryEntity ..> Category : converts to
|
||||
|
||||
MySubscription --> Subscription
|
||||
@@ -0,0 +1,74 @@
|
||||
!theme mono
|
||||
title ResourceGroup - 물리아키텍처
|
||||
|
||||
' Azure Resource Group
|
||||
rectangle "ResourceGroup" {
|
||||
' Virtual Network
|
||||
rectangle "VirtualNetwork" {
|
||||
' AKS Cluster
|
||||
rectangle "AKSCluster" {
|
||||
rectangle "SystemNodePool" {
|
||||
[IngressController] as ingress
|
||||
}
|
||||
|
||||
rectangle "UserNodePool" {
|
||||
rectangle "MemberServicePod" {
|
||||
[회원서비스] as memberservice
|
||||
}
|
||||
|
||||
rectangle "MySubscriptionServicePod" {
|
||||
[마이구독서비스] as mysubservice
|
||||
}
|
||||
|
||||
rectangle "RecommendServicePod" {
|
||||
[구독추천서비스] as recommendservice
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
' Managed Databases
|
||||
database "AzureDatabasePostgreSQL" {
|
||||
database "MemberDB" as memberdb {
|
||||
[Members]
|
||||
}
|
||||
|
||||
database "MySubscriptionDB" as mysubdb {
|
||||
[MySubscriptions]
|
||||
[Subscriptions]
|
||||
[Categories]
|
||||
}
|
||||
|
||||
database "RecommendDB" as recommenddb {
|
||||
[SpendingHistory]
|
||||
[RecommendedCategories]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
' External Actors
|
||||
actor "Client" as client
|
||||
actor "Developer" as developer
|
||||
|
||||
' Network Flow
|
||||
client --> ingress
|
||||
ingress --> memberservice
|
||||
ingress --> mysubservice
|
||||
ingress --> recommendservice
|
||||
|
||||
' Database Connections
|
||||
memberservice --> memberdb
|
||||
mysubservice --> mysubdb
|
||||
recommendservice --> recommenddb
|
||||
|
||||
' Development Access
|
||||
developer --> ingress : "kubectl"
|
||||
|
||||
' Legend
|
||||
legend right
|
||||
| Component | Description |
|
||||
|---|---|
|
||||
| Infrastructure | AKSCluster |
|
||||
| Services | Member, MySubscription, Recommend |
|
||||
| Databases | PostgreSQL(Member, MySubscription, Recommend) |
|
||||
endlegend
|
||||
@@ -0,0 +1,42 @@
|
||||
!theme mono
|
||||
title 구독관리 서비스 - 외부 시퀀스 다이어그램
|
||||
|
||||
actor Client
|
||||
participant "회원 서비스" as MemberService
|
||||
participant "마이구독 서비스" as MySubService
|
||||
participant "구독추천 서비스" as RecommendService
|
||||
|
||||
' 회원 서비스 호출
|
||||
Client -> MemberService: POST /api/auth/login\n[로그인]
|
||||
Client -> MemberService: POST /api/auth/logout\n[로그아웃]
|
||||
|
||||
' 마이구독 서비스 호출
|
||||
Client -> MySubService: GET /api/mysub/total-fee\n[총 구독료 조회]
|
||||
Client -> MySubService: GET /api/mysub/list\n[구독 목록 조회]
|
||||
Client -> MySubService: GET /api/mysub/categories\n[전체 카테고리 목록 조회]
|
||||
Client -> MySubService: GET /api/mysub/services\n[카테고리별 서비스 목록 조회]
|
||||
Client -> MySubService: GET /api/mysub/services/{subscriptionId}\n[구독 상세 조회]
|
||||
Client -> MySubService: POST /api/mysub/services/{subscriptionId}/subscribe\n[구독 신청]
|
||||
Client -> MySubService: DELETE /api/mysub/services/{subscriptionId}\n[구독 취소]
|
||||
|
||||
' 구독추천 서비스 호출
|
||||
Client -> RecommendService: GET /api/recommend/categories\n[지출 기반 추천 카테고리 조회]
|
||||
|
||||
note right of MemberService
|
||||
인증/인가 처리
|
||||
- JWT 기반 토큰 인증
|
||||
- Role 기반 권한 관리
|
||||
end note
|
||||
|
||||
note right of MySubService
|
||||
구독 서비스 관리
|
||||
- 구독 정보 관리
|
||||
- 카테고리별 서비스 제공
|
||||
- 구독료 계산
|
||||
end note
|
||||
|
||||
note right of RecommendService
|
||||
지출 분석 기반 추천
|
||||
- 사용자별 지출 패턴 분석
|
||||
- 최적 구독 카테고리 추천
|
||||
end note
|
||||
@@ -0,0 +1,18 @@
|
||||
서비스명|회원|회원
|
||||
마이크로서비스 이름|Member|Member
|
||||
유저스토리 ID|USR-005|USR-015
|
||||
유저스토리 제목|로그인|로그아웃
|
||||
Controller 이름|MemberController|MemberController
|
||||
API 목적|사용자 로그인|로그아웃
|
||||
API Method|POST|POST
|
||||
API 그룹 Path|/api/auth|/api/auth
|
||||
API Path|/login|/logout
|
||||
Path <변수유형> <변수명>||
|
||||
Query Key||
|
||||
Query <변수유형> <변수명>||
|
||||
Request DTO 이름|LoginRequest|LogoutRequest
|
||||
Request DTO 배열 여부|No|No
|
||||
Request DTO 구조|String userId; String password|String userId
|
||||
Response DTO 이름|JwtTokenDTO|LogoutResponse
|
||||
Response DTO 배열 여부|No|No
|
||||
Response DTO 구조|String accessToken; String refreshToken|String message
|
||||
@@ -0,0 +1,17 @@
|
||||
!theme mono
|
||||
title Member Service - 데이터 모델
|
||||
|
||||
entity "Members" as members {
|
||||
* userId: varchar(50) <<PK>>
|
||||
--
|
||||
userName: varchar(100)
|
||||
password: varchar(255)
|
||||
roles: varchar(255)
|
||||
createdAt: timestamp
|
||||
updatedAt: timestamp
|
||||
}
|
||||
|
||||
note right of members
|
||||
roles는 ARRAY 또는 JSON 타입으로
|
||||
['USER', 'ADMIN'] 형태로 저장
|
||||
end note
|
||||
@@ -0,0 +1,72 @@
|
||||
!theme mono
|
||||
title 회원 서비스 - 내부 시퀀스 다이어그램
|
||||
|
||||
actor Client
|
||||
participant "회원 컨트롤러\n(MemberController)" as Controller
|
||||
participant "회원 서비스\n(MemberService)" as Service
|
||||
participant "JWT 토큰 제공자\n(JwtTokenProvider)" as TokenProvider
|
||||
participant "비밀번호 인코더\n(PasswordEncoder)" as PwEncoder
|
||||
database "회원 DB" as DB
|
||||
|
||||
' 로그인 flow
|
||||
Client -> Controller: POST /api/auth/login\n[로그인]
|
||||
activate Controller
|
||||
|
||||
Controller -> Service: login(LoginRequest)
|
||||
activate Service
|
||||
|
||||
Service -> DB: findByUserId(userId)
|
||||
activate DB
|
||||
DB --> Service: Member
|
||||
deactivate DB
|
||||
|
||||
Service -> PwEncoder: matches(rawPassword, encodedPassword)
|
||||
activate PwEncoder
|
||||
PwEncoder --> Service: matched result
|
||||
deactivate PwEncoder
|
||||
|
||||
alt 인증 성공
|
||||
Service -> TokenProvider: createToken(member)
|
||||
activate TokenProvider
|
||||
TokenProvider --> Service: access/refresh tokens
|
||||
deactivate TokenProvider
|
||||
|
||||
Service --> Controller: TokenResponse
|
||||
else 인증 실패
|
||||
Service --> Controller: throw InvalidCredentialsException
|
||||
end
|
||||
|
||||
Controller --> Client: HTTP Response\n(tokens or error)
|
||||
deactivate Service
|
||||
deactivate Controller
|
||||
|
||||
' 로그아웃 flow
|
||||
Client -> Controller: POST /api/auth/logout\n[로그아웃]
|
||||
activate Controller
|
||||
|
||||
Controller -> Service: logout(LogoutRequest)
|
||||
activate Service
|
||||
|
||||
Service --> Controller: LogoutResponse
|
||||
Controller --> Client: HTTP Response\n(success message)
|
||||
|
||||
deactivate Service
|
||||
deactivate Controller
|
||||
|
||||
note right of Controller
|
||||
1. 요청 유효성 검증
|
||||
2. 서비스 계층 호출
|
||||
3. 응답 변환 및 반환
|
||||
end note
|
||||
|
||||
note right of Service
|
||||
1. 비즈니스 로직 처리
|
||||
2. 사용자 인증
|
||||
3. 토큰 관리
|
||||
end note
|
||||
|
||||
note right of TokenProvider
|
||||
1. JWT 토큰 생성
|
||||
2. 토큰 검증
|
||||
3. 토큰 무효화
|
||||
end note
|
||||
@@ -0,0 +1,89 @@
|
||||
!theme mono
|
||||
title Member Service - Class Diagram
|
||||
|
||||
package "com.unicorn.lifesub.member" {
|
||||
package "domain" {
|
||||
class Member {
|
||||
-userId: String
|
||||
-userName: String
|
||||
-password: String
|
||||
-roles: Set<String>
|
||||
+Member(userId: String, userName: String, password: String, roles: Set<String>)
|
||||
}
|
||||
}
|
||||
|
||||
package "service" {
|
||||
interface MemberService {
|
||||
+login(request: LoginRequest): JwtTokenDTO
|
||||
+logout(request: LogoutRequest): LogoutResponse
|
||||
}
|
||||
|
||||
class MemberServiceImpl {
|
||||
-memberRepository: MemberRepository
|
||||
-passwordEncoder: PasswordEncoder
|
||||
-jwtTokenProvider: JwtTokenProvider
|
||||
+login(request: LoginRequest): JwtTokenDTO
|
||||
+logout(request: LogoutRequest): LogoutResponse
|
||||
}
|
||||
}
|
||||
|
||||
package "controller" {
|
||||
class MemberController {
|
||||
-memberService: MemberService
|
||||
+login(request: LoginRequest): ResponseEntity<ApiResponse<JwtTokenDTO>>
|
||||
+logout(request: LogoutRequest): ResponseEntity<ApiResponse<LogoutResponse>>
|
||||
}
|
||||
}
|
||||
|
||||
package "dto" {
|
||||
class LoginRequest {
|
||||
-userId: String
|
||||
-password: String
|
||||
}
|
||||
|
||||
class LogoutRequest {
|
||||
-userId: String
|
||||
}
|
||||
|
||||
class LogoutResponse {
|
||||
-message: String
|
||||
}
|
||||
}
|
||||
|
||||
package "repository" {
|
||||
package "jpa" {
|
||||
interface MemberRepository {
|
||||
+findByUserId(userId: String): Optional<MemberEntity>
|
||||
}
|
||||
}
|
||||
|
||||
package "entity" {
|
||||
class MemberEntity {
|
||||
-userId: String
|
||||
-userName: String
|
||||
-password: String
|
||||
-roles: Set<String>
|
||||
+toDomain(): Member
|
||||
+fromDomain(member: Member): MemberEntity
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
package "config" {
|
||||
class SecurityConfig {
|
||||
-jwtTokenProvider: JwtTokenProvider
|
||||
+securityFilterChain(http: HttpSecurity): SecurityFilterChain
|
||||
+corsConfigurationSource(): CorsConfigurationSource
|
||||
+passwordEncoder(): PasswordEncoder
|
||||
}
|
||||
|
||||
class JwtTokenProvider {
|
||||
-algorithm: Algorithm
|
||||
-accessTokenValidityInMilliseconds: long
|
||||
-refreshTokenValidityInMilliseconds: long
|
||||
+createToken(member: MemberEntity): JwtTokenDTO
|
||||
+validateToken(token: String): boolean
|
||||
+getAuthentication(token: String): Authentication
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user