Repository: Gosrock/DuDoong-Backend Branch: dev Commit: 1d7d7134fb4a Files: 1032 Total size: 1.5 MB Directory structure: gitextract_57vblqu2/ ├── .claude/ │ └── settings.local.json ├── .github/ │ ├── CODEOWNERS │ ├── PULL_REQUEST_TEMPLATE.md │ └── workflows/ │ ├── BuildApiServer.yml │ ├── BuildBatchServer.yml │ ├── BuildSocketServer.yml │ └── ci.yml ├── .gitignore ├── CLAUDE.md ├── DuDoong-Admin/ │ ├── build.gradle.kts │ └── src/ │ ├── main/ │ │ └── kotlin/ │ │ └── band/ │ │ └── gosrock/ │ │ └── admin/ │ │ ├── controller/ │ │ │ ├── AdminCommentController.kt │ │ │ ├── AdminDashboardController.kt │ │ │ ├── AdminEventController.kt │ │ │ ├── AdminHostController.kt │ │ │ ├── AdminOrderController.kt │ │ │ ├── AdminRefundController.kt │ │ │ └── AdminUserController.kt │ │ ├── exception/ │ │ │ ├── AdminErrorCode.kt │ │ │ └── AdminForbiddenException.kt │ │ ├── model/ │ │ │ └── dto/ │ │ │ ├── request/ │ │ │ │ ├── AdminAddHostMemberRequest.kt │ │ │ │ ├── AdminAdjustTicketStockRequest.kt │ │ │ │ ├── AdminCancelOrderRequest.kt │ │ │ │ ├── AdminChangeNameRequest.kt │ │ │ │ ├── AdminRefundStatusRequest.kt │ │ │ │ ├── AdminTransferMasterRequest.kt │ │ │ │ ├── AdminUpdateEventRequest.kt │ │ │ │ ├── AdminUpdateEventStatusRequest.kt │ │ │ │ ├── AdminUpdateHostMemberRoleRequest.kt │ │ │ │ ├── AdminUpdateHostPartnerRequest.kt │ │ │ │ ├── AdminUpdateHostProfileRequest.kt │ │ │ │ ├── AdminUpdateTicketItemRequest.kt │ │ │ │ ├── AdminUpdateUserRoleRequest.kt │ │ │ │ └── AdminUpdateUserStatusRequest.kt │ │ │ └── response/ │ │ │ ├── AdminCommentResponse.kt │ │ │ ├── AdminEventResponse.kt │ │ │ ├── AdminHostDetailResponse.kt │ │ │ ├── AdminHostMemberResponse.kt │ │ │ ├── AdminHostResponse.kt │ │ │ ├── AdminIssuedTicketResponse.kt │ │ │ ├── AdminOrderResponse.kt │ │ │ ├── AdminRefundResponse.kt │ │ │ ├── AdminTicketItemResponse.kt │ │ │ ├── AdminUserDetailResponse.kt │ │ │ ├── AdminUserResponse.kt │ │ │ └── DashboardResponse.kt │ │ └── service/ │ │ ├── AdminAddHostMemberUseCase.kt │ │ ├── AdminAdjustTicketStockUseCase.kt │ │ ├── AdminAuthValidator.kt │ │ ├── AdminCancelOrderUseCase.kt │ │ ├── AdminChangeNameUseCase.kt │ │ ├── AdminCompleteRefundUseCase.kt │ │ ├── AdminDeleteCommentUseCase.kt │ │ ├── AdminDeleteEventUseCase.kt │ │ ├── AdminExcelService.kt │ │ ├── AdminExportIssuedTicketsUseCase.kt │ │ ├── AdminGetCommentsUseCase.kt │ │ ├── AdminGetEventDetailUseCase.kt │ │ ├── AdminGetEventsUseCase.kt │ │ ├── AdminGetHostDetailUseCase.kt │ │ ├── AdminGetHostEventsUseCase.kt │ │ ├── AdminGetHostMembersUseCase.kt │ │ ├── AdminGetHostsUseCase.kt │ │ ├── AdminGetIssuedTicketsUseCase.kt │ │ ├── AdminGetMeUseCase.kt │ │ ├── AdminGetOrderDetailUseCase.kt │ │ ├── AdminGetOrdersUseCase.kt │ │ ├── AdminGetRefundDetailUseCase.kt │ │ ├── AdminGetRefundsUseCase.kt │ │ ├── AdminGetTicketItemsUseCase.kt │ │ ├── AdminGetUserDetailUseCase.kt │ │ ├── AdminGetUsersUseCase.kt │ │ ├── AdminRemoveHostMemberUseCase.kt │ │ ├── AdminTransferMasterUseCase.kt │ │ ├── AdminUpdateEventStatusUseCase.kt │ │ ├── AdminUpdateEventUseCase.kt │ │ ├── AdminUpdateHostMemberRoleUseCase.kt │ │ ├── AdminUpdateHostPartnerUseCase.kt │ │ ├── AdminUpdateHostProfileUseCase.kt │ │ ├── AdminUpdateRefundStatusUseCase.kt │ │ ├── AdminUpdateTicketItemUseCase.kt │ │ ├── AdminUpdateUserRoleUseCase.kt │ │ ├── AdminUpdateUserStatusUseCase.kt │ │ └── GetDashboardUseCase.kt │ └── test/ │ └── kotlin/ │ └── band/ │ └── gosrock/ │ └── admin/ │ └── service/ │ ├── AdminAuthValidatorTest.kt │ ├── AdminChangeNameUseCaseTest.kt │ ├── AdminCompleteRefundUseCaseTest.kt │ ├── AdminExcelServiceTest.kt │ └── AdminUpdateRefundStatusUseCaseTest.kt ├── DuDoong-Api/ │ ├── Dockerfile │ ├── build.gradle.kts │ └── src/ │ ├── main/ │ │ ├── kotlin/ │ │ │ └── band/ │ │ │ └── gosrock/ │ │ │ ├── DuDoongApiServerApplication.kt │ │ │ └── api/ │ │ │ ├── admin/ │ │ │ │ └── controller/ │ │ │ │ └── AdminAuthController.kt │ │ │ ├── alimTalk/ │ │ │ │ ├── dto/ │ │ │ │ │ └── OrderAlimTalkDto.kt │ │ │ │ ├── handler/ │ │ │ │ │ ├── DoneOrderEventAlimTalkHandler.kt │ │ │ │ │ ├── RegisterUserEventAlimTalkHandler.kt │ │ │ │ │ └── WithDrawOrderEventAlimTalkHandler.kt │ │ │ │ └── service/ │ │ │ │ ├── SendDoneOrderAlimTalkService.kt │ │ │ │ ├── SendRegisterAlimTalkService.kt │ │ │ │ ├── SendWithdrawOrderAlimTalkService.kt │ │ │ │ └── helper/ │ │ │ │ └── OrderAlimTalkInfoHelper.kt │ │ │ ├── auth/ │ │ │ │ ├── controller/ │ │ │ │ │ └── AuthController.kt │ │ │ │ ├── model/ │ │ │ │ │ └── dto/ │ │ │ │ │ ├── KakaoUserInfoDto.kt │ │ │ │ │ ├── request/ │ │ │ │ │ │ └── RegisterRequest.kt │ │ │ │ │ └── response/ │ │ │ │ │ ├── AvailableRegisterResponse.kt │ │ │ │ │ ├── OauthLoginLinkResponse.kt │ │ │ │ │ ├── OauthTokenResponse.kt │ │ │ │ │ ├── OauthUserInfoResponse.kt │ │ │ │ │ └── TokenAndUserResponse.kt │ │ │ │ └── service/ │ │ │ │ ├── LocalDevLoginUseCase.kt │ │ │ │ ├── LoginUseCase.kt │ │ │ │ ├── LogoutUseCase.kt │ │ │ │ ├── OauthUserInfoUseCase.kt │ │ │ │ ├── RefreshUseCase.kt │ │ │ │ ├── RegisterUseCase.kt │ │ │ │ ├── WithDrawUseCase.kt │ │ │ │ └── helper/ │ │ │ │ ├── CookieHelper.kt │ │ │ │ ├── KakaoOauthHelper.kt │ │ │ │ ├── OauthOIDCHelper.kt │ │ │ │ └── TokenGenerateHelper.kt │ │ │ ├── cart/ │ │ │ │ ├── controller/ │ │ │ │ │ └── CartController.kt │ │ │ │ ├── docs/ │ │ │ │ │ └── CreateCartExceptionDocs.kt │ │ │ │ ├── model/ │ │ │ │ │ ├── dto/ │ │ │ │ │ │ ├── request/ │ │ │ │ │ │ │ ├── AddCartLineDto.kt │ │ │ │ │ │ │ ├── AddCartOptionAnswerDto.kt │ │ │ │ │ │ │ └── AddCartRequest.kt │ │ │ │ │ │ └── response/ │ │ │ │ │ │ ├── CartItemResponse.kt │ │ │ │ │ │ └── CartResponse.kt │ │ │ │ │ └── mapper/ │ │ │ │ │ └── CartMapper.kt │ │ │ │ └── service/ │ │ │ │ ├── CheckOptionUseCase.kt │ │ │ │ ├── CreateCartUseCase.kt │ │ │ │ └── ReadCartUseCase.kt │ │ │ ├── comment/ │ │ │ │ ├── controller/ │ │ │ │ │ └── CommentController.kt │ │ │ │ ├── mapper/ │ │ │ │ │ └── CommentMapper.kt │ │ │ │ ├── model/ │ │ │ │ │ ├── request/ │ │ │ │ │ │ └── CreateCommentRequest.kt │ │ │ │ │ └── response/ │ │ │ │ │ ├── CreateCommentResponse.kt │ │ │ │ │ ├── RetrieveCommentCountResponse.kt │ │ │ │ │ ├── RetrieveCommentDTO.kt │ │ │ │ │ ├── RetrieveCommentListResponse.kt │ │ │ │ │ └── RetrieveRandomCommentResponse.kt │ │ │ │ └── service/ │ │ │ │ ├── CreateCommentUseCase.kt │ │ │ │ ├── DeleteCommentUseCase.kt │ │ │ │ ├── RetrieveCommentCountUseCase.kt │ │ │ │ ├── RetrieveCommentUseCase.kt │ │ │ │ └── RetrieveRandomCommentUseCase.kt │ │ │ ├── common/ │ │ │ │ ├── UserUtils.kt │ │ │ │ ├── aop/ │ │ │ │ │ ├── hostPartner/ │ │ │ │ │ │ ├── HostPartnerAllowed.kt │ │ │ │ │ │ ├── HostPartnerAop.kt │ │ │ │ │ │ ├── HostPartnerCallTransaction.kt │ │ │ │ │ │ ├── HostPartnerCallTransactionFactory.kt │ │ │ │ │ │ ├── HostPartnerEventTransaction.kt │ │ │ │ │ │ └── HostPartnerHostTransaction.kt │ │ │ │ │ └── hostRole/ │ │ │ │ │ ├── FindHostFrom.kt │ │ │ │ │ ├── HostCallTransactionFactory.kt │ │ │ │ │ ├── HostQualification.kt │ │ │ │ │ ├── HostRoleAop.kt │ │ │ │ │ ├── HostRoleCallTransaction.kt │ │ │ │ │ ├── HostRoleEventTransaction.kt │ │ │ │ │ ├── HostRoleHostTransaction.kt │ │ │ │ │ └── HostRolesAllowed.kt │ │ │ │ ├── customizer/ │ │ │ │ │ └── EnumValuePropertyCustomizer.kt │ │ │ │ ├── page/ │ │ │ │ │ └── PageResponse.kt │ │ │ │ └── slice/ │ │ │ │ ├── SliceParam.kt │ │ │ │ └── SliceResponse.kt │ │ │ ├── config/ │ │ │ │ ├── ExampleHolder.kt │ │ │ │ ├── HttpContentCacheFilter.kt │ │ │ │ ├── MdcFilter.kt │ │ │ │ ├── ServletFilterConfig.kt │ │ │ │ ├── SwaggerConfig.kt │ │ │ │ ├── WebMvcConfig.kt │ │ │ │ ├── rateLimit/ │ │ │ │ │ ├── IPRateLimiter.kt │ │ │ │ │ ├── ThrottlingInterceptor.kt │ │ │ │ │ ├── ThrottlingWebConfigure.kt │ │ │ │ │ └── UserRateLimiter.kt │ │ │ │ ├── response/ │ │ │ │ │ ├── GlobalExceptionHandler.kt │ │ │ │ │ └── SuccessResponseAdvice.kt │ │ │ │ └── security/ │ │ │ │ ├── AccessDeniedFilter.kt │ │ │ │ ├── AuthDetails.kt │ │ │ │ ├── CorsConfig.kt │ │ │ │ ├── CurrentUserIdResolver.kt │ │ │ │ ├── JwtExceptionFilter.kt │ │ │ │ ├── JwtTokenFilter.kt │ │ │ │ ├── SecurityConfig.kt │ │ │ │ └── SecurityUtils.kt │ │ │ ├── coupon/ │ │ │ │ ├── controller/ │ │ │ │ │ └── CouponController.kt │ │ │ │ ├── dto/ │ │ │ │ │ ├── reqeust/ │ │ │ │ │ │ └── CreateCouponCampaignRequest.kt │ │ │ │ │ └── response/ │ │ │ │ │ ├── CreateCouponCampaignResponse.kt │ │ │ │ │ ├── CreateUserCouponResponse.kt │ │ │ │ │ ├── ReadIssuedCouponOrderResponse.kt │ │ │ │ │ └── ReadIssuedCouponResponse.kt │ │ │ │ ├── mapper/ │ │ │ │ │ ├── CouponCampaignMapper.kt │ │ │ │ │ └── IssuedCouponMapper.kt │ │ │ │ └── service/ │ │ │ │ ├── CreateCouponUseCase.kt │ │ │ │ ├── CreateUserCouponUseCase.kt │ │ │ │ └── ReadIssuedCouponUseCase.kt │ │ │ ├── email/ │ │ │ │ ├── dto/ │ │ │ │ │ ├── IssuedTicketMailDTO.kt │ │ │ │ │ └── OrderMailDto.kt │ │ │ │ ├── handler/ │ │ │ │ │ ├── CreateOrderEventEmailHandler.kt │ │ │ │ │ ├── DoneOrderEventEmailHandler.kt │ │ │ │ │ ├── EntranceIssuedTicketEventEmailHandler.kt │ │ │ │ │ ├── RegisterUserEventEmailHandler.kt │ │ │ │ │ └── WithDrawOrderEventEmailHandler.kt │ │ │ │ └── service/ │ │ │ │ ├── EntranceIssuedTicketEmailService.kt │ │ │ │ ├── HostMasterChangeEmailService.kt │ │ │ │ ├── HostUserDisabledEmailService.kt │ │ │ │ ├── HostUserInvitationEmailService.kt │ │ │ │ ├── HostUserRoleChangeEmailService.kt │ │ │ │ ├── IssuedTicketMailInfoHelper.kt │ │ │ │ ├── OrderApproveConfirmEmailService.kt │ │ │ │ ├── OrderApproveRequestEmailService.kt │ │ │ │ ├── OrderMailInfoHelper.kt │ │ │ │ ├── OrderPaymentDoneEmailService.kt │ │ │ │ ├── OrderWithDrawCancelEmailService.kt │ │ │ │ ├── OrderWithDrawRefundEmailService.kt │ │ │ │ └── SendRegisterEmailService.kt │ │ │ ├── event/ │ │ │ │ ├── controller/ │ │ │ │ │ └── EventController.kt │ │ │ │ ├── model/ │ │ │ │ │ ├── dto/ │ │ │ │ │ │ ├── request/ │ │ │ │ │ │ │ ├── CreateEventRequest.kt │ │ │ │ │ │ │ ├── UpdateEventBasicRequest.kt │ │ │ │ │ │ │ ├── UpdateEventDetailRequest.kt │ │ │ │ │ │ │ └── UpdateEventStatusRequest.kt │ │ │ │ │ │ └── response/ │ │ │ │ │ │ ├── EventChecklistResponse.kt │ │ │ │ │ │ ├── EventDetailResponse.kt │ │ │ │ │ │ ├── EventProfileResponse.kt │ │ │ │ │ │ └── EventResponse.kt │ │ │ │ │ └── mapper/ │ │ │ │ │ └── EventMapper.kt │ │ │ │ └── service/ │ │ │ │ ├── CreateEventUseCase.kt │ │ │ │ ├── DeleteEventUseCase.kt │ │ │ │ ├── OpenEventUseCase.kt │ │ │ │ ├── ReadEventChecklistUseCase.kt │ │ │ │ ├── ReadEventDetailUseCase.kt │ │ │ │ ├── ReadUserEventProfilesUseCase.kt │ │ │ │ ├── SearchEventsUseCase.kt │ │ │ │ ├── UpdateEventBasicUseCase.kt │ │ │ │ ├── UpdateEventDetailUseCase.kt │ │ │ │ └── UpdateEventStatusUseCase.kt │ │ │ ├── example/ │ │ │ │ ├── controller/ │ │ │ │ │ └── ExampleController.kt │ │ │ │ ├── docs/ │ │ │ │ │ ├── ExampleException2Docs.kt │ │ │ │ │ └── ExampleExceptionDocs.kt │ │ │ │ ├── dto/ │ │ │ │ │ └── ExampleResponse.kt │ │ │ │ └── service/ │ │ │ │ └── ExampleApiService.kt │ │ │ ├── host/ │ │ │ │ ├── controller/ │ │ │ │ │ └── HostController.kt │ │ │ │ ├── model/ │ │ │ │ │ ├── dto/ │ │ │ │ │ │ ├── request/ │ │ │ │ │ │ │ ├── CreateHostRequest.kt │ │ │ │ │ │ │ ├── InviteHostRequest.kt │ │ │ │ │ │ │ ├── TransferMasterRequest.kt │ │ │ │ │ │ │ ├── UpdateHostRequest.kt │ │ │ │ │ │ │ ├── UpdateHostSlackRequest.kt │ │ │ │ │ │ │ └── UpdateHostUserRoleRequest.kt │ │ │ │ │ │ └── response/ │ │ │ │ │ │ ├── HostDetailResponse.kt │ │ │ │ │ │ ├── HostEventProfileResponse.kt │ │ │ │ │ │ ├── HostProfileResponse.kt │ │ │ │ │ │ └── HostResponse.kt │ │ │ │ │ └── mapper/ │ │ │ │ │ └── HostMapper.kt │ │ │ │ └── service/ │ │ │ │ ├── CreateHostUseCase.kt │ │ │ │ ├── InviteHostUseCase.kt │ │ │ │ ├── JoinHostUseCase.kt │ │ │ │ ├── ReadHostEventsUseCase.kt │ │ │ │ ├── ReadHostProfilesUseCase.kt │ │ │ │ ├── ReadHostUseCase.kt │ │ │ │ ├── ReadInviteUsersUseCase.kt │ │ │ │ ├── RejectHostUseCase.kt │ │ │ │ ├── TransferMasterUseCase.kt │ │ │ │ ├── UpdateHostProfileUseCase.kt │ │ │ │ ├── UpdateHostSlackUrlUseCase.kt │ │ │ │ └── UpdateHostUserRoleUseCase.kt │ │ │ ├── image/ │ │ │ │ ├── controller/ │ │ │ │ │ └── ImageController.kt │ │ │ │ ├── dto/ │ │ │ │ │ ├── ImageUrlRequest.kt │ │ │ │ │ └── ImageUrlResponse.kt │ │ │ │ └── service/ │ │ │ │ └── GetImageUploadUrlUseCase.kt │ │ │ ├── issuedTicket/ │ │ │ │ ├── controller/ │ │ │ │ │ ├── AdminIssuedTicketController.kt │ │ │ │ │ └── IssuedTicketController.kt │ │ │ │ ├── dto/ │ │ │ │ │ ├── request/ │ │ │ │ │ │ └── AdminIssuedTicketTableQueryRequest.kt │ │ │ │ │ └── response/ │ │ │ │ │ ├── IssuedTicketAdminTableElement.kt │ │ │ │ │ ├── RetrieveIssuedTicketDTO.kt │ │ │ │ │ ├── RetrieveIssuedTicketDetailResponse.kt │ │ │ │ │ └── RetrieveIssuedTicketListResponse.kt │ │ │ │ ├── mapper/ │ │ │ │ │ └── IssuedTicketMapper.kt │ │ │ │ └── service/ │ │ │ │ ├── EntranceIssuedTicketUseCase.kt │ │ │ │ ├── ReadIssuedTicketUseCase.kt │ │ │ │ └── ReadIssuedTicketsUseCase.kt │ │ │ ├── order/ │ │ │ │ ├── controller/ │ │ │ │ │ ├── OrderAdminController.kt │ │ │ │ │ └── OrderController.kt │ │ │ │ ├── docs/ │ │ │ │ │ ├── ApproveOrderExceptionDocs.kt │ │ │ │ │ ├── CancelOrderExceptionDocs.kt │ │ │ │ │ ├── ConfirmOrderExceptionDocs.kt │ │ │ │ │ ├── CreateOrderExceptionDocs.kt │ │ │ │ │ ├── FreeOrderExceptionDocs.kt │ │ │ │ │ └── RefundOrderExceptionDocs.kt │ │ │ │ ├── model/ │ │ │ │ │ ├── dto/ │ │ │ │ │ │ ├── request/ │ │ │ │ │ │ │ ├── AdminOrderTableQueryRequest.kt │ │ │ │ │ │ │ ├── CancelReasonRequest.kt │ │ │ │ │ │ │ ├── ConfirmOrderRequest.kt │ │ │ │ │ │ │ └── CreateOrderRequest.kt │ │ │ │ │ │ └── response/ │ │ │ │ │ │ ├── CreateOrderResponse.kt │ │ │ │ │ │ ├── OrderAdminTableElement.kt │ │ │ │ │ │ ├── OrderBriefElement.kt │ │ │ │ │ │ ├── OrderLineTicketResponse.kt │ │ │ │ │ │ ├── OrderPaymentResponse.kt │ │ │ │ │ │ ├── OrderResponse.kt │ │ │ │ │ │ └── OrderTicketResponse.kt │ │ │ │ │ └── mapper/ │ │ │ │ │ └── OrderMapper.kt │ │ │ │ └── service/ │ │ │ │ ├── ApproveOrderUseCase.kt │ │ │ │ ├── CancelOrderUseCase.kt │ │ │ │ ├── ConfirmOrderUseCase.kt │ │ │ │ ├── CreateOrderUseCase.kt │ │ │ │ ├── CreateTossOrderUseCase.kt │ │ │ │ ├── FreeOrderUseCase.kt │ │ │ │ ├── ReadOrderUseCase.kt │ │ │ │ ├── RefundOrderUseCase.kt │ │ │ │ └── RefuseOrderUseCase.kt │ │ │ ├── refund/ │ │ │ │ ├── controller/ │ │ │ │ │ └── RefundController.kt │ │ │ │ ├── dto/ │ │ │ │ │ └── response/ │ │ │ │ │ └── RefundResponse.kt │ │ │ │ └── service/ │ │ │ │ ├── CompleteRefundUseCase.kt │ │ │ │ └── GetRefundsUseCase.kt │ │ │ ├── slack/ │ │ │ │ ├── handler/ │ │ │ │ │ ├── event/ │ │ │ │ │ │ ├── EventContentChangeEventHandler.kt │ │ │ │ │ │ ├── EventCreationEventHandler.kt │ │ │ │ │ │ ├── EventDeletionEventHandler.kt │ │ │ │ │ │ └── EventStatusChangeEventHandler.kt │ │ │ │ │ ├── host/ │ │ │ │ │ │ ├── HostRegisterSlackEventHandler.kt │ │ │ │ │ │ ├── HostUserDisabledEventHandler.kt │ │ │ │ │ │ ├── HostUserInvitationEventHandler.kt │ │ │ │ │ │ ├── HostUserJoinEventHandler.kt │ │ │ │ │ │ └── HostUserRoleChangeEventHandler.kt │ │ │ │ │ └── order/ │ │ │ │ │ ├── DudoongTicketCancelOrderEventHandler.kt │ │ │ │ │ ├── DudoongTicketRefundOrderEventHandler.kt │ │ │ │ │ ├── NewApproveOrderAlarmEventHandler.kt │ │ │ │ │ ├── NewConfirmOrderAlarmEventHandler.kt │ │ │ │ │ ├── OrderApprovedAlarmEventHandler.kt │ │ │ │ │ └── WithDrawOrderEventHandler.kt │ │ │ │ └── sender/ │ │ │ │ ├── SlackBootNotificationSender.kt │ │ │ │ ├── SlackInternalErrorSender.kt │ │ │ │ └── SlackThrottleErrorSender.kt │ │ │ ├── statistic/ │ │ │ │ ├── controller/ │ │ │ │ │ └── AdminStatisticController.kt │ │ │ │ ├── dto/ │ │ │ │ │ └── DashBoardStatisticResponse.kt │ │ │ │ ├── query/ │ │ │ │ │ ├── IssuedTicketQueryRepository.kt │ │ │ │ │ ├── OrderQueryRepository.kt │ │ │ │ │ └── result/ │ │ │ │ │ ├── IssuedTicketStatistic.kt │ │ │ │ │ └── OrderStatistic.kt │ │ │ │ └── useCase/ │ │ │ │ └── StatisticUseCase.kt │ │ │ ├── ticketItem/ │ │ │ │ ├── controller/ │ │ │ │ │ ├── TicketItemController.kt │ │ │ │ │ └── TicketOptionController.kt │ │ │ │ ├── dto/ │ │ │ │ │ ├── request/ │ │ │ │ │ │ ├── ApplyTicketOptionRequest.kt │ │ │ │ │ │ ├── CreateTicketItemRequest.kt │ │ │ │ │ │ ├── CreateTicketOptionRequest.kt │ │ │ │ │ │ └── UnapplyTicketOptionRequest.kt │ │ │ │ │ └── response/ │ │ │ │ │ ├── AppliedOptionGroupResponse.kt │ │ │ │ │ ├── ApplyTicketOptionResponse.kt │ │ │ │ │ ├── GetAppliedOptionGroupsResponse.kt │ │ │ │ │ ├── GetEventOptionsResponse.kt │ │ │ │ │ ├── GetEventTicketItemsResponse.kt │ │ │ │ │ ├── GetTicketItemOptionsResponse.kt │ │ │ │ │ ├── OptionGroupResponse.kt │ │ │ │ │ ├── OptionResponse.kt │ │ │ │ │ └── TicketItemResponse.kt │ │ │ │ ├── mapper/ │ │ │ │ │ ├── TicketItemMapper.kt │ │ │ │ │ └── TicketOptionMapper.kt │ │ │ │ └── service/ │ │ │ │ ├── ApplyTicketOptionUseCase.kt │ │ │ │ ├── CreateTicketItemUseCase.kt │ │ │ │ ├── CreateTicketOptionUseCase.kt │ │ │ │ ├── DeleteOptionGroupUseCase.kt │ │ │ │ ├── DeleteTicketItemUseCase.kt │ │ │ │ ├── GetAppliedOptionGroupsUseCase.kt │ │ │ │ ├── GetEventOptionsUseCase.kt │ │ │ │ ├── GetEventTicketItemsUseCase.kt │ │ │ │ ├── GetTicketOptionsUseCase.kt │ │ │ │ └── UnapplyTicketOptionUseCase.kt │ │ │ └── user/ │ │ │ ├── controller/ │ │ │ │ └── UserController.kt │ │ │ ├── model/ │ │ │ │ └── dto/ │ │ │ │ └── request/ │ │ │ │ └── ChangeNameRequest.kt │ │ │ └── service/ │ │ │ ├── ChangeNameUseCase.kt │ │ │ ├── MarketingUserUseCase.kt │ │ │ └── ReadUserUseCase.kt │ │ └── resources/ │ │ ├── application-local.yml │ │ └── application.yml │ └── test/ │ ├── java/ │ │ └── band/ │ │ └── gosrock/ │ │ ├── DuDoongApiServerApplication.java │ │ └── api/ │ │ ├── email/ │ │ │ └── RegisterUserEventHandlerTest.java │ │ └── supports/ │ │ ├── ApiIntegrateProfileResolver.java │ │ ├── ApiIntegrateSpringBootTest.java │ │ └── ApiIntegrateTestConfig.java │ ├── kotlin/ │ │ └── band/ │ │ └── gosrock/ │ │ └── api/ │ │ ├── auth/ │ │ │ └── service/ │ │ │ └── helper/ │ │ │ └── CookieHelperTest.kt │ │ ├── common/ │ │ │ └── aop/ │ │ │ └── hostRole/ │ │ │ └── HostRoleAopTest.kt │ │ ├── refund/ │ │ │ └── service/ │ │ │ └── CompleteRefundUseCaseTest.kt │ │ └── statistic/ │ │ └── query/ │ │ └── result/ │ │ ├── IssuedTicketStatisticTest.kt │ │ └── OrderStatisticTest.kt │ └── resources/ │ └── logback-test.xml ├── DuDoong-Batch/ │ ├── Dockerfile │ ├── build.gradle.kts │ └── src/ │ └── main/ │ ├── kotlin/ │ │ └── band/ │ │ └── gosrock/ │ │ ├── BatchApplication.kt │ │ ├── dto/ │ │ │ └── SettlementPDFDto.kt │ │ ├── helper/ │ │ │ ├── SettlementEmailHelper.kt │ │ │ ├── SettlementPdfHelper.kt │ │ │ ├── excel/ │ │ │ │ ├── ExcelOrderDto.kt │ │ │ │ └── ExcelOrderHelper.kt │ │ │ └── slack/ │ │ │ ├── SlackEventExpirationSender.kt │ │ │ └── SlackUserNotificationSender.kt │ │ ├── job/ │ │ │ ├── EventExpiration.kt │ │ │ ├── EventOrdersToExcel.kt │ │ │ ├── EventSettlementAlimTalkToHost.kt │ │ │ ├── EventSettlementEmailToAdmin.kt │ │ │ ├── EventSettlementEmailToHost.kt │ │ │ ├── EventSettlementPDF.kt │ │ │ ├── EventSummarySettlement.kt │ │ │ ├── EventTransactionSettlement.kt │ │ │ └── SlackUserStatistic.kt │ │ └── parameter/ │ │ ├── DateJobParameter.kt │ │ ├── DateTimeJobParameter.kt │ │ └── EventJobParameter.kt │ └── resources/ │ └── application.yml ├── DuDoong-Common/ │ ├── build.gradle.kts │ └── src/ │ ├── main/ │ │ ├── kotlin/ │ │ │ └── band/ │ │ │ └── gosrock/ │ │ │ └── common/ │ │ │ ├── DuDoongCommonApplication.kt │ │ │ ├── annotation/ │ │ │ │ ├── Adaptor.kt │ │ │ │ ├── ApiErrorCodeExample.kt │ │ │ │ ├── ApiErrorExceptionsExample.kt │ │ │ │ ├── CurrentUserId.kt │ │ │ │ ├── DateFormat.kt │ │ │ │ ├── DevelopOnlyApi.kt │ │ │ │ ├── DisableSwaggerSecurity.kt │ │ │ │ ├── DomainService.kt │ │ │ │ ├── Enum.kt │ │ │ │ ├── EnumClass.kt │ │ │ │ ├── ExceptionDoc.kt │ │ │ │ ├── ExplainError.kt │ │ │ │ ├── Helper.kt │ │ │ │ ├── Mapper.kt │ │ │ │ ├── Phone.kt │ │ │ │ ├── Policy.kt │ │ │ │ ├── Port.kt │ │ │ │ ├── UseCase.kt │ │ │ │ └── Validator.kt │ │ │ ├── aop/ │ │ │ │ └── ApiBlockingAspect.kt │ │ │ ├── config/ │ │ │ │ └── ConfigurationPropertiesConfig.kt │ │ │ ├── consts/ │ │ │ │ └── DuDoongStatic.kt │ │ │ ├── deserializer/ │ │ │ │ └── CustomEnumDeserializer.kt │ │ │ ├── dto/ │ │ │ │ ├── AccessTokenInfo.kt │ │ │ │ ├── ErrorReason.kt │ │ │ │ ├── ErrorResponse.kt │ │ │ │ ├── OIDCDecodePayload.kt │ │ │ │ └── SuccessResponse.kt │ │ │ ├── exception/ │ │ │ │ ├── BadFileExtensionException.kt │ │ │ │ ├── BadLockIdentifierException.kt │ │ │ │ ├── BaseErrorCode.kt │ │ │ │ ├── DuDoongCodeException.kt │ │ │ │ ├── DuDoongDynamicException.kt │ │ │ │ ├── ExpiredTokenException.kt │ │ │ │ ├── GlobalErrorCode.kt │ │ │ │ ├── InvalidTokenException.kt │ │ │ │ ├── NotAvailableRedissonLockException.kt │ │ │ │ ├── OtherServerBadRequestException.kt │ │ │ │ ├── OtherServerExpiredTokenException.kt │ │ │ │ ├── OtherServerForbiddenException.kt │ │ │ │ ├── OtherServerInternalSeverErrorException.kt │ │ │ │ ├── OtherServerNotFoundException.kt │ │ │ │ ├── OtherServerUnauthorizedException.kt │ │ │ │ ├── RefreshTokenExpiredException.kt │ │ │ │ ├── SecurityContextNotFoundException.kt │ │ │ │ └── TooManyRequestException.kt │ │ │ ├── helper/ │ │ │ │ └── SpringEnvironmentHelper.kt │ │ │ ├── interfaces/ │ │ │ │ └── SwaggerExampleExceptions.kt │ │ │ ├── jwt/ │ │ │ │ ├── JwtOIDCProvider.kt │ │ │ │ └── JwtTokenProvider.kt │ │ │ ├── properties/ │ │ │ │ ├── JwtProperties.kt │ │ │ │ ├── OauthProperties.kt │ │ │ │ └── TossPaymentsProperties.kt │ │ │ └── validator/ │ │ │ ├── EnumValidator.kt │ │ │ └── PhoneValidator.kt │ │ └── resources/ │ │ ├── application-common-local.yml │ │ └── application-common.yml │ └── test/ │ ├── kotlin/ │ │ └── band/ │ │ └── gosrock/ │ │ └── common/ │ │ ├── jwt/ │ │ │ └── JwtTokenProviderTest.kt │ │ └── properties/ │ │ ├── JwtPropertiesTest.kt │ │ └── TossPaymentsPropertiesTest.kt │ └── resources/ │ ├── application.yml │ └── logback-test.xml ├── DuDoong-Domain/ │ ├── build.gradle.kts │ └── src/ │ ├── main/ │ │ ├── kotlin/ │ │ │ └── band/ │ │ │ └── gosrock/ │ │ │ └── domain/ │ │ │ ├── DomainPackageLocation.kt │ │ │ ├── DuDoongDomainApplication.kt │ │ │ ├── common/ │ │ │ │ ├── alarm/ │ │ │ │ │ ├── EventSlackAlarm.kt │ │ │ │ │ ├── HostSlackAlarm.kt │ │ │ │ │ ├── OrderKakaoTalkAlarm.kt │ │ │ │ │ ├── SettlementKakaoTalkAlarm.kt │ │ │ │ │ └── UserKakaoTalkAlarm.kt │ │ │ │ ├── aop/ │ │ │ │ │ ├── domainEvent/ │ │ │ │ │ │ ├── DomainEvent.kt │ │ │ │ │ │ ├── EventPublisherAspect.kt │ │ │ │ │ │ └── Events.kt │ │ │ │ │ └── redissonLock/ │ │ │ │ │ ├── CallTransaction.kt │ │ │ │ │ ├── CallTransactionFactory.kt │ │ │ │ │ ├── RedissonCallNewTransaction.kt │ │ │ │ │ ├── RedissonCallSameTransaction.kt │ │ │ │ │ ├── RedissonLock.kt │ │ │ │ │ └── RedissonLockAop.kt │ │ │ │ ├── converter/ │ │ │ │ │ └── BigDecimalScale6WithBankersRoundingConverter.kt │ │ │ │ ├── dto/ │ │ │ │ │ └── ProfileViewDto.kt │ │ │ │ ├── events/ │ │ │ │ │ ├── event/ │ │ │ │ │ │ ├── EventContentChangeEvent.kt │ │ │ │ │ │ ├── EventCreationEvent.kt │ │ │ │ │ │ ├── EventDeletionEvent.kt │ │ │ │ │ │ └── EventStatusChangeEvent.kt │ │ │ │ │ ├── host/ │ │ │ │ │ │ ├── HostRegisterSlackEvent.kt │ │ │ │ │ │ ├── HostUserDisabledEvent.kt │ │ │ │ │ │ ├── HostUserInvitationEvent.kt │ │ │ │ │ │ ├── HostUserJoinEvent.kt │ │ │ │ │ │ └── HostUserRoleChangeEvent.kt │ │ │ │ │ ├── issuedTicket/ │ │ │ │ │ │ └── EntranceIssuedTicketEvent.kt │ │ │ │ │ ├── order/ │ │ │ │ │ │ ├── CreateOrderEvent.kt │ │ │ │ │ │ ├── DoneOrderEvent.kt │ │ │ │ │ │ └── WithDrawOrderEvent.kt │ │ │ │ │ └── user/ │ │ │ │ │ └── UserRegisterEvent.kt │ │ │ │ ├── model/ │ │ │ │ │ └── BaseTimeEntity.kt │ │ │ │ ├── util/ │ │ │ │ │ ├── PhoneNumberInstance.kt │ │ │ │ │ ├── QueryDslUtil.kt │ │ │ │ │ └── SliceUtil.kt │ │ │ │ └── vo/ │ │ │ │ ├── AccountInfoVo.kt │ │ │ │ ├── CommentInfoVo.kt │ │ │ │ ├── DateTimePeriod.kt │ │ │ │ ├── EventBasicVo.kt │ │ │ │ ├── EventDetailVo.kt │ │ │ │ ├── EventInfoVo.kt │ │ │ │ ├── EventPlaceVo.kt │ │ │ │ ├── EventProfileVo.kt │ │ │ │ ├── HostInfoVo.kt │ │ │ │ ├── HostProfileVo.kt │ │ │ │ ├── HostUserVo.kt │ │ │ │ ├── ImageVo.kt │ │ │ │ ├── IssuedCouponInfoVo.kt │ │ │ │ ├── IssuedTicketInfoVo.kt │ │ │ │ ├── IssuedTicketOptionAnswerVo.kt │ │ │ │ ├── Money.kt │ │ │ │ ├── OptionAnswerVo.kt │ │ │ │ ├── PhoneNumberVo.kt │ │ │ │ ├── RefundInfoVo.kt │ │ │ │ ├── UserInfoVo.kt │ │ │ │ └── UserProfileVo.kt │ │ │ ├── config/ │ │ │ │ ├── CustomAsyncExceptionHandler.kt │ │ │ │ ├── EnableAsyncConfig.kt │ │ │ │ ├── JpaConfig.kt │ │ │ │ ├── MdcTaskDecorator.kt │ │ │ │ └── QueryDslConfig.kt │ │ │ └── domains/ │ │ │ ├── cart/ │ │ │ │ ├── adaptor/ │ │ │ │ │ └── CartAdaptor.kt │ │ │ │ ├── domain/ │ │ │ │ │ ├── Cart.kt │ │ │ │ │ ├── CartLineItem.kt │ │ │ │ │ ├── CartOptionAnswer.kt │ │ │ │ │ └── CartValidator.kt │ │ │ │ ├── exception/ │ │ │ │ │ ├── CartErrorCode.kt │ │ │ │ │ ├── CartInvalidOptionAnswerException.kt │ │ │ │ │ ├── CartItemNotOneTypeException.kt │ │ │ │ │ ├── CartLineItemNotFoundException.kt │ │ │ │ │ ├── CartNotAnswerAllOptionGroupException.kt │ │ │ │ │ └── CartNotFoundException.kt │ │ │ │ ├── repository/ │ │ │ │ │ ├── CartCustomRepository.kt │ │ │ │ │ ├── CartCustomRepositoryImpl.kt │ │ │ │ │ └── CartRepository.kt │ │ │ │ └── service/ │ │ │ │ ├── CartDomainService.kt │ │ │ │ └── DoneOrderEventHandler.kt │ │ │ ├── comment/ │ │ │ │ ├── adaptor/ │ │ │ │ │ └── CommentAdaptor.kt │ │ │ │ ├── domain/ │ │ │ │ │ ├── Comment.kt │ │ │ │ │ └── CommentStatus.kt │ │ │ │ ├── dto/ │ │ │ │ │ └── condition/ │ │ │ │ │ └── CommentCondition.kt │ │ │ │ ├── exception/ │ │ │ │ │ ├── CommentAlreadyDeleteException.kt │ │ │ │ │ ├── CommentErrorCode.kt │ │ │ │ │ ├── CommentNotFoundException.kt │ │ │ │ │ ├── CommentNotMatchEventException.kt │ │ │ │ │ └── RetrieveRandomCommentNotFoundException.kt │ │ │ │ ├── repository/ │ │ │ │ │ ├── CommentCustomRepository.kt │ │ │ │ │ ├── CommentCustomRepositoryImpl.kt │ │ │ │ │ └── CommentRepository.kt │ │ │ │ └── service/ │ │ │ │ └── CommentDomainService.kt │ │ │ ├── coupon/ │ │ │ │ ├── adaptor/ │ │ │ │ │ ├── CouponCampaignAdaptor.kt │ │ │ │ │ └── IssuedCouponAdaptor.kt │ │ │ │ ├── domain/ │ │ │ │ │ ├── ApplyTarget.kt │ │ │ │ │ ├── CouponCampaign.kt │ │ │ │ │ ├── CouponStockInfo.kt │ │ │ │ │ ├── DiscountType.kt │ │ │ │ │ └── IssuedCoupon.kt │ │ │ │ ├── exception/ │ │ │ │ │ ├── AlreadyExistCouponCampaignException.kt │ │ │ │ │ ├── AlreadyIssuedCouponException.kt │ │ │ │ │ ├── AlreadyRecoveredCouponException.kt │ │ │ │ │ ├── AlreadyUsedCouponException.kt │ │ │ │ │ ├── CouponCampaignNotFoundException.kt │ │ │ │ │ ├── CouponErrorCode.kt │ │ │ │ │ ├── CouponNotFoundException.kt │ │ │ │ │ ├── NoCouponStockLeftException.kt │ │ │ │ │ ├── NotIssuingCouponPeriodException.kt │ │ │ │ │ ├── NotMyCouponException.kt │ │ │ │ │ ├── SupplyLessThenDiscountException.kt │ │ │ │ │ ├── SupplyLessThenMinimumException.kt │ │ │ │ │ └── WrongDiscountAmountException.kt │ │ │ │ ├── repository/ │ │ │ │ │ ├── CouponCampaignRepository.kt │ │ │ │ │ ├── IssuedCouponCustomRepository.kt │ │ │ │ │ ├── IssuedCouponCustomRepositoryImpl.kt │ │ │ │ │ └── IssuedCouponRepository.kt │ │ │ │ └── service/ │ │ │ │ ├── CreateCouponCampaignDomainService.kt │ │ │ │ ├── CreateIssuedCouponDomainService.kt │ │ │ │ ├── RecoveryCouponService.kt │ │ │ │ ├── UseCouponService.kt │ │ │ │ └── handler/ │ │ │ │ ├── CreateOrderCouponHandler.kt │ │ │ │ └── WithDrawOrderCouponHandler.kt │ │ │ ├── event/ │ │ │ │ ├── adaptor/ │ │ │ │ │ └── EventAdaptor.kt │ │ │ │ ├── domain/ │ │ │ │ │ ├── Event.kt │ │ │ │ │ ├── EventBasic.kt │ │ │ │ │ ├── EventDetail.kt │ │ │ │ │ ├── EventDetailImage.kt │ │ │ │ │ ├── EventPlace.kt │ │ │ │ │ └── EventStatus.kt │ │ │ │ ├── exception/ │ │ │ │ │ ├── AlreadyCalculatingStatusException.kt │ │ │ │ │ ├── AlreadyCloseStatusException.kt │ │ │ │ │ ├── AlreadyDeletedStatusException.kt │ │ │ │ │ ├── AlreadyExistEventUrlNameException.kt │ │ │ │ │ ├── AlreadyOpenStatusException.kt │ │ │ │ │ ├── AlreadyPreparingStatusException.kt │ │ │ │ │ ├── CannotDeleteByIssuedTicketException.kt │ │ │ │ │ ├── CannotDeleteByOpenEventException.kt │ │ │ │ │ ├── CannotModifyOpenEventException.kt │ │ │ │ │ ├── CannotOpenEventException.kt │ │ │ │ │ ├── EventCannotEndBeforeStartException.kt │ │ │ │ │ ├── EventErrorCode.kt │ │ │ │ │ ├── EventNotFoundException.kt │ │ │ │ │ ├── EventNotOpenException.kt │ │ │ │ │ ├── EventOpenTimeExpiredException.kt │ │ │ │ │ ├── EventTicketingTimeIsPassedException.kt │ │ │ │ │ ├── HostNotAuthEventException.kt │ │ │ │ │ ├── InvalidEventStatusTransitionException.kt │ │ │ │ │ └── UseOtherApiException.kt │ │ │ │ ├── repository/ │ │ │ │ │ ├── EventCustomRepository.kt │ │ │ │ │ ├── EventCustomRepositoryImpl.kt │ │ │ │ │ ├── EventDetailImageRepository.kt │ │ │ │ │ └── EventRepository.kt │ │ │ │ └── service/ │ │ │ │ └── EventService.kt │ │ │ ├── example/ │ │ │ │ ├── domain/ │ │ │ │ │ └── ExampleEntity.kt │ │ │ │ ├── repository/ │ │ │ │ │ └── ExampleRepository.kt │ │ │ │ └── service/ │ │ │ │ └── ExampleDomainService.kt │ │ │ ├── host/ │ │ │ │ ├── adaptor/ │ │ │ │ │ └── HostAdaptor.kt │ │ │ │ ├── domain/ │ │ │ │ │ ├── Host.kt │ │ │ │ │ ├── HostProfile.kt │ │ │ │ │ ├── HostRole.kt │ │ │ │ │ └── HostUser.kt │ │ │ │ ├── exception/ │ │ │ │ │ ├── AlreadyJoinedHostException.kt │ │ │ │ │ ├── CannotModifyMasterHostRoleException.kt │ │ │ │ │ ├── DuplicateSlackUrlException.kt │ │ │ │ │ ├── ForbiddenHostException.kt │ │ │ │ │ ├── HostErrorCode.kt │ │ │ │ │ ├── HostNotFoundException.kt │ │ │ │ │ ├── HostUserNotFoundException.kt │ │ │ │ │ ├── InvalidSlackUrlException.kt │ │ │ │ │ ├── NotAcceptedHostException.kt │ │ │ │ │ ├── NotManagerHostException.kt │ │ │ │ │ ├── NotMasterHostException.kt │ │ │ │ │ └── NotPartnerHostException.kt │ │ │ │ ├── repository/ │ │ │ │ │ ├── HostCustomRepository.kt │ │ │ │ │ ├── HostCustomRepositoryImpl.kt │ │ │ │ │ └── HostRepository.kt │ │ │ │ └── service/ │ │ │ │ └── HostService.kt │ │ │ ├── issuedTicket/ │ │ │ │ ├── adaptor/ │ │ │ │ │ ├── IssuedTicketAdaptor.kt │ │ │ │ │ └── IssuedTicketOptionAnswerAdaptor.kt │ │ │ │ ├── domain/ │ │ │ │ │ ├── IssuedTicket.kt │ │ │ │ │ ├── IssuedTicketCancelReason.kt │ │ │ │ │ ├── IssuedTicketItemInfoVo.kt │ │ │ │ │ ├── IssuedTicketOptionAnswer.kt │ │ │ │ │ ├── IssuedTicketStatus.kt │ │ │ │ │ ├── IssuedTicketUserInfoVo.kt │ │ │ │ │ ├── IssuedTickets.kt │ │ │ │ │ └── IssuedTicketsStage.kt │ │ │ │ ├── dto/ │ │ │ │ │ ├── condition/ │ │ │ │ │ │ └── IssuedTicketCondition.kt │ │ │ │ │ ├── request/ │ │ │ │ │ │ ├── CreateIssuedTicketDTO.kt │ │ │ │ │ │ ├── CreateIssuedTicketForDevDTO.kt │ │ │ │ │ │ ├── CreateIssuedTicketRequestDTOs.kt │ │ │ │ │ │ └── CreateIssuedTicketRequestForDev.kt │ │ │ │ │ └── response/ │ │ │ │ │ ├── CreateIssuedTicketResponse.kt │ │ │ │ │ ├── IssuedTicketDTO.kt │ │ │ │ │ └── IssuedTicketPageDTO.kt │ │ │ │ ├── exception/ │ │ │ │ │ ├── CanNotCancelEntranceException.kt │ │ │ │ │ ├── CanNotCancelException.kt │ │ │ │ │ ├── CanNotEntranceException.kt │ │ │ │ │ ├── IssuedTicketAlreadyEntranceException.kt │ │ │ │ │ ├── IssuedTicketErrorCode.kt │ │ │ │ │ ├── IssuedTicketNotFoundException.kt │ │ │ │ │ ├── IssuedTicketNotMatchedEventException.kt │ │ │ │ │ └── IssuedTicketUserNotMatchedException.kt │ │ │ │ ├── repository/ │ │ │ │ │ ├── IssuedTicketCustomRepository.kt │ │ │ │ │ ├── IssuedTicketCustomRepositoryImpl.kt │ │ │ │ │ ├── IssuedTicketOptionAnswerRepository.kt │ │ │ │ │ ├── IssuedTicketRepository.kt │ │ │ │ │ └── condition/ │ │ │ │ │ └── FindEventIssuedTicketsCondition.kt │ │ │ │ ├── service/ │ │ │ │ │ ├── IssuedTicketDomainService.kt │ │ │ │ │ ├── OrderToIssuedTicketService.kt │ │ │ │ │ └── handlers/ │ │ │ │ │ ├── OrderEventHandler.kt │ │ │ │ │ └── WithdrawOrderEventHandler.kt │ │ │ │ └── validator/ │ │ │ │ └── IssuedTicketValidator.kt │ │ │ ├── order/ │ │ │ │ ├── adaptor/ │ │ │ │ │ └── OrderAdaptor.kt │ │ │ │ ├── domain/ │ │ │ │ │ ├── Order.kt │ │ │ │ │ ├── OrderCouponVo.kt │ │ │ │ │ ├── OrderItemVo.kt │ │ │ │ │ ├── OrderLineItem.kt │ │ │ │ │ ├── OrderMethod.kt │ │ │ │ │ ├── OrderOptionAnswer.kt │ │ │ │ │ ├── OrderStatus.kt │ │ │ │ │ ├── PaymentInfo.kt │ │ │ │ │ ├── PaymentMethod.kt │ │ │ │ │ ├── PgPaymentInfo.kt │ │ │ │ │ ├── RefundStatus.kt │ │ │ │ │ └── validator/ │ │ │ │ │ └── OrderValidator.kt │ │ │ │ ├── exception/ │ │ │ │ │ ├── ApproveWaitingOrderPurchaseLimitException.kt │ │ │ │ │ ├── CanNotApproveDeletedUserOrderException.kt │ │ │ │ │ ├── CanNotCancelOrderException.kt │ │ │ │ │ ├── CanNotRefundOrderException.kt │ │ │ │ │ ├── CanNotRefuseOrderException.kt │ │ │ │ │ ├── InvalidOrderException.kt │ │ │ │ │ ├── LessThanMinmumPaymentOrderException.kt │ │ │ │ │ ├── NotApprovalOrderException.kt │ │ │ │ │ ├── NotFreeOrderException.kt │ │ │ │ │ ├── NotOwnerOrderException.kt │ │ │ │ │ ├── NotPaymentOrderException.kt │ │ │ │ │ ├── NotPendingOrderException.kt │ │ │ │ │ ├── NotRefundAvailableDateOrderException.kt │ │ │ │ │ ├── NotSupportedOrderMethodException.kt │ │ │ │ │ ├── OrdeItemNotOneTypeException.kt │ │ │ │ │ ├── OrderErrorCode.kt │ │ │ │ │ ├── OrderItemOptionChangedException.kt │ │ │ │ │ ├── OrderLineNotFountException.kt │ │ │ │ │ └── OrderNotFoundException.kt │ │ │ │ ├── repository/ │ │ │ │ │ ├── OrderCustomRepository.kt │ │ │ │ │ ├── OrderCustomRepositoryImpl.kt │ │ │ │ │ ├── OrderRepository.kt │ │ │ │ │ └── condition/ │ │ │ │ │ ├── AdminTableOrderFilterType.kt │ │ │ │ │ ├── AdminTableSearchType.kt │ │ │ │ │ ├── FindEventOrdersCondition.kt │ │ │ │ │ └── FindMyPageOrderCondition.kt │ │ │ │ └── service/ │ │ │ │ ├── CreateOrderService.kt │ │ │ │ ├── FreeOrderService.kt │ │ │ │ ├── OrderApproveService.kt │ │ │ │ ├── OrderConfirmService.kt │ │ │ │ ├── OrderFactory.kt │ │ │ │ ├── WithdrawOrderService.kt │ │ │ │ ├── WithdrawPaymentService.kt │ │ │ │ └── handler/ │ │ │ │ ├── ConfirmOrderFailHandler.kt │ │ │ │ └── WithDrawOrderHandler.kt │ │ │ ├── settlement/ │ │ │ │ ├── adaptor/ │ │ │ │ │ ├── EventSettlementAdaptor.kt │ │ │ │ │ └── TransactionSettlementAdaptor.kt │ │ │ │ ├── domain/ │ │ │ │ │ ├── EventSettlement.kt │ │ │ │ │ ├── EventSettlementStatus.kt │ │ │ │ │ ├── SettlementFeeVo.kt │ │ │ │ │ └── TransactionSettlement.kt │ │ │ │ ├── repository/ │ │ │ │ │ ├── EventSettlementRepository.kt │ │ │ │ │ └── TransactionSettlementRepository.kt │ │ │ │ └── service/ │ │ │ │ └── EventSettlementDomainService.kt │ │ │ ├── ticket_item/ │ │ │ │ ├── adaptor/ │ │ │ │ │ ├── OptionAdaptor.kt │ │ │ │ │ ├── OptionGroupAdaptor.kt │ │ │ │ │ └── TicketItemAdaptor.kt │ │ │ │ ├── domain/ │ │ │ │ │ ├── ItemOptionGroup.kt │ │ │ │ │ ├── Option.kt │ │ │ │ │ ├── OptionGroup.kt │ │ │ │ │ ├── OptionGroupStatus.kt │ │ │ │ │ ├── OptionGroupType.kt │ │ │ │ │ ├── TicketItem.kt │ │ │ │ │ ├── TicketItemStatus.kt │ │ │ │ │ ├── TicketPayType.kt │ │ │ │ │ └── TicketType.kt │ │ │ │ ├── exception/ │ │ │ │ │ ├── DuplicatedItemOptionGroupException.kt │ │ │ │ │ ├── EmptyAccountInfoException.kt │ │ │ │ │ ├── ForbiddenOptionChangeException.kt │ │ │ │ │ ├── ForbiddenOptionGroupDeleteException.kt │ │ │ │ │ ├── ForbiddenOptionPriceException.kt │ │ │ │ │ ├── ForbiddenTicketItemDeleteException.kt │ │ │ │ │ ├── InvalidOptionGroupException.kt │ │ │ │ │ ├── InvalidOptionPriceException.kt │ │ │ │ │ ├── InvalidPartnerException.kt │ │ │ │ │ ├── InvalidTicketItemException.kt │ │ │ │ │ ├── InvalidTicketPriceException.kt │ │ │ │ │ ├── InvalidTicketTypeException.kt │ │ │ │ │ ├── NotAppliedItemOptionGroupException.kt │ │ │ │ │ ├── NotCorrectOptionAnswerException.kt │ │ │ │ │ ├── OptionGroupNotFoundException.kt │ │ │ │ │ ├── OptionNotFoundException.kt │ │ │ │ │ ├── TicketItemErrorCode.kt │ │ │ │ │ ├── TicketItemNotFoundException.kt │ │ │ │ │ ├── TicketItemQuantityException.kt │ │ │ │ │ ├── TicketItemQuantityLackException.kt │ │ │ │ │ ├── TicketItemQuantityLargeException.kt │ │ │ │ │ └── TicketPurchaseLimitException.kt │ │ │ │ ├── repository/ │ │ │ │ │ ├── OptionGroupRepository.kt │ │ │ │ │ ├── OptionRepository.kt │ │ │ │ │ └── TicketItemRepository.kt │ │ │ │ └── service/ │ │ │ │ ├── ItemOptionGroupService.kt │ │ │ │ ├── TicketItemService.kt │ │ │ │ └── TicketOptionService.kt │ │ │ └── user/ │ │ │ ├── adaptor/ │ │ │ │ ├── RefreshTokenAdaptor.kt │ │ │ │ └── UserAdaptor.kt │ │ │ ├── domain/ │ │ │ │ ├── AccountRole.kt │ │ │ │ ├── AccountState.kt │ │ │ │ ├── OauthInfo.kt │ │ │ │ ├── OauthProvider.kt │ │ │ │ ├── Profile.kt │ │ │ │ ├── RefreshTokenEntity.kt │ │ │ │ └── User.kt │ │ │ ├── exception/ │ │ │ │ ├── AlreadyDeletedUserException.kt │ │ │ │ ├── AlreadySignUpUserException.kt │ │ │ │ ├── EmptyPhoneNumException.kt │ │ │ │ ├── ForbiddenUserException.kt │ │ │ │ ├── UserErrorCode.kt │ │ │ │ ├── UserNotFoundException.kt │ │ │ │ └── UserPhoneNumberInvalidException.kt │ │ │ ├── repository/ │ │ │ │ ├── RefreshTokenRepository.kt │ │ │ │ └── UserRepository.kt │ │ │ └── service/ │ │ │ └── UserDomainService.kt │ │ └── resources/ │ │ ├── application-domain-local.yml │ │ └── application-domain.yml │ └── test/ │ ├── java/ │ │ └── band/ │ │ └── gosrock/ │ │ ├── domain/ │ │ │ ├── CunCurrencyExecutorService.java │ │ │ ├── DisableDomainEvent.java │ │ │ ├── DisableRedissonLock.java │ │ │ ├── DomainIntegrateProfileResolver.java │ │ │ ├── DomainIntegrateSpringBootTest.java │ │ │ ├── DomainIntegrateTestConfig.java │ │ │ ├── common/ │ │ │ │ ├── aop/ │ │ │ │ │ └── redissonLock/ │ │ │ │ │ └── RedissonLockAopTest.java │ │ │ │ └── vo/ │ │ │ │ └── RefundInfoVoTest.java │ │ │ └── domains/ │ │ │ ├── cart/ │ │ │ │ └── domain/ │ │ │ │ ├── CartLineItemTest.java │ │ │ │ ├── CartOptionAnswerTest.java │ │ │ │ ├── CartTest.java │ │ │ │ └── CartValidatorTest.java │ │ │ ├── coupon/ │ │ │ │ └── domain/ │ │ │ │ ├── CouponCampaignTest.java │ │ │ │ ├── CouponStockInfoTest.java │ │ │ │ └── IssuedCouponTest.java │ │ │ ├── event/ │ │ │ │ └── EventTest.java │ │ │ ├── host/ │ │ │ │ ├── domain/ │ │ │ │ │ ├── HostProfileTest.java │ │ │ │ │ ├── HostRoleTest.java │ │ │ │ │ ├── HostTest.java │ │ │ │ │ └── HostUserTest.java │ │ │ │ └── service/ │ │ │ │ ├── HostServiceConcurrencyFailureTest.java │ │ │ │ └── HostServiceConcurrencyTest.java │ │ │ ├── issuedTicket/ │ │ │ │ ├── adaptor/ │ │ │ │ │ └── IssuedTicketAdaptorTest.java │ │ │ │ ├── domain/ │ │ │ │ │ ├── IssuedTicketItemInfoVoTest.java │ │ │ │ │ ├── IssuedTicketOptionAnswerTest.java │ │ │ │ │ ├── IssuedTicketTest.java │ │ │ │ │ ├── IssuedTicketUserInfoVoTest.java │ │ │ │ │ └── validator/ │ │ │ │ │ └── IssuedTicketValidatorTest.java │ │ │ │ └── service/ │ │ │ │ ├── IssuedTicketDomainServiceTest.java │ │ │ │ └── handlers/ │ │ │ │ ├── OrderEventHandlerTest.java │ │ │ │ └── WithdrawOrderEventHandlerTest.java │ │ │ └── order/ │ │ │ ├── domain/ │ │ │ │ ├── OrderLineItemTest.java │ │ │ │ ├── OrderOptionAnswerTest.java │ │ │ │ ├── OrderTest.java │ │ │ │ └── validator/ │ │ │ │ └── OrderValidatorTest.java │ │ │ └── service/ │ │ │ ├── OrderApproveServiceConcurrencyFailTest.java │ │ │ ├── OrderApproveServiceConcurrencyTest.java │ │ │ ├── OrderApproveServiceTest.java │ │ │ ├── WithdrawOrderServiceTest.java │ │ │ └── handler/ │ │ │ └── WithDrawOrderHandlerTest.java │ │ └── domains/ │ │ └── user/ │ │ └── domain/ │ │ ├── OauthInfoTest.java │ │ └── UserTest.java │ ├── kotlin/ │ │ └── band/ │ │ └── gosrock/ │ │ └── domain/ │ │ ├── common/ │ │ │ └── vo/ │ │ │ ├── DateTimePeriodTest.kt │ │ │ └── MoneyTest.kt │ │ └── domains/ │ │ ├── cart/ │ │ │ └── service/ │ │ │ └── DoneOrderEventHandlerTest.kt │ │ ├── event/ │ │ │ └── EventStatusTransitionTest.kt │ │ ├── host/ │ │ │ └── HostTransferMasterTest.kt │ │ ├── issuedTicket/ │ │ │ ├── IssuedTicketStatusTransitionTest.kt │ │ │ ├── adaptor/ │ │ │ │ └── IssuedTicketAdaptorTest.kt │ │ │ └── domain/ │ │ │ └── IssuedTicketItemInfoVoTest.kt │ │ ├── order/ │ │ │ ├── OrderPaymentCalculationTest.kt │ │ │ ├── OrderRefundTest.kt │ │ │ └── service/ │ │ │ └── handler/ │ │ │ └── ConfirmOrderFailHandlerTest.kt │ │ └── user/ │ │ └── domain/ │ │ └── ProfileTest.kt │ └── resources/ │ ├── application-test.yml │ ├── logback-test.xml │ └── mockito-extensions/ │ └── org.mockito.plugins.MockMaker ├── DuDoong-Infrastructure/ │ ├── build.gradle.kts │ └── src/ │ ├── main/ │ │ ├── kotlin/ │ │ │ └── band/ │ │ │ └── gosrock/ │ │ │ └── infrastructure/ │ │ │ ├── DuDoongInfraApplication.kt │ │ │ ├── config/ │ │ │ │ ├── alilmTalk/ │ │ │ │ │ ├── NcpHelper.kt │ │ │ │ │ └── dto/ │ │ │ │ │ ├── AlimTalkEventInfo.kt │ │ │ │ │ ├── AlimTalkOrderInfo.kt │ │ │ │ │ ├── AlimTalkUserInfo.kt │ │ │ │ │ └── MessageDto.kt │ │ │ │ ├── feign/ │ │ │ │ │ └── FeignCommonConfig.kt │ │ │ │ ├── mail/ │ │ │ │ │ └── dto/ │ │ │ │ │ ├── EmailEventInfo.kt │ │ │ │ │ ├── EmailIssuedTicketInfo.kt │ │ │ │ │ ├── EmailOrderInfo.kt │ │ │ │ │ └── EmailUserInfo.kt │ │ │ │ ├── pdf/ │ │ │ │ │ ├── B64ImgReplacedElementFactory.kt │ │ │ │ │ └── PdfRender.kt │ │ │ │ ├── redis/ │ │ │ │ │ ├── RedisCacheConfig.kt │ │ │ │ │ ├── RedisConfig.kt │ │ │ │ │ └── RedissonConfig.kt │ │ │ │ ├── s3/ │ │ │ │ │ ├── ImageFileExtension.kt │ │ │ │ │ ├── ImageUrlDto.kt │ │ │ │ │ ├── S3Config.kt │ │ │ │ │ ├── S3PrivateFileService.kt │ │ │ │ │ └── S3UploadPresignedUrlService.kt │ │ │ │ ├── ses/ │ │ │ │ │ ├── AwsSesConfig.kt │ │ │ │ │ ├── AwsSesUtils.kt │ │ │ │ │ ├── RawEmailAttachmentDto.kt │ │ │ │ │ └── SendRawEmailDto.kt │ │ │ │ └── slack/ │ │ │ │ ├── SlackAsyncErrorSender.kt │ │ │ │ ├── SlackErrorNotificationProvider.kt │ │ │ │ ├── SlackHelper.kt │ │ │ │ ├── SlackMessageProvider.kt │ │ │ │ ├── SlackServiceNotificationProvider.kt │ │ │ │ └── config/ │ │ │ │ └── SlackApiConfig.kt │ │ │ └── outer/ │ │ │ └── api/ │ │ │ ├── BaseFeignClientPackage.kt │ │ │ ├── alimTalk/ │ │ │ │ ├── client/ │ │ │ │ │ └── NcpClient.kt │ │ │ │ └── config/ │ │ │ │ ├── NcpConfig.kt │ │ │ │ └── NcpErrorDecoder.kt │ │ │ ├── oauth/ │ │ │ │ ├── client/ │ │ │ │ │ ├── KakaoInfoClient.kt │ │ │ │ │ └── KakaoOauthClient.kt │ │ │ │ ├── config/ │ │ │ │ │ ├── KakaoInfoConfig.kt │ │ │ │ │ ├── KakaoInfoErrorDecoder.kt │ │ │ │ │ ├── KakaoKauthConfig.kt │ │ │ │ │ └── KauthErrorDecoder.kt │ │ │ │ ├── dto/ │ │ │ │ │ ├── KakaoInformationResponse.kt │ │ │ │ │ ├── KakaoKauthErrorResponse.kt │ │ │ │ │ ├── KakaoTokenResponse.kt │ │ │ │ │ ├── OIDCPublicKeyDto.kt │ │ │ │ │ ├── OIDCPublicKeysResponse.kt │ │ │ │ │ └── UnlinkKaKaoTarget.kt │ │ │ │ └── exception/ │ │ │ │ └── KakaoKauthErrorCode.kt │ │ │ └── tossPayments/ │ │ │ ├── client/ │ │ │ │ ├── PaymentsCancelClient.kt │ │ │ │ ├── PaymentsConfirmClient.kt │ │ │ │ ├── PaymentsCreateClient.kt │ │ │ │ ├── SettlementClient.kt │ │ │ │ └── TransactionGetClient.kt │ │ │ ├── config/ │ │ │ │ ├── FeignTossConfig.kt │ │ │ │ ├── PaymentCancelErrorDecoder.kt │ │ │ │ ├── PaymentConfirmErrorDecoder.kt │ │ │ │ ├── PaymentCreateErrorDecoder.kt │ │ │ │ ├── PaymentsCancelConfig.kt │ │ │ │ ├── PaymentsConfirmConfig.kt │ │ │ │ ├── PaymentsCreateConfig.kt │ │ │ │ ├── TossErrorDecoder.kt │ │ │ │ ├── TossHeaderConfig.kt │ │ │ │ ├── TransactionGetConfig.kt │ │ │ │ └── TransactionGetErrorDecoder.kt │ │ │ ├── dto/ │ │ │ │ ├── request/ │ │ │ │ │ ├── CancelPaymentsRequest.kt │ │ │ │ │ ├── ConfirmPaymentsRequest.kt │ │ │ │ │ └── CreatePaymentsRequest.kt │ │ │ │ └── response/ │ │ │ │ ├── CardAcquireStatus.kt │ │ │ │ ├── CardCode.kt │ │ │ │ ├── EasyPayCode.kt │ │ │ │ ├── FeeCode.kt │ │ │ │ ├── PaymentCheckout.kt │ │ │ │ ├── PaymentEasyPay.kt │ │ │ │ ├── PaymentReceipt.kt │ │ │ │ ├── PaymentStatus.kt │ │ │ │ ├── PaymentsCancels.kt │ │ │ │ ├── PaymentsCard.kt │ │ │ │ ├── PaymentsCardPromotion.kt │ │ │ │ ├── PaymentsCashReceipt.kt │ │ │ │ ├── PaymentsFailure.kt │ │ │ │ ├── PaymentsResponse.kt │ │ │ │ ├── SettlementFeeDto.kt │ │ │ │ ├── SettlementResponse.kt │ │ │ │ └── TossPaymentMethod.kt │ │ │ └── exception/ │ │ │ ├── PaymentsCancelErrorCode.kt │ │ │ ├── PaymentsConfirmErrorCode.kt │ │ │ ├── PaymentsCreateErrorCode.kt │ │ │ ├── PaymentsEnumNotMatchException.kt │ │ │ ├── PaymentsUnHandleException.kt │ │ │ ├── TossPaymentsErrorDto.kt │ │ │ └── TransactionGetErrorCode.kt │ │ └── resources/ │ │ ├── application-infrastructure.yml │ │ └── templates/ │ │ ├── entranceIssuedTicket.html │ │ ├── eventSettlement.html │ │ ├── fragments/ │ │ │ ├── button.html │ │ │ ├── divider.html │ │ │ ├── footer.html │ │ │ ├── header.html │ │ │ ├── issuedTicketInfo.html │ │ │ ├── orderInfo.html │ │ │ ├── subTilte.html │ │ │ └── title.html │ │ ├── hostInvite.html │ │ ├── layouts/ │ │ │ └── mailFormat.html │ │ ├── orderApproveConfirm.html │ │ ├── orderApproveRequest.html │ │ ├── orderPaymentDone.html │ │ ├── orderWithdrawCancel.html │ │ ├── orderWithdrawRefund.html │ │ ├── settlement.html │ │ └── signUp.html │ └── test/ │ ├── java/ │ │ └── band/ │ │ └── gosrock/ │ │ └── infrastructure/ │ │ ├── InfraIntegrateProfileResolver.java │ │ ├── InfraIntegrateSpringBootTest.java │ │ ├── InfraIntegrateTestConfig.java │ │ ├── config/ │ │ │ ├── redis/ │ │ │ │ └── AutoConfigureTestFeign.java │ │ │ └── s3/ │ │ │ └── S3UploadPresignedUrlServiceTest.java │ │ └── outer/ │ │ └── api/ │ │ └── tossPayments/ │ │ ├── client/ │ │ │ ├── PaymentsCancelClientTest.java │ │ │ ├── TossPaymentsClientTest.java │ │ │ └── TossSettlementClientTest.java │ │ └── config/ │ │ └── PaymentConfirmErrorDecoderTest.java │ └── resources/ │ ├── application.yml │ ├── logback-test.xml │ └── payload/ │ └── settlement-response.json ├── LICENSE ├── README.md ├── build.gradle.kts ├── docker-compose.yml ├── e2e-tests/ │ ├── .gitignore │ ├── README.md │ ├── conftest.py │ ├── requirements.txt │ ├── test_01_auth.py │ ├── test_02_host.py │ ├── test_03_event.py │ ├── test_04_ticket_item.py │ ├── test_05_order_flow.py │ ├── test_06_issued_ticket.py │ ├── test_07_comment.py │ ├── test_08_refund.py │ ├── test_09_error_cases.py │ ├── test_10_event_lifecycle.py │ ├── test_11_multi_user.py │ ├── test_12_host_management.py │ ├── test_13_user_profile.py │ ├── test_14_ticket_stock.py │ ├── test_15_event_crud.py │ ├── test_16_event_open_conditions.py │ ├── test_17_event_modification_rules.py │ ├── test_18_event_status_matrix.py │ ├── test_19_host_invite.py │ ├── test_20_ticket_options.py │ ├── test_21_dudoong_ticket.py │ ├── test_22_order_detail_verification.py │ ├── test_23_refund_edge_cases.py │ ├── test_24_admin_features.py │ ├── test_25_coupon_flow.py │ ├── test_26_order_edge_cases.py │ ├── test_27_host_role_change.py │ ├── test_28_ticket_quantity_public.py │ ├── test_29_order_cancel.py │ ├── test_30_order_before_open.py │ ├── test_31_admin_auth_role.py │ ├── test_32_admin_token_separation.py │ ├── test_33_host_role_authorization.py │ ├── test_34_admin_usecase_authorization.py │ ├── test_35_user_nickname.py │ ├── test_36_host_master_transfer.py │ ├── test_37_order_cancel_reason.py │ └── test_38_refund_api.py ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── lombok.config ├── scripts/ │ └── local-start.sh └── settings.gradle.kts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .claude/settings.local.json ================================================ { "permissions": { "allow": [ "mcp__mysql__mysql_query" ] } } ================================================ FILE: .github/CODEOWNERS ================================================ * @ImNM @sanbonai06 @cofls6581 @gengminy @kim-wonjin ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ ## 개요 - close #issueNumber ## 작업사항 - 내용을 적어주세요. ## 변경로직 - 내용을 적어주세요. ================================================ FILE: .github/workflows/BuildApiServer.yml ================================================ name: Build Api Server on: push: tags: - Api-v*.*.* jobs: build: runs-on: ubuntu-latest strategy: matrix: java-version: [ 21 ] outputs: version: ${{ steps.get_version.outputs.BRANCH_NAME }} steps: - name: Check Out The Repository uses: actions/checkout@v4 - name: Set up Java uses: actions/setup-java@v4 with: java-version: ${{ matrix.java-version }} distribution: 'temurin' - name: Get the version id: get_version run: | RELEASE_VERSION_WITHOUT_V="$(cut -d'v' -f2 <<< ${GITHUB_REF#refs/*/})" echo "VERSION=$RELEASE_VERSION_WITHOUT_V" >> $GITHUB_OUTPUT #테스트 수행용 도커 컴포즈 - name: Start containers run: docker compose up -d - name: Gradle Build uses: gradle/gradle-build-action@v3 - name: Execute Gradle build run: ./gradlew :DuDoong-Api:build --no-daemon - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Login to Docker Hub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push uses: docker/build-push-action@v5 with: context: ./DuDoong-Api push: true tags: water0641/dudoong-api:${{ steps.get_version.outputs.VERSION }} ================================================ FILE: .github/workflows/BuildBatchServer.yml ================================================ name: Build Batch Server on: push: tags: - Batch-v*.*.* jobs: build: runs-on: ubuntu-latest strategy: matrix: java-version: [ 21 ] outputs: version: ${{ steps.get_version.outputs.BRANCH_NAME }} steps: - name: Check Out The Repository uses: actions/checkout@v4 - name: Set up Java uses: actions/setup-java@v4 with: java-version: ${{ matrix.java-version }} distribution: 'temurin' - name: Get the version id: get_version run: | RELEASE_VERSION_WITHOUT_V="$(cut -d'v' -f2 <<< ${GITHUB_REF#refs/*/})" echo "VERSION=$RELEASE_VERSION_WITHOUT_V" >> $GITHUB_OUTPUT #테스트 수행용 도커 컴포즈 - name: Start containers run: docker compose up -d - name: Gradle Build uses: gradle/gradle-build-action@v3 - name: Execute Gradle build run: ./gradlew :DuDoong-Batch:build --no-daemon - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Login to Docker Hub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push uses: docker/build-push-action@v5 with: context: ./DuDoong-Batch push: true tags: | water0641/dudoong-batch:${{ steps.get_version.outputs.VERSION }} water0641/dudoong-batch:latest ================================================ FILE: .github/workflows/BuildSocketServer.yml ================================================ name: Build Socket Server on: push: tags: - Socket-v*.*.* jobs: build: runs-on: ubuntu-latest strategy: matrix: java-version: [ 17 ] outputs: version: ${{ steps.get_version.outputs.BRANCH_NAME }} steps: - name: Check Out The Repository uses: actions/checkout@v3 - name: Set up Java uses: actions/setup-java@v3 with: java-version: ${{ matrix.java-version }} distribution: 'corretto' - name: Get the version id: get_version run: | RELEASE_VERSION_WITHOUT_V="$(cut -d'v' -f2 <<< ${GITHUB_REF#refs/*/})" echo ::set-output name=VERSION::$RELEASE_VERSION_WITHOUT_V #테스트 수행용 도커 컴포즈 - name: Start containers run: docker compose up -d - name: Gradle Build uses: gradle/gradle-build-action@v2 - name: Execute Gradle build run: ./gradlew :DuDoong-Socket:build --no-daemon - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 - name: Login to Docker Hub uses: docker/login-action@v2 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push uses: docker/build-push-action@v3 with: context: ./DuDoong-Socket push: true tags: water0641/dudoong-socket:${{ steps.get_version.outputs.VERSION }} ================================================ FILE: .github/workflows/ci.yml ================================================ name: ci on: pull_request: branch: 'dev' jobs: ktlintCheck: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: SetUp JDK 21 uses: actions/setup-java@v4 with: java-version: "21" distribution: 'temurin' - name: Gradle Caching uses: actions/cache@v4 with: path: | ~/.gradle/caches ~/.gradle/wrapper key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle.kts', '**/gradle-wrapper.properties') }} restore-keys: | ${{ runner.os }}-gradle- - name: Grant execute permission for gradlew run: chmod +x ./gradlew - name: ktlint check run: ./gradlew ktlintCheck ================================================ FILE: .gitignore ================================================ DuDoong-Domain/HELP.md .gradle build/ !gradle/wrapper/gradle-wrapper.jar !**/src/main/**/build/ !**/src/test/**/build/ ### STS ### .apt_generated .classpath .factorypath .project .settings .springBeans .sts4-cache bin/ !**/src/main/**/bin/ !**/src/test/**/bin/ ### IntelliJ IDEA ### .idea *.iws *.iml *.ipr out/ !**/src/main/**/out/ !**/src/test/**/out/ ### NetBeans ### /nbproject/private/ /nbbuild/ /dist/ /nbdist/ /.nb-gradle/ ### VS Code ### .vscode/ .env .env.* *.xlsx **/src/main/generated/ .omc/ ================================================ FILE: CLAUDE.md ================================================ # DuDoong Backend - Claude 컨텍스트 ## 🎯 현재 진행 중인 작업 **Java → Kotlin 전체 마이그레이션** 마스터 트래킹 이슈: https://github.com/Gosrock/DuDoong-Backend/issues/582 --- ## 🏗️ 프로젝트 아키텍처 ### 기술 스택 - **언어**: Kotlin 1.9.22 (Java → Kotlin 마이그레이션 완료) - **프레임워크**: Spring Boot 3.2.0 - **런타임**: Java 21 - **빌드**: Gradle 8.5 Kotlin DSL - **DB**: MySQL + Spring Data JPA + QueryDSL - **캐시/락**: Redis + Redisson (분산락) - **외부 API**: Toss Payments (OpenFeign), AWS S3/SES, NCP AlimTalk, Slack API - **인증**: Spring Security + JWT + Kakao OAuth - **문서화**: SpringDoc OpenAPI (Swagger) - **배치**: Spring Batch - **실시간**: Spring WebSocket (STOMP) - **코드 품질**: SonarQube, Spotless (google-java-format), Jacoco ### 멀티모듈 구조 (의존성 순서) ``` DuDoong-Backend/ ├── DuDoong-Common/ # 최하위: JWT, 어노테이션, 예외처리, DTO, 유틸 ├── DuDoong-Infrastructure/ # Common 의존: Redis, S3, SES, Feign, Slack, NCP ├── DuDoong-Domain/ # Common+Infra 의존: JPA 엔티티, 도메인 서비스, AOP ├── DuDoong-Api/ # 모든 모듈 의존: REST API, Security, Swagger ├── DuDoong-Socket/ # 모든 모듈 의존: WebSocket STOMP 서버 └── DuDoong-Batch/ # 모든 모듈 의존: Spring Batch 정산/알림/엑셀 ``` ### 도메인 영역 (DuDoong-Domain/domains/) | 도메인 | 설명 | |--------|------| | `user` | 사용자 (Kakao OAuth 로그인) | | `host` | 이벤트 주최자 | | `event` | 이벤트 | | `ticket_item` | 티켓 종류 | | `order` | 주문/결제 (Toss Payments) | | `cart` | 장바구니 | | `coupon` | 쿠폰 | | `issuedTicket` | 발급 티켓 (QR 입장 검증) | | `settlement` | 정산 | | `comment` | 댓글 | ### API 컨트롤러 목록 - `AuthController` - 카카오 OAuth 인증 - `UserController` - 유저 관리 - `HostController` - 호스트 관리 - `EventController` - 이벤트 CRUD - `TicketItemController` / `TicketOptionController` - 티켓 종류 - `OrderController` / `OrderAdminController` - 주문/결제 - `CartController` - 장바구니 - `CouponController` - 쿠폰 - `IssuedTicketController` / `AdminIssuedTicketController` - 발급 티켓 - `ImageController` - S3 Pre-signed URL 이미지 업로드 - `AdminStatisticController` - 통계 - `CommentController` - 댓글 ### GitHub 저장소 - **Remote**: https://github.com/Gosrock/DuDoong-Backend.git - **기본 브랜치**: `dev` --- ## 🚀 Kotlin 마이그레이션 진행 상황 ### 마이그레이션 전략: Bottom-up 방식 ``` Phase 0 → Phase 1 → Phase 2 → Phase 3 → Phase 4 (Gradle) (Common) (Infra) (Domain) (Api) ↘ Phase 5 (Socket) ↘ Phase 6 (Batch) ``` ### 이슈 목록 | Phase | 이슈 | 내용 | 상태 | |-------|------|------|------| | Master | [#582](https://github.com/Gosrock/DuDoong-Backend/issues/582) | 전체 트래킹 | 🔵 진행예정 | | Phase 0 | [#583](https://github.com/Gosrock/DuDoong-Backend/issues/583) | Gradle Kotlin DSL 전환 | ⬜ 대기 | | Phase 1-1 | [#584](https://github.com/Gosrock/DuDoong-Backend/issues/584) | Common: 어노테이션 & 유틸 | ⬜ 대기 | | Phase 1-2 | [#585](https://github.com/Gosrock/DuDoong-Backend/issues/585) | Common: 예외처리 & DTO | ⬜ 대기 | | Phase 1-3 | [#586](https://github.com/Gosrock/DuDoong-Backend/issues/586) | Common: JWT & Properties | ⬜ 대기 | | Phase 2-1 | [#587](https://github.com/Gosrock/DuDoong-Backend/issues/587) | Infra: Redis & Redisson | ⬜ 대기 | | Phase 2-2 | [#588](https://github.com/Gosrock/DuDoong-Backend/issues/588) | Infra: AWS S3 & SES | ⬜ 대기 | | Phase 2-3 | [#589](https://github.com/Gosrock/DuDoong-Backend/issues/589) | Infra: Toss Payments Feign | ⬜ 대기 | | Phase 2-4 | [#590](https://github.com/Gosrock/DuDoong-Backend/issues/590) | Infra: Slack & AlimTalk | ⬜ 대기 | | Phase 3-1 | [#591](https://github.com/Gosrock/DuDoong-Backend/issues/591) | Domain: AOP & 도메인 이벤트 | ⬜ 대기 | | Phase 3-2 | [#592](https://github.com/Gosrock/DuDoong-Backend/issues/592) | Domain: User & Host | ⬜ 대기 | | Phase 3-3 | [#593](https://github.com/Gosrock/DuDoong-Backend/issues/593) | Domain: Event & TicketItem | ⬜ 대기 | | Phase 3-4 | [#594](https://github.com/Gosrock/DuDoong-Backend/issues/594) | Domain: Order & Cart | ⬜ 대기 | | Phase 3-5 | [#595](https://github.com/Gosrock/DuDoong-Backend/issues/595) | Domain: IssuedTicket & Coupon | ⬜ 대기 | | Phase 3-6 | [#596](https://github.com/Gosrock/DuDoong-Backend/issues/596) | Domain: Settlement & Comment | ⬜ 대기 | | Phase 4-1 | [#597](https://github.com/Gosrock/DuDoong-Backend/issues/597) | Api: Spring Security & Auth | ⬜ 대기 | | Phase 4-2 | [#598](https://github.com/Gosrock/DuDoong-Backend/issues/598) | Api: User, Host, Event | ⬜ 대기 | | Phase 4-3 | [#599](https://github.com/Gosrock/DuDoong-Backend/issues/599) | Api: Order, Cart, Coupon | ⬜ 대기 | | Phase 4-4 | [#600](https://github.com/Gosrock/DuDoong-Backend/issues/600) | Api: IssuedTicket, TicketItem | ⬜ 대기 | | Phase 4-5 | [#601](https://github.com/Gosrock/DuDoong-Backend/issues/601) | Api: Image, Statistic, 공통 | ⬜ 대기 | | Phase 5 | [#602](https://github.com/Gosrock/DuDoong-Backend/issues/602) | Socket: WebSocket 서버 | ⬜ 대기 | | Phase 6-1 | [#603](https://github.com/Gosrock/DuDoong-Backend/issues/603) | Batch: 정산 Job | ⬜ 대기 | | Phase 6-2 | [#604](https://github.com/Gosrock/DuDoong-Backend/issues/604) | Batch: 만료/엑셀/Slack 통계 | ⬜ 대기 | --- ## 🔐 인증 및 권한 체계 ### 인증 방식 - **로그인**: 카카오 OAuth (`/api/v1/auth/oauth/kakao`) 또는 로컬 개발용 (`/api/v1/auth/oauth/local/login`, dev 전용) - **토큰 전달**: `accessToken` 쿠키 (기본) 또는 `Authorization: Bearer` 헤더 - **토큰 갱신**: `POST /api/v1/auth/token/refresh` ### 역할 (AccountRole) | 역할 | 설명 | |------|------| | `USER` | 일반 유저 | | `ADMIN` | 어드민 (내부 관리자) | | `SUPER_ADMIN` | 최고 관리자 | - **MANAGER 역할은 삭제됨** (호스트 멤버십의 HostRole과 혼동 방지) - Role hierarchy: `SUPER_ADMIN > ADMIN > USER` ### API 접근 제어 (SecurityConfig) | 경로 | 접근 권한 | |------|----------| | `/api/v1/auth/oauth/**` | permitAll | | `/api/v1/events/{id}` (GET) | permitAll | | `/api/v1/events/search` (GET) | permitAll | | `/internal-api/**` | **ADMIN, SUPER_ADMIN만** | | 나머지 `/api/**` | USER 이상 (인증 필수) | ### 호스트 권한 (HostRole AOP) - `@HostRolesAllowed` AOP로 호스트 멤버십 기반 권한 체크 - **userId를 메서드 파라미터로 명시적 전달** (SecurityContext 미사용) - `SUPER_ADMIN`은 호스트 멤버가 아니어도 모든 이벤트/호스트 접근 가능 (바이패스) - HostQualification: `MASTER` > `MANAGER` > `GUEST` ### Admin API (internal-api) - SecurityConfig 레벨: ADMIN/SUPER_ADMIN role 체크 - UseCase 레벨: `AdminAuthValidator`로 이중 권한 체크 - 읽기: `validateAdminOrAbove` (ADMIN+) - 쓰기: `validateAdminOrAbove` (ADMIN+) - 역할 변경: `validateSuperAdmin` (SUPER_ADMIN만) - 어드민 전용 로그인 엔드포인트 없음 — 일반 로그인 쿠키 사용 ### 어드민 토큰 관련 (레거시) - `X-Admin-Token` 헤더, `aud:admin` JWT claim — **사용하지 않음** - `AdminLoginUseCase`, `AdminLocalDevLoginUseCase` — **삭제됨** - 어드민 접근은 쿠키 기반 + DB role 체크로 통일 --- ## 🧪 로컬 개발 & E2E 테스트 ### 서버 기동 (로컬 MySQL 사용) ```bash # docker-compose 먼저 (MySQL + Redis) docker compose up -d # local 프로필로 기동 — 반드시 이 방식으로! ./gradlew :DuDoong-Api:bootRun --args='--spring.profiles.active=local,infrastructure,domain,domain-local,common,common-local' ``` **주의**: `local` 프로필 없이 기동하면 **H2 인메모리 DB**를 사용하게 되어 MySQL과 불일치 발생. - `spring.profiles.group.local` = `infrastructure, domain-local, common-local` - `domain-local` 프로필이 `jdbc:mysql://127.0.0.1:13306/dudoong` 설정 ### E2E 테스트 (Python pytest) ```bash cd e2e-tests pytest -v # 전체 실행 pytest test_33* test_34* -v # 권한 테스트만 ``` ### 디버깅 팁 - API 안 되면 **프로필 먼저 확인** (`profiles are active` 로그) - DB 연결 확인: 로그에서 `jdbc:mysql` vs `jdbc:h2:mem` 확인 - 403 나오면: DB에서 `account_role` 확인 + `hasAnyRole` 매칭 확인 - 한번에 안 되면 **curl로 한 단계씩 확인** (로그인 → DB 확인 → role 변경 → API 호출) --- ## 🔑 Kotlin 마이그레이션 핵심 원칙 1. **Lombok 제거**: `@Data` → `data class`, `@Builder` → named params + `copy()`, `@RequiredArgsConstructor` → 주생성자 2. **Null Safety**: `@Nullable`/`@NonNull` → Kotlin `?` / non-null 타입 3. **KAPT**: QueryDSL Q클래스 생성을 APT → KAPT로 전환 4. **JPA 엔티티**: `open class` 필수 (`allOpen` plugin), 기본 생성자 자동생성 (`noArg` plugin) 5. **Jackson**: `jackson-module-kotlin` 등록 필수 6. **Bean Validation**: `@field:NotBlank` 접두사 필수 (Kotlin data class) 7. **Spring Security**: Kotlin lambda DSL 활용 ## 📋 PR 규칙 - 브랜치명: `feature/kotlin-migration-phase-{N}` 또는 `feature/kotlin-migration-phase-{N}-{name}` - PR 본문: `close #이슈번호` 포함 - 각 Phase 완료 후 빌드(`./gradlew :{Module}:build`) 확인 필수 ## ⚠️ 주요 주의사항 - Java ↔ Kotlin 혼재 허용 (마이그레이션 기간 중) - CI (GitHub Actions) 빌드 항상 유지 - `lombok.config` 파일 → 최종 완료 시 삭제 - SonarQube 설정 → 마지막에 Kotlin 소스로 업데이트 ================================================ FILE: DuDoong-Admin/build.gradle.kts ================================================ tasks.bootJar { enabled = false } tasks.jar { enabled = true } dependencies { implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.boot:spring-boot-starter-validation") implementation("org.springframework.boot:spring-boot-starter-security") implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0") implementation(project(":DuDoong-Domain")) implementation(project(":DuDoong-Common")) implementation("org.apache.poi:poi:5.2.0") implementation("org.apache.poi:poi-ooxml:5.2.0") } ================================================ FILE: DuDoong-Admin/src/main/kotlin/band/gosrock/admin/controller/AdminCommentController.kt ================================================ package band.gosrock.admin.controller import band.gosrock.admin.model.dto.response.AdminCommentResponse import band.gosrock.admin.service.AdminDeleteCommentUseCase import band.gosrock.admin.service.AdminGetCommentsUseCase import band.gosrock.common.annotation.CurrentUserId import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.security.SecurityRequirement import io.swagger.v3.oas.annotations.tags.Tag import org.springframework.data.domain.Page import org.springframework.data.domain.Pageable import org.springframework.data.web.PageableDefault import org.springframework.http.HttpStatus import org.springframework.web.bind.annotation.DeleteMapping import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.ResponseStatus import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("/internal-api/v1/comments") @SecurityRequirement(name = "admin-token") @Tag(name = "Admin") class AdminCommentController( private val adminGetCommentsUseCase: AdminGetCommentsUseCase, private val adminDeleteCommentUseCase: AdminDeleteCommentUseCase, ) { @Operation(summary = "댓글 목록을 조회합니다.") @GetMapping fun getComments( @CurrentUserId userId: Long, @RequestParam(required = false) keyword: String?, @RequestParam(required = false) eventId: Long?, @PageableDefault(size = 20) pageable: Pageable, ): Page { return adminGetCommentsUseCase.execute(userId, keyword, eventId, pageable) } @Operation(summary = "댓글을 삭제합니다. (소프트 삭제)") @DeleteMapping("/{commentId}") @ResponseStatus(HttpStatus.NO_CONTENT) fun deleteComment(@CurrentUserId userId: Long, @PathVariable commentId: Long) { adminDeleteCommentUseCase.execute(userId, commentId) } } ================================================ FILE: DuDoong-Admin/src/main/kotlin/band/gosrock/admin/controller/AdminDashboardController.kt ================================================ package band.gosrock.admin.controller import band.gosrock.admin.model.dto.response.DashboardResponse import band.gosrock.admin.service.GetDashboardUseCase import band.gosrock.common.annotation.CurrentUserId import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.security.SecurityRequirement import io.swagger.v3.oas.annotations.tags.Tag import java.time.LocalDate import org.springframework.format.annotation.DateTimeFormat import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("/internal-api/v1") @SecurityRequirement(name = "admin-token") @Tag(name = "Admin") class AdminDashboardController( private val getDashboardUseCase: GetDashboardUseCase, ) { @Operation(summary = "어드민 대시보드 통계를 조회합니다.") @GetMapping("/dashboard") fun getDashboard( @CurrentUserId userId: Long, @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) startDate: LocalDate?, @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) endDate: LocalDate?, ): DashboardResponse { return getDashboardUseCase.execute(userId, startDate, endDate) } } ================================================ FILE: DuDoong-Admin/src/main/kotlin/band/gosrock/admin/controller/AdminEventController.kt ================================================ package band.gosrock.admin.controller import band.gosrock.admin.model.dto.request.AdminAdjustTicketStockRequest import band.gosrock.admin.model.dto.request.AdminUpdateEventRequest import band.gosrock.admin.model.dto.request.AdminUpdateEventStatusRequest import band.gosrock.admin.model.dto.request.AdminUpdateTicketItemRequest import band.gosrock.admin.model.dto.response.AdminEventResponse import band.gosrock.admin.model.dto.response.AdminIssuedTicketResponse import band.gosrock.admin.model.dto.response.AdminTicketItemResponse import band.gosrock.admin.service.AdminAdjustTicketStockUseCase import band.gosrock.admin.service.AdminDeleteEventUseCase import band.gosrock.admin.service.AdminExcelService import band.gosrock.admin.service.AdminExportIssuedTicketsUseCase import band.gosrock.admin.service.AdminGetEventDetailUseCase import band.gosrock.admin.service.AdminGetEventsUseCase import band.gosrock.admin.service.AdminGetIssuedTicketsUseCase import band.gosrock.admin.service.AdminGetTicketItemsUseCase import band.gosrock.admin.service.AdminUpdateEventStatusUseCase import band.gosrock.admin.service.AdminUpdateEventUseCase import band.gosrock.admin.service.AdminUpdateTicketItemUseCase import band.gosrock.common.annotation.CurrentUserId import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.security.SecurityRequirement import io.swagger.v3.oas.annotations.tags.Tag import org.springframework.data.domain.Page import org.springframework.data.domain.Pageable import org.springframework.data.web.PageableDefault import org.springframework.http.HttpHeaders import org.springframework.http.HttpStatus import org.springframework.http.MediaType import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.DeleteMapping import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PatchMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.ResponseStatus import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("/internal-api/v1/events") @SecurityRequirement(name = "admin-token") @Tag(name = "Admin") class AdminEventController( private val adminGetEventsUseCase: AdminGetEventsUseCase, private val adminGetEventDetailUseCase: AdminGetEventDetailUseCase, private val adminDeleteEventUseCase: AdminDeleteEventUseCase, private val adminUpdateEventStatusUseCase: AdminUpdateEventStatusUseCase, private val adminUpdateEventUseCase: AdminUpdateEventUseCase, private val adminGetIssuedTicketsUseCase: AdminGetIssuedTicketsUseCase, private val adminExcelService: AdminExcelService, private val adminExportIssuedTicketsUseCase: AdminExportIssuedTicketsUseCase, private val adminGetTicketItemsUseCase: AdminGetTicketItemsUseCase, private val adminUpdateTicketItemUseCase: AdminUpdateTicketItemUseCase, private val adminAdjustTicketStockUseCase: AdminAdjustTicketStockUseCase, ) { @Operation(summary = "이벤트 목록을 엑셀로 다운로드합니다.") @GetMapping("/export") fun exportEvents( @CurrentUserId userId: Long, @RequestParam(required = false) keyword: String?, @RequestParam(required = false) status: String?, ): ResponseEntity { val events = adminGetEventsUseCase.executeAll(userId, keyword, status) val bytes = adminExcelService.generateEventsExcel(events) return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=events.xlsx") .contentType(MediaType.APPLICATION_OCTET_STREAM) .body(bytes) } @Operation(summary = "이벤트 목록을 조회합니다.") @GetMapping fun getEvents( @CurrentUserId userId: Long, @RequestParam(required = false) keyword: String?, @RequestParam(required = false) status: String?, @PageableDefault(size = 20) pageable: Pageable, ): Page { return adminGetEventsUseCase.execute(userId, keyword, status, pageable) } @Operation(summary = "이벤트 상세 정보를 조회합니다.") @GetMapping("/{eventId}") fun getEventDetail(@CurrentUserId userId: Long, @PathVariable eventId: Long): AdminEventResponse { return adminGetEventDetailUseCase.execute(userId, eventId) } @Operation(summary = "이벤트를 소프트 삭제합니다.") @DeleteMapping("/{eventId}") @ResponseStatus(HttpStatus.NO_CONTENT) fun deleteEvent(@CurrentUserId userId: Long, @PathVariable eventId: Long) { adminDeleteEventUseCase.execute(userId, eventId) } @Operation(summary = "이벤트 상태를 변경합니다. (어드민 전용, 밸리데이션 우회)") @PatchMapping("/{eventId}/status") @ResponseStatus(HttpStatus.NO_CONTENT) fun updateEventStatus( @CurrentUserId userId: Long, @PathVariable eventId: Long, @RequestBody request: AdminUpdateEventStatusRequest, ) { adminUpdateEventStatusUseCase.execute(userId, eventId, request) } @Operation(summary = "이벤트 정보를 수정합니다. (어드민 전용, OPEN 상태에서도 수정 가능)") @PatchMapping("/{eventId}") fun updateEvent( @CurrentUserId userId: Long, @PathVariable eventId: Long, @RequestBody request: AdminUpdateEventRequest, ): AdminEventResponse { return adminUpdateEventUseCase.execute(userId, eventId, request) } @Operation(summary = "이벤트별 발급 티켓 목록을 조회합니다.") @GetMapping("/{eventId}/issued-tickets") fun getIssuedTickets( @CurrentUserId userId: Long, @PathVariable eventId: Long, @PageableDefault(size = 20) pageable: Pageable, ): Page { return adminGetIssuedTicketsUseCase.execute(userId, eventId, pageable) } @Operation(summary = "이벤트별 티켓 종류 목록을 조회합니다.") @GetMapping("/{eventId}/ticket-items") fun getTicketItems(@CurrentUserId userId: Long, @PathVariable eventId: Long): List { return adminGetTicketItemsUseCase.execute(userId, eventId) } @Operation(summary = "이벤트별 티켓 종류 목록을 엑셀로 다운로드합니다.") @GetMapping("/{eventId}/ticket-items/export") fun exportTicketItems(@CurrentUserId userId: Long, @PathVariable eventId: Long): ResponseEntity { val items = adminGetTicketItemsUseCase.execute(userId, eventId) val bytes = adminExcelService.generateTicketItemsExcel(items) return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=ticket-items-${eventId}.xlsx") .contentType(MediaType.APPLICATION_OCTET_STREAM) .body(bytes) } @Operation(summary = "이벤트별 발급 티켓 목록을 엑셀로 다운로드합니다. (옵션 응답 동적 컬럼 포함)") @GetMapping("/{eventId}/issued-tickets/export") fun exportIssuedTickets(@CurrentUserId userId: Long, @PathVariable eventId: Long): ResponseEntity { val bytes = adminExportIssuedTicketsUseCase.execute(userId, eventId) return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=issued-tickets-${eventId}.xlsx") .contentType(MediaType.APPLICATION_OCTET_STREAM) .body(bytes) } @Operation(summary = "티켓 종류를 수정합니다. (어드민 전용, 이벤트 상태 체크 없이 수정 가능)") @PatchMapping("/{eventId}/ticket-items/{ticketItemId}") fun updateTicketItem( @CurrentUserId userId: Long, @PathVariable eventId: Long, @PathVariable ticketItemId: Long, @RequestBody request: AdminUpdateTicketItemRequest, ): AdminTicketItemResponse { return adminUpdateTicketItemUseCase.execute(userId, eventId, ticketItemId, request) } @Operation(summary = "티켓 재고를 조정합니다. (어드민 전용, delta 양수=증가 음수=감소)") @PostMapping("/{eventId}/ticket-items/{ticketItemId}/adjust-stock") fun adjustTicketStock( @CurrentUserId userId: Long, @PathVariable eventId: Long, @PathVariable ticketItemId: Long, @RequestBody request: AdminAdjustTicketStockRequest, ): AdminTicketItemResponse { return adminAdjustTicketStockUseCase.execute(userId, ticketItemId, request.delta) } } ================================================ FILE: DuDoong-Admin/src/main/kotlin/band/gosrock/admin/controller/AdminHostController.kt ================================================ package band.gosrock.admin.controller import band.gosrock.admin.model.dto.request.AdminAddHostMemberRequest import band.gosrock.admin.model.dto.request.AdminTransferMasterRequest import band.gosrock.admin.model.dto.request.AdminUpdateHostMemberRoleRequest import band.gosrock.admin.model.dto.request.AdminUpdateHostPartnerRequest import band.gosrock.admin.model.dto.request.AdminUpdateHostProfileRequest import band.gosrock.admin.model.dto.response.AdminEventResponse import band.gosrock.admin.model.dto.response.AdminHostDetailResponse import band.gosrock.admin.model.dto.response.AdminHostMemberResponse import band.gosrock.admin.model.dto.response.AdminHostResponse import band.gosrock.admin.service.AdminAddHostMemberUseCase import band.gosrock.admin.service.AdminTransferMasterUseCase import band.gosrock.admin.service.AdminGetHostDetailUseCase import band.gosrock.admin.service.AdminGetHostEventsUseCase import band.gosrock.admin.service.AdminGetHostMembersUseCase import band.gosrock.admin.service.AdminGetHostsUseCase import band.gosrock.admin.service.AdminRemoveHostMemberUseCase import band.gosrock.admin.service.AdminUpdateHostMemberRoleUseCase import band.gosrock.admin.service.AdminUpdateHostPartnerUseCase import band.gosrock.admin.service.AdminUpdateHostProfileUseCase import band.gosrock.common.annotation.CurrentUserId import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.security.SecurityRequirement import io.swagger.v3.oas.annotations.tags.Tag import org.springframework.data.domain.Page import org.springframework.data.domain.Pageable import org.springframework.data.web.PageableDefault import org.springframework.http.HttpStatus import org.springframework.web.bind.annotation.DeleteMapping import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PatchMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.ResponseStatus import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("/internal-api/v1/hosts") @SecurityRequirement(name = "admin-token") @Tag(name = "Admin") class AdminHostController( private val adminGetHostsUseCase: AdminGetHostsUseCase, private val adminGetHostDetailUseCase: AdminGetHostDetailUseCase, private val adminGetHostMembersUseCase: AdminGetHostMembersUseCase, private val adminUpdateHostMemberRoleUseCase: AdminUpdateHostMemberRoleUseCase, private val adminAddHostMemberUseCase: AdminAddHostMemberUseCase, private val adminRemoveHostMemberUseCase: AdminRemoveHostMemberUseCase, private val adminGetHostEventsUseCase: AdminGetHostEventsUseCase, private val adminUpdateHostPartnerUseCase: AdminUpdateHostPartnerUseCase, private val adminUpdateHostProfileUseCase: AdminUpdateHostProfileUseCase, private val adminTransferMasterUseCase: AdminTransferMasterUseCase, ) { @Operation(summary = "호스트 목록을 조회합니다.") @GetMapping fun getHosts( @CurrentUserId userId: Long, @RequestParam(required = false) keyword: String?, @PageableDefault(size = 20) pageable: Pageable, ): Page { return adminGetHostsUseCase.execute(userId, keyword, pageable) } @Operation(summary = "호스트 상세 정보를 조회합니다.") @GetMapping("/{hostId}") fun getHostDetail(@CurrentUserId userId: Long, @PathVariable hostId: Long): AdminHostDetailResponse { return adminGetHostDetailUseCase.execute(userId, hostId) } @Operation(summary = "호스트 소속 멤버 목록을 조회합니다.") @GetMapping("/{hostId}/members") fun getHostMembers(@CurrentUserId userId: Long, @PathVariable hostId: Long): List { return adminGetHostMembersUseCase.execute(userId, hostId) } @Operation(summary = "호스트 멤버 역할을 변경합니다.") @PatchMapping("/{hostId}/members/{targetUserId}/role") fun updateHostMemberRole( @CurrentUserId userId: Long, @PathVariable hostId: Long, @PathVariable targetUserId: Long, @RequestBody request: AdminUpdateHostMemberRoleRequest, ): AdminHostMemberResponse { return adminUpdateHostMemberRoleUseCase.execute(userId, hostId, targetUserId, request) } @Operation(summary = "호스트에 멤버를 추가합니다.") @PostMapping("/{hostId}/members") @ResponseStatus(HttpStatus.CREATED) fun addHostMember( @CurrentUserId userId: Long, @PathVariable hostId: Long, @RequestBody request: AdminAddHostMemberRequest, ): AdminHostMemberResponse { return adminAddHostMemberUseCase.execute(userId, hostId, request) } @Operation(summary = "호스트에서 멤버를 제거합니다.") @DeleteMapping("/{hostId}/members/{targetUserId}") @ResponseStatus(HttpStatus.NO_CONTENT) fun removeHostMember( @CurrentUserId userId: Long, @PathVariable hostId: Long, @PathVariable targetUserId: Long, ) { adminRemoveHostMemberUseCase.execute(userId, hostId, targetUserId) } @Operation(summary = "호스트별 이벤트 목록을 조회합니다.") @GetMapping("/{hostId}/events") fun getHostEvents( @CurrentUserId userId: Long, @PathVariable hostId: Long, @PageableDefault(size = 20) pageable: Pageable, ): Page { return adminGetHostEventsUseCase.execute(userId, hostId, pageable) } @Operation(summary = "호스트의 파트너 여부를 변경합니다.") @PatchMapping("/{hostId}/partner") fun updateHostPartner( @CurrentUserId userId: Long, @PathVariable hostId: Long, @RequestBody request: AdminUpdateHostPartnerRequest, ): AdminHostDetailResponse { return adminUpdateHostPartnerUseCase.execute(userId, hostId, request) } @Operation(summary = "호스트 프로필을 수정합니다.") @PatchMapping("/{hostId}/profile") fun updateHostProfile( @CurrentUserId userId: Long, @PathVariable hostId: Long, @RequestBody request: AdminUpdateHostProfileRequest, ): AdminHostDetailResponse { return adminUpdateHostProfileUseCase.execute(userId, hostId, request) } @Operation(summary = "호스트 마스터 권한을 강제 양도합니다. (어드민)") @PostMapping("/{hostId}/transfer-master") fun transferMaster( @CurrentUserId userId: Long, @PathVariable hostId: Long, @RequestBody request: AdminTransferMasterRequest, ): AdminHostDetailResponse { return adminTransferMasterUseCase.execute(userId, hostId, request) } } ================================================ FILE: DuDoong-Admin/src/main/kotlin/band/gosrock/admin/controller/AdminOrderController.kt ================================================ package band.gosrock.admin.controller import band.gosrock.admin.model.dto.request.AdminCancelOrderRequest import band.gosrock.admin.model.dto.request.AdminRefundStatusRequest import band.gosrock.admin.model.dto.response.AdminOrderResponse import band.gosrock.admin.service.AdminCancelOrderUseCase import band.gosrock.admin.service.AdminExcelService import band.gosrock.admin.service.AdminGetOrderDetailUseCase import band.gosrock.admin.service.AdminGetOrdersUseCase import band.gosrock.admin.service.AdminUpdateRefundStatusUseCase import band.gosrock.common.annotation.CurrentUserId import band.gosrock.domain.domains.order.domain.OrderStatus import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.security.SecurityRequirement import io.swagger.v3.oas.annotations.tags.Tag import jakarta.validation.Valid import org.springframework.data.domain.Page import org.springframework.data.domain.Pageable import org.springframework.data.web.PageableDefault import org.springframework.http.HttpHeaders import org.springframework.http.MediaType import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PatchMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("/internal-api/v1/orders") @SecurityRequirement(name = "admin-token") @Tag(name = "Admin") class AdminOrderController( private val adminGetOrdersUseCase: AdminGetOrdersUseCase, private val adminGetOrderDetailUseCase: AdminGetOrderDetailUseCase, private val adminCancelOrderUseCase: AdminCancelOrderUseCase, private val adminUpdateRefundStatusUseCase: AdminUpdateRefundStatusUseCase, private val adminExcelService: AdminExcelService, ) { @Operation(summary = "주문 목록을 엑셀로 다운로드합니다.") @GetMapping("/export") fun exportOrders( @CurrentUserId userId: Long, @RequestParam(required = false) keyword: String?, @RequestParam(required = false) status: OrderStatus?, @RequestParam(required = false) eventId: Long?, ): ResponseEntity { val orders = adminGetOrdersUseCase.executeAll(userId, keyword, status, eventId) val bytes = adminExcelService.generateOrdersExcel(orders) return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=orders.xlsx") .contentType(MediaType.APPLICATION_OCTET_STREAM) .body(bytes) } @Operation(summary = "주문 목록을 조회합니다.") @GetMapping fun getOrders( @CurrentUserId userId: Long, @RequestParam(required = false) keyword: String?, @RequestParam(required = false) status: OrderStatus?, @RequestParam(required = false) eventId: Long?, @PageableDefault(size = 20) pageable: Pageable, ): Page { return adminGetOrdersUseCase.execute(userId, keyword, status, eventId, pageable) } @Operation(summary = "주문 상세 정보를 조회합니다.") @GetMapping("/{orderUuid}") fun getOrderDetail(@CurrentUserId userId: Long, @PathVariable orderUuid: String): AdminOrderResponse { return adminGetOrderDetailUseCase.execute(userId, orderUuid) } @Operation(summary = "주문을 취소합니다.") @PostMapping("/{orderUuid}/cancel") fun cancelOrder( @CurrentUserId userId: Long, @PathVariable orderUuid: String, @RequestBody(required = false) request: AdminCancelOrderRequest?, ): AdminOrderResponse { return adminCancelOrderUseCase.execute(userId, orderUuid, request?.reason) } @Operation(summary = "주문의 환불 상태를 변경합니다. (REFUND_COMPLETED)") @PatchMapping("/{orderUuid}/refund-status") fun updateRefundStatus( @CurrentUserId userId: Long, @PathVariable orderUuid: String, @RequestBody @Valid request: AdminRefundStatusRequest, ): AdminOrderResponse { return adminUpdateRefundStatusUseCase.execute(userId, orderUuid, request) } } ================================================ FILE: DuDoong-Admin/src/main/kotlin/band/gosrock/admin/controller/AdminRefundController.kt ================================================ package band.gosrock.admin.controller import band.gosrock.admin.model.dto.response.AdminRefundResponse import band.gosrock.admin.service.AdminCompleteRefundUseCase import band.gosrock.admin.service.AdminGetRefundDetailUseCase import band.gosrock.admin.service.AdminGetRefundsUseCase import band.gosrock.common.annotation.CurrentUserId import band.gosrock.domain.domains.order.domain.RefundStatus import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.security.SecurityRequirement import io.swagger.v3.oas.annotations.tags.Tag import org.springframework.data.domain.Page import org.springframework.data.domain.Pageable import org.springframework.data.web.PageableDefault import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PatchMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("/internal-api/v1/refunds") @SecurityRequirement(name = "admin-token") @Tag(name = "Admin") class AdminRefundController( private val adminGetRefundsUseCase: AdminGetRefundsUseCase, private val adminGetRefundDetailUseCase: AdminGetRefundDetailUseCase, private val adminCompleteRefundUseCase: AdminCompleteRefundUseCase, ) { @Operation(summary = "전체 환불 목록을 조회합니다.") @GetMapping fun getRefunds( @CurrentUserId userId: Long, @RequestParam(required = false) refundStatus: RefundStatus?, @RequestParam(required = false) eventId: Long?, @RequestParam(required = false) keyword: String?, @PageableDefault(size = 20) pageable: Pageable, ): Page = adminGetRefundsUseCase.execute(userId, refundStatus, eventId, keyword, pageable) @Operation(summary = "환불 상세 정보를 조회합니다.") @GetMapping("/{orderUuid}") fun getRefundDetail( @CurrentUserId userId: Long, @PathVariable orderUuid: String, ): AdminRefundResponse = adminGetRefundDetailUseCase.execute(userId, orderUuid) @Operation(summary = "환불을 확인 처리합니다. (REFUND_COMPLETED)") @PatchMapping("/{orderUuid}/complete") fun completeRefund( @CurrentUserId userId: Long, @PathVariable orderUuid: String, ): AdminRefundResponse = adminCompleteRefundUseCase.execute(userId, orderUuid) } ================================================ FILE: DuDoong-Admin/src/main/kotlin/band/gosrock/admin/controller/AdminUserController.kt ================================================ package band.gosrock.admin.controller import band.gosrock.admin.model.dto.request.AdminChangeNameRequest import band.gosrock.admin.model.dto.request.AdminUpdateUserRoleRequest import band.gosrock.admin.model.dto.request.AdminUpdateUserStatusRequest import band.gosrock.admin.model.dto.response.AdminUserDetailResponse import band.gosrock.admin.model.dto.response.AdminUserResponse import band.gosrock.admin.service.AdminChangeNameUseCase import band.gosrock.admin.service.AdminExcelService import band.gosrock.admin.service.AdminGetUserDetailUseCase import band.gosrock.admin.service.AdminGetUsersUseCase import band.gosrock.admin.service.AdminUpdateUserRoleUseCase import band.gosrock.admin.service.AdminUpdateUserStatusUseCase import band.gosrock.common.annotation.CurrentUserId import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.security.SecurityRequirement import io.swagger.v3.oas.annotations.tags.Tag import jakarta.validation.Valid import org.springframework.data.domain.Page import org.springframework.data.domain.Pageable import org.springframework.data.web.PageableDefault import org.springframework.http.HttpHeaders import org.springframework.http.MediaType import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PatchMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("/internal-api/v1/users") @SecurityRequirement(name = "admin-token") @Tag(name = "Admin") class AdminUserController( private val adminGetUsersUseCase: AdminGetUsersUseCase, private val adminGetUserDetailUseCase: AdminGetUserDetailUseCase, private val adminUpdateUserRoleUseCase: AdminUpdateUserRoleUseCase, private val adminUpdateUserStatusUseCase: AdminUpdateUserStatusUseCase, private val adminChangeNameUseCase: AdminChangeNameUseCase, private val adminExcelService: AdminExcelService, ) { @Operation(summary = "유저 목록을 엑셀로 다운로드합니다.") @GetMapping("/export") fun exportUsers( @CurrentUserId currentUserId: Long, @RequestParam(required = false) keyword: String?, ): ResponseEntity { val users = adminGetUsersUseCase.executeAll(currentUserId, keyword) val bytes = adminExcelService.generateUsersExcel(users) return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=users.xlsx") .contentType(MediaType.APPLICATION_OCTET_STREAM) .body(bytes) } @Operation(summary = "유저 목록을 조회합니다.") @GetMapping fun getUsers( @CurrentUserId currentUserId: Long, @RequestParam(required = false) keyword: String?, @PageableDefault(size = 20) pageable: Pageable, ): Page { return adminGetUsersUseCase.execute(currentUserId, keyword, pageable) } @Operation(summary = "유저 상세 정보를 조회합니다.") @GetMapping("/{userId}") fun getUserDetail( @CurrentUserId currentUserId: Long, @PathVariable userId: Long, ): AdminUserDetailResponse { return adminGetUserDetailUseCase.execute(currentUserId, userId) } @Operation(summary = "유저 역할을 변경합니다. (SUPER_ADMIN 전용)") @PatchMapping("/{userId}/role") fun updateUserRole( @CurrentUserId currentUserId: Long, @PathVariable userId: Long, @RequestBody request: AdminUpdateUserRoleRequest, ): AdminUserResponse { return adminUpdateUserRoleUseCase.execute(currentUserId, userId, request) } @Operation(summary = "유저 상태를 변경합니다.") @PatchMapping("/{userId}/status") fun updateUserStatus( @CurrentUserId currentUserId: Long, @PathVariable userId: Long, @RequestBody request: AdminUpdateUserStatusRequest, ): AdminUserResponse { return adminUpdateUserStatusUseCase.execute(currentUserId, userId, request) } @Operation(summary = "유저 이름 변경") @PatchMapping("/{userId}/name") fun changeUserName( @CurrentUserId adminUserId: Long, @PathVariable userId: Long, @Valid @RequestBody request: AdminChangeNameRequest, ) { adminChangeNameUseCase.execute(adminUserId, userId, request) } } ================================================ FILE: DuDoong-Admin/src/main/kotlin/band/gosrock/admin/exception/AdminErrorCode.kt ================================================ package band.gosrock.admin.exception import band.gosrock.common.annotation.ExplainError import band.gosrock.common.consts.DuDoongStatic.FORBIDDEN import band.gosrock.common.dto.ErrorReason import band.gosrock.common.exception.BaseErrorCode import java.lang.reflect.Field enum class AdminErrorCode( private val status: Int, private val code: String, private val reason: String, ) : BaseErrorCode { @ExplainError("어드민 권한이 없는 유저가 어드민 기능에 접근하려는 경우") ADMIN_FORBIDDEN(FORBIDDEN, "ADMIN_403_1", "어드민 권한이 필요합니다. MANAGER 이상의 역할이 필요합니다."), @ExplainError("SUPER_ADMIN 전용 기능에 일반 관리자가 접근하려는 경우") ADMIN_SUPER_ADMIN_REQUIRED(FORBIDDEN, "ADMIN_403_2", "SUPER_ADMIN 권한이 필요합니다."); override fun getErrorReason(): ErrorReason = ErrorReason(status = status, code = code, reason = reason) override fun getExplainError(): String { val field: Field = this.javaClass.getField(this.name) val annotation = field.getAnnotation(ExplainError::class.java) return annotation?.value ?: reason } } ================================================ FILE: DuDoong-Admin/src/main/kotlin/band/gosrock/admin/exception/AdminForbiddenException.kt ================================================ package band.gosrock.admin.exception import band.gosrock.common.exception.DuDoongCodeException class AdminForbiddenException private constructor() : DuDoongCodeException(AdminErrorCode.ADMIN_FORBIDDEN) { companion object { @JvmField val EXCEPTION: DuDoongCodeException = AdminForbiddenException() } } ================================================ FILE: DuDoong-Admin/src/main/kotlin/band/gosrock/admin/model/dto/request/AdminAddHostMemberRequest.kt ================================================ package band.gosrock.admin.model.dto.request import band.gosrock.domain.domains.host.domain.HostRole data class AdminAddHostMemberRequest( val userId: Long, val role: HostRole, ) ================================================ FILE: DuDoong-Admin/src/main/kotlin/band/gosrock/admin/model/dto/request/AdminAdjustTicketStockRequest.kt ================================================ package band.gosrock.admin.model.dto.request data class AdminAdjustTicketStockRequest( val delta: Long, // 양수 = 증가, 음수 = 감소 ) ================================================ FILE: DuDoong-Admin/src/main/kotlin/band/gosrock/admin/model/dto/request/AdminCancelOrderRequest.kt ================================================ package band.gosrock.admin.model.dto.request data class AdminCancelOrderRequest( val reason: String? = null, ) ================================================ FILE: DuDoong-Admin/src/main/kotlin/band/gosrock/admin/model/dto/request/AdminChangeNameRequest.kt ================================================ package band.gosrock.admin.model.dto.request import jakarta.validation.constraints.NotBlank import jakarta.validation.constraints.Size data class AdminChangeNameRequest( @field:NotBlank(message = "이름을 입력해주세요.") @field:Size(min = 2, max = 7, message = "이름은 2~7자여야 합니다.") val name: String, ) ================================================ FILE: DuDoong-Admin/src/main/kotlin/band/gosrock/admin/model/dto/request/AdminRefundStatusRequest.kt ================================================ package band.gosrock.admin.model.dto.request import jakarta.validation.constraints.NotNull data class AdminRefundStatusRequest( @field:NotNull val refundStatus: String, val reason: String? = null, ) ================================================ FILE: DuDoong-Admin/src/main/kotlin/band/gosrock/admin/model/dto/request/AdminTransferMasterRequest.kt ================================================ package band.gosrock.admin.model.dto.request import jakarta.validation.constraints.NotNull /** 어드민 호스트 마스터 강제 양도 요청 DTO */ data class AdminTransferMasterRequest( @field:NotNull(message = "양도 대상 유저 아이디를 입력해주세요") val newMasterUserId: Long, ) ================================================ FILE: DuDoong-Admin/src/main/kotlin/band/gosrock/admin/model/dto/request/AdminUpdateEventRequest.kt ================================================ package band.gosrock.admin.model.dto.request import java.time.LocalDateTime data class AdminUpdateEventRequest( val name: String? = null, val startAt: LocalDateTime? = null, val runTime: Int? = null, val content: String? = null, val placeName: String? = null, val placeAddress: String? = null, ) ================================================ FILE: DuDoong-Admin/src/main/kotlin/band/gosrock/admin/model/dto/request/AdminUpdateEventStatusRequest.kt ================================================ package band.gosrock.admin.model.dto.request import band.gosrock.domain.domains.event.domain.EventStatus data class AdminUpdateEventStatusRequest( val status: EventStatus, ) ================================================ FILE: DuDoong-Admin/src/main/kotlin/band/gosrock/admin/model/dto/request/AdminUpdateHostMemberRoleRequest.kt ================================================ package band.gosrock.admin.model.dto.request import band.gosrock.domain.domains.host.domain.HostRole data class AdminUpdateHostMemberRoleRequest( val role: HostRole, ) ================================================ FILE: DuDoong-Admin/src/main/kotlin/band/gosrock/admin/model/dto/request/AdminUpdateHostPartnerRequest.kt ================================================ package band.gosrock.admin.model.dto.request data class AdminUpdateHostPartnerRequest( val partner: Boolean, ) ================================================ FILE: DuDoong-Admin/src/main/kotlin/band/gosrock/admin/model/dto/request/AdminUpdateHostProfileRequest.kt ================================================ package band.gosrock.admin.model.dto.request data class AdminUpdateHostProfileRequest( val name: String?, val introduce: String?, val contactEmail: String?, val contactNumber: String?, ) ================================================ FILE: DuDoong-Admin/src/main/kotlin/band/gosrock/admin/model/dto/request/AdminUpdateTicketItemRequest.kt ================================================ package band.gosrock.admin.model.dto.request import java.math.BigDecimal data class AdminUpdateTicketItemRequest( val name: String? = null, val description: String? = null, val price: BigDecimal? = null, val quantity: Long? = null, val purchaseLimit: Long? = null, ) ================================================ FILE: DuDoong-Admin/src/main/kotlin/band/gosrock/admin/model/dto/request/AdminUpdateUserRoleRequest.kt ================================================ package band.gosrock.admin.model.dto.request import band.gosrock.domain.domains.user.domain.AccountRole data class AdminUpdateUserRoleRequest( val role: AccountRole, ) ================================================ FILE: DuDoong-Admin/src/main/kotlin/band/gosrock/admin/model/dto/request/AdminUpdateUserStatusRequest.kt ================================================ package band.gosrock.admin.model.dto.request import band.gosrock.domain.domains.user.domain.AccountState data class AdminUpdateUserStatusRequest( val status: AccountState, ) ================================================ FILE: DuDoong-Admin/src/main/kotlin/band/gosrock/admin/model/dto/response/AdminCommentResponse.kt ================================================ package band.gosrock.admin.model.dto.response import band.gosrock.domain.domains.comment.domain.Comment import band.gosrock.domain.domains.comment.domain.CommentStatus import java.time.LocalDateTime data class AdminCommentResponse( val id: Long, val userName: String?, val eventName: String?, val content: String?, val commentStatus: CommentStatus?, val createdAt: LocalDateTime?, val userId: Long?, val eventId: Long?, ) { companion object { fun of(comment: Comment, eventName: String?): AdminCommentResponse = AdminCommentResponse( id = comment.id!!, userName = comment.nickName, eventName = eventName, content = comment.content, commentStatus = comment.commentStatus, createdAt = comment.createdAt, userId = comment.user?.id, eventId = comment.eventId, ) } } ================================================ FILE: DuDoong-Admin/src/main/kotlin/band/gosrock/admin/model/dto/response/AdminEventResponse.kt ================================================ package band.gosrock.admin.model.dto.response import band.gosrock.domain.domains.event.domain.Event import band.gosrock.domain.domains.event.domain.EventStatus import java.time.LocalDateTime data class AdminEventResponse( val id: Long, val name: String?, val hostName: String?, val status: EventStatus, val startAt: LocalDateTime?, val runTime: Long?, val createdAt: LocalDateTime?, val ticketItemCount: Int = 0, val issuedTicketCount: Int = 0, val totalOrderCount: Int = 0, val content: String? = null, val placeName: String? = null, val placeAddress: String? = null, val hostId: Long? = null, val posterImageKey: String? = null, val latitude: Double? = null, val longitude: Double? = null, ) { companion object { fun of(event: Event, hostName: String?): AdminEventResponse = AdminEventResponse( id = event.id!!, name = event.eventBasic?.name, hostName = hostName, status = event.status, startAt = event.eventBasic?.startAt, runTime = event.eventBasic?.runTime, createdAt = event.createdAt, hostId = event.hostId, posterImageKey = event.eventDetail?.posterImage?.imageKey, latitude = event.eventPlace?.latitude, longitude = event.eventPlace?.longitude, ) fun ofDetail( event: Event, hostName: String?, ticketItemCount: Int, issuedTicketCount: Int, totalOrderCount: Int, ): AdminEventResponse = AdminEventResponse( id = event.id!!, name = event.eventBasic?.name, hostName = hostName, status = event.status, startAt = event.eventBasic?.startAt, runTime = event.eventBasic?.runTime, createdAt = event.createdAt, ticketItemCount = ticketItemCount, issuedTicketCount = issuedTicketCount, totalOrderCount = totalOrderCount, content = event.eventDetail?.content, placeName = event.eventPlace?.placeName, placeAddress = event.eventPlace?.placeAddress, hostId = event.hostId, posterImageKey = event.eventDetail?.posterImage?.imageKey, latitude = event.eventPlace?.latitude, longitude = event.eventPlace?.longitude, ) } } ================================================ FILE: DuDoong-Admin/src/main/kotlin/band/gosrock/admin/model/dto/response/AdminHostDetailResponse.kt ================================================ package band.gosrock.admin.model.dto.response import band.gosrock.domain.domains.host.domain.Host import java.time.LocalDateTime data class AdminHostDetailResponse( val id: Long, val name: String?, val introduce: String?, val contactEmail: String?, val contactNumber: String?, val profileImage: String?, val partner: Boolean, val masterUserId: Long?, val createdAt: LocalDateTime?, val memberCount: Int, val slackUrl: String?, ) { companion object { fun from(host: Host): AdminHostDetailResponse = AdminHostDetailResponse( id = host.id!!, name = host.profile?.name, introduce = host.profile?.introduce, contactEmail = host.profile?.contactEmail, contactNumber = host.profile?.contactNumber, profileImage = host.profile?.profileImage?.imageKey, partner = host.partner, masterUserId = host.masterUserId, createdAt = host.createdAt, memberCount = host.hostUsers.size, slackUrl = host.slackUrl, ) } } ================================================ FILE: DuDoong-Admin/src/main/kotlin/band/gosrock/admin/model/dto/response/AdminHostMemberResponse.kt ================================================ package band.gosrock.admin.model.dto.response import band.gosrock.domain.domains.host.domain.HostRole import band.gosrock.domain.domains.host.domain.HostUser import java.time.LocalDateTime data class AdminHostMemberResponse( val userId: Long?, val userName: String?, val role: HostRole, val active: Boolean, val createdAt: LocalDateTime?, ) { companion object { fun of(hostUser: HostUser, userName: String?): AdminHostMemberResponse = AdminHostMemberResponse( userId = hostUser.userId, userName = userName, role = hostUser.role, active = hostUser.active, createdAt = hostUser.createdAt, ) } } ================================================ FILE: DuDoong-Admin/src/main/kotlin/band/gosrock/admin/model/dto/response/AdminHostResponse.kt ================================================ package band.gosrock.admin.model.dto.response import band.gosrock.domain.domains.host.domain.Host import java.time.LocalDateTime data class AdminHostResponse( val id: Long, val name: String?, val introduce: String?, val contactEmail: String?, val contactNumber: String?, val profileImage: String?, val partner: Boolean, val masterUserId: Long?, val createdAt: LocalDateTime?, ) { companion object { fun from(host: Host): AdminHostResponse = AdminHostResponse( id = host.id!!, name = host.profile?.name, introduce = host.profile?.introduce, contactEmail = host.profile?.contactEmail, contactNumber = host.profile?.contactNumber, profileImage = host.profile?.profileImage?.imageKey, partner = host.partner, masterUserId = host.masterUserId, createdAt = host.createdAt, ) } } ================================================ FILE: DuDoong-Admin/src/main/kotlin/band/gosrock/admin/model/dto/response/AdminIssuedTicketResponse.kt ================================================ package band.gosrock.admin.model.dto.response import band.gosrock.domain.domains.issuedTicket.domain.IssuedTicket import band.gosrock.domain.domains.issuedTicket.domain.IssuedTicketStatus import java.time.LocalDateTime data class AdminIssuedTicketResponse( val id: Long, val issuedTicketNo: String?, val userName: String?, val ticketName: String?, val orderUuid: String?, val enteredAt: LocalDateTime?, val status: IssuedTicketStatus, val createdAt: LocalDateTime?, ) { companion object { fun from(issuedTicket: IssuedTicket): AdminIssuedTicketResponse = AdminIssuedTicketResponse( id = issuedTicket.id!!, issuedTicketNo = issuedTicket.issuedTicketNo, userName = issuedTicket.userInfo?.userName, ticketName = issuedTicket.itemInfo?.ticketName, orderUuid = issuedTicket.orderUuid, enteredAt = issuedTicket.enteredAt, status = issuedTicket.issuedTicketStatus, createdAt = issuedTicket.createdAt, ) } } ================================================ FILE: DuDoong-Admin/src/main/kotlin/band/gosrock/admin/model/dto/response/AdminOrderResponse.kt ================================================ package band.gosrock.admin.model.dto.response import band.gosrock.domain.domains.order.domain.Order import band.gosrock.domain.domains.order.domain.OrderStatus import java.time.LocalDateTime data class AdminOrderResponse( val orderId: String?, val userName: String?, val eventName: String?, val ticketName: String?, val totalAmount: Long, val orderStatus: OrderStatus, val createdAt: LocalDateTime?, val orderNo: String?, val orderMethod: String?, val userId: Long?, val eventId: Long?, val approvedAt: LocalDateTime?, val withDrawAt: LocalDateTime?, val paymentMethod: String?, val receiptUrl: String?, val supplyAmount: String?, val discountAmount: String?, val couponName: String?, val failReason: String? = null, val cancelReason: String? = null, val refundStatus: String? = null, val refundStatusChangedAt: String? = null, ) { companion object { fun of(order: Order, userName: String?, eventName: String?): AdminOrderResponse = AdminOrderResponse( orderId = order.uuid, userName = userName, eventName = eventName, ticketName = order.orderName, totalAmount = order.getTotalPaymentPrice().longValue(), orderStatus = order.orderStatus, createdAt = order.createdAt, orderNo = order.orderNo, orderMethod = order.orderMethod?.name, userId = order.userId, eventId = order.eventId, approvedAt = order.approvedAt, withDrawAt = order.withDrawAt, paymentMethod = order.pgPaymentInfo.paymentMethod.name, receiptUrl = order.pgPaymentInfo.receiptUrl, supplyAmount = order.totalPaymentInfo?.supplyAmount?.toString(), discountAmount = order.totalPaymentInfo?.discountAmount?.toString(), couponName = order.orderCouponVo.name, failReason = order.failReason, cancelReason = order.cancelReason, refundStatus = order.refundStatus.name, refundStatusChangedAt = order.refundStatusChangedAt?.toString(), ) } } ================================================ FILE: DuDoong-Admin/src/main/kotlin/band/gosrock/admin/model/dto/response/AdminRefundResponse.kt ================================================ package band.gosrock.admin.model.dto.response import band.gosrock.domain.domains.order.domain.Order import band.gosrock.domain.domains.order.domain.RefundStatus import java.time.LocalDateTime data class AdminRefundResponse( val orderId: String?, val orderNo: String?, val userName: String?, val eventName: String?, val eventId: Long?, val ticketName: String?, val totalAmount: Long, val cancelReason: String?, val refundStatus: RefundStatus, val refundStatusChangedAt: LocalDateTime?, val withDrawAt: LocalDateTime?, val createdAt: LocalDateTime?, val userId: Long?, ) { companion object { fun of(order: Order, userName: String?, eventName: String?): AdminRefundResponse = AdminRefundResponse( orderId = order.uuid, orderNo = order.orderNo, userName = userName, eventName = eventName, eventId = order.eventId, ticketName = order.orderName, totalAmount = order.getTotalPaymentPrice().longValue(), cancelReason = order.cancelReason, refundStatus = order.refundStatus, refundStatusChangedAt = order.refundStatusChangedAt, withDrawAt = order.withDrawAt, createdAt = order.createdAt, userId = order.userId, ) } } ================================================ FILE: DuDoong-Admin/src/main/kotlin/band/gosrock/admin/model/dto/response/AdminTicketItemResponse.kt ================================================ package band.gosrock.admin.model.dto.response import band.gosrock.domain.domains.ticket_item.domain.TicketItem import band.gosrock.domain.domains.ticket_item.domain.TicketItemStatus import band.gosrock.domain.domains.ticket_item.domain.TicketPayType import band.gosrock.domain.domains.ticket_item.domain.TicketType import java.math.BigDecimal import java.time.LocalDateTime data class AdminTicketItemResponse( val id: Long, val name: String?, val description: String?, val price: BigDecimal?, val quantity: Long?, val supplyCount: Long?, val purchaseLimit: Long?, val payType: TicketPayType?, val type: TicketType?, val isQuantityPublic: Boolean?, val isSellable: Boolean?, val saleStartAt: LocalDateTime?, val saleEndAt: LocalDateTime?, val ticketItemStatus: TicketItemStatus, val eventId: Long?, ) { companion object { fun from(ticketItem: TicketItem): AdminTicketItemResponse = AdminTicketItemResponse( id = ticketItem.id!!, name = ticketItem.name, description = ticketItem.description, price = ticketItem.price?.amount, quantity = ticketItem.quantity, supplyCount = ticketItem.supplyCount, purchaseLimit = ticketItem.purchaseLimit, payType = ticketItem.payType, type = ticketItem.type, isQuantityPublic = ticketItem.isQuantityPublic, isSellable = ticketItem.isSellable, saleStartAt = ticketItem.saleStartAt, saleEndAt = ticketItem.saleEndAt, ticketItemStatus = ticketItem.ticketItemStatus, eventId = ticketItem.eventId, ) } } ================================================ FILE: DuDoong-Admin/src/main/kotlin/band/gosrock/admin/model/dto/response/AdminUserDetailResponse.kt ================================================ package band.gosrock.admin.model.dto.response import band.gosrock.domain.domains.user.domain.AccountRole import band.gosrock.domain.domains.user.domain.AccountState import band.gosrock.domain.domains.user.domain.OauthProvider import band.gosrock.domain.domains.user.domain.User import java.time.LocalDateTime data class AdminUserDetailResponse( val id: Long, val name: String?, val email: String?, val profileImage: String?, val accountRole: AccountRole, val accountState: AccountState, val createdAt: LocalDateTime?, val phoneNumber: String?, val marketingAgree: Boolean, val oauthProvider: OauthProvider?, val lastLoginAt: LocalDateTime?, val receiveMail: Boolean, ) { companion object { fun from(user: User): AdminUserDetailResponse = AdminUserDetailResponse( id = user.id!!, name = user.profile?.name, email = user.profile?.email, profileImage = user.profile?.profileImage?.imageKey, accountRole = user.accountRole, accountState = user.accountState, createdAt = user.createdAt, phoneNumber = user.profile?.phoneNumberVo?.phoneNumber, marketingAgree = user.marketingAgree, oauthProvider = user.oauthInfo?.provider, lastLoginAt = user.lastLoginAt, receiveMail = user.receiveMail, ) } } ================================================ FILE: DuDoong-Admin/src/main/kotlin/band/gosrock/admin/model/dto/response/AdminUserResponse.kt ================================================ package band.gosrock.admin.model.dto.response import band.gosrock.domain.domains.user.domain.AccountRole import band.gosrock.domain.domains.user.domain.AccountState import band.gosrock.domain.domains.user.domain.User import java.time.LocalDateTime data class AdminUserResponse( val id: Long, val name: String?, val email: String?, val profileImage: String?, val accountRole: AccountRole, val accountState: AccountState, val createdAt: LocalDateTime?, ) { companion object { fun from(user: User): AdminUserResponse = AdminUserResponse( id = user.id!!, name = user.profile?.name, email = user.profile?.email, profileImage = user.profile?.profileImage?.imageKey, accountRole = user.accountRole, accountState = user.accountState, createdAt = user.createdAt, ) } } ================================================ FILE: DuDoong-Admin/src/main/kotlin/band/gosrock/admin/model/dto/response/DashboardResponse.kt ================================================ package band.gosrock.admin.model.dto.response data class DashboardResponse( val totalUsers: Long, val todayNewUsers: Long, val todayOrders: Long, val todayRevenue: Long, val activeEvents: Long, val todayRefunds: Long, val recentOrders: List = emptyList(), val recentEvents: List = emptyList(), ) ================================================ FILE: DuDoong-Admin/src/main/kotlin/band/gosrock/admin/service/AdminAddHostMemberUseCase.kt ================================================ package band.gosrock.admin.service import band.gosrock.admin.model.dto.request.AdminAddHostMemberRequest import band.gosrock.admin.model.dto.response.AdminHostMemberResponse import band.gosrock.common.annotation.UseCase import band.gosrock.domain.domains.host.adaptor.HostAdaptor import band.gosrock.domain.domains.host.domain.HostUser import band.gosrock.domain.domains.host.repository.HostRepository import band.gosrock.domain.domains.user.adaptor.UserAdaptor import org.springframework.transaction.annotation.Transactional @UseCase class AdminAddHostMemberUseCase( private val hostAdaptor: HostAdaptor, private val hostRepository: HostRepository, private val userAdaptor: UserAdaptor, private val adminAuthValidator: AdminAuthValidator, ) { @Transactional fun execute(userId: Long, hostId: Long, request: AdminAddHostMemberRequest): AdminHostMemberResponse { adminAuthValidator.validateAdminOrAbove(userId) val host = hostAdaptor.findById(hostId) val hostUser = HostUser(host, request.userId, request.role) host.addHostUsers(setOf(hostUser)) hostRepository.save(host) val userName = runCatching { userAdaptor.queryUser(request.userId).profile?.name }.getOrNull() val savedHostUser = host.getHostUserByUserId(request.userId) return AdminHostMemberResponse.of(savedHostUser, userName) } } ================================================ FILE: DuDoong-Admin/src/main/kotlin/band/gosrock/admin/service/AdminAdjustTicketStockUseCase.kt ================================================ package band.gosrock.admin.service import band.gosrock.admin.model.dto.response.AdminTicketItemResponse import band.gosrock.common.annotation.UseCase import band.gosrock.domain.common.aop.redissonLock.RedissonLock import band.gosrock.domain.domains.ticket_item.adaptor.TicketItemAdaptor @UseCase class AdminAdjustTicketStockUseCase( private val ticketItemAdaptor: TicketItemAdaptor, private val adminAuthValidator: AdminAuthValidator, ) { @RedissonLock(LockName = "티켓관리", identifier = "ticketItemId") fun execute(userId: Long, ticketItemId: Long, delta: Long): AdminTicketItemResponse { adminAuthValidator.validateAdminOrAbove(userId) val ticketItem = ticketItemAdaptor.queryTicketItem(ticketItemId) ticketItem.adminAdjustStock(delta) ticketItemAdaptor.save(ticketItem) return AdminTicketItemResponse.from(ticketItem) } } ================================================ FILE: DuDoong-Admin/src/main/kotlin/band/gosrock/admin/service/AdminAuthValidator.kt ================================================ package band.gosrock.admin.service import band.gosrock.admin.exception.AdminForbiddenException import band.gosrock.domain.domains.user.adaptor.UserAdaptor import band.gosrock.domain.domains.user.domain.AccountRole import band.gosrock.domain.domains.user.domain.User import org.slf4j.LoggerFactory import org.springframework.stereotype.Component @Component class AdminAuthValidator( private val userAdaptor: UserAdaptor, ) { private val log = LoggerFactory.getLogger(javaClass) fun validateAdminOrAbove(userId: Long): User { val user = userAdaptor.queryUser(userId) if (user.accountRole != AccountRole.ADMIN && user.accountRole != AccountRole.SUPER_ADMIN) { log.info("[ADMIN-AUTH] DENIED - userId={}, role={}, required=ADMIN+", userId, user.accountRole) throw AdminForbiddenException.EXCEPTION } log.info("[ADMIN-AUTH] GRANTED - userId={}, role={}", userId, user.accountRole) return user } fun validateSuperAdmin(userId: Long): User { val user = userAdaptor.queryUser(userId) if (user.accountRole != AccountRole.SUPER_ADMIN) { log.info("[ADMIN-AUTH] DENIED - userId={}, role={}, required=SUPER_ADMIN", userId, user.accountRole) throw AdminForbiddenException.EXCEPTION } log.info("[ADMIN-AUTH] GRANTED - userId={}, role=SUPER_ADMIN", userId) return user } } ================================================ FILE: DuDoong-Admin/src/main/kotlin/band/gosrock/admin/service/AdminCancelOrderUseCase.kt ================================================ package band.gosrock.admin.service import band.gosrock.admin.model.dto.response.AdminOrderResponse import band.gosrock.common.annotation.UseCase import band.gosrock.domain.domains.event.adaptor.EventAdaptor import band.gosrock.domain.domains.order.adaptor.OrderAdaptor import band.gosrock.domain.domains.order.domain.validator.OrderValidator import band.gosrock.domain.domains.user.adaptor.UserAdaptor import org.springframework.transaction.annotation.Transactional @UseCase class AdminCancelOrderUseCase( private val orderAdaptor: OrderAdaptor, private val orderValidator: OrderValidator, private val userAdaptor: UserAdaptor, private val eventAdaptor: EventAdaptor, private val adminAuthValidator: AdminAuthValidator, ) { @Transactional fun execute(userId: Long, orderUuid: String, reason: String? = null): AdminOrderResponse { adminAuthValidator.validateAdminOrAbove(userId) val order = orderAdaptor.findByOrderUuid(orderUuid) order.cancel(orderValidator, reason) val userName = order.userId?.let { runCatching { userAdaptor.queryUser(it).profile?.name }.getOrNull() } val eventName = order.eventId?.let { runCatching { eventAdaptor.findById(it).eventBasic?.name }.getOrNull() } return AdminOrderResponse.of(order, userName, eventName) } } ================================================ FILE: DuDoong-Admin/src/main/kotlin/band/gosrock/admin/service/AdminChangeNameUseCase.kt ================================================ package band.gosrock.admin.service import band.gosrock.admin.model.dto.request.AdminChangeNameRequest import band.gosrock.common.annotation.UseCase import band.gosrock.domain.domains.user.adaptor.UserAdaptor import org.springframework.transaction.annotation.Transactional @UseCase class AdminChangeNameUseCase( private val adminAuthValidator: AdminAuthValidator, private val userAdaptor: UserAdaptor, ) { @Transactional fun execute(adminUserId: Long, targetUserId: Long, request: AdminChangeNameRequest) { adminAuthValidator.validateAdminOrAbove(adminUserId) val user = userAdaptor.queryUser(targetUserId) user.changeName(request.name) } } ================================================ FILE: DuDoong-Admin/src/main/kotlin/band/gosrock/admin/service/AdminCompleteRefundUseCase.kt ================================================ package band.gosrock.admin.service import band.gosrock.admin.model.dto.response.AdminRefundResponse import band.gosrock.common.annotation.UseCase import band.gosrock.domain.domains.event.adaptor.EventAdaptor import band.gosrock.domain.domains.order.adaptor.OrderAdaptor import band.gosrock.domain.domains.user.adaptor.UserAdaptor import org.springframework.transaction.annotation.Transactional @UseCase class AdminCompleteRefundUseCase( private val orderAdaptor: OrderAdaptor, private val userAdaptor: UserAdaptor, private val eventAdaptor: EventAdaptor, private val adminAuthValidator: AdminAuthValidator, ) { @Transactional fun execute(userId: Long, orderUuid: String): AdminRefundResponse { adminAuthValidator.validateAdminOrAbove(userId) val order = orderAdaptor.findByOrderUuid(orderUuid) order.completeRefund() val userName = order.userId?.let { runCatching { userAdaptor.queryUser(it).profile?.name }.getOrNull() } val eventName = order.eventId?.let { runCatching { eventAdaptor.findById(it).eventBasic?.name }.getOrNull() } return AdminRefundResponse.of(order, userName, eventName) } } ================================================ FILE: DuDoong-Admin/src/main/kotlin/band/gosrock/admin/service/AdminDeleteCommentUseCase.kt ================================================ package band.gosrock.admin.service import band.gosrock.common.annotation.UseCase import band.gosrock.domain.domains.comment.adaptor.CommentAdaptor import org.springframework.transaction.annotation.Transactional @UseCase class AdminDeleteCommentUseCase( private val commentAdaptor: CommentAdaptor, private val adminAuthValidator: AdminAuthValidator, ) { @Transactional fun execute(userId: Long, commentId: Long) { adminAuthValidator.validateAdminOrAbove(userId) val comment = commentAdaptor.queryComment(commentId) comment.delete() } } ================================================ FILE: DuDoong-Admin/src/main/kotlin/band/gosrock/admin/service/AdminDeleteEventUseCase.kt ================================================ package band.gosrock.admin.service import band.gosrock.common.annotation.UseCase import band.gosrock.domain.domains.event.adaptor.EventAdaptor import band.gosrock.domain.domains.event.domain.EventStatus import band.gosrock.domain.domains.event.repository.EventRepository import org.springframework.transaction.annotation.Transactional @UseCase class AdminDeleteEventUseCase( private val eventAdaptor: EventAdaptor, private val eventRepository: EventRepository, private val adminAuthValidator: AdminAuthValidator, ) { @Transactional fun execute(userId: Long, eventId: Long) { adminAuthValidator.validateAdminOrAbove(userId) val event = eventAdaptor.findById(eventId) // 어드민은 밸리데이션 없이 직접 DELETED 상태로 변경 event.adminUpdateStatus(EventStatus.DELETED) eventRepository.save(event) } } ================================================ FILE: DuDoong-Admin/src/main/kotlin/band/gosrock/admin/service/AdminExcelService.kt ================================================ package band.gosrock.admin.service import band.gosrock.admin.model.dto.response.AdminEventResponse import band.gosrock.admin.model.dto.response.AdminIssuedTicketResponse import band.gosrock.admin.model.dto.response.AdminOrderResponse import band.gosrock.admin.model.dto.response.AdminTicketItemResponse import band.gosrock.admin.model.dto.response.AdminUserResponse import band.gosrock.domain.domains.issuedTicket.domain.IssuedTicket import band.gosrock.domain.domains.ticket_item.domain.Option import org.apache.poi.xssf.usermodel.XSSFWorkbook import org.springframework.stereotype.Service import java.io.ByteArrayOutputStream @Service class AdminExcelService { fun generateOrdersExcel(orders: List): ByteArray { val workbook = XSSFWorkbook() val sheet = workbook.createSheet("주문 목록") val headerRow = sheet.createRow(0) val headers = listOf( "주문번호", "주문번호(읽기용)", "사용자", "이벤트", "티켓", "금액", "상태", "주문방식", "결제수단", "승인일시", "취소일시", "할인금액", "쿠폰명", "주문일", ) headers.forEachIndexed { i, h -> headerRow.createCell(i).setCellValue(h) } orders.forEachIndexed { idx, order -> val row = sheet.createRow(idx + 1) var col = 0 row.createCell(col++).setCellValue(order.orderId ?: "") row.createCell(col++).setCellValue(order.orderNo ?: "") row.createCell(col++).setCellValue(order.userName ?: "") row.createCell(col++).setCellValue(order.eventName ?: "") row.createCell(col++).setCellValue(order.ticketName ?: "") row.createCell(col++).setCellValue(order.totalAmount.toDouble()) row.createCell(col++).setCellValue(order.orderStatus.toString()) row.createCell(col++).setCellValue(order.orderMethod ?: "") row.createCell(col++).setCellValue(order.paymentMethod ?: "") row.createCell(col++).setCellValue(order.approvedAt?.toString() ?: "") row.createCell(col++).setCellValue(order.withDrawAt?.toString() ?: "") row.createCell(col++).setCellValue(order.discountAmount ?: "") row.createCell(col++).setCellValue(order.couponName ?: "") row.createCell(col++).setCellValue(order.createdAt?.toString() ?: "") } return toByteArray(workbook) } fun generateEventsExcel(events: List): ByteArray { val workbook = XSSFWorkbook() val sheet = workbook.createSheet("이벤트 목록") val headerRow = sheet.createRow(0) listOf("ID", "이벤트명", "호스트", "상태", "시작일", "런타임(분)", "생성일").forEachIndexed { i, h -> headerRow.createCell(i).setCellValue(h) } events.forEachIndexed { idx, event -> val row = sheet.createRow(idx + 1) row.createCell(0).setCellValue(event.id.toDouble()) row.createCell(1).setCellValue(event.name ?: "") row.createCell(2).setCellValue(event.hostName ?: "") row.createCell(3).setCellValue(event.status.toString()) row.createCell(4).setCellValue(event.startAt?.toString() ?: "") row.createCell(5).setCellValue(event.runTime?.toDouble() ?: 0.0) row.createCell(6).setCellValue(event.createdAt?.toString() ?: "") } return toByteArray(workbook) } fun generateUsersExcel(users: List): ByteArray { val workbook = XSSFWorkbook() val sheet = workbook.createSheet("유저 목록") val headerRow = sheet.createRow(0) listOf("ID", "이름", "이메일", "역할", "상태", "가입일").forEachIndexed { i, h -> headerRow.createCell(i).setCellValue(h) } users.forEachIndexed { idx, user -> val row = sheet.createRow(idx + 1) row.createCell(0).setCellValue(user.id.toDouble()) row.createCell(1).setCellValue(user.name ?: "") row.createCell(2).setCellValue(user.email ?: "") row.createCell(3).setCellValue(user.accountRole.toString()) row.createCell(4).setCellValue(user.accountState.toString()) row.createCell(5).setCellValue(user.createdAt?.toString() ?: "") } return toByteArray(workbook) } fun generateTicketItemsExcel(items: List): ByteArray { val workbook = XSSFWorkbook() val sheet = workbook.createSheet("티켓 종류 목록") val headerRow = sheet.createRow(0) listOf("이름", "설명", "가격", "수량", "판매수", "구매제한", "타입", "상태").forEachIndexed { i, h -> headerRow.createCell(i).setCellValue(h) } items.forEachIndexed { idx, item -> val row = sheet.createRow(idx + 1) row.createCell(0).setCellValue(item.name ?: "") row.createCell(1).setCellValue(item.description ?: "") row.createCell(2).setCellValue(item.price?.toDouble() ?: 0.0) row.createCell(3).setCellValue(item.quantity?.toDouble() ?: 0.0) row.createCell(4).setCellValue(item.supplyCount?.toDouble() ?: 0.0) row.createCell(5).setCellValue(item.purchaseLimit?.toDouble() ?: 0.0) row.createCell(6).setCellValue(item.type?.toString() ?: "") row.createCell(7).setCellValue(item.ticketItemStatus.toString()) } return toByteArray(workbook) } fun generateIssuedTicketsExcel(tickets: List): ByteArray { return generateIssuedTicketsExcelWithOptions(tickets, emptyList(), emptyList()) } /** * 발급 티켓 엑셀에 옵션 응답을 동적 컬럼으로 포함하여 생성합니다. * * @param tickets 발급 티켓 응답 목록 * @param options 이벤트에 등록된 옵션 목록 (동적 컬럼 헤더용) * @param issuedTickets 발급 티켓 엔티티 목록 (옵션 응답 데이터 접근용) */ fun generateIssuedTicketsExcelWithOptions( tickets: List, options: List