From fbfbbe2ce4c4a63c7c45652a167747ea411fd622 Mon Sep 17 00:00:00 2001 From: Whitedoggy <82828857+Kimgooner@users.noreply.github.com> Date: Tue, 14 Oct 2025 11:06:38 +0900 Subject: [PATCH] Revert "Develop -> Main merge : 2025.10.14 - 10:52 (#177)" This reverts commit 94267a35a98f7fa369ea43e409df0333e2c112b2. --- .github/workflows/ci.yml | 52 + .github/workflows/prod-server.yml | 182 ---- .github/workflows/terraform.yml | 56 -- .github/workflows/test-server-cd.yml | 141 --- .github/workflows/test-server-ci.yml | 144 --- .gitignore | 12 +- .idea/.gitignore | 10 + .idea/.name | 1 + .idea/compiler.xml | 18 + .idea/misc.xml | 13 + .idea/vcs.xml | 6 + DEV_GUIDE.md | 203 ---- Dockerfile | 6 - README.md | 134 +-- build.gradle | 85 -- db_dev.trace.db | 3 + docker-compose.yml | 31 - infra/terraform/.gitignore | 7 - infra/terraform/main.tf | 68 -- infra/terraform/modules/ec2/main.tf | 61 -- infra/terraform/modules/ec2/outputs.tf | 3 - infra/terraform/modules/ec2/variables.tf | 20 - infra/terraform/modules/iam/main.tf | 31 - infra/terraform/modules/iam/outputs.tf | 3 - infra/terraform/modules/iam/variables.tf | 1 - infra/terraform/modules/rds/main.tf | 22 - infra/terraform/modules/rds/outputs.tf | 7 - infra/terraform/modules/rds/variables.tf | 14 - infra/terraform/modules/sg/ec2_sg.tf | 64 -- infra/terraform/modules/sg/outputs.tf | 7 - infra/terraform/modules/sg/rds_sg.tf | 28 - infra/terraform/modules/sg/variables.tf | 3 - infra/terraform/modules/vpc/main.tf | 71 -- infra/terraform/modules/vpc/outputs.tf | 18 - infra/terraform/modules/vpc/variables.tf | 2 - infra/terraform/outputs.tf | 3 - infra/terraform/variables.tf | 68 -- .../zoopzoop/backend/BackendApplication.java | 6 +- .../ApiV1NotificationController.java | 34 - .../domain/SSE/service/EmitterService.java | 75 -- .../archive/archive/entity/Archive.java | 28 +- .../archive/entity/PersonalArchive.java | 32 +- .../archive/entity/SharingArchive.java | 5 - .../repository/PersonalArchiveRepository.java | 24 - .../folder/controller/FolderController.java | 129 --- .../archive/folder/dto/FolderResponse.java | 6 - .../folder/dto/reqBodyForCreateFolder.java | 7 - .../folder/dto/resBodyForCreateFolder.java | 6 - .../domain/archive/folder/entity/Folder.java | 33 +- .../folder/repository/FolderRepository.java | 88 -- .../archive/folder/service/FolderService.java | 187 ---- .../service/PersonalArchiveFolderService.java | 69 -- .../auth/controller/ApiV1AuthController.java | 155 --- .../auth/deprecated/KakaoAuthService.java | 67 -- .../auth/deprecated/KakaoLoginController.java | 45 - .../auth/deprecated/KakaoTokenResponse.java | 10 - .../deprecated/KakaoUserInfoResponse.java | 10 - .../auth/dev/controller/DevController.java | 35 - .../domain/auth/dto/AuthResultData.java | 15 - .../domain/auth/entity/AuthResult.java | 26 - .../domain/auth/entity/RefreshToken.java | 39 - ...tomOAuth2AuthorizationRequestResolver.java | 57 -- .../auth/handler/OAuth2FailureHandler.java | 47 - .../auth/handler/OAuth2SuccessHandler.java | 169 ---- .../repository/RefreshTokenRepository.java | 18 - .../oauth2/CustomOAuth2UserService.java | 42 - .../service/oauth2/GoogleUserInfoService.java | 38 - .../service/oauth2/KakaoUserInfoService.java | 45 - .../service/oauth2/OAuth2UserInfoService.java | 10 - .../refresh/RefreshTokenCleanupService.java | 24 - .../service/refresh/RefreshTokenService.java | 63 -- .../controller/ApiV1DashboardController.java | 75 -- .../dashboard/dto/BodyForReactFlow.java | 145 --- .../dashboard/dto/GraphUpdateMessage.java | 7 - .../dto/ReqBodyForLiveblocksAuth.java | 15 - .../dashboard/dto/ResBodyForAuthToken.java | 5 - .../domain/dashboard/entity/Dashboard.java | 36 - .../backend/domain/dashboard/entity/Edge.java | 38 - .../domain/dashboard/entity/Graph.java | 27 - .../backend/domain/dashboard/entity/Node.java | 38 - .../domain/dashboard/enums/EdgeType.java | 8 - .../domain/dashboard/enums/NodeType.java | 5 - .../extraComponent/GraphUpdateConsumer.java | 39 - .../repository/DashboardRepository.java | 9 - .../dashboard/repository/EdgeRepository.java | 7 - .../dashboard/repository/GraphRepository.java | 10 - .../dashboard/repository/NodeRepository.java | 7 - .../dashboard/service/DashboardService.java | 201 ---- .../dashboard/service/GraphService.java | 26 - .../dashboard/service/SignatureService.java | 77 -- .../datasource/ai/dto/AiExtractorDto.java | 12 - .../datasource/ai/dto/AnalyzeContentDto.java | 12 - .../domain/datasource/ai/prompt/AiPrompt.java | 57 -- .../datasource/ai/service/AiService.java | 79 -- .../controller/DataSourceController.java | 232 ----- .../controller/CrawlerTestController.java | 27 - .../datasource/crawler/dto/CrawlerResult.java | 10 - .../crawler/dto/SpecificSiteDto.java | 22 - .../crawler/dto/UnspecificSiteDto.java | 6 - .../datasource/crawler/service/Crawler.java | 18 - .../service/CrawlerManagerService.java | 24 - .../crawler/service/GenericCrawler.java | 57 -- .../crawler/service/NaverBlogCrawler.java | 140 --- .../crawler/service/NaverNewsCrawler.java | 68 -- .../crawler/service/SupportedDomain.java | 18 - .../crawler/service/TistoryCralwer.java | 78 -- .../crawler/service/VelogCrawler.java | 69 -- .../service/DataProcessorService.java | 111 -- .../domain/datasource/dto/ArticleData.java | 12 - .../domain/datasource/dto/DataSourceDto.java | 18 - .../dto/DataSourceSearchCondition.java | 19 - .../datasource/dto/DataSourceSearchItem.java | 21 - .../domain/datasource/dto/FileSummary.java | 17 - .../domain/datasource/dto/FolderFilesDto.java | 9 - .../domain/datasource/dto/IdsRequest.java | 11 - .../domain/datasource/dto/PageInfo.java | 11 - .../domain/datasource/dto/SearchResponse.java | 8 - .../domain/datasource/dto/UpdateOutcome.java | 3 - .../dto/reqBodyForCreateDataSource.java | 7 - .../datasource/dto/reqBodyForDeleteMany.java | 10 - .../dto/reqBodyForMoveDataSource.java | 7 - .../datasource/dto/reqBodyForMoveMany.java | 11 - .../dto/reqBodyForUpdateDataSource.java | 16 - .../dto/resBodyForMoveDataSource.java | 6 - .../dto/resBodyForUpdateDataSource.java | 5 - .../domain/datasource/entity/Category.java | 52 - .../domain/datasource/entity/DataSource.java | 33 +- .../backend/domain/datasource/entity/Tag.java | 14 +- .../exception/DataSourceExceptionHandler.java | 22 - .../exception/ServiceException.java | 7 - .../repository/DataSourceQRepository.java | 11 - .../repository/DataSourceQRepositoryImpl.java | 268 ----- .../repository/DataSourceRepository.java | 103 -- .../datasource/repository/TagRepository.java | 52 - .../datasource/service/DataSourceService.java | 384 ------- .../service/PersonalDataSourceService.java | 194 ---- .../home/controller/HomeController.java | 50 +- .../controller/ApiV1MemberController.java | 241 ----- .../ApiV1MemberSearchController.java | 47 - .../domain/member/dto/etc/SimpleUserInfo.java | 7 - .../member/dto/req/ReqBodyForEditMember.java | 17 - .../dto/req/ReqBodyForEditMemberName.java | 12 - .../req/ReqBodyForEditMemberProfileImage.java | 13 - .../member/dto/res/ResBodyForEditMember.java | 11 - .../dto/res/ResBodyForEditMemberName.java | 9 - .../res/ResBodyForEditMemberProfileImage.java | 9 - .../dto/res/ResBodyForGetMemberInfo.java | 17 - .../dto/res/ResBodyForGetMemberInfoV2.java | 23 - .../dto/res/ResBodyForSearchMember.java | 18 - .../backend/domain/member/entity/Member.java | 33 +- .../domain/member/entity/MemberDocument.java | 24 - .../backend/domain/member/enums/Provider.java | 6 - .../member/repository/MemberRepository.java | 12 - .../repository/MemberSearchRepository.java | 11 - .../member/service/MemberSearchService.java | 18 - .../domain/member/service/MemberService.java | 191 ---- .../news/controller/ApiV1NewsController.java | 131 --- .../news/dto/req/ReqBodyForKeyword.java | 8 - .../news/dto/res/ResBodyForNaverNews.java | 28 - .../domain/news/service/NewsAPIService.java | 114 --- .../domain/news/service/NewsService.java | 72 -- .../SpaceArchiveDataSourceController.java | 259 ----- .../SpaceArchiveFolderController.java | 126 --- .../dto/reqBodyForCreateDataSourceAI.java | 14 - .../service/SpaceArchiveFolderService.java | 124 --- .../service/SpaceDataSourceService.java | 292 ------ .../controller/ApiV1InviteController.java | 102 -- .../controller/ApiV1MembershipController.java | 234 ----- .../membership/dto/etc/SpaceMemberInfo.java | 11 - .../req/ReqBodyForChangeMemberAuthority.java | 15 - .../dto/req/ReqBodyForExpelMember.java | 6 - .../dto/req/ReqBodyForInviteMembers.java | 8 - .../res/ResBodyForChangeMemberAuthority.java | 10 - .../dto/res/ResBodyForExpelMember.java | 10 - .../dto/res/ResBodyForInviteMembers.java | 12 - .../res/ResBodyForSpaceInvitationList.java | 11 - .../dto/res/ResBodyForSpaceMemberList.java | 12 - .../{Membership.java => MemberShip.java} | 2 +- .../space/membership/enums/Authority.java | 5 - .../space/membership/enums/JoinState.java | 6 - .../repository/MembershipRepository.java | 63 -- .../membership/service/MembershipService.java | 431 -------- .../service/NotificationService.java | 17 - .../repository/SpaceRepository.java | 6 +- .../controller/ApiV1SpaceController.java | 245 ----- .../domain/space/space/dto/etc/SpaceInfo.java | 20 - .../dto/etc/SpaceInfoWithoutAuthority.java | 8 - .../space/dto/etc/SpaceInvitationInfo.java | 9 - .../space/dto/req/ReqBodyForSpaceSave.java | 12 - .../space/dto/res/ResBodyForSpaceInfo.java | 11 - .../dto/res/ResBodyForSpaceInviteList.java | 11 - .../space/dto/res/ResBodyForSpaceList.java | 10 - .../dto/res/ResBodyForSpaceListPage.java | 26 - .../space/dto/res/ResBodyForSpaceSave.java | 6 - .../domain/space/space/entity/Space.java | 31 +- .../DuplicateSpaceNameException.java | 10 - .../exception/SpaceExceptionHandler.java | 29 - .../space/space/service/SpaceService.java | 201 ---- .../backend/global/aws/S3Service.java | 70 -- .../backend/global/aws/S3TestController.java | 42 - .../clients/liveblocks/LiveblocksClient.java | 114 --- .../backend/global/config/JacksonConfig.java | 21 - .../backend/global/config/QuerydslConfig.java | 19 - .../elasticSearch/ElasticSearchConfig.java | 30 - .../global/config/jwt/JwtProperties.java | 22 - .../global/config/mq/RabbitMQConfig.java | 69 -- .../global/config/redis/RedisConfig.java | 28 - .../restTemplate/RestTemplateConfig.java | 14 - .../global/config/sentry/ProfileChecker.java | 17 - .../global/config/sentry/SentryConfig.java | 28 - .../config/webflux/WebClientConfig.java | 13 - .../exception/GlobalExceptionHandler.java | 227 +---- .../TestThumbnailController.java | 21 - .../ThumbnailGeneratorService.java | 50 - .../backend/global/initData/BaseInitData.java | 37 +- .../initData/PersonalArchiveInitData.java | 107 -- .../backend/global/rsData/RsData.java | 3 +- .../global/security/SecurityConfig.java | 60 +- .../backend/global/security/StubAuthUtil.java | 15 - .../jwt/CustomAuthenticationEntryPoint.java | 27 - .../security/jwt/CustomUserDetails.java | 48 - .../security/jwt/JwtAuthenticationFilter.java | 89 -- .../backend/global/security/jwt/JwtUtil.java | 153 --- .../service/CustomUserDetailsService.java | 42 - .../global/test/ApiV1TestController.java | 38 - .../backend/global/webMvc/AiConfig.java | 19 - .../backend/global/webMvc/RetryConfig.java | 12 - src/main/resources/application-dev.yml | 32 +- .../application-secrets.yml.template | 17 - src/main/resources/application-server.yml | 55 - src/main/resources/application-test.yml | 33 - src/main/resources/application.yml | 128 +-- src/main/resources/ngram.json | 16 - .../backend/BackendApplicationTests.java | 2 - .../controller/FolderControllerTest.java | 286 ------ .../folder/service/FolderServiceTest.java | 225 ----- .../PersonalArchiveFolderServiceTest.java | 289 ------ .../controller/DashboardControllerTest.java | 293 ------ .../GraphUpdateConsumerTest.java | 106 -- .../dashboard/service/GraphServiceTest.java | 79 -- .../controller/DataSourceControllerTest.java | 270 ----- .../crawler/service/VelogCrawlerTest.java | 43 - .../DataSourceQRepositoryImplTest.java | 241 ----- .../datasource/service/AiServiceTest.java | 64 -- .../service/CrawlerManagerServiceTest.java | 297 ------ .../service/DataProcessorServiceTest.java | 121 --- .../service/DataSourceServiceTest.java | 303 ------ .../PersonalArchiveDataSourceServiceTest.java | 189 ---- .../controller/MemberControllerTest.java | 227 ----- .../MemberSearchControllerTest.java | 108 -- .../repository/MemberRepositoryTest.java | 61 -- .../member/service/MemberServiceTest.java | 179 ---- .../news/service/NewsAPIServiceTest.java | 37 - .../domain/news/service/NewsServiceTest.java | 117 --- .../SpaceArchiveDataSourceControllerTest.java | 353 ------- .../SpaceArchiveFolderControllerTest.java | 395 -------- .../SpaceArchiveDataSourceServiceTest.java | 168 ---- .../SpaceArchiveFolderServiceTest.java | 395 -------- .../controller/ApiV1InviteControllerTest.java | 302 ------ .../ApiV1MembershipControllerTest.java | 946 ------------------ .../service/MembershipServiceTest.java | 373 ------- .../service/NotificationServiceTest.java | 39 - .../controller/ApiV1SpaceControllerTest.java | 627 ------------ .../space/space/service/SpaceServiceTest.java | 155 --- .../testSupport/ControllerTestSupport.java | 231 ----- 265 files changed, 199 insertions(+), 18953 deletions(-) create mode 100644 .github/workflows/ci.yml delete mode 100644 .github/workflows/prod-server.yml delete mode 100644 .github/workflows/terraform.yml delete mode 100644 .github/workflows/test-server-cd.yml delete mode 100644 .github/workflows/test-server-ci.yml create mode 100644 .idea/.gitignore create mode 100644 .idea/.name create mode 100644 .idea/compiler.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/vcs.xml delete mode 100644 DEV_GUIDE.md delete mode 100644 Dockerfile create mode 100644 db_dev.trace.db delete mode 100644 docker-compose.yml delete mode 100644 infra/terraform/.gitignore delete mode 100644 infra/terraform/main.tf delete mode 100644 infra/terraform/modules/ec2/main.tf delete mode 100644 infra/terraform/modules/ec2/outputs.tf delete mode 100644 infra/terraform/modules/ec2/variables.tf delete mode 100644 infra/terraform/modules/iam/main.tf delete mode 100644 infra/terraform/modules/iam/outputs.tf delete mode 100644 infra/terraform/modules/iam/variables.tf delete mode 100644 infra/terraform/modules/rds/main.tf delete mode 100644 infra/terraform/modules/rds/outputs.tf delete mode 100644 infra/terraform/modules/rds/variables.tf delete mode 100644 infra/terraform/modules/sg/ec2_sg.tf delete mode 100644 infra/terraform/modules/sg/outputs.tf delete mode 100644 infra/terraform/modules/sg/rds_sg.tf delete mode 100644 infra/terraform/modules/sg/variables.tf delete mode 100644 infra/terraform/modules/vpc/main.tf delete mode 100644 infra/terraform/modules/vpc/outputs.tf delete mode 100644 infra/terraform/modules/vpc/variables.tf delete mode 100644 infra/terraform/outputs.tf delete mode 100644 infra/terraform/variables.tf delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/SSE/controller/ApiV1NotificationController.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/SSE/service/EmitterService.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/archive/archive/repository/PersonalArchiveRepository.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/archive/folder/controller/FolderController.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/archive/folder/dto/FolderResponse.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/archive/folder/dto/reqBodyForCreateFolder.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/archive/folder/dto/resBodyForCreateFolder.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/archive/folder/repository/FolderRepository.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/archive/folder/service/FolderService.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/archive/folder/service/PersonalArchiveFolderService.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/auth/controller/ApiV1AuthController.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/auth/deprecated/KakaoAuthService.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/auth/deprecated/KakaoLoginController.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/auth/deprecated/KakaoTokenResponse.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/auth/deprecated/KakaoUserInfoResponse.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/auth/dev/controller/DevController.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/auth/dto/AuthResultData.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/auth/entity/AuthResult.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/auth/entity/RefreshToken.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/auth/global/CustomOAuth2AuthorizationRequestResolver.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/auth/handler/OAuth2FailureHandler.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/auth/handler/OAuth2SuccessHandler.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/auth/repository/RefreshTokenRepository.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/auth/service/oauth2/CustomOAuth2UserService.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/auth/service/oauth2/GoogleUserInfoService.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/auth/service/oauth2/KakaoUserInfoService.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/auth/service/oauth2/OAuth2UserInfoService.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/auth/service/refresh/RefreshTokenCleanupService.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/auth/service/refresh/RefreshTokenService.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/controller/ApiV1DashboardController.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/dto/BodyForReactFlow.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/dto/GraphUpdateMessage.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/dto/ReqBodyForLiveblocksAuth.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/dto/ResBodyForAuthToken.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/entity/Dashboard.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/entity/Edge.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/entity/Graph.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/entity/Node.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/enums/EdgeType.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/enums/NodeType.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/extraComponent/GraphUpdateConsumer.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/repository/DashboardRepository.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/repository/EdgeRepository.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/repository/GraphRepository.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/repository/NodeRepository.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/service/DashboardService.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/service/GraphService.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/service/SignatureService.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/datasource/ai/dto/AiExtractorDto.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/datasource/ai/dto/AnalyzeContentDto.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/datasource/ai/prompt/AiPrompt.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/datasource/ai/service/AiService.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/datasource/controller/DataSourceController.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/datasource/crawler/controller/CrawlerTestController.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/datasource/crawler/dto/CrawlerResult.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/datasource/crawler/dto/SpecificSiteDto.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/datasource/crawler/dto/UnspecificSiteDto.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/datasource/crawler/service/Crawler.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/datasource/crawler/service/CrawlerManagerService.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/datasource/crawler/service/GenericCrawler.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/datasource/crawler/service/NaverBlogCrawler.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/datasource/crawler/service/NaverNewsCrawler.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/datasource/crawler/service/SupportedDomain.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/datasource/crawler/service/TistoryCralwer.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/datasource/crawler/service/VelogCrawler.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/datasource/dataprocessor/service/DataProcessorService.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/datasource/dto/ArticleData.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/datasource/dto/DataSourceDto.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/datasource/dto/DataSourceSearchCondition.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/datasource/dto/DataSourceSearchItem.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/datasource/dto/FileSummary.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/datasource/dto/FolderFilesDto.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/datasource/dto/IdsRequest.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/datasource/dto/PageInfo.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/datasource/dto/SearchResponse.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/datasource/dto/UpdateOutcome.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/datasource/dto/reqBodyForCreateDataSource.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/datasource/dto/reqBodyForDeleteMany.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/datasource/dto/reqBodyForMoveDataSource.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/datasource/dto/reqBodyForMoveMany.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/datasource/dto/reqBodyForUpdateDataSource.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/datasource/dto/resBodyForMoveDataSource.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/datasource/dto/resBodyForUpdateDataSource.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/datasource/entity/Category.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/datasource/exception/DataSourceExceptionHandler.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/datasource/exception/ServiceException.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/datasource/repository/DataSourceQRepository.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/datasource/repository/DataSourceQRepositoryImpl.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/datasource/repository/DataSourceRepository.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/datasource/repository/TagRepository.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/datasource/service/DataSourceService.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/datasource/service/PersonalDataSourceService.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/member/controller/ApiV1MemberController.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/member/controller/ApiV1MemberSearchController.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/member/dto/etc/SimpleUserInfo.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/member/dto/req/ReqBodyForEditMember.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/member/dto/req/ReqBodyForEditMemberName.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/member/dto/req/ReqBodyForEditMemberProfileImage.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/member/dto/res/ResBodyForEditMember.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/member/dto/res/ResBodyForEditMemberName.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/member/dto/res/ResBodyForEditMemberProfileImage.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/member/dto/res/ResBodyForGetMemberInfo.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/member/dto/res/ResBodyForGetMemberInfoV2.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/member/dto/res/ResBodyForSearchMember.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/member/entity/MemberDocument.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/member/enums/Provider.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/member/repository/MemberSearchRepository.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/member/service/MemberSearchService.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/member/service/MemberService.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/news/controller/ApiV1NewsController.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/news/dto/req/ReqBodyForKeyword.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/news/dto/res/ResBodyForNaverNews.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/news/service/NewsAPIService.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/news/service/NewsService.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/space/archive/controller/SpaceArchiveDataSourceController.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/space/archive/controller/SpaceArchiveFolderController.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/space/archive/dto/reqBodyForCreateDataSourceAI.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/space/archive/service/SpaceArchiveFolderService.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/space/archive/service/SpaceDataSourceService.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/space/membership/controller/ApiV1InviteController.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/space/membership/controller/ApiV1MembershipController.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/space/membership/dto/etc/SpaceMemberInfo.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/space/membership/dto/req/ReqBodyForChangeMemberAuthority.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/space/membership/dto/req/ReqBodyForExpelMember.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/space/membership/dto/req/ReqBodyForInviteMembers.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/space/membership/dto/res/ResBodyForChangeMemberAuthority.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/space/membership/dto/res/ResBodyForExpelMember.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/space/membership/dto/res/ResBodyForInviteMembers.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/space/membership/dto/res/ResBodyForSpaceInvitationList.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/space/membership/dto/res/ResBodyForSpaceMemberList.java rename src/main/java/org/tuna/zoopzoop/backend/domain/space/membership/entity/{Membership.java => MemberShip.java} (95%) delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/space/membership/enums/JoinState.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/space/membership/repository/MembershipRepository.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/space/membership/service/MembershipService.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/space/membership/service/NotificationService.java rename src/main/java/org/tuna/zoopzoop/backend/domain/space/{space => }/repository/SpaceRepository.java (59%) delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/space/space/controller/ApiV1SpaceController.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/space/space/dto/etc/SpaceInfo.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/space/space/dto/etc/SpaceInfoWithoutAuthority.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/space/space/dto/etc/SpaceInvitationInfo.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/space/space/dto/req/ReqBodyForSpaceSave.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/space/space/dto/res/ResBodyForSpaceInfo.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/space/space/dto/res/ResBodyForSpaceInviteList.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/space/space/dto/res/ResBodyForSpaceList.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/space/space/dto/res/ResBodyForSpaceListPage.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/space/space/dto/res/ResBodyForSpaceSave.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/space/space/exception/DuplicateSpaceNameException.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/space/space/exception/SpaceExceptionHandler.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/space/space/service/SpaceService.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/global/aws/S3Service.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/global/aws/S3TestController.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/global/clients/liveblocks/LiveblocksClient.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/global/config/JacksonConfig.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/global/config/QuerydslConfig.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/global/config/elasticSearch/ElasticSearchConfig.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/global/config/jwt/JwtProperties.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/global/config/mq/RabbitMQConfig.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/global/config/redis/RedisConfig.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/global/config/restTemplate/RestTemplateConfig.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/global/config/sentry/ProfileChecker.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/global/config/sentry/SentryConfig.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/global/config/webflux/WebClientConfig.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/global/headlessBrowser/TestThumbnailController.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/global/headlessBrowser/ThumbnailGeneratorService.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/global/initData/PersonalArchiveInitData.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/global/security/StubAuthUtil.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/global/security/jwt/CustomAuthenticationEntryPoint.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/global/security/jwt/CustomUserDetails.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/global/security/jwt/JwtAuthenticationFilter.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/global/security/jwt/JwtUtil.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/global/security/service/CustomUserDetailsService.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/global/test/ApiV1TestController.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/global/webMvc/AiConfig.java delete mode 100644 src/main/java/org/tuna/zoopzoop/backend/global/webMvc/RetryConfig.java delete mode 100644 src/main/resources/application-secrets.yml.template delete mode 100644 src/main/resources/application-server.yml delete mode 100644 src/main/resources/ngram.json delete mode 100644 src/test/java/org/tuna/zoopzoop/backend/domain/archive/folder/controller/FolderControllerTest.java delete mode 100644 src/test/java/org/tuna/zoopzoop/backend/domain/archive/folder/service/FolderServiceTest.java delete mode 100644 src/test/java/org/tuna/zoopzoop/backend/domain/archive/folder/service/PersonalArchiveFolderServiceTest.java delete mode 100644 src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/controller/DashboardControllerTest.java delete mode 100644 src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/extraComponent/GraphUpdateConsumerTest.java delete mode 100644 src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/service/GraphServiceTest.java delete mode 100644 src/test/java/org/tuna/zoopzoop/backend/domain/datasource/controller/DataSourceControllerTest.java delete mode 100644 src/test/java/org/tuna/zoopzoop/backend/domain/datasource/crawler/service/VelogCrawlerTest.java delete mode 100644 src/test/java/org/tuna/zoopzoop/backend/domain/datasource/repository/DataSourceQRepositoryImplTest.java delete mode 100644 src/test/java/org/tuna/zoopzoop/backend/domain/datasource/service/AiServiceTest.java delete mode 100644 src/test/java/org/tuna/zoopzoop/backend/domain/datasource/service/CrawlerManagerServiceTest.java delete mode 100644 src/test/java/org/tuna/zoopzoop/backend/domain/datasource/service/DataProcessorServiceTest.java delete mode 100644 src/test/java/org/tuna/zoopzoop/backend/domain/datasource/service/DataSourceServiceTest.java delete mode 100644 src/test/java/org/tuna/zoopzoop/backend/domain/datasource/service/PersonalArchiveDataSourceServiceTest.java delete mode 100644 src/test/java/org/tuna/zoopzoop/backend/domain/member/controller/MemberControllerTest.java delete mode 100644 src/test/java/org/tuna/zoopzoop/backend/domain/member/controller/MemberSearchControllerTest.java delete mode 100644 src/test/java/org/tuna/zoopzoop/backend/domain/member/repository/MemberRepositoryTest.java delete mode 100644 src/test/java/org/tuna/zoopzoop/backend/domain/member/service/MemberServiceTest.java delete mode 100644 src/test/java/org/tuna/zoopzoop/backend/domain/news/service/NewsAPIServiceTest.java delete mode 100644 src/test/java/org/tuna/zoopzoop/backend/domain/news/service/NewsServiceTest.java delete mode 100644 src/test/java/org/tuna/zoopzoop/backend/domain/space/archive/controller/SpaceArchiveDataSourceControllerTest.java delete mode 100644 src/test/java/org/tuna/zoopzoop/backend/domain/space/archive/controller/SpaceArchiveFolderControllerTest.java delete mode 100644 src/test/java/org/tuna/zoopzoop/backend/domain/space/archive/service/SpaceArchiveDataSourceServiceTest.java delete mode 100644 src/test/java/org/tuna/zoopzoop/backend/domain/space/archive/service/SpaceArchiveFolderServiceTest.java delete mode 100644 src/test/java/org/tuna/zoopzoop/backend/domain/space/membership/controller/ApiV1InviteControllerTest.java delete mode 100644 src/test/java/org/tuna/zoopzoop/backend/domain/space/membership/controller/ApiV1MembershipControllerTest.java delete mode 100644 src/test/java/org/tuna/zoopzoop/backend/domain/space/membership/service/MembershipServiceTest.java delete mode 100644 src/test/java/org/tuna/zoopzoop/backend/domain/space/membership/service/NotificationServiceTest.java delete mode 100644 src/test/java/org/tuna/zoopzoop/backend/domain/space/space/controller/ApiV1SpaceControllerTest.java delete mode 100644 src/test/java/org/tuna/zoopzoop/backend/domain/space/space/service/SpaceServiceTest.java delete mode 100644 src/test/java/org/tuna/zoopzoop/backend/testSupport/ControllerTestSupport.java diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..1229302b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,52 @@ +# 워크플로우 이름 +name: Spring CI on main/develop + +# 워크플로우 실행 조건: main 또는 develop 브랜치로 Pull Request가 생성될 때 실행 +on: + pull_request: + branches: [ "main", "develop" ] + paths: + - 'src/**' # src 디렉토리 하위 파일이 변경될 때만 실행 + +jobs: + # ================================== + # CI Job: Gradle 테스트 및 빌드 실행 + # ================================== + build-and-test: + runs-on: ubuntu-latest + + steps: + # 1. 소스 코드 체크아웃 + - name: Checkout source code + uses: actions/checkout@v4 + + # 2. JDK 21 설치 + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + + # 3. Gradle 캐시 설정 + # 프로젝트 루트의 gradle 파일들을 기준으로 캐시를 설정합니다. + - name: Cache Gradle packages + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + # 4. gradlew 실행 권한 부여 + - name: Grant execute permission for gradlew + run: chmod +x ./gradlew + + # 5. Gradle 테스트 실행 + - name: Test with Gradle + run: ./gradlew test + + # 6. Gradle 빌드 실행 (테스트 성공 시) + - name: Build with Gradle + run: ./gradlew build diff --git a/.github/workflows/prod-server.yml b/.github/workflows/prod-server.yml deleted file mode 100644 index 302e2f9d..00000000 --- a/.github/workflows/prod-server.yml +++ /dev/null @@ -1,182 +0,0 @@ -# 워크플로우 이름 -name: Spring CD (Production) - -# main 브랜치 PR에서만 실행 (이미 빌드된 Docker 이미지 사용) -on: - push: - branches: - - main - paths: - - 'src/**' - - 'build.gradle*' - - 'settings.gradle*' - - 'gradle/**' - - 'Dockerfile' - - '.github/workflows/**' - -jobs: - # ================================== - # CD: Deploy to Production Environment - # ================================== - cd-prod: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Deploy to Test Environment - uses: appleboy/ssh-action@v0.1.7 - with: - host: ${{ secrets.PROD_SERVER_HOST }} - username: ec2-user - key: ${{ secrets.SSH_PRIVATE_KEY }} - script: | - - # GHCR 로그인 (EC2) - echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{github.repository_owner}}" --password-stdin - - # 최신 이미지 pull - docker pull ghcr.io/${{ github.repository }}/zoopzoop:latest - - # NPM API 설정 - NPM_HOST="localhost:81" - NPM_EMAIL="${{secrets.NPM_ADMIN_EMAIL}}" - NPM_PASSWORD="${{secrets.NPM_ADMIN_PASSWORD}}" - PROXY_HOST_ID="${{secrets.NPM_PROXY_HOST_ID}}" - - # NPM API 토큰 가져오기 - echo "Getting NPM API token..." - TOKEN=$(curl -s -X POST "http://${NPM_HOST}/api/tokens" \ - -H "Content-Type: application/json" \ - -d "{\"identity\":\"${NPM_EMAIL}\",\"secret\":\"${NPM_PASSWORD}\"}" | \ - jq -r '.token') - if [ "$TOKEN" == "null" ] || [ -z "$TOKEN" ]; then - echo "❌ Failed to get NPM API token" - exit 1 - fi - - # 현재 NPM Proxy Host 설정 확인 - echo "📋 Checking current NPM configuration... 📋" - CURRENT_CONFIG=$(curl -s -H "Authorization: Bearer $TOKEN" \ - "http://${NPM_HOST}/api/nginx/proxy-hosts/${PROXY_HOST_ID}") - - CURRENT_TARGET=$(echo $CURRENT_CONFIG | jq -r '.[0].forward_host // .forward_host') - CURRENT_PORT=$(echo $CURRENT_CONFIG | jq -r '.[0].forward_port // .forward_port') - - echo "Current NPM target: $CURRENT_TARGET:$CURRENT_PORT" - - # Blue-Green 배포 - if [ "$(docker ps -q -f name=zoopzoop-blue)" ]; then - NEW_CONTAINER=zoopzoop-green - OLD_CONTAINER=zoopzoop-blue - NEW_PORT=8082 - else - NEW_CONTAINER=zoopzoop-blue - OLD_CONTAINER=zoopzoop-green - NEW_PORT=8081 - fi - - echo "Starting new container: $NEW_CONTAINER on port $NEW_PORT" - docker run -d --restart unless-stopped \ - -p $NEW_PORT:8080 \ - --name $NEW_CONTAINER \ - --network common \ - -e SPRING_PROFILES_ACTIVE=server \ - -e SPRING_DATASOURCE_URL="${{secrets.PROD_DB_URL}}" \ - -e SPRING_DATASOURCE_USERNAME="${{secrets.PROD_DB_USERNAME}}" \ - -e SPRING_DATASOURCE_PASSWORD="${{secrets.PROD_DB_PASSWORD}}" \ - -e AWS_S3_BUCKET_NAME="${{secrets.AWS_S3_BUCKET_NAME}}" \ - -e AWS_S3_PREFIX="${{secrets.PROD_AWS_S3_PREFIX}}" \ - -e SPRING_RABBITMQ_HOST="${{secrets.PROD_RABBITMQ_HOST}}" \ - -e SPRING_RABBITMQ_PORT="${{secrets.PROD_RABBITMQ_PORT}}" \ - -e SPRING_RABBITMQ_USERNAME="${{secrets.PROD_RABBITMQ_USERNAME}}" \ - -e SPRING_RABBITMQ_PASSWORD="${{secrets.PROD_RABBITMQ_PASSWORD}}" \ - -e REDIS_HOST="${{secrets.PROD_REDIS_HOST}}" \ - -e REDIS_PASSWORD="${{secrets.PROD_REDIS_PASSWORD}}" \ - -e KAKAO_CLIENT_ID="${{secrets.OAUTH_KAKAO_CLIENT_ID}}" \ - -e GOOGLE_CLIENT_ID="${{secrets.OAUTH_GOOGLE_CLIENT_ID}}" \ - -e GOOGLE_CLIENT_SECRET="${{secrets.OAUTH_GOOGLE_CLIENT_SECRET}}" \ - -e KAKAO_REDIRECT_URI="${{secrets.PROD_OAUTH_KAKAO_REDIRECT_URI}}" \ - -e GOOGLE_REDIRECT_URI="${{secrets.PROD_OAUTH_GOOGLE_REDIRECT_URI}}" \ - -e SENTRY_DSN="${{secrets.SENTRY_DSN}}" \ - -e OPENAI_API_KEY="${{secrets.PROD_OPENAI_API_KEY}}" \ - -e LIVEBLOCKS_SECRET_KEY="${{secrets.LIVEBLOCKS_SECRET_KEY}}" \ - -e NAVER_CLIENT_ID="${{secrets.NAVER_CLIENT_ID}}" \ - -e NAVER_CLIENT_SECRET="${{secrets.NAVER_CLIENT_SECRET}}" \ - -e JWT_SECRET_KEY="${{secrets.JWT_SECRET_KEY}}" \ - -e JWT_ACCESS_TOKEN_VALIDITY="${{secrets.JWT_ACCESS_TOKEN_VALIDITY}}" \ - -e JWT_REFRESH_TOKEN_VALIDITY="${{secrets.JWT_REFRESH_TOKEN_VALIDITY}}" \ - -e FRONT_REDIRECT_DOMAIN="${{secrets.FRONT_REDIRECT_DOMAIN}}" \ - -e FRONT_MAIN_DOMAIN="${{secrets.MAIN_DOMAIN}}" \ - -e ELASTIC_HOST="${{secrets.PROD_ELASTIC_HOST}}" \ - ghcr.io/${{ github.repository }}/zoopzoop:latest - - - # 헬스체크 (Spring Boot Actuator) - for i in {1..30}; do - if curl -s http://localhost:$NEW_PORT/actuator/health | grep -q '"status":"UP"'; then - echo "✅New container is healthy!" - break - else - echo "Waiting for new container to be healthy..." - sleep 5 - fi - - if [ $i -eq 30 ]; then - echo "❌ Health check failed. Rolling back..." - docker stop $NEW_CONTAINER || true - docker rm $NEW_CONTAINER || true - exit 1 - fi - done - - # NPM에서 트래픽 스위칭 - echo "🔄 Switching traffic in Nginx Proxy Manager..." - DOMAIN_NAME=$(echo $CURRENT_CONFIG | jq -r '.domain_names[0]') - CERT_ID=$(echo "$CURRENT_CONFIG" | jq -r '.certificate_id') - - SWITCH_RESPONSE=$(curl -s -w "%{http_code}" -X PUT "http://${NPM_HOST}/api/nginx/proxy-hosts/${PROXY_HOST_ID}" \ - -H "Authorization: Bearer $TOKEN" \ - -H "Content-Type: application/json" \ - -d "{ - \"domain_names\": [\"$DOMAIN_NAME\"], - \"forward_scheme\": \"http\", - \"forward_host\": \"$NEW_CONTAINER\", - \"forward_port\": 8080, - \"caching_enabled\": false, - \"block_exploits\": true, - \"advanced_config\": \"\", - \"locations\": [], - \"certificate_id\": $CERT_ID, - \"ssl_forced\": 1, - \"hsts_enabled\": 1, - \"hsts_subdomains\": 1 - }") - - HTTP_CODE=${SWITCH_RESPONSE: -3} - if [ "$HTTP_CODE" -eq 200 ] || [ "$HTTP_CODE" -eq 201 ]; then - echo "✅ Traffic switching completed successfully!" - echo "🎯 NPM now points to: $NEW_CONTAINER:8080" - - # 최종 확인 - sleep 5 - echo "🔍 Final verification..." - VERIFY_CONFIG=$(curl -s -H "Authorization: Bearer $TOKEN" \ - "http://${NPM_HOST}/api/nginx/proxy-hosts/${PROXY_HOST_ID}") - VERIFY_TARGET=$(echo $VERIFY_CONFIG | jq -r '.forward_host') - echo "✅ Verified NPM target: $VERIFY_TARGET" - - else - echo "❌ Traffic switching failed! HTTP Code: $HTTP_CODE" - echo "Response: ${SWITCH_RESPONSE%???}" - echo "🔄 Rolling back new container..." - docker stop $NEW_CONTAINER || true - docker rm $NEW_CONTAINER || true - exit 1 - fi - - # 이전 컨테이너 종료 및 제거 - echo "Stopping old container: $OLD_CONTAINER" - docker stop $OLD_CONTAINER || true - docker rm $OLD_CONTAINER || true \ No newline at end of file diff --git a/.github/workflows/terraform.yml b/.github/workflows/terraform.yml deleted file mode 100644 index 3b7369f3..00000000 --- a/.github/workflows/terraform.yml +++ /dev/null @@ -1,56 +0,0 @@ -name: Terraform Apply - -on: - workflow_dispatch: - -jobs: - terraform-apply: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Setup Terraform - uses: hashicorp/setup-terraform@v2 - with: - terraform_version: 1.7.6 - - - name: Terraform Init - working-directory: ./infra/terraform #워크플로우 파일 위치 폴더 기준 x, 체크아웃된 저장소 루트 기준 - run: terraform init - - - name: Set Environment based on Branch - run: | - BRANCH=${{ github.ref_name }} - if [ "$BRANCH" = "develop" ] ; then - ENV="test" - elif [ "$BRANCH" = "main" ] ; then - ENV="prod" - else - echo "Unsupported branch: $BRANCH" - exit 1 - fi - echo "Environment: $ENV" - - if [ "$ENV" = "prod" ] ; then - echo "${{secrets.TFVARS_PROD}}" > ./infra/terraform/env/terraform.tfvars - else - echo "${{secrets.TFVARS_TEST}}" > ./infra/terraform/env/terraform.tfvars - fi - - - name: Select or Create Workspace - working-directory: ./infra/terraform - run: | - if terraform workspace list | grep -q "$ENV"; then - terraform workspace select "$ENV" - else - terraform workspace new "$ENV" - fi - - - name: Terraform Apply - working-directory: ./infra/terraform/env - run: terraform apply -auto-approve -var-file="terraform.tfvars" - env: - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} diff --git a/.github/workflows/test-server-cd.yml b/.github/workflows/test-server-cd.yml deleted file mode 100644 index 541561ed..00000000 --- a/.github/workflows/test-server-cd.yml +++ /dev/null @@ -1,141 +0,0 @@ -name: Spring CD (Test Server) - -on: - push: - branches: - - develop - -jobs: - cd-test: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Deploy to Test Environment - uses: appleboy/ssh-action@v0.1.7 - with: - host: ${{ secrets.TEST_SERVER_HOST }} - username: ec2-user - key: ${{ secrets.SSH_PRIVATE_KEY }} - script: | - # GHCR 로그인 - echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.repository_owner }}" --password-stdin - docker pull ghcr.io/${{ github.repository }}/zoopzoop:latest - - NPM_HOST="localhost:81" - NPM_EMAIL="${{ secrets.NPM_ADMIN_EMAIL }}" - NPM_PASSWORD="${{ secrets.NPM_ADMIN_PASSWORD }}" - PROXY_HOST_ID="${{ secrets.NPM_PROXY_HOST_ID }}" - - # NPM 토큰 - TOKEN=$(curl -s -X POST "http://${NPM_HOST}/api/tokens" \ - -H "Content-Type: application/json" \ - -d "{\"identity\":\"${NPM_EMAIL}\",\"secret\":\"${NPM_PASSWORD}\"}" | jq -r '.token') - if [ -z "$TOKEN" ] || [ "$TOKEN" == "null" ]; then - echo "❌ Failed to get NPM API token" - exit 1 - fi - - CURRENT_CONFIG=$(curl -s -H "Authorization: Bearer $TOKEN" \ - "http://${NPM_HOST}/api/nginx/proxy-hosts/${PROXY_HOST_ID}") - - CURRENT_TARGET=$(echo $CURRENT_CONFIG | jq -r '.[0].forward_host // .forward_host') - CURRENT_PORT=$(echo $CURRENT_CONFIG | jq -r '.[0].forward_port // .forward_port') - echo "Current NPM target: $CURRENT_TARGET:$CURRENT_PORT" - - if [ "$(docker ps -q -f name=zoopzoop-blue)" ]; then - NEW_CONTAINER=zoopzoop-green - OLD_CONTAINER=zoopzoop-blue - NEW_PORT=8082 - else - NEW_CONTAINER=zoopzoop-blue - OLD_CONTAINER=zoopzoop-green - NEW_PORT=8081 - fi - - docker run -d --restart unless-stopped \ - -p $NEW_PORT:8080 \ - --name $NEW_CONTAINER \ - --network common \ - -e SPRING_PROFILES_ACTIVE=server \ - -e SPRING_DATASOURCE_URL="${{secrets.TEST_DB_URL}}" \ - -e SPRING_DATASOURCE_USERNAME="${{secrets.TEST_DB_USERNAME}}" \ - -e SPRING_DATASOURCE_PASSWORD="${{secrets.TEST_DB_PASSWORD}}" \ - -e AWS_S3_BUCKET_NAME="${{secrets.AWS_S3_BUCKET_NAME}}" \ - -e AWS_S3_PREFIX="${{secrets.TEST_AWS_S3_PREFIX}}" \ - -e SPRING_RABBITMQ_HOST="${{secrets.TEST_RABBITMQ_HOST}}" \ - -e SPRING_RABBITMQ_PORT="${{secrets.TEST_RABBITMQ_PORT}}" \ - -e SPRING_RABBITMQ_USERNAME="${{secrets.TEST_RABBITMQ_USERNAME}}" \ - -e SPRING_RABBITMQ_PASSWORD="${{secrets.TEST_RABBITMQ_PASSWORD}}" \ - -e REDIS_HOST="${{secrets.TEST_REDIS_HOST}}" \ - -e REDIS_PASSWORD="${{secrets.TEST_REDIS_PASSWORD}}" \ - -e KAKAO_CLIENT_ID="${{secrets.OAUTH_KAKAO_CLIENT_ID}}" \ - -e GOOGLE_CLIENT_ID="${{secrets.OAUTH_GOOGLE_CLIENT_ID}}" \ - -e GOOGLE_CLIENT_SECRET="${{secrets.OAUTH_GOOGLE_CLIENT_SECRET}}" \ - -e KAKAO_REDIRECT_URI="${{secrets.TEST_OAUTH_KAKAO_REDIRECT_URI}}" \ - -e GOOGLE_REDIRECT_URI="${{secrets.TEST_OAUTH_GOOGLE_REDIRECT_URI}}" \ - -e SENTRY_DSN="${{secrets.SENTRY_DSN}}" \ - -e OPENAI_API_KEY="${{secrets.OPENAI_API_KEY}}" \ - -e LIVEBLOCKS_SECRET_KEY="${{secrets.LIVEBLOCKS_SECRET_KEY}}" \ - -e NAVER_CLIENT_ID="${{secrets.NAVER_CLIENT_ID}}" \ - -e NAVER_CLIENT_SECRET="${{secrets.NAVER_CLIENT_SECRET}}" \ - -e JWT_SECRET_KEY="${{secrets.JWT_SECRET_KEY}}" \ - -e JWT_ACCESS_TOKEN_VALIDITY="${{secrets.JWT_ACCESS_TOKEN_VALIDITY}}" \ - -e JWT_REFRESH_TOKEN_VALIDITY="${{secrets.JWT_REFRESH_TOKEN_VALIDITY}}" \ - -e FRONT_REDIRECT_DOMAIN="${{secrets.TEST_REDIRECT_DOMAIN}}" \ - -e FRONT_MAIN_DOMAIN="${{secrets.MAIN_DOMAIN}}" \ - -e ELASTIC_HOST="${{secrets.TEST_ELASTIC_HOST}}" \ - ghcr.io/${{ github.repository }}/zoopzoop:latest - - # 헬스체크 - for i in {1..30}; do - if curl -s http://localhost:$NEW_PORT/actuator/health | grep -q '"status":"UP"'; then - echo "✅ New container is healthy!" - break - else - echo "Waiting for new container to be healthy..." - sleep 5 - fi - if [ $i -eq 30 ]; then - echo "❌ Health check failed. Rolling back..." - docker stop $NEW_CONTAINER || true - docker rm $NEW_CONTAINER || true - exit 1 - fi - done - - # NPM 트래픽 스위칭 - DOMAIN_NAME=$(echo $CURRENT_CONFIG | jq -r '.domain_names[0]') - CERT_ID=$(echo "$CURRENT_CONFIG" | jq -r '.certificate_id') - - SWITCH_RESPONSE=$(curl -s -w "%{http_code}" -X PUT "http://${NPM_HOST}/api/nginx/proxy-hosts/${PROXY_HOST_ID}" \ - -H "Authorization: Bearer $TOKEN" \ - -H "Content-Type: application/json" \ - -d "{ - \"domain_names\": [\"$DOMAIN_NAME\"], - \"forward_scheme\": \"http\", - \"forward_host\": \"$NEW_CONTAINER\", - \"forward_port\": 8080, - \"caching_enabled\": false, - \"block_exploits\": true, - \"advanced_config\": \"\", - \"locations\": [], - \"certificate_id\": $CERT_ID, - \"ssl_forced\": 1, - \"hsts_enabled\": 1, - \"hsts_subdomains\": 1 - }") - - HTTP_CODE=${SWITCH_RESPONSE: -3} - if [ "$HTTP_CODE" -ne 200 ] && [ "$HTTP_CODE" -ne 201 ]; then - echo "❌ Traffic switching failed! HTTP Code: $HTTP_CODE" - echo "Response: ${SWITCH_RESPONSE%???}" - docker stop $NEW_CONTAINER || true - docker rm $NEW_CONTAINER || true - exit 1 - fi - - docker stop $OLD_CONTAINER || true - docker rm $OLD_CONTAINER || true diff --git a/.github/workflows/test-server-ci.yml b/.github/workflows/test-server-ci.yml deleted file mode 100644 index 9eb6d672..00000000 --- a/.github/workflows/test-server-ci.yml +++ /dev/null @@ -1,144 +0,0 @@ -# 워크플로우 이름 -name: Spring CI/CD Pipeline (Develop) - -# develop 브랜치 PR에서만 실행 -on: - pull_request: - branches: - - develop - paths: - - 'src/**' - - 'build.gradle*' - - 'settings.gradle*' - - 'gradle/**' - - 'Dockerfile' - - '.github/workflows/**' - -jobs: - # ================================== - # CI: Test and Build and Push Docker Image - # ================================== - ci: - runs-on: ubuntu-latest - env: - SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - - services: - # CI 작업이 실행되는 동안 RabbitMQ 서비스 컨테이너를 함께 실행 - rabbitmq: - image: rabbitmq:3-management - ports: - - 5672:5672 - # RabbitMQ가 완전히 준비될 때까지 기다리는 상태 확인 옵션 - options: >- - --health-cmd "rabbitmq-diagnostics check_running" - --health-interval 10s - --health-timeout 5s - --health-retries 5 - - # CI 작업이 실행되는 동안 ElasticSearch 서비스 컨테이너 함께 실행 - elasticsearch: - image: docker.elastic.co/elasticsearch/elasticsearch:8.18.5 - ports: - - 9200:9200 - options: >- - --env discovery.type=single-node - --env xpack.security.enabled=false - --env ES_JAVA_OPTS="-Xms512m -Xmx512m" - - steps: - # 1. 소스 코드 체크아웃 - - name: Checkout source code - uses: actions/checkout@v4 - - # 2. JDK 21 설치 - - name: Set up JDK 21 - uses: actions/setup-java@v4 - with: - java-version: '21' - distribution: 'temurin' - - # 3. Gradle 캐시 설정 - - name: Cache Gradle packages - uses: actions/cache@v4 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - restore-keys: | - ${{ runner.os }}-gradle- - - # 4. gradlew 실행 권한 부여 - - name: Grant execute permission for gradlew - run: chmod +x ./gradlew - - # 5. Gradle 테스트 실행 - - name: Test with Gradle - # 테스트 단계에서 RabbitMQ 연결을 위한 환경 변수 설정 - env: - SPRING_PROFILES_ACTIVE: test - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - SPRING_RABBITMQ_HOST: localhost - SPRING_RABBITMQ_USERNAME: guest - SPRING_RABBITMQ_PASSWORD: guest - SPRING_DATA_ELASTICSEARCH_HOST: localhost - SPRING_DATA_ELASTICSEARCH_PORT: 9200 - KAKAO_CLIENT_ID: ${{ secrets.OAUTH_KAKAO_CLIENT_ID }} - GOOGLE_CLIENT_ID: ${{ secrets.OAUTH_GOOGLE_CLIENT_ID }} - GOOGLE_CLIENT_SECRET: ${{ secrets.OAUTH_GOOGLE_CLIENT_SECRET }} - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_S3_BUCKET_NAME: ${{ secrets.AWS_S3_BUCKET_NAME }} - AWS_S3_PREFIX: test/ - SENTRY_DSN: ${{ secrets.SENTRY_DSN }} - JWT_SECRET_KEY: ${{ secrets.JWT_SECRET_KEY }} - JWT_ACCESS_TOKEN_VALIDITY: ${{ secrets.JWT_ACCESS_TOKEN_VALIDITY }} - JWT_REFRESH_TOKEN_VALIDITY: ${{ secrets.JWT_REFRESH_TOKEN_VALIDITY }} - NAVER_CLIENT_ID: ${{ secrets.NAVER_CLIENT_ID }} - NAVER_CLIENT_SECRET: ${{ secrets.NAVER_CLIENT_SECRET }} - LIVEBLOCKS_SECRET_KEY: ${{ secrets.LIVEBLOCKS_SECRET_KEY }} - FRONT_MAIN_DOMAIN: ${{secrets.MAIN_DOMAIN}} - run: ./gradlew test --stacktrace - - # 6. 테스트 결과 요약 출력 - - name: Show test results - if: always() # 테스트 실패 여부와 상관없이 항상 실행 - run: | - echo "==== Test Results ====" - if compgen -G "build/test-results/test/TEST-*.xml" > /dev/null; then - total=$(grep ' + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 00000000..d963bed8 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 00000000..35eb1ddf --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/DEV_GUIDE.md b/DEV_GUIDE.md deleted file mode 100644 index 25ea04c2..00000000 --- a/DEV_GUIDE.md +++ /dev/null @@ -1,203 +0,0 @@ -# ZoopZoop 백엔드 개발 컨벤션 - ---- - -## 1. 기본 규칙 - -- **언어** : Java -- **변수명 규칙** : camelCase -- **Merge 방식** : Squash & Merge - ---- - -## 2. 이슈 템플릿 - -### 분류 - -- `design` : UI 관련 (프론트엔드) -- `fix` : 버그 수정 -- `feat` : 기능 추가 -- `refactor` : 리팩토링 -- `chore` : 문서, 환경 설정 등 - -### 이슈 네이밍 규칙 - -- `[분류] 작업 제목` -- EX: `[feat] 로그인 쿠키 설정` - ---- - -## 3. 지라 티켓 - -- 레이블은 기존과 동일하게 사용 -- 티켓 네이밍 규칙: `[BE] 분류 : 작업 제목` -- EX: `[BE] feat : 로그인 쿠키 설정` - ---- - -## 4. 브랜치 전략 - -### 네이밍 규칙 - -- 형식: `{분류}#{이슈 번호}` -- EX: `feat#19` (19번 이슈에서 파생된 브랜치) - -### 전략 - -- **main** : 메인 서버 자동 배포, 고정 브랜치 -- **develop** : 내부 테스트 서버 자동 배포, 고정 브랜치 -- **feature** : 각 기능 개발마다 생성/삭제 -- **hotfix** : 긴급 수정 시 생성/삭제 - -### 지라 연동 - -- 형식: `분류/지라 디폴트 생성 브랜치명` -- EX: `feat/OPS-87-FE-필터링` - ---- - -## 5. 커밋 메시지 규칙 - -- 분류 - - `design` : UI - - `fix` : 버그 수정 - - `feat` : 기능 추가 - - `refactor` : 리팩토링 - - `chore` : 문서, 환경 설정 - - `docs` : 주석, 문서화 처리 - - `new` : 새로운 파일 생성 - -- 복수 성격인 경우 핵심 키워드 하나만 사용 -- 지라 연동: `분류/지라 티켓 키 : 내용` -- EX: `feat/PROJ-123 : implement login service` - ---- - -## 6. PR 템플릿 - -### 네이밍 - -- PR 이름: 이슈 이름 -- EX: `[feat] 로그인 쿠키 설정` - -### 코드 리뷰 - -- **develop**: 페어 개발자 검토 (부재 시 팀장 대행), 자동 CI -- **main**: 팀장 주도 확인, 자동 CI/CD -- 지라 연동: `[분류/지라 티켓 키] 이슈 이름` - 예: `[feat/PROJ-123] 로그인 쿠키 설정` - ---- - -## 7. 티켓 상태 관리 - -- `Backlog` : 시작 전 -- `Ready` : 작업자 지정, 시작 가능 -- `In progress` : 개발 중 -- `In review` : PR 작성 및 검토 중 -- `Done` : 완료 - -### 작업 순서 - -1. Issue 생성 → 상태: `Backlog`, Labels/Projects 설정 -2. Assignee 지정 → 상태 변경: `Ready` -3. 개발 시작 → 상태: `In progress`, 브랜치 생성 -4. 브랜치에서 작업 진행 -5. PR 생성 → 상태: `In review` -6. PR 머지 후 → Issue & PR `Done`, 브랜치 삭제 - ---- - -## 8. 폴더 구조 규칙 - -```text -com -└── back - ├── domain - │ ├── member - │ └── team - │ ├── repository - │ ├── service - │ ├── controller - │ ├── entity - │ └── dto - └── global -``` -- domain 하위 depth는 1 유지 (필요 시 리팩토링) - ---- - -## 9. DTO 규칙 -### 규칙 -- Controller 단에서 request/response body와 매칭되는 경우 DTO 사용 -- Response body는 최소 정보 전달 (id 등 내부 키값은 숨김) -- Service 단에서는 DTO 재활용 지양 -- Controller DTO와 별도 정의 -- 필요한 경우 주석 충실히 작성 - -### 네이밍 -- reqBodyFor~ : Request Body DTO -- resBodyFor~ : Response Body DTO - ---- - -## 10. 테스트 코드 컨벤션 - -- 형식: given-when-then - - - Controller 단에서 단위 테스트 작성 - - Service 핵심 메서드 단위 테스트 작성 - - 예외 케이스, 엣지 케이스 포함 - ---- - -## 11. 예외 처리 방식 - -- 에러 코드: 개발 진행 중 판단 - -- 핵심 에러는 global 핸들러 사용 - ---- - -## 12. 보안 처리 - -- Spring Security 사용 - -- Secrets 값 관리 - -- 개발 환경: application-secret.yml (gitignore) - -- 운영 환경: Github Secrets, CI/CD에서 컨테이너 환경 변수로 입력 - ---- - -## 13. 문서화 컨벤션 - -- OpenAPI: Swagger 사용 - -- Controller, DTO, Entity에 API 어노테이션 충실 - -- 주석 처리: Javadoc 스타일 - -```java -/** - * @param aa - * @param bb - * @param cc - */ -``` - ---- - -## 14. HTTP 응답 양식 -```json -{ - "status": 200, - "msg": "사용자 정보를 조회했습니다.", - "data": { - "name": "$name", - "profileUrl": "$profileUrl" - } -} -``` -- 성공/실패 관계 없이 RsData 형태로 반환 diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index e7568269..00000000 --- a/Dockerfile +++ /dev/null @@ -1,6 +0,0 @@ -FROM openjdk:21-jdk-slim -COPY build/libs/backend-0.0.1-SNAPSHOT.jar /app.jar - -ENV SPRING_PROFILES_ACTIVE=server - -ENTRYPOINT ["java","-jar","/app.jar"] \ No newline at end of file diff --git a/README.md b/README.md index 2635cb28..89e78abf 100644 --- a/README.md +++ b/README.md @@ -1,134 +1,2 @@ - WEB6_8_ZOOPZOOP_BE +# WEB6_8_ZOOPZOOP_BE 사용자 맞춤형 자료 추천과 시각화 아카이빙을 결합한 협업 플랫폼 - 서버 파트 - ---- - -## 목차 -1. [소개](#소개) -2. [핵심 기능](#핵심-기능) -3. [기술 스택](#기술-스택) -4. [프로젝트 구조](#프로젝트-구조) -5. [설치 및 실행](#설치-및-실행) -6. [추가 자료](#추가-자료) - ---- - -## 💡소개 -본 프로젝트는 웹 서핑 중 발견한 정보를 **효율적으로 수집, 요약, 정리**하고, 이를 팀 단위로 **공유 및 브레인스토밍**까지 이어갈 수 있는 **지식 관리 및 협업 플랫폼**을 목표로 합니다. - -**홈페이지:** [ZoopZoop](https://www.zoopzoop.kro.kr/) - -FE Repository: [Link](https://github.com/prgrms-web-devcourse-final-project/WEB5_6_ZOOPZOOP_FE) - -Chrome Extension Repository: [Link](https://github.com/prgrms-web-devcourse-final-project/WEB5_6_ZOOPS_TENSION_FE) - ---- - -## 📄핵심 기능 - -#### A. 정보 수집 -- **크롬 확장자(Extension) 제공** - - 웹 페이지에서 원하는 부분 선택 후 저장 - - 제목, 본문 요약, URL, 태그, 썸네일을 카드뷰 형태로 개인 아카이브에 저장 -- **직접 URL 저장** - - 사용자가 원하는 웹 페이지 URL을 개인 아카이브에 직접 추가 가능 - -#### B. 개인 아카이브 -- **카테고리 분류** - - 폴더 생성 후 데이터를 카테고리별로 분류 가능 -- **대시보드** - - 수집한 정보 카드 형태로 한눈에 확인 - -#### C. 협업 기능 (공유 아카이브) -- 팀원 초대 → 동일한 대시보드 공유 -- 팀원이 공유한 데이터에 댓글 작성 가능 → 브레인스토밍 지원 -- 여러 카드 배열/연결 → **지식 맵(마인드맵) 형태 구성** -- 공통 수정 및 실시간 동기화 (Liveblocks 사용) -- **AI 추천 기능** → 공유된 URL 태그 기반 맞춤형 뉴스 추천 - -#### D. AI 기반 맞춤형 뉴스 추천 -- 뉴스 API 연동 → 특정 키워드 관련 최신 뉴스 수집 -- **오늘의 뉴스 추천** -- 개인 & 공유 아카이브 내용 기반 **맞춤형 뉴스 추천** - ---- - -## 🔧기술 스택 -- **Framework & Language:** Spring Boot 3.5.5, Java 21 -- **Database:** MySQL (production), H2 (development) -- **Security & Auth:** Spring Security, OAuth2, JWT -- **AI:** Spring AI, Groq -- **Testing:** JUnit, Mockito -- **Deployment:** Docker -- **Utilities & External Services:** - - Messaging: RabbitMQ - - Caching & TTL: Redis - - Documentation: Swagger - - Monitoring: Sentry - - Crawling: Jsoup - - Storage: AWS S3 - - Search Engine: Elastic Search - ---- - -## 📁프로젝트 구조 -``` -src/main/java/org/tuna/zoopzoop/backend -├── domain -│ └── archive # 아카이브 -│ ├── archive # 아카이브 로직 -│ └── folder # 아카이브 내 폴더 로직 -│ ├── auth # 인증/인가 비즈니스 로직 -│ ├── dashboard # Liveblocks 그래프 데이터 -│ ├── datasource # 자료 데이터 크롤링 및 저장 -│ ├── home # 백엔드 홈 화면 컨트롤러 -│ ├── member # 사용자 -│ ├── news # 뉴스 조회 API -│ ├── space # 스페이스(협업 공간) 관련 -│ ├── archive # 스페이스 공용 아카이브 -│ ├── membership # 스페이스 권한 관리 -│ └── space # 스페이스 비즈니스 로직 -│ └── SSE # SSE 연결 -└── global - ├── aspect # AOP 공통 로직 (로깅/응답 등) - ├── aws # AWS 관련 유틸리티 - ├── clients # 외부 API 클라이언트 - ├── config # 각종 환경 설정(JWT, ElasticSearch, Redis, RabbitMQ 등) - ├── exception # 글로벌 예외 처리 - ├── headlessBrowser # 크롤링용 헤드리스 브라우저 - ├── initData # 테스트용 초기 데이터 - ├── jpa # 공용 엔티티 - ├── rsData # RsData 응답 객체 - ├── security # Spring Security - ├── springDoc # OpenAPI/Swagger & 문서화 - ├── test # 모니터링 테스트 - └── webMvc # 공통 WebMVC 설정 -``` - ---- - -## 🔌설치 및 실행 - -``` -# 1. 환경 설정 -# application-secrets.yml.template를 참고하여 application-secrets.yml을 작성합니다. - -# 2. 의존성 설치 -./gradlew build - -# 3. RabbitMQ, Elastic Search 컨테이너 실행 -# 별도로 로컬 환경에 Redis가 설치되어 있어야 합니다. -docker compose up -d -docker ps // 정상적으로 실행 중인지 확인. - -# 4. 로컬 서버 실행 -java -jar build/libs/backend-0.0.1-SNAPSHOT.jar - -# 5. 정상 작동 확인 -# API 문서: http://localhost:8080/swagger-ui.html -``` - ---- - -## 📑추가 자료 -[**백엔드 개발 컨벤션**](./DEV_GUIDE.md) diff --git a/build.gradle b/build.gradle index cb6516a7..d6120730 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,6 @@ plugins { id 'java' id 'org.springframework.boot' version '3.5.5' id 'io.spring.dependency-management' version '1.1.7' - id "io.sentry.jvm.gradle" version "5.12.0" } group = 'org.tuna.zoopzoop' @@ -25,14 +24,6 @@ repositories { mavenCentral() } -ext { - springAiVersion = "1.0.0" -} - -test { - useJUnitPlatform() -} - dependencies { // Spring Data JPA implementation 'org.springframework.boot:spring-boot-starter-data-jpa' @@ -40,26 +31,12 @@ dependencies { // Spring Security implementation 'org.springframework.boot:spring-boot-starter-security' - // Spring Oauth2 - implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' - // Spring Web (MVC) implementation 'org.springframework.boot:spring-boot-starter-web' - // WebFlux (소셜 로그인을 위한 비동기 프레임워크) - implementation 'org.springframework.boot:spring-boot-starter-webflux' - // Bean Validation implementation 'org.springframework.boot:spring-boot-starter-validation' - // QueryDSL JPA + APT (Jakarta) - implementation "com.querydsl:querydsl-jpa:5.0.0:jakarta" - annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta" - - // APT가 jakarta 패키지 인식하도록 추가 - annotationProcessor "jakarta.annotation:jakarta.annotation-api" - annotationProcessor "jakarta.persistence:jakarta.persistence-api" - // Lombok (compile only) compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' @@ -82,75 +59,13 @@ dependencies { // OpenAPI / Swagger UI implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.9' - // OpenAPI - nullable support - implementation "org.openapitools:jackson-databind-nullable:0.2.6" - // JWT implementation 'io.jsonwebtoken:jjwt-api:0.12.6' runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' - // Spring AI - implementation ('org.springframework.ai:spring-ai-starter-model-openai'){ - exclude group: 'io.swagger.core.v3', module: 'swagger-annotations' - } - - // 크롤링 - implementation("org.jsoup:jsoup:1.21.2") - - // Spring Boot Actuator - implementation 'org.springframework.boot:spring-boot-starter-actuator' - - // Mysql driver - implementation 'mysql:mysql-connector-java:8.0.33' - - // AWS SDK for S3 - implementation 'io.awspring.cloud:spring-cloud-aws-starter-s3:3.4.0' - - // Playwright for Java - implementation 'com.microsoft.playwright:playwright:1.54.0' - - // Sentry (모니터링 용) - implementation 'io.sentry:sentry-spring-boot-starter-jakarta:8.22.0' - - // Apache Commons Codec - implementation"commons-codec:commons-codec:1.19.0" - - // Redis (Spring starter) - implementation 'org.springframework.boot:spring-boot-starter-data-redis' - - // RabbitMQ (Spring starter) - implementation 'org.springframework.boot:spring-boot-starter-amqp' - testImplementation 'org.springframework.amqp:spring-rabbit-test' - - // Awaitility (비동기 테스트 지원) - testImplementation 'org.awaitility:awaitility:4.2.0' - - // Elastic Search - implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch' - - // retry (ai retry용) - implementation 'org.springframework.retry:spring-retry' - implementation 'org.springframework:spring-aspects' -} - -dependencyManagement { - imports { - mavenBom("org.springframework.ai:spring-ai-bom:${property("springAiVersion")}") - } } tasks.named('test') { useJUnitPlatform() } - -sentry { - // Generates a JVM (Java, Kotlin, etc.) source bundle and uploads your source code to Sentry. - // This enables source context, allowing you to see your source - // code as part of your stack traces in Sentry. - includeSourceContext = true - - org = "whitedoggy" - projectName = "zoopzoop-backend" - authToken = System.getenv("SENTRY_AUTH_TOKEN") -} diff --git a/db_dev.trace.db b/db_dev.trace.db new file mode 100644 index 00000000..12a29b94 --- /dev/null +++ b/db_dev.trace.db @@ -0,0 +1,3 @@ +2025-09-17 16:09:58.499216+09:00 jdbc[13]: exception +org.h2.jdbc.JdbcSQLSyntaxErrorException: Table "TSPACE" not found; SQL statement: +SELECT * FROM TSPACE AG [42102-232] diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 981efe7f..00000000 --- a/docker-compose.yml +++ /dev/null @@ -1,31 +0,0 @@ -version: '3.8' -services: - rabbitmq: - image: rabbitmq:3-management - container_name: local-rabbitmq - ports: - - "5672:5672" # AMQP - - "15672:15672" # Management UI - environment: - RABBITMQ_DEFAULT_USER: guest - RABBITMQ_DEFAULT_PASS: guest - restart: unless-stopped - volumes: - - rabbitmq-data:/var/lib/rabbitmq - - elasticsearch: - image: docker.elastic.co/elasticsearch/elasticsearch:8.18.5 - container_name: local-elasticsearch - ports: - - "9200:9200" - environment: - discovery.type: single-node - xpack.security.enabled: "false" - ES_JAVA_OPTS: "-Xms512m -Xmx512m" - restart: unless-stopped - volumes: - - elasticsearch-data:/usr/share/elasticsearch/data - -volumes: - rabbitmq-data: - elasticsearch-data: diff --git a/infra/terraform/.gitignore b/infra/terraform/.gitignore deleted file mode 100644 index 233b2d07..00000000 --- a/infra/terraform/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -.terraform/ -terraform.tfstate -terraform.tfstate.backup -.terraform.tfstate.lock.info -.terraform.lock.hcl -env/*.tfvars -.terraform.tfstate.d/ \ No newline at end of file diff --git a/infra/terraform/main.tf b/infra/terraform/main.tf deleted file mode 100644 index 43e3be14..00000000 --- a/infra/terraform/main.tf +++ /dev/null @@ -1,68 +0,0 @@ -#루트에서 모듈 호출 -terraform { - required_providers { - aws = { - source = "hashicorp/aws" - } - } -} - -provider "aws" { - region = var.region -} - - -module "vpc" { - source = "./modules/vpc" - prefix = var.prefix - region = var.region -} - -module "sg"{ - source = "./modules/sg" - vpc_id = module.vpc.vpc_id - prefix = var.prefix - create_rds = var.create_rds -} - -module "iam"{ - source = "./modules/iam" - prefix = var.prefix -} - -module "ec2" { - source = "./modules/ec2" - ami = var.ami - ec2_instance_type = var.ec2_instance_type - subnet_id = module.vpc.subnet_ids[0] - ec2_sg_id = module.sg.ec2_sg_id - iam_instance_profile = module.iam.instance_profile_name - key_name = var.key_name - prefix = var.prefix - test_mysql_root_password = var.test_mysql_root_password - test_mysql_db_name = var.test_mysql_db_name - create_rds = var.create_rds -} - -module "rds" { - source = "./modules/rds" - - count = var.create_rds ? 1 : 0 - - identifier = var.identifier - engine = var.engine - engine_version = var.engine_version - rds_instance_class = var.rds_instance_type - allocated_storage = var.allocated_storage - storage_type = var.storage_type - prod_mysql_db_username = var.prod_mysql_db_username - prod_mysql_root_password = var.prod_mysql_root_password - prod_mysql_db_name = var.prod_mysql_db_name - vpc_security_group_ids = [module.sg.rds_sg_id] - private_subnet_ids = module.vpc.private_subnet_ids - multi_az = var.multi_az - skip_final_snapshot = var.skip_final_snapshot - tags = { - Name = "${var.prefix}-rds" - } -} diff --git a/infra/terraform/modules/ec2/main.tf b/infra/terraform/modules/ec2/main.tf deleted file mode 100644 index 000697c5..00000000 --- a/infra/terraform/modules/ec2/main.tf +++ /dev/null @@ -1,61 +0,0 @@ -locals { - mysql_user_data= var.create_rds?"": <<-END1 - # MySQL 컨테이너 실행 (테스트 환경일 때만) -docker run -d --name mysql \ - --restart unless-stopped \ - --network common \ - -p 3306:3306 \ - -e MYSQL_ROOT_PASSWORD=${var.test_mysql_root_password} \ - -e MYSQL_DATABASE=${var.test_mysql_db_name} \ - -v /opt/mysql/data:/var/lib/mysql \ - mysql:latest -END1 - - user_data = <<-END2 -#!/bin/bash -# Swap 설정 -dd if=/dev/zero of=/swapfile bs=128M count=32 -chmod 600 /swapfile -mkswap /swapfile -swapon /swapfile -echo "/swapfile swap swap defaults 0 0" >> /etc/fstab - -# Docker 설치 -yum install -y docker -systemctl enable docker -systemctl start docker -docker network create common - -# Nginx, Redis, MySQL 컨테이너 실행 (테스트용) -docker run -d --name npm \ - --restart unless-stopped \ - --network common \ - -p 80:80 -p 443:443 -p 81:81 \ - -v /opt/npm/data:/data \ - -v /opt/npm/letsencrypt:/etc/letsencrypt \ - jc21/nginx-proxy-manager:latest -# docker run -d --name redis_1 --restart unless-stopped --network common -p 6379:6379 -e TZ=Asia/Seoul redis - -${local.mysql_user_data} -END2 -} - -resource "aws_instance" "this" { - ami = var.ami - instance_type = var.ec2_instance_type - subnet_id = var.subnet_id - vpc_security_group_ids = [var.ec2_sg_id] - iam_instance_profile = var.iam_instance_profile - associate_public_ip_address = true - key_name = var.key_name - - root_block_device { - volume_type = "gp3" - volume_size = 25 - tags = { Name = "${var.prefix}-ebs" } - } - - user_data = local.user_data - - tags = { Name = "${var.prefix}-ec2" } -} diff --git a/infra/terraform/modules/ec2/outputs.tf b/infra/terraform/modules/ec2/outputs.tf deleted file mode 100644 index b7c05684..00000000 --- a/infra/terraform/modules/ec2/outputs.tf +++ /dev/null @@ -1,3 +0,0 @@ -output "ec2_public_ip" { - value = aws_instance.this.public_ip -} diff --git a/infra/terraform/modules/ec2/variables.tf b/infra/terraform/modules/ec2/variables.tf deleted file mode 100644 index 9980bedb..00000000 --- a/infra/terraform/modules/ec2/variables.tf +++ /dev/null @@ -1,20 +0,0 @@ -variable "ami" { type = string } -variable "ec2_instance_type" { type = string } -variable "subnet_id" { type = string } -variable "ec2_sg_id" { type = string } -variable "iam_instance_profile" { type = string } -variable "key_name" { type = string } -variable "prefix" { type = string } -# variable "redis_password" { type = string } -variable "test_mysql_root_password" { - type = string - default = null -} -variable "test_mysql_db_name" { - type=string - default = null -} -variable "create_rds" { - type = bool - default = false -} \ No newline at end of file diff --git a/infra/terraform/modules/iam/main.tf b/infra/terraform/modules/iam/main.tf deleted file mode 100644 index 202b41d5..00000000 --- a/infra/terraform/modules/iam/main.tf +++ /dev/null @@ -1,31 +0,0 @@ -# EC2 역할 생성 -resource "aws_iam_role" "ec2_role" { - name = "${var.prefix}-ec2-role" - - assume_role_policy = jsonencode({ - Version = "2012-10-17" - Statement = [{ - Effect = "Allow" - Principal = { Service = "ec2.amazonaws.com" } - Action = "sts:AssumeRole" - }] - }) -} - -# 역할에 S3 접근 정책 부착 (사용하지 않을 경우 주석 처리) -resource "aws_iam_role_policy_attachment" "s3_full" { - role = aws_iam_role.ec2_role.name - policy_arn = "arn:aws:iam::aws:policy/AmazonS3FullAccess" -} - -# 역할에 SSM 접근 정책 부착 (AWS Systems Manager) -resource "aws_iam_role_policy_attachment" "ssm" { - role = aws_iam_role.ec2_role.name - policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonEC2RoleforSSM" -} - -# EC2에서 역할을 사용할 수 있게 인스턴스 프로파일 생성 -resource "aws_iam_instance_profile" "this" { - name = "${var.prefix}-instance-profile" - role = aws_iam_role.ec2_role.name -} diff --git a/infra/terraform/modules/iam/outputs.tf b/infra/terraform/modules/iam/outputs.tf deleted file mode 100644 index 87ca677d..00000000 --- a/infra/terraform/modules/iam/outputs.tf +++ /dev/null @@ -1,3 +0,0 @@ -output "instance_profile_name" { - value = aws_iam_instance_profile.this.name -} diff --git a/infra/terraform/modules/iam/variables.tf b/infra/terraform/modules/iam/variables.tf deleted file mode 100644 index c9bd54cb..00000000 --- a/infra/terraform/modules/iam/variables.tf +++ /dev/null @@ -1 +0,0 @@ -variable "prefix" {type=string} \ No newline at end of file diff --git a/infra/terraform/modules/rds/main.tf b/infra/terraform/modules/rds/main.tf deleted file mode 100644 index 21aa8314..00000000 --- a/infra/terraform/modules/rds/main.tf +++ /dev/null @@ -1,22 +0,0 @@ -resource "aws_db_instance" "this" { - identifier = var.identifier - engine = var.engine - engine_version = var.engine_version - instance_class = var.rds_instance_class - allocated_storage = var.allocated_storage - storage_type = var.storage_type - username = var.prod_mysql_db_username - password = var.prod_mysql_root_password - db_name = var.prod_mysql_db_name - vpc_security_group_ids = var.vpc_security_group_ids - db_subnet_group_name = aws_db_subnet_group.this.name - skip_final_snapshot = var.skip_final_snapshot - multi_az = var.multi_az - tags = var.tags -} - -resource "aws_db_subnet_group" "this" { - name = "${var.identifier}-db-subnet-group" - subnet_ids = var.private_subnet_ids - tags = var.tags -} \ No newline at end of file diff --git a/infra/terraform/modules/rds/outputs.tf b/infra/terraform/modules/rds/outputs.tf deleted file mode 100644 index e1166aa2..00000000 --- a/infra/terraform/modules/rds/outputs.tf +++ /dev/null @@ -1,7 +0,0 @@ -output "endpoint"{ - value = aws_db_instance.this.endpoint -} - -output "arn"{ - value = aws_db_instance.this.arn -} \ No newline at end of file diff --git a/infra/terraform/modules/rds/variables.tf b/infra/terraform/modules/rds/variables.tf deleted file mode 100644 index 83584a94..00000000 --- a/infra/terraform/modules/rds/variables.tf +++ /dev/null @@ -1,14 +0,0 @@ -variable "identifier" {} -variable "engine" {} -variable "engine_version" {} -variable "rds_instance_class" {} -variable "allocated_storage" {} -variable "storage_type" {} -variable "prod_mysql_db_username" {} -variable "prod_mysql_root_password" {} -variable "prod_mysql_db_name" {} -variable "vpc_security_group_ids" { type = list(string) } -variable "private_subnet_ids" { type = list(string) } -variable "multi_az" { type = bool } -variable "tags" { type = map(string) } -variable "skip_final_snapshot" { type = bool } \ No newline at end of file diff --git a/infra/terraform/modules/sg/ec2_sg.tf b/infra/terraform/modules/sg/ec2_sg.tf deleted file mode 100644 index 262b3b81..00000000 --- a/infra/terraform/modules/sg/ec2_sg.tf +++ /dev/null @@ -1,64 +0,0 @@ -resource "aws_security_group" "ec2_sg" { - name = "${var.prefix}-ec2-sg" - vpc_id = var.vpc_id - - description = "EC2 security group" - - ingress { - description = "Allow HTTP" - from_port = 80 - to_port = 80 - protocol = "tcp" - cidr_blocks = ["0.0.0.0/0"] - } - - ingress { - description = "Allow HTTPS" - from_port = 443 - to_port = 443 - protocol = "tcp" - cidr_blocks = ["0.0.0.0/0"] - } - - ingress { - description = "Allow SSH" - from_port = 22 - to_port = 22 - protocol = "tcp" - cidr_blocks = ["221.154.167.13/32"] - } - - ingress{ - from_port = 81 - to_port = 81 - protocol = "tcp" - cidr_blocks = ["221.154.167.13/32"] - } - - # 개발시에만 열어놓고, 운영시에는 닫기 - ingress{ - from_port = 8080 - to_port = 8080 - protocol = "tcp" - cidr_blocks = ["221.154.167.13/32"] - } - - # 개발시에만 열어놓고, 운영시에는 닫기 - ingress{ - from_port = 3306 - to_port = 3306 - protocol = "tcp" - cidr_blocks = ["221.154.167.13/32"] - } - - egress { - from_port = 0 - to_port = 0 - protocol = "-1" - cidr_blocks = ["0.0.0.0/0"] - } - - tags = { - Name = "${var.prefix}-ec2-sg" - } -} diff --git a/infra/terraform/modules/sg/outputs.tf b/infra/terraform/modules/sg/outputs.tf deleted file mode 100644 index c598e120..00000000 --- a/infra/terraform/modules/sg/outputs.tf +++ /dev/null @@ -1,7 +0,0 @@ -output "ec2_sg_id" { - value = aws_security_group.ec2_sg.id -} - -output "rds_sg_id" { - value = var.create_rds?aws_security_group.rds_sg[0].id:null -} \ No newline at end of file diff --git a/infra/terraform/modules/sg/rds_sg.tf b/infra/terraform/modules/sg/rds_sg.tf deleted file mode 100644 index b8b940b1..00000000 --- a/infra/terraform/modules/sg/rds_sg.tf +++ /dev/null @@ -1,28 +0,0 @@ -resource "aws_security_group" "rds_sg" { - count = var.create_rds ? 1 : 0 - - name = "${var.prefix}-rds-sg" - vpc_id = var.vpc_id - - description = "RDS security group" - - # EC2에서 RDS 접속 허용 - ingress { - description = "Allow MySQL from EC2" - from_port = 3306 - to_port = 3306 - protocol = "tcp" - security_groups = [aws_security_group.ec2_sg.id] - } - - egress { - from_port = 0 - to_port = 0 - protocol = "-1" - cidr_blocks = ["0.0.0.0/0"] - } - - tags = { - Name = "${var.prefix}-rds-sg" - } -} diff --git a/infra/terraform/modules/sg/variables.tf b/infra/terraform/modules/sg/variables.tf deleted file mode 100644 index 97aeb59a..00000000 --- a/infra/terraform/modules/sg/variables.tf +++ /dev/null @@ -1,3 +0,0 @@ -variable "prefix" {type=string} -variable "vpc_id" {type=string} -variable "create_rds" {type = string} \ No newline at end of file diff --git a/infra/terraform/modules/vpc/main.tf b/infra/terraform/modules/vpc/main.tf deleted file mode 100644 index e210cb48..00000000 --- a/infra/terraform/modules/vpc/main.tf +++ /dev/null @@ -1,71 +0,0 @@ - -resource "aws_vpc" "this"{ - cidr_block = "10.0.0.0/16" - enable_dns_support = true - enable_dns_hostnames = true - - tags = { - Name = "${var.prefix}-vpc" - } -} - -resource "aws_subnet" "public"{ - vpc_id = aws_vpc.this.id - cidr_block = "10.0.1.0/24" - availability_zone = "${var.region}a" - map_public_ip_on_launch = true - tags = {Name = "${var.prefix}-subnet-public"} -} - -resource "aws_subnet" "private"{ - vpc_id = aws_vpc.this.id - cidr_block = "10.0.2.0/24" - availability_zone = "${var.region}b" - map_public_ip_on_launch = false - tags = {Name = "${var.prefix}-subnet-private"} -} - -resource "aws_subnet" "private2"{ - vpc_id = aws_vpc.this.id - cidr_block = "10.0.3.0/24" - availability_zone = "${var.region}c" - map_public_ip_on_launch = false - tags = {Name = "${var.prefix}-subnet-private2"} -} - -resource "aws_internet_gateway" "this"{ - vpc_id = aws_vpc.this.id - - tags = { - Name = "${var.prefix}-igw" - } -} - -resource "aws_route_table" "this" { - vpc_id = aws_vpc.this.id - - route{ - cidr_block = "0.0.0.0/0" - gateway_id = aws_internet_gateway.this.id - } - - tags = { - Name = "${var.prefix}-public-rt" - } -} - -resource "aws_route_table_association" "public" { - subnet_id = aws_subnet.public.id - route_table_id = aws_route_table.this.id -} - -# 고가용성 구성이 필요할때 -# resource "aws_route_table_association" "c" { -# subnet_id = aws_subnet.c.id -# route_table_id = aws_route_table.this.id -# } -# -# resource "aws_route_table_association" "d" { -# subnet_id = aws_subnet.d.id -# route_table_id = aws_route_table.this.id -# } diff --git a/infra/terraform/modules/vpc/outputs.tf b/infra/terraform/modules/vpc/outputs.tf deleted file mode 100644 index a30b9948..00000000 --- a/infra/terraform/modules/vpc/outputs.tf +++ /dev/null @@ -1,18 +0,0 @@ -output "vpc_id" { - value = aws_vpc.this.id -} - -output "subnet_ids" { - value = [ - aws_subnet.public.id, - aws_subnet.private.id, - aws_subnet.private2.id - ] -} - -output "private_subnet_ids"{ - value = [ - aws_subnet.private.id, - aws_subnet.private2.id - ] -} \ No newline at end of file diff --git a/infra/terraform/modules/vpc/variables.tf b/infra/terraform/modules/vpc/variables.tf deleted file mode 100644 index 04c7959d..00000000 --- a/infra/terraform/modules/vpc/variables.tf +++ /dev/null @@ -1,2 +0,0 @@ -variable "prefix" {type=string} -variable "region" {type=string} \ No newline at end of file diff --git a/infra/terraform/outputs.tf b/infra/terraform/outputs.tf deleted file mode 100644 index 9a446954..00000000 --- a/infra/terraform/outputs.tf +++ /dev/null @@ -1,3 +0,0 @@ -output "ec2_public_ip" { - value = module.ec2.ec2_public_ip -} diff --git a/infra/terraform/variables.tf b/infra/terraform/variables.tf deleted file mode 100644 index 061aa598..00000000 --- a/infra/terraform/variables.tf +++ /dev/null @@ -1,68 +0,0 @@ -#공통 변수 정의 - -#EC2 -variable "region" { type = string } -variable "prefix" { type = string } -variable "ami" { type = string } -variable "ec2_instance_type" { type = string } -variable "key_name" { type = string } -#variable "redis_password" { type = string } -variable "test_mysql_root_password" { - type = string - default = null -} -variable "test_mysql_db_name" { - type = string - default = null -} - - -#RDS -variable "create_rds" { - description = "RDS 생성 여부" - type = bool -} -variable "identifier" { - type = string - default = null -} -variable "engine" { - type = string - default = null -} -variable "engine_version" { - type = string - default = null -} -variable "rds_instance_type" { - type = string - default = null -} -variable "allocated_storage" { - type = number - default = null -} -variable "storage_type" { - type = string - default = null -} -variable "multi_az" { - type = bool - default = null -} -variable "prod_mysql_db_username" { - type = string - default = null -} -variable "prod_mysql_root_password" { - type = string - default = null -} -variable "prod_mysql_db_name" { - type = string - default = null -} -variable "skip_final_snapshot" { - type = string - default = null -} \ No newline at end of file diff --git a/src/main/java/org/tuna/zoopzoop/backend/BackendApplication.java b/src/main/java/org/tuna/zoopzoop/backend/BackendApplication.java index a4896d5d..ba492291 100644 --- a/src/main/java/org/tuna/zoopzoop/backend/BackendApplication.java +++ b/src/main/java/org/tuna/zoopzoop/backend/BackendApplication.java @@ -2,16 +2,12 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.data.jpa.repository.config.EnableJpaAuditing; -import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication -@EnableJpaAuditing -@EnableScheduling public class BackendApplication { + public static void main(String[] args) { SpringApplication.run(BackendApplication.class, args); } - } diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/SSE/controller/ApiV1NotificationController.java b/src/main/java/org/tuna/zoopzoop/backend/domain/SSE/controller/ApiV1NotificationController.java deleted file mode 100644 index 06528c19..00000000 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/SSE/controller/ApiV1NotificationController.java +++ /dev/null @@ -1,34 +0,0 @@ -package org.tuna.zoopzoop.backend.domain.SSE.controller; - -import lombok.RequiredArgsConstructor; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; -import org.tuna.zoopzoop.backend.domain.SSE.service.EmitterService; -import org.tuna.zoopzoop.backend.global.security.jwt.CustomUserDetails; - -@RestController -@RequestMapping("/api/v1/notifications") -@RequiredArgsConstructor -public class ApiV1NotificationController { - private final EmitterService emitterService; - - /** - * SSE 구독 엔드포인트 - * @param userDetails - 현재 인증된 사용자 정보 - * @return SseEmitter - 클라이언트와의 SSE 연결을 관리하는 객체 - */ - @GetMapping(value = "/subscribe", produces = "text/event-stream") - public SseEmitter subscribe( - @AuthenticationPrincipal CustomUserDetails userDetails - ) { - // 1. 현재 로그인한 사용자의 ID를 가져옴 - Long memberId = (long) userDetails.getMember().getId(); - - // 2. EmitterService를 통해 Emitter를 생성하고 반환 - return emitterService.addEmitter(memberId); - } -} diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/SSE/service/EmitterService.java b/src/main/java/org/tuna/zoopzoop/backend/domain/SSE/service/EmitterService.java deleted file mode 100644 index a5ef4f2c..00000000 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/SSE/service/EmitterService.java +++ /dev/null @@ -1,75 +0,0 @@ -package org.tuna.zoopzoop.backend.domain.SSE.service; - -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Service; -import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; - -import java.io.IOException; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -@Service -public class EmitterService { - // 1. 모든 Emitter를 저장하는 ConcurrentHashMap - private final Map emitters = new ConcurrentHashMap<>(); - - /** - * 새로운 Emitter 생성 및 저장 - * @param memberId - 사용자 ID - * @return SseEmitter - 생성된 Emitter 객체 - */ - public SseEmitter addEmitter(Long memberId) { - // 1시간 타임아웃 설정 - SseEmitter emitter = new SseEmitter(3600L * 1000); - this.emitters.put(memberId, emitter); - - // Emitter 완료 또는 타임아웃 시 Map에서 삭제 - emitter.onCompletion(() -> this.emitters.remove(memberId)); - emitter.onTimeout(() -> this.emitters.remove(memberId)); - - // 503 에러 방지를 위한 더미 이벤트 전송 - try { - emitter.send(SseEmitter.event().name("connect").data("SSE connected!")); - } catch (IOException e) { - // 예외 처리 - } - - return emitter; - } - - /** - * 특정 사용자에게 이벤트 전송 - * @param memberId - 사용자 ID - * @param eventName - 이벤트 이름 - * @param data - 전송할 데이터 객체 - */ - public void sendNotification(Long memberId, String eventName, Object data) { - SseEmitter emitter = this.emitters.get(memberId); - if (emitter != null) { - try { - // data 객체를 JSON 문자열로 변환하여 전송해야 함 (Controller에서는 자동 변환) - emitter.send(SseEmitter.event().name(eventName).data(data)); - } catch (IOException e) { - this.emitters.remove(memberId); - } - } - } - - /** - * 20초마다 모든 Emitter에 하트비트 전송 - * 클라이언트와의 연결 유지를 위해 주기적으로 빈 이벤트를 전송 - */ - @Scheduled(fixedRate = 20000) - public void sendHeartbeat() { - // 모든 Emitter에 하트비트 전송 - emitters.forEach((userId, emitter) -> { - try { - // SSE 주석(comment)을 사용하여 클라이언트에서 별도 이벤트를 발생시키지 않음 - emitter.send(SseEmitter.event().comment("keep-alive")); - } catch (IOException e) { - // 전송 실패 시, 클라이언트 연결이 끊어진 것으로 간주하고 Map에서 제거 - emitters.remove(userId); - } - }); - } -} diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/archive/archive/entity/Archive.java b/src/main/java/org/tuna/zoopzoop/backend/domain/archive/archive/entity/Archive.java index f68480f8..51049ee9 100644 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/archive/archive/entity/Archive.java +++ b/src/main/java/org/tuna/zoopzoop/backend/domain/archive/archive/entity/Archive.java @@ -5,43 +5,19 @@ import lombok.NoArgsConstructor; import lombok.Setter; import org.tuna.zoopzoop.backend.domain.archive.archive.enums.ArchiveType; -import org.tuna.zoopzoop.backend.domain.archive.folder.entity.Folder; import org.tuna.zoopzoop.backend.global.jpa.entity.BaseEntity; -import java.util.ArrayList; -import java.util.List; - @Getter @Setter @Entity @NoArgsConstructor +@Inheritance(strategy = InheritanceType.JOINED) public class Archive extends BaseEntity { - // Personal / Shared 생성 후 불변 - @Column(nullable = false, updatable = false) + @Column @Enumerated(EnumType.STRING) private ArchiveType archiveType; - //아카이브 삭제(아마도 계정 탈퇴) 시 폴더 일괄 삭제 - @OneToMany(mappedBy = "archive", cascade = CascadeType.ALL, orphanRemoval = true) - private List folders = new ArrayList<>(); - public Archive(ArchiveType archiveType) { this.archiveType = archiveType; } - - public void addFolder(Folder folder) { - if (!this.folders.contains(folder)) { - this.folders.add(folder); - } - if (folder.getArchive() != this) { - folder.setArchive(this); - } - } - - public void removeFolder(Folder folder) { - this.folders.remove(folder); - if (folder.getArchive() == this) { - folder.setArchive(null); - } - } } diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/archive/archive/entity/PersonalArchive.java b/src/main/java/org/tuna/zoopzoop/backend/domain/archive/archive/entity/PersonalArchive.java index 4cd90b9c..69ad4ff6 100644 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/archive/archive/entity/PersonalArchive.java +++ b/src/main/java/org/tuna/zoopzoop/backend/domain/archive/archive/entity/PersonalArchive.java @@ -1,11 +1,13 @@ package org.tuna.zoopzoop.backend.domain.archive.archive.entity; -import jakarta.persistence.*; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToOne; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import org.tuna.zoopzoop.backend.domain.archive.archive.enums.ArchiveType; -import org.tuna.zoopzoop.backend.domain.archive.folder.entity.Folder; import org.tuna.zoopzoop.backend.domain.member.entity.Member; import org.tuna.zoopzoop.backend.global.jpa.entity.BaseEntity; @@ -13,35 +15,17 @@ @Setter @Entity @NoArgsConstructor -@Table( - uniqueConstraints = { - // Archive가 하나의 PersonalArchive에 연결됨 - @UniqueConstraint( - name = "uk_personal_archive__archive_id", - columnNames = "archive_id" - ), - // Member가 하나의 PersonalArchive만 가짐 - @UniqueConstraint( - name = "uk_personal_archive__member_id", - columnNames = "member_id" - ) - } -) public class PersonalArchive extends BaseEntity { - @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, optional = false) - @JoinColumn(name = "archive_id") + @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true) + @JoinColumn(name = "archive_id", nullable = false) public Archive archive; - @OneToOne(optional = false, fetch = FetchType.LAZY) - @JoinColumn(name = "member_id") + @OneToOne + @JoinColumn(name = "member_id", nullable = false) private Member member; public PersonalArchive(Member member) { this.member = member; this.archive = new Archive(ArchiveType.PERSONAL); - - // default 폴더 자동 생성 및 연결 - Folder defaultFolder = new Folder("default"); - archive.addFolder(defaultFolder); } } diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/archive/archive/entity/SharingArchive.java b/src/main/java/org/tuna/zoopzoop/backend/domain/archive/archive/entity/SharingArchive.java index 599f4c4d..564107c2 100644 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/archive/archive/entity/SharingArchive.java +++ b/src/main/java/org/tuna/zoopzoop/backend/domain/archive/archive/entity/SharingArchive.java @@ -8,7 +8,6 @@ import lombok.NoArgsConstructor; import lombok.Setter; import org.tuna.zoopzoop.backend.domain.archive.archive.enums.ArchiveType; -import org.tuna.zoopzoop.backend.domain.archive.folder.entity.Folder; import org.tuna.zoopzoop.backend.domain.space.space.entity.Space; import org.tuna.zoopzoop.backend.global.jpa.entity.BaseEntity; @@ -28,9 +27,5 @@ public class SharingArchive extends BaseEntity { public SharingArchive(Space space) { this.space = space; this.archive = new Archive(ArchiveType.SHARED); - - // 🔧 default 폴더 자동 생성 - Folder defaultFolder = new Folder("default"); - this.archive.addFolder(defaultFolder); } } diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/archive/archive/repository/PersonalArchiveRepository.java b/src/main/java/org/tuna/zoopzoop/backend/domain/archive/archive/repository/PersonalArchiveRepository.java deleted file mode 100644 index 127afa95..00000000 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/archive/archive/repository/PersonalArchiveRepository.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.tuna.zoopzoop.backend.domain.archive.archive.repository; - -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; -import org.tuna.zoopzoop.backend.domain.archive.archive.entity.PersonalArchive; - -import java.util.Optional; - -public interface PersonalArchiveRepository extends JpaRepository { - /** - * 회원의 PersonalArchive 조회 - * - * @param memberId 회원 Id - * @return PersonalArchive 엔티티 - */ - @Query(""" - select pa - from PersonalArchive pa - join fetch pa.archive a - where pa.member.id = :memberId -""") - Optional findByMemberId(@Param("memberId") Integer memberId); -} diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/archive/folder/controller/FolderController.java b/src/main/java/org/tuna/zoopzoop/backend/domain/archive/folder/controller/FolderController.java deleted file mode 100644 index 7142e46d..00000000 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/archive/folder/controller/FolderController.java +++ /dev/null @@ -1,129 +0,0 @@ -package org.tuna.zoopzoop.backend.domain.archive.folder.controller; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.*; -import org.tuna.zoopzoop.backend.domain.archive.folder.dto.FolderResponse; -import org.tuna.zoopzoop.backend.domain.archive.folder.dto.reqBodyForCreateFolder; -import org.tuna.zoopzoop.backend.domain.archive.folder.dto.resBodyForCreateFolder; -import org.tuna.zoopzoop.backend.domain.archive.folder.service.PersonalArchiveFolderService; -import org.tuna.zoopzoop.backend.domain.datasource.dto.FolderFilesDto; -import org.tuna.zoopzoop.backend.domain.member.entity.Member; -import org.tuna.zoopzoop.backend.global.rsData.RsData; -import org.tuna.zoopzoop.backend.global.security.jwt.CustomUserDetails; - -import java.util.List; -import java.util.Map; - -@RestController -@RequestMapping("/api/v1/archive/folder") -@RequiredArgsConstructor -@Tag(name = "ApiV1Folder", description = "개인 아카이브의 폴더 CRUD") -public class FolderController { - - private final PersonalArchiveFolderService personalArchiveFolderService; - - /** - * 내 PersonalArchive 안에 새 폴더 생성 - */ - @Operation(summary = "폴더 생성", description = "내 PersonalArchive 안에 새 폴더를 생성합니다.") - @PostMapping - public RsData createFolder( - @Valid @RequestBody reqBodyForCreateFolder rq, - @AuthenticationPrincipal CustomUserDetails userDetails - ) { - Member member = userDetails.getMember(); - FolderResponse createFile = personalArchiveFolderService.createFolder(member.getId(), rq.folderName()); - resBodyForCreateFolder rs = new resBodyForCreateFolder(createFile.folderName(), createFile.folderId()); - - return new RsData<>("200",rq.folderName() + " 폴더가 생성됐습니다.", rs); - } - - /** - * 내 PersonalArchive 안의 folder 삭제 - */ - @DeleteMapping("/{folderId}") - public ResponseEntity> deleteFolder( - @PathVariable Integer folderId, - @AuthenticationPrincipal CustomUserDetails userDetails - ) { - if (folderId == 0) - throw new IllegalArgumentException("default 폴더는 삭제할 수 없습니다."); - - - Member member = userDetails.getMember(); - String deletedFolderName = personalArchiveFolderService.deleteFolder(member.getId(), folderId); - - return ResponseEntity.ok( - new RsData<>("200", deletedFolderName + " 폴더가 삭제됐습니다.", null) - ); - } - - /** - * 폴더 이름 수정 - * @param folderId 수정할 폴더 Id - * @param body 수정할 폴더 값 - */ - @PatchMapping("/{folderId}") - public ResponseEntity>> updateFolderName( - @PathVariable Integer folderId, - @RequestBody Map body, - @AuthenticationPrincipal CustomUserDetails userDetails - ) { - if (folderId == 0) - throw new IllegalArgumentException("default 폴더는 이름을 변경할 수 없습니다."); - - - Member member = userDetails.getMember(); - String newName = body.get("folderName"); - String updatedName = personalArchiveFolderService.updateFolderName(member.getId(), folderId, newName); - - return ResponseEntity.ok( - new RsData<>("200", "폴더 이름이 " + updatedName + " 으로 변경됐습니다.", - Map.of("folderName", updatedName)) - ); - } - - /** - * 개인 아카이브의 폴더 이름 전부 조회 - * "default", "폴더1", "폴더2" - */ - @Operation(summary = "폴더 이름 조회", description = "내 PersonalArchive 안에 이름을 전부 조회합니다.") - @GetMapping - public ResponseEntity>> getFolders( - @AuthenticationPrincipal CustomUserDetails userDetails - ) { - Member member = userDetails.getMember(); - List folders = personalArchiveFolderService.getFolders(member.getId()); - - return ResponseEntity.ok( - new RsData<>("200", "개인 아카이브의 폴더 목록을 불러왔습니다.", folders) - ); - } - - /** - * 폴더 안의 파일 목록 조회 - */ - @GetMapping("/{folderId}/files") - public ResponseEntity getFilesInFolder( - @PathVariable Integer folderId, - @AuthenticationPrincipal CustomUserDetails userDetails - ) { - int memberId = userDetails.getMember().getId(); - - Integer targetFolderId = (folderId == 0) - ? personalArchiveFolderService.getDefaultFolderId(memberId) - : folderId; - - FolderFilesDto rs = personalArchiveFolderService.getFilesInFolder(memberId, targetFolderId); - - return ResponseEntity.ok( - new RsData<>("200","해당 폴더의 파일 목록을 불러왔습니다.", rs) - ); - } - -} diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/archive/folder/dto/FolderResponse.java b/src/main/java/org/tuna/zoopzoop/backend/domain/archive/folder/dto/FolderResponse.java deleted file mode 100644 index 78d38b04..00000000 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/archive/folder/dto/FolderResponse.java +++ /dev/null @@ -1,6 +0,0 @@ -package org.tuna.zoopzoop.backend.domain.archive.folder.dto; - -public record FolderResponse( - String folderName, - int folderId -) {} diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/archive/folder/dto/reqBodyForCreateFolder.java b/src/main/java/org/tuna/zoopzoop/backend/domain/archive/folder/dto/reqBodyForCreateFolder.java deleted file mode 100644 index 73690c42..00000000 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/archive/folder/dto/reqBodyForCreateFolder.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.tuna.zoopzoop.backend.domain.archive.folder.dto; - -import jakarta.validation.constraints.NotBlank; - -public record reqBodyForCreateFolder( - @NotBlank String folderName -) {} diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/archive/folder/dto/resBodyForCreateFolder.java b/src/main/java/org/tuna/zoopzoop/backend/domain/archive/folder/dto/resBodyForCreateFolder.java deleted file mode 100644 index 8288dd65..00000000 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/archive/folder/dto/resBodyForCreateFolder.java +++ /dev/null @@ -1,6 +0,0 @@ -package org.tuna.zoopzoop.backend.domain.archive.folder.dto; - -public record resBodyForCreateFolder( - String folderName, - int folderId -) {} diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/archive/folder/entity/Folder.java b/src/main/java/org/tuna/zoopzoop/backend/domain/archive/folder/entity/Folder.java index c86b1cb4..4bb27d21 100644 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/archive/folder/entity/Folder.java +++ b/src/main/java/org/tuna/zoopzoop/backend/domain/archive/folder/entity/Folder.java @@ -5,33 +5,16 @@ import lombok.NoArgsConstructor; import lombok.Setter; import org.tuna.zoopzoop.backend.domain.archive.archive.entity.Archive; -import org.tuna.zoopzoop.backend.domain.datasource.entity.DataSource; import org.tuna.zoopzoop.backend.global.jpa.entity.BaseEntity; -import java.util.ArrayList; -import java.util.List; - @Getter @Setter @Entity @NoArgsConstructor -@Table( - // 복합 Unique 제약(archive_id, name) - uniqueConstraints = { - @UniqueConstraint( - name = "uk_folder__archive_id__name", - columnNames = {"archive_id", "name"} - ) - }, - // Archive 별 조회 속도 개선 - indexes = { - @Index( name = "idx_folder__archive_id", columnList = "archive_id") - } -) public class Folder extends BaseEntity { //연결된 아카이브 id - @ManyToOne(fetch = FetchType.LAZY, optional = false) - @JoinColumn(name = "archive_id") + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "archive_id", nullable = false) private Archive archive; //폴더 이름 @@ -39,16 +22,6 @@ public class Folder extends BaseEntity { private String name; //디폴트 폴더 여부 - @Column(nullable = false, name = "is_default") + @Column(nullable = false) private boolean isDefault = false; - - // 폴더 삭제 시 데이터 softdelete - @OneToMany(mappedBy = "folder") - private List dataSources = new ArrayList<>(); - - - public Folder(String name) { - this.name = name; - this.isDefault = true; - } } diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/archive/folder/repository/FolderRepository.java b/src/main/java/org/tuna/zoopzoop/backend/domain/archive/folder/repository/FolderRepository.java deleted file mode 100644 index 91b68ca7..00000000 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/archive/folder/repository/FolderRepository.java +++ /dev/null @@ -1,88 +0,0 @@ -package org.tuna.zoopzoop.backend.domain.archive.folder.repository; - -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; -import org.tuna.zoopzoop.backend.domain.archive.archive.entity.Archive; -import org.tuna.zoopzoop.backend.domain.archive.folder.entity.Folder; - -import java.util.List; -import java.util.Optional; - -public interface FolderRepository extends JpaRepository{ - /** - * 폴더 중복명 검사 - * @param archiveId 아카이브 Id - * @param filename "파일명" - * @param filenameEnd "파일명 + \ufffff" - */ - @Query(""" - select f.name - from Folder f - where f.archive.id = :archiveId - and f.name >= :filename - and f.name < :filenameEnd - """) - List findNamesForConflictCheck(@Param("archiveId") Integer archiveId, - @Param("filename") String filename, - @Param("filenameEnd") String filenameEnd); - // 개인 아카이브의 폴더 조회 - List findByArchive(Archive archive); - - /** - * 아카이브 Id로 default 폴더 조회 - * @param archiveId 조회할 archive Id - */ - Optional findByArchiveIdAndIsDefaultTrue(Integer archiveId); - - /** - * 회원 Id로 default 폴더 조회 - * @param memberId 조회할 회원 Id - */ - @Query(""" - select f - from Folder f - join f.archive a - join PersonalArchive pa on pa.archive = a - where pa.member.id = :memberId - and f.isDefault = true - """) - Optional findDefaultFolderByMemberId(@Param("memberId") Integer memberId); - - // 한 번의 조인으로 존재 + 소유권(memberId) 검증 - @Query(""" - select f - from Folder f - join f.archive a - join PersonalArchive pa on pa.archive = a - where f.id = :folderId - and pa.member.id = :memberId - """) - Optional findByIdAndMemberId(@Param("folderId") Integer folderId, - @Param("memberId") Integer memberId); - - Optional findByArchiveIdAndName(Integer archiveId, String name); - - List findAllByArchiveId(Integer archiveId); - - @Query(""" - select f from Folder f - join f.archive a - join PersonalArchive pa on pa.archive.id = a.id - where pa.member.id = :memberId and f.isDefault = true - """) - Optional findDefaultByMemberId(@Param("memberId") Integer memberId); - - Optional findByIdAndArchiveId(Integer folderId, Integer archiveId); - - @Query(""" - select f.name - from Folder f - where f.archive.id = :archiveId - and f.name = :name - and f.id <> :excludeFolderId - """) - List existsNameInArchiveExceptSelf(@Param("archiveId") Integer archiveId, - @Param("name") String name, - @Param("excludeFolderId") Integer excludeFolderId); -} diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/archive/folder/service/FolderService.java b/src/main/java/org/tuna/zoopzoop/backend/domain/archive/folder/service/FolderService.java deleted file mode 100644 index 213aceab..00000000 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/archive/folder/service/FolderService.java +++ /dev/null @@ -1,187 +0,0 @@ -package org.tuna.zoopzoop.backend.domain.archive.folder.service; - -import jakarta.persistence.NoResultException; -import lombok.RequiredArgsConstructor; -import org.springframework.dao.DataIntegrityViolationException; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.tuna.zoopzoop.backend.domain.archive.archive.entity.Archive; -import org.tuna.zoopzoop.backend.domain.archive.folder.dto.FolderResponse; -import org.tuna.zoopzoop.backend.domain.archive.folder.entity.Folder; -import org.tuna.zoopzoop.backend.domain.archive.folder.repository.FolderRepository; -import org.tuna.zoopzoop.backend.domain.datasource.dto.FileSummary; -import org.tuna.zoopzoop.backend.domain.datasource.dto.FolderFilesDto; -import org.tuna.zoopzoop.backend.domain.datasource.entity.DataSource; -import org.tuna.zoopzoop.backend.domain.datasource.entity.Tag; -import org.tuna.zoopzoop.backend.domain.datasource.repository.DataSourceRepository; - -import java.time.LocalDate; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.regex.Pattern; - -@Service -@RequiredArgsConstructor -public class FolderService { - - private final FolderRepository folderRepository; - private final DataSourceRepository dataSourceRepository; - - // ===== 생성 ===== - @Transactional - public FolderResponse createFolder(Archive archive, String folderName) { - if (archive == null) throw new NoResultException("아카이브가 존재하지 않습니다."); - if (folderName == null || folderName.trim().isEmpty()) - throw new IllegalArgumentException("폴더 이름은 비어 있을 수 없습니다."); - - final String requested = folderName.trim(); - String unique = generateUniqueFolderName(archive.getId(), requested); - - for (int attempt = 0; attempt < 2; attempt++) { - try { - Folder folder = new Folder(); - folder.setArchive(archive); - folder.setName(unique); - folder.setDefault(false); - - Folder saved = folderRepository.save(folder); - return new FolderResponse(saved.getName(), saved.getId()); - } catch (DataIntegrityViolationException e) { - unique = generateUniqueFolderName(archive.getId(), requested); - } - } - throw new IllegalStateException("동시성 충돌로 폴더 생성에 실패했습니다. 잠시 후 다시 시도해주세요."); - } - - // ===== 삭제 ===== - @Transactional - public String deleteFolder(Archive archive, Integer folderId) { - Folder folder = folderRepository.findByIdAndArchiveId(folderId, archive.getId()) - .orElseThrow(() -> new NoResultException("존재하지 않는 폴더입니다.")); - - if (folder.isDefault()) - throw new IllegalArgumentException("default 폴더는 삭제할 수 없습니다."); - - // 기본 폴더 확보 (같은 archive) - Folder defaultFolder = folderRepository.findByArchiveIdAndIsDefaultTrue(archive.getId()) - .orElseThrow(() -> new IllegalStateException("default 폴더가 존재하지 않습니다.")); - - // 폴더 내 자료 이관 + soft delete(네 정책 유지) - List dataSources = dataSourceRepository.findAllByFolderId(folderId); - LocalDate now = LocalDate.now(); - for (DataSource ds : dataSources) { - ds.setFolder(defaultFolder); - ds.setActive(false); - ds.setDeletedAt(now); - } - - String name = folder.getName(); - folderRepository.delete(folder); - return name; - } - - // ===== 이름 변경 ===== - @Transactional - public String updateFolderName(Archive archive, Integer folderId, String newName) { - if (newName == null || newName.trim().isEmpty()) - throw new IllegalArgumentException("폴더 이름은 비어 있을 수 없습니다."); - - Folder folder = folderRepository.findByIdAndArchiveId(folderId, archive.getId()) - .orElseThrow(() -> new NoResultException("존재하지 않는 폴더입니다.")); - - if (folder.isDefault()) - throw new IllegalArgumentException("default 폴더는 이름을 변경할 수 없습니다."); - - // 같은 Archive 내 동명 검사 (자기 자신 제외) - List conflict = folderRepository.existsNameInArchiveExceptSelf( - archive.getId(), newName.trim(), folder.getId()); - if (!conflict.isEmpty()) { - throw new IllegalArgumentException("이미 존재하는 폴더명입니다."); - } - - folder.setName(newName.trim()); - folderRepository.save(folder); - return folder.getName(); - } - - // ===== 목록 조회 ===== - @Transactional(readOnly = true) - public List getFolders(Archive archive) { - return folderRepository.findByArchive(archive).stream() - .map(f -> new FolderResponse(f.getName(), f.getId())) - .toList(); - } - - // ===== 폴더 내 파일 조회 ===== - @Transactional(readOnly = true) - public FolderFilesDto getFilesInFolder(Archive archive, Integer folderId) { - Folder folder = folderRepository.findByIdAndArchiveId(folderId, archive.getId()) - .orElseThrow(() -> new NoResultException("존재하지 않는 폴더입니다.")); - - var files = dataSourceRepository.findAllByFolderAndIsActiveTrue(folder).stream() - .map(ds -> new FileSummary( - ds.getId(), - ds.getTitle(), - ds.getDataCreatedDate(), - ds.getSummary(), - ds.getSourceUrl(), - ds.getImageUrl(), - ds.getTags() == null ? List.of() : ds.getTags().stream().map(Tag::getTagName).toList(), - ds.getCategory() == null ? null : ds.getCategory().name() - )) - .toList(); - - return new FolderFilesDto(folder.getId(), folder.getName(), files); - } - - // ===== 기본 폴더 ID 조회 (Archive 스코프) ===== - @Transactional(readOnly = true) - public Integer getDefaultFolderId(Archive archive) { - return folderRepository.findByArchiveIdAndIsDefaultTrue(archive.getId()) - .orElseThrow(() -> new NoResultException("default 폴더를 찾을 수 없습니다.")) - .getId(); - } - - // ===== 이름 충돌 유틸 ===== - private static final Pattern SUFFIX_PATTERN = Pattern.compile("^(.*?)(?: \\((\\d+)\\))?$"); - - private String generateUniqueFolderName(Integer archiveId, String requested) { - NameParts nameParts = NameParts.split(requested); - String file = nameParts.base(); - String fileEnd = file + "\uffff"; - List existing = folderRepository.findNamesForConflictCheck(archiveId, file, fileEnd); - return pickNextAvailable(file, existing); - } - - private static String pickNextAvailable(String file, List existing) { - boolean baseUsed = false; - Set used = new HashSet<>(); - Pattern p = Pattern.compile("^" + Pattern.quote(file) + "(?: \\((\\d+)\\))?$"); - for (String s : existing) { - var m = p.matcher(s); - if (m.matches()) { - if (m.group(1) == null) baseUsed = true; - else used.add(Integer.parseInt(m.group(1))); - } - } - if (!baseUsed) return file; - for (int k = 1; k <= used.size() + 1; k++) { - if (!used.contains(k)) return file + " (" + k + ")"; - } - return file + " (" + (used.size() + 1) + ")"; - } - - private record NameParts(String base, Integer num) { - static NameParts split(String name) { - var m = SUFFIX_PATTERN.matcher(name.trim()); - if (m.matches()) { - String base = m.group(1).trim(); - Integer n = m.group(2) != null ? Integer.valueOf(m.group(2)) : null; - return new NameParts(base, n); - } - return new NameParts(name.trim(), null); - } - } -} - diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/archive/folder/service/PersonalArchiveFolderService.java b/src/main/java/org/tuna/zoopzoop/backend/domain/archive/folder/service/PersonalArchiveFolderService.java deleted file mode 100644 index 73edaa7d..00000000 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/archive/folder/service/PersonalArchiveFolderService.java +++ /dev/null @@ -1,69 +0,0 @@ -package org.tuna.zoopzoop.backend.domain.archive.folder.service; - -import jakarta.persistence.NoResultException; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.tuna.zoopzoop.backend.domain.archive.archive.entity.Archive; -import org.tuna.zoopzoop.backend.domain.archive.archive.entity.PersonalArchive; -import org.tuna.zoopzoop.backend.domain.archive.archive.repository.PersonalArchiveRepository; -import org.tuna.zoopzoop.backend.domain.archive.folder.dto.FolderResponse; -import org.tuna.zoopzoop.backend.domain.archive.folder.entity.Folder; -import org.tuna.zoopzoop.backend.domain.archive.folder.repository.FolderRepository; -import org.tuna.zoopzoop.backend.domain.datasource.dto.FolderFilesDto; - -import java.util.List; - -@Service -@RequiredArgsConstructor -public class PersonalArchiveFolderService { - - private final PersonalArchiveRepository personalArchiveRepository; - private final FolderRepository folderRepository; - private final FolderService folderService; - - @Transactional - public FolderResponse createFolder(Integer memberId, String folderName) { - Archive archive = personalArchiveRepository.findByMemberId(memberId) - .map(PersonalArchive::getArchive) - .orElseThrow(() -> new NoResultException("개인 아카이브가 없습니다.")); - return folderService.createFolder(archive, folderName); - } - - @Transactional - public String deleteFolder(Integer memberId, Integer folderId) { - // 개인 전용 “소유 확인” 쿼리로 빠르게 가드 - Folder folder = folderRepository.findByIdAndMemberId(folderId, memberId) - .orElseThrow(() -> new NoResultException("존재하지 않는 폴더입니다.")); - return folderService.deleteFolder(folder.getArchive(), folderId); - } - - @Transactional - public String updateFolderName(Integer memberId, Integer folderId, String newName) { - Folder folder = folderRepository.findByIdAndMemberId(folderId, memberId) - .orElseThrow(() -> new NoResultException("존재하지 않는 폴더입니다.")); - return folderService.updateFolderName(folder.getArchive(), folderId, newName); - } - - @Transactional(readOnly = true) - public List getFolders(Integer memberId) { - Archive archive = personalArchiveRepository.findByMemberId(memberId) - .map(PersonalArchive::getArchive) - .orElseThrow(() -> new NoResultException("개인 아카이브가 존재하지 않습니다.")); - return folderService.getFolders(archive); - } - - @Transactional(readOnly = true) - public FolderFilesDto getFilesInFolder(Integer memberId, Integer folderId) { - Folder folder = folderRepository.findByIdAndMemberId(folderId, memberId) - .orElseThrow(() -> new NoResultException("존재하지 않는 폴더입니다.")); - return folderService.getFilesInFolder(folder.getArchive(), folderId); - } - - @Transactional(readOnly = true) - public Integer getDefaultFolderId(Integer memberId) { - Folder folder = folderRepository.findDefaultFolderByMemberId(memberId) - .orElseThrow(() -> new NoResultException("default 폴더를 찾을 수 없습니다.")); - return folder.getId(); - } -} diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/auth/controller/ApiV1AuthController.java b/src/main/java/org/tuna/zoopzoop/backend/domain/auth/controller/ApiV1AuthController.java deleted file mode 100644 index 2fae5d32..00000000 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/auth/controller/ApiV1AuthController.java +++ /dev/null @@ -1,155 +0,0 @@ -package org.tuna.zoopzoop.backend.domain.auth.controller; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.servlet.http.HttpServletResponse; -import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseCookie; -import org.springframework.http.ResponseEntity; -import org.springframework.security.core.AuthenticationException; -import org.springframework.web.bind.annotation.*; -import org.tuna.zoopzoop.backend.domain.auth.dto.AuthResultData; -import org.tuna.zoopzoop.backend.domain.auth.entity.AuthResult; -import org.tuna.zoopzoop.backend.domain.auth.entity.RefreshToken; -import org.tuna.zoopzoop.backend.domain.auth.service.refresh.RefreshTokenService; -import org.tuna.zoopzoop.backend.domain.member.entity.Member; -import org.tuna.zoopzoop.backend.global.rsData.RsData; -import org.tuna.zoopzoop.backend.global.security.jwt.JwtUtil; - -@RestController -@RequiredArgsConstructor -@RequestMapping("api/v1/auth") -@Tag(name = "ApiV1AuthController", description = "인증/인가 REST API 컨트롤러") -public class ApiV1AuthController { - private final JwtUtil jwtUtil; - private final RefreshTokenService refreshTokenService; - private final AuthResult authResult; - - /** - * 사용자 로그아웃 API - * @param response Servlet 기반 웹에서 server -> client로 http 응답을 보내기 위한 객체, 자동 주입. - */ - @GetMapping("/logout") - @Operation(summary = "사용자 로그아웃") - public ResponseEntity> logout( - @CookieValue(name = "sessionId") - String sessionId, - HttpServletResponse response) { - - // 서버에서 RefreshToken 삭제 - refreshTokenService.deleteBySessionId(sessionId); - - // 클라이언트 쿠키 삭제 (AccessToken + SessionId) - ResponseCookie accessCookie = ResponseCookie.from("accessToken", "") - .httpOnly(true) - .path("/") - .maxAge(0) - .sameSite("Lax") - .build(); - - ResponseCookie sessionCookie = ResponseCookie.from("sessionId", "") - .httpOnly(true) - .path("/") - .maxAge(0) - .sameSite("Lax") - .build(); - - response.addHeader(HttpHeaders.SET_COOKIE, accessCookie.toString()); - response.addHeader(HttpHeaders.SET_COOKIE, sessionCookie.toString()); - - return ResponseEntity - .status(HttpStatus.OK) - .body(new RsData<>("200", "정상적으로 로그아웃 했습니다.", null)); - } - - /** - * refreshToken 기반으로 accessToken 재발급 - * @param sessionId 쿠키에 포함된 현재 로그인한 사용자의 sessionId. - * @param response Servlet 기반 웹에서 server -> client로 http 응답을 보내기 위한 객체, 자동 주입. - */ - @PostMapping("/refresh") - @Operation(summary = "사용자 액세스 토큰 재발급 (서버 저장 RefreshToken 사용)") - public ResponseEntity> refreshToken( - @CookieValue(name = "sessionId") - String sessionId, - HttpServletResponse response - ) { - if (sessionId == null) { - return ResponseEntity - .status(HttpStatus.UNAUTHORIZED) - .body(new RsData<>("401", "세션이 존재하지 않습니다.", null)); - } - - // sessionId로 RefreshToken 조회 - RefreshToken refreshTokenEntity; - try { - refreshTokenEntity = refreshTokenService.getBySessionId(sessionId); - } catch (AuthenticationException e) { - return ResponseEntity - .status(HttpStatus.UNAUTHORIZED) - .body(new RsData<>( - "401", - e.getMessage(), - null - )); - } - - String refreshToken = refreshTokenEntity.getRefreshToken(); - - // RefreshToken 유효성 검사 - if (!jwtUtil.validateToken(refreshToken) || !jwtUtil.isRefreshToken(refreshToken)) { - return ResponseEntity - .status(HttpStatus.UNAUTHORIZED) - .body(new RsData<>("401", "유효하지 않은 리프레시 토큰입니다.", null)); - } - - Member member = refreshTokenEntity.getMember(); - - // 새 AccessToken 발급 - String newAccessToken = jwtUtil.generateToken(member); - - ResponseCookie accessCookie = ResponseCookie.from("accessToken", newAccessToken) - .httpOnly(true) - .path("/") - .maxAge(jwtUtil.getAccessTokenValiditySeconds()) - .sameSite("Lax") - .build(); - - response.addHeader(HttpHeaders.SET_COOKIE, accessCookie.toString()); - - return ResponseEntity - .status(HttpStatus.OK) - .body(new RsData<>("200", "액세스 토큰을 재발급 했습니다.", null)); - } - - /** - * 확장프로그램의 액세스 토큰 발급을 위한 백그라운드 풀링에 대응하는 API - * @param state 확장프로그램 로그인 시 전달한 state 값. - */ - @GetMapping("/result") - @Operation(summary = "확장프로그램 백그라운드 풀링 대응 API") - public ResponseEntity> pullingResult( - @RequestParam String state - ) { - AuthResultData resultData = authResult.get(state); - if(resultData == null) { - return ResponseEntity - .status(HttpStatus.NOT_FOUND) - .body(new RsData<>( - "404", - "state에 해당하는 토큰이 준비되지 않았거나, 잘못된 state 입니다.", - null - ) - ); - } - return ResponseEntity - .status(HttpStatus.OK) - .body(new RsData<>( - "200", - "토큰이 정상적으로 발급되었습니다.", - resultData - )); - } -} diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/auth/deprecated/KakaoAuthService.java b/src/main/java/org/tuna/zoopzoop/backend/domain/auth/deprecated/KakaoAuthService.java deleted file mode 100644 index 4256c7c7..00000000 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/auth/deprecated/KakaoAuthService.java +++ /dev/null @@ -1,67 +0,0 @@ -package org.tuna.zoopzoop.backend.domain.auth.deprecated; - -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class KakaoAuthService { -// private final WebClient webClient; -// private final MemberRepository memberRepository; -// private final JwtUtil jwtUtil; -// -// @Value("${kakao.client_id}") -// private String CLIENT_ID; -// @Value("${kakao.redirect_uri}") -// private String REDIRECT_URI; -// -// private static final String TOKEN_URL = "https://kauth.kakao.com/oauth/token"; -// private static final String USER_INFO_URL = "https://kapi.kakao.com/v2/user/me"; -// -// public Map loginWithKakao(String code) { -// // 1. 카카오에서 토큰 발급 -// KakaoTokenResponse tokenResponse = webClient.post() -// .uri(TOKEN_URL) -// .contentType(MediaType.APPLICATION_FORM_URLENCODED) -// .body(BodyInserters.fromFormData("grant_type", "authorization_code") -// .with("client_id", CLIENT_ID) -// .with("redirect_uri", REDIRECT_URI) -// .with("code", code)) -// .retrieve() -// .bodyToMono(KakaoTokenResponse.class) -// .block(); -// -// // 2. 토큰에서 AccessToken 가져오기. -// String accessToken = tokenResponse.access_token(); -// -// // 3. AccessToken을 통해 카카오 사용자 정보 가져오기. -// KakaoUserInfoResponse userInfo = webClient.get() -// .uri(USER_INFO_URL) -// .headers(headers -> headers.setBearerAuth(accessToken)) -// .retrieve() -// .bodyToMono(KakaoUserInfoResponse.class) -// .block(); -// -// // 4. Member 엔티티 리턴 -// // a. kakaoKey 값을 가진 Member 객체가 이미 존재하는 경우, 그대로 가져옴. -// // b. 존재하지 않을 경우, 새로 만듬. -// Member member = memberRepository.findByKakaoKey(userInfo.id()) -// .orElseGet(() -> memberRepository.save( -// Member.builder() -// .name(userInfo.kakao_account().profile().nickname()) -// .profileImageUrl(userInfo.kakao_account().profile().profile_image_url()) -// .kakaoKey(userInfo.id()) -// .build() -// )); -// -// // 5. AccessToken 및 RefreshToken 생성. -// String jwtAccessToken = jwtUtil.generateToken(member); -// String jwtRefreshToken = jwtUtil.generateRefreshToken(member); -// -// Map tokens = new HashMap<>(); -// tokens.put("accessToken", jwtAccessToken); -// tokens.put("refreshToken", jwtRefreshToken); -// -// return tokens; -// } -} \ No newline at end of file diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/auth/deprecated/KakaoLoginController.java b/src/main/java/org/tuna/zoopzoop/backend/domain/auth/deprecated/KakaoLoginController.java deleted file mode 100644 index af6a2e3c..00000000 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/auth/deprecated/KakaoLoginController.java +++ /dev/null @@ -1,45 +0,0 @@ -package org.tuna.zoopzoop.backend.domain.auth.deprecated; - -import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequiredArgsConstructor -public class KakaoLoginController { -// private final KakaoAuthService kakaoAuthService; -// private final JwtUtil jwtUtil; -// private final JwtProperties jwtProperties; -// -// @GetMapping("/oauth/kakao") -// public ResponseEntity>> kakaoCallback(@RequestParam String code) { -// Map tokens = kakaoAuthService.loginWithKakao(code); -// ResponseCookie accessCookie = ResponseCookie.from("accessToken", tokens.get("accessToken")) -// .httpOnly(true) -// .path("/") -// .maxAge(jwtProperties.getAccessTokenValidity() / 1000) -// .sameSite("Lax") -// .build(); -// -// ResponseCookie refreshCookie = ResponseCookie.from("refreshToken", tokens.get("refreshToken")) -// .httpOnly(true) -// .secure(false) -// .path("/") -// .maxAge(jwtProperties.getRefreshTokenValidity() / 1000) -// .sameSite("Lax") -// .build(); -// -// HttpHeaders headers = new HttpHeaders(); -// headers.add(HttpHeaders.SET_COOKIE, accessCookie.toString()); -// headers.add(HttpHeaders.SET_COOKIE, refreshCookie.toString()); -// -// return ResponseEntity -// .status(HttpStatus.OK) -// .headers(headers) -// .body(new RsData<>( -// "200", -// "카카오 로그인에 성공했습니다.", -// tokens -// ) -// ); -// } -} diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/auth/deprecated/KakaoTokenResponse.java b/src/main/java/org/tuna/zoopzoop/backend/domain/auth/deprecated/KakaoTokenResponse.java deleted file mode 100644 index 56e3338e..00000000 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/auth/deprecated/KakaoTokenResponse.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.tuna.zoopzoop.backend.domain.auth.deprecated; - -public record KakaoTokenResponse( - String access_token, - String token_type, - String refresh_token, - Long expires_in, - String scope, - Long refresh_token_expires_in -) {} diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/auth/deprecated/KakaoUserInfoResponse.java b/src/main/java/org/tuna/zoopzoop/backend/domain/auth/deprecated/KakaoUserInfoResponse.java deleted file mode 100644 index 8ed5994e..00000000 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/auth/deprecated/KakaoUserInfoResponse.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.tuna.zoopzoop.backend.domain.auth.deprecated; - -public record KakaoUserInfoResponse( - Long id, - KakaoAccount kakao_account -) { - public record KakaoAccount(Profile profile) { - public record Profile(String nickname, String profile_image_url) {} - } -} \ No newline at end of file diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/auth/dev/controller/DevController.java b/src/main/java/org/tuna/zoopzoop/backend/domain/auth/dev/controller/DevController.java deleted file mode 100644 index 44161877..00000000 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/auth/dev/controller/DevController.java +++ /dev/null @@ -1,35 +0,0 @@ -package org.tuna.zoopzoop.backend.domain.auth.dev.controller; - -import lombok.RequiredArgsConstructor; -import org.springframework.context.annotation.Profile; -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.*; -import org.springframework.web.server.ResponseStatusException; -import org.tuna.zoopzoop.backend.domain.member.entity.Member; -import org.tuna.zoopzoop.backend.domain.member.enums.Provider; -import org.tuna.zoopzoop.backend.domain.member.repository.MemberRepository; -import org.tuna.zoopzoop.backend.global.security.jwt.JwtUtil; - -import java.util.Map; - -@Profile({"local","dev","staging","test"}) -@RestController -@RequestMapping("/dev") -@RequiredArgsConstructor -public class DevController { - - private final MemberRepository memberRepository; - private final JwtUtil jwtUtil; - - @GetMapping("/token") - public Map issueToken( - @RequestParam(name = "provider") Provider provider, - @RequestParam(name = "key") String key - ) { - Member m = memberRepository.findByProviderAndProviderKey(provider, key) - .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "member not found")); - - String accessToken = jwtUtil.generateToken(m); - return Map.of("accessToken", accessToken); - } -} diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/auth/dto/AuthResultData.java b/src/main/java/org/tuna/zoopzoop/backend/domain/auth/dto/AuthResultData.java deleted file mode 100644 index 07694675..00000000 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/auth/dto/AuthResultData.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.tuna.zoopzoop.backend.domain.auth.dto; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.io.Serializable; - -@Data -@NoArgsConstructor -@AllArgsConstructor -public class AuthResultData implements Serializable { - private String accessToken; - private String sessionId; -} \ No newline at end of file diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/auth/entity/AuthResult.java b/src/main/java/org/tuna/zoopzoop/backend/domain/auth/entity/AuthResult.java deleted file mode 100644 index f80c0dfb..00000000 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/auth/entity/AuthResult.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.tuna.zoopzoop.backend.domain.auth.entity; - -import lombok.RequiredArgsConstructor; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.stereotype.Component; -import org.tuna.zoopzoop.backend.domain.auth.dto.AuthResultData; - -import java.time.Duration; - -@Component -@RequiredArgsConstructor -public class AuthResult { - private final RedisTemplate redisTemplate; - private static final String PREFIX = "auth:result:"; - - public void put(String state, String accessToken, String sessionId) { - AuthResultData data = new AuthResultData(accessToken, sessionId); - redisTemplate.opsForValue().set(PREFIX + state, data, Duration.ofMinutes(1)); // TTL 1분, 프론트단에선 백그라운드 풀링 형식으로 계속 작동할 것이므로. - } - - public AuthResultData get(String state) { - AuthResultData data = redisTemplate.opsForValue().get(PREFIX + state); - if (data != null) redisTemplate.delete(PREFIX + state); - return data; - } -} \ No newline at end of file diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/auth/entity/RefreshToken.java b/src/main/java/org/tuna/zoopzoop/backend/domain/auth/entity/RefreshToken.java deleted file mode 100644 index b1a8ec8a..00000000 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/auth/entity/RefreshToken.java +++ /dev/null @@ -1,39 +0,0 @@ -package org.tuna.zoopzoop.backend.domain.auth.entity; - -import jakarta.persistence.*; -import lombok.*; -import org.tuna.zoopzoop.backend.domain.member.entity.Member; -import org.tuna.zoopzoop.backend.global.jpa.entity.BaseEntity; - -import java.time.LocalDateTime; - -@Getter -@Setter -@Entity -@NoArgsConstructor -@AllArgsConstructor -@Builder -public class RefreshToken extends BaseEntity { - @OneToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "member_id", unique = true, nullable = false) - private Member member; - - @Column(name = "session_id", unique = true, nullable = false) - private String sessionId; - - @Column(unique = true, nullable = false, length = 512) - private String refreshToken; - - @Column(name = "created_at", nullable = false, updatable = false) - private LocalDateTime createdAt; - - @Column(name = "expired_at") - private LocalDateTime expiredAt; - - @PrePersist - public void prePersist() { - if(createdAt == null) { - this.createdAt = LocalDateTime.now(); - } - } -} diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/auth/global/CustomOAuth2AuthorizationRequestResolver.java b/src/main/java/org/tuna/zoopzoop/backend/domain/auth/global/CustomOAuth2AuthorizationRequestResolver.java deleted file mode 100644 index f673afba..00000000 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/auth/global/CustomOAuth2AuthorizationRequestResolver.java +++ /dev/null @@ -1,57 +0,0 @@ -package org.tuna.zoopzoop.backend.domain.auth.global; - -import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.servlet.http.HttpServletRequest; -import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; -import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver; -import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver; -import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; - -import java.util.Base64; -import java.util.HashMap; -import java.util.Map; - -public class CustomOAuth2AuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver { - - private final OAuth2AuthorizationRequestResolver defaultResolver; - - public CustomOAuth2AuthorizationRequestResolver(ClientRegistrationRepository repo, String authorizationRequestBaseUri) { - this.defaultResolver = new DefaultOAuth2AuthorizationRequestResolver(repo, authorizationRequestBaseUri); - } - - @Override - public OAuth2AuthorizationRequest resolve(HttpServletRequest request) { - return customize(defaultResolver.resolve(request), request); - } - - @Override - public OAuth2AuthorizationRequest resolve(HttpServletRequest request, String clientRegistrationId) { - return customize(defaultResolver.resolve(request, clientRegistrationId), request); - } - - private OAuth2AuthorizationRequest customize(OAuth2AuthorizationRequest req, HttpServletRequest request) { - if (req == null) return null; - - String source = request.getParameter("source"); // 로그인 시작 시 전달된 source - OAuth2AuthorizationRequest.Builder builder = OAuth2AuthorizationRequest.from(req); - - if ("extension".equals(source)) { - String state = request.getParameter("state"); - Map stateData = new HashMap<>(); - stateData.put("source", "extension"); - stateData.put("customState", state); - stateData.put("originalState", req.getState()); - - try { - String encodedState = Base64.getUrlEncoder() - .encodeToString(new ObjectMapper().writeValueAsBytes(stateData)); - builder.state(encodedState); - } catch (Exception e) { - e.printStackTrace(); - return builder.build(); - } - } - - return builder.build(); - } -} \ No newline at end of file diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/auth/handler/OAuth2FailureHandler.java b/src/main/java/org/tuna/zoopzoop/backend/domain/auth/handler/OAuth2FailureHandler.java deleted file mode 100644 index f9ce1e20..00000000 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/auth/handler/OAuth2FailureHandler.java +++ /dev/null @@ -1,47 +0,0 @@ -package org.tuna.zoopzoop.backend.domain.auth.handler; - -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.web.authentication.AuthenticationFailureHandler; -import org.springframework.stereotype.Component; - -import java.io.IOException; -import java.net.URLEncoder; - -@Component -@RequiredArgsConstructor -public class OAuth2FailureHandler implements AuthenticationFailureHandler { - @Value("${front.redirect_domain}") - private String redirect_domain; - - @Override - public void onAuthenticationFailure(HttpServletRequest request, - HttpServletResponse response, - AuthenticationException exception) throws IOException, ServletException { - - // 프론트로 리다이렉트 - // 필요하면 쿼리 파라미터로 에러 정보 전달 - - String source = request.getParameter("source"); - - if("extension".equals(source)){ - String redirectUrl = - "https://" + redirect_domain + "/extension/callback " - + "?success=false" - + "&error=" + URLEncoder.encode(exception.getMessage(), "UTF-8"); - response.sendRedirect(redirectUrl); - return; - } - - String redirectUrl = - "https://" + redirect_domain + "/auth/callback" - + "?success=false" - + "&error=" + URLEncoder.encode(exception.getMessage(), "UTF-8"); - - response.sendRedirect(redirectUrl); - } -} diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/auth/handler/OAuth2SuccessHandler.java b/src/main/java/org/tuna/zoopzoop/backend/domain/auth/handler/OAuth2SuccessHandler.java deleted file mode 100644 index a692ea89..00000000 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/auth/handler/OAuth2SuccessHandler.java +++ /dev/null @@ -1,169 +0,0 @@ -package org.tuna.zoopzoop.backend.domain.auth.handler; - -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.HttpHeaders; -import org.springframework.http.ResponseCookie; -import org.springframework.security.core.Authentication; -import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; -import org.springframework.security.oauth2.core.user.OAuth2User; -import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; -import org.springframework.stereotype.Component; -import org.tuna.zoopzoop.backend.domain.auth.entity.AuthResult; -import org.tuna.zoopzoop.backend.domain.auth.service.refresh.RefreshTokenService; -import org.tuna.zoopzoop.backend.domain.member.entity.Member; -import org.tuna.zoopzoop.backend.domain.member.repository.MemberRepository; -import org.tuna.zoopzoop.backend.domain.member.service.MemberService; -import org.tuna.zoopzoop.backend.global.config.jwt.JwtProperties; -import org.tuna.zoopzoop.backend.global.security.jwt.JwtUtil; - -import java.io.IOException; -import java.net.URLEncoder; -import java.util.Base64; -import java.util.Map; - -@Component -@RequiredArgsConstructor -@Slf4j -public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler { - private final JwtUtil jwtUtil; - private final JwtProperties jwtProperties; - private final MemberRepository memberRepository; - private final MemberService memberService; - private final RefreshTokenService refreshTokenService; - private final AuthResult authResult; - - @Value("${front.redirect_domain}") - private String redirect_domain; - - @Value("${front.main_domain}") - private String main_domain; - - @Value("${spring.profiles.active:dev}") - private String activeProfile; - - @Override - public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, - Authentication authentication) throws IOException { - - // OAuth2 로그인 사용자의 속성 - // 소셜 로그인 공급자(Google, Kakao) - OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal(); - String registrationId = ((OAuth2AuthenticationToken) authentication).getAuthorizedClientRegistrationId(); - - // 공급자 별로 DB 에서 회원 조회 - Member member; - if ("kakao".equals(registrationId)) { - String kakaoId = oAuth2User.getAttributes().get("id").toString(); - member = memberService.findByKakaoKey(kakaoId); - } else if ("google".equals(registrationId)) { - String googleId = (String) oAuth2User.getAttributes().get("sub"); - member = memberService.findByGoogleKey(googleId); - } else { - throw new IllegalArgumentException(registrationId + "는 지원하지 않는 소셜 로그인입니다."); - } - - // 조회된 회원 정보를 기반으로 AccessToken 생성 - String accessToken = jwtUtil.generateToken(member); - - // RefreshToken 생성 및 DB 저장, SessionId 생성 - String refreshToken = jwtUtil.generateRefreshToken(member); - String sessionId = refreshTokenService.saveSession(member, refreshToken); - - log.info("[OAuth2SuccessHandler] Member: {}, SessionId: {}", member.getId(), sessionId); - - String state = request.getParameter("state"); - if(state != null && state.startsWith("ey")) { - Map stateData = new ObjectMapper().readValue( - Base64.getUrlDecoder().decode(state), - new TypeReference>() { - } - ); - - String source = stateData.get("source"); - String customState = stateData.get("customState"); - - log.info("[OAuth2SuccessHandler] Source: {}", source); - log.info("[OAuth2SuccessHandler] CustomState: {}", customState); - - // 확장 프로그램에서 로그인 했을 경우. - if ("extension".equals(source)) { - authResult.put(customState, accessToken, sessionId); - response.sendRedirect("https://" + redirect_domain + "/extension/success"); - response.flushBuffer(); - return; - } - } - - if ("dev".equalsIgnoreCase(activeProfile)) { - // 로컬 테스트용. profile이 dev인 경우. - ResponseCookie accessCookie = ResponseCookie.from("accessToken", accessToken) - .httpOnly(true) - .path("/") - .maxAge(jwtProperties.getAccessTokenValidity() / 1000) - .secure(false) - .sameSite("Lax") - .build(); - - ResponseCookie sessionCookie = ResponseCookie.from("sessionId", sessionId) - .httpOnly(true) - .path("/") - .maxAge(jwtProperties.getRefreshTokenValidity() / 1000) // RefreshToken 유효기간과 동일하게 - .secure(false) - .sameSite("Lax") - .build(); - - response.addHeader(HttpHeaders.SET_COOKIE, accessCookie.toString()); - response.addHeader(HttpHeaders.SET_COOKIE, sessionCookie.toString()); - - response.sendRedirect("http://localhost:8080"); - return; - } - - if ("http://localhost:3000".equals(redirect_domain)) { - // server 환경일 때: URL 파라미터로 토큰 전달 - // 프론트엔드 local 테스트용. - String redirectUrl = redirect_domain + "/api/auth/callback" - + "?success=true" - + "&accessToken=" + URLEncoder.encode(accessToken, "UTF-8") - + "&sessionId=" + URLEncoder.encode(sessionId, "UTF-8"); - response.sendRedirect(redirectUrl); - - } else { - ResponseCookie accessCookie = ResponseCookie.from("accessToken", accessToken) - .httpOnly(true) - .path("/") - .maxAge(jwtProperties.getAccessTokenValidity() / 1000) - // .domain() // 프론트엔드 & 백엔드 상위 도메인 - // .secure(true) // https 필수 설정. - .domain(main_domain) - .secure(true) - .sameSite("None") - .build(); - - ResponseCookie sessionCookie = ResponseCookie.from("sessionId", sessionId) - .httpOnly(true) - .path("/") - .maxAge(jwtProperties.getRefreshTokenValidity() / 1000) // RefreshToken 유효기간과 동일하게 - .domain(main_domain) - .secure(true) - .sameSite("None") - .build(); - - // HTTP 응답에서 쿠키 값 추가. - response.addHeader(HttpHeaders.SET_COOKIE, accessCookie.toString()); - response.addHeader(HttpHeaders.SET_COOKIE, sessionCookie.toString()); - - // 로그인 성공 후 리다이렉트. - // 배포 시에 프론트엔드와 조율이 필요한 부분일 듯 함. - response.sendRedirect("https://" + redirect_domain + "/api/auth/callback"); - } - // 보안을 좀 더 강화하고자 한다면 CSRF 토큰 같은 걸 생각해볼 수 있겠으나, - // 일단은 구현하지 않음.(개발 과정 중에 번거로워질 수 있을 듯 함.) - } -} \ No newline at end of file diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/auth/repository/RefreshTokenRepository.java b/src/main/java/org/tuna/zoopzoop/backend/domain/auth/repository/RefreshTokenRepository.java deleted file mode 100644 index 2d328fff..00000000 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/auth/repository/RefreshTokenRepository.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.tuna.zoopzoop.backend.domain.auth.repository; - -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; -import org.tuna.zoopzoop.backend.domain.auth.entity.RefreshToken; -import org.tuna.zoopzoop.backend.domain.member.entity.Member; - -import java.time.LocalDateTime; -import java.util.List; -import java.util.Optional; - -@Repository -public interface RefreshTokenRepository extends JpaRepository { - Optional findBySessionId(String sessionId); - Optional findByMember(Member member); - List findAllByMember(Member member); - List findAllByExpiredAtBefore(LocalDateTime dateTime); -} diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/auth/service/oauth2/CustomOAuth2UserService.java b/src/main/java/org/tuna/zoopzoop/backend/domain/auth/service/oauth2/CustomOAuth2UserService.java deleted file mode 100644 index 1c3219b1..00000000 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/auth/service/oauth2/CustomOAuth2UserService.java +++ /dev/null @@ -1,42 +0,0 @@ -package org.tuna.zoopzoop.backend.domain.auth.service.oauth2; - -import lombok.RequiredArgsConstructor; -import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; -import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; -import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; -import org.springframework.security.oauth2.core.OAuth2AuthenticationException; -import org.springframework.security.oauth2.core.user.OAuth2User; -import org.springframework.stereotype.Service; -import org.tuna.zoopzoop.backend.domain.member.entity.Member; - -import java.util.List; - -@Service -@RequiredArgsConstructor -public class CustomOAuth2UserService implements OAuth2UserService { - // @RequiredArgsConstructor 어노테이션을 통해, OAuth2UserInfoService를 인터페이스로 사용하는 - // GoogleUserInfoService, KakaoUserInfoService를 한번에 주입. - private final List oauth2UserInfoServices; - - @Override - public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { - // SpringBoot OAuth2 공급자에서 사용자 정보 받아오기. - OAuth2User oAuth2User = new DefaultOAuth2UserService().loadUser(userRequest); - - // 공급자(Google, Kakao) 받아오기. - String registrationId = userRequest.getClientRegistration().getRegistrationId(); - - // oauth2UserInfoService 리스트를 순회하며 공급자를 지원하는 서비스를 찾음. - OAuth2UserInfoService userInfoService = oauth2UserInfoServices.stream() - .filter(service -> service.supports(registrationId)) - .findFirst() - .orElseThrow(() -> new IllegalArgumentException(registrationId + "는 지원하지 않는 소셜 로그인입니다.")); - // 지원하지 않는 공급자의 경우 예외 발생. - // 하지만 발생할 일 없는 예외. - - // 선택된 서비스에서 사용자 정보 처리. - Member member = userInfoService.processUser(oAuth2User.getAttributes()); - - return oAuth2User; - } -} \ No newline at end of file diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/auth/service/oauth2/GoogleUserInfoService.java b/src/main/java/org/tuna/zoopzoop/backend/domain/auth/service/oauth2/GoogleUserInfoService.java deleted file mode 100644 index 77832d64..00000000 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/auth/service/oauth2/GoogleUserInfoService.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.tuna.zoopzoop.backend.domain.auth.service.oauth2; - -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.tuna.zoopzoop.backend.domain.member.entity.Member; -import org.tuna.zoopzoop.backend.domain.member.enums.Provider; -import org.tuna.zoopzoop.backend.domain.member.repository.MemberRepository; -import org.tuna.zoopzoop.backend.domain.member.service.MemberService; - -import java.util.Map; - -@Service -@RequiredArgsConstructor -public class GoogleUserInfoService implements OAuth2UserInfoService { - // Google 소셜 로그인의 경우 - private final MemberRepository memberRepository; - private final MemberService memberService; - - // 이 서비스(=GoogleUserInfoService)가, 해당 공급자를 지원하는 지에 대한 여부 확인. - // Google 공급자만 지원. - @Override - public boolean supports(String registrationId) { - return "google".equalsIgnoreCase(registrationId); - } - - - // Google 에서 받은 사용자 정보 Map(=attributes)에서 필요한 값 추출. - // 이후 추출한 값을 통해 Member 엔티티 생성. - @Override - public Member processUser(Map attributes) { - String googleId = (String) attributes.get("sub"); // 구글 user-id - String name = (String) attributes.get("name"); - String profileImage = (String) attributes.get("picture"); - - return memberRepository.findByProviderAndProviderKey(Provider.GOOGLE, googleId) - .orElseGet(() -> memberService.createMember(name, profileImage, googleId, Provider.GOOGLE)); - } -} \ No newline at end of file diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/auth/service/oauth2/KakaoUserInfoService.java b/src/main/java/org/tuna/zoopzoop/backend/domain/auth/service/oauth2/KakaoUserInfoService.java deleted file mode 100644 index 52f0c784..00000000 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/auth/service/oauth2/KakaoUserInfoService.java +++ /dev/null @@ -1,45 +0,0 @@ -package org.tuna.zoopzoop.backend.domain.auth.service.oauth2; - -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.tuna.zoopzoop.backend.domain.member.entity.Member; -import org.tuna.zoopzoop.backend.domain.member.enums.Provider; -import org.tuna.zoopzoop.backend.domain.member.repository.MemberRepository; -import org.tuna.zoopzoop.backend.domain.member.service.MemberService; - -import java.util.Map; - -@Service -@RequiredArgsConstructor -public class KakaoUserInfoService implements OAuth2UserInfoService { - // Kakao 소셜 로그인의 경우 - private final MemberRepository memberRepository; - private final MemberService memberService; - - // 이 서비스(=KakaoUserInfoService)가, 해당 공급자를 지원하는 지에 대한 여부 확인. - // Kakao 공급자만 지원. - @Override - public boolean supports(String registrationId) { - return "kakao".equalsIgnoreCase(registrationId); - } - - // Kakao 에서 받은 사용자 정보 Map(=attributes)에서 필요한 값 추출. - // 이후 추출한 값을 통해 Member 엔티티 생성. - @Override - public Member processUser(Map attributes) { - String kakaoId = attributes.get("id").toString(); - Map kakaoAccount = (Map) attributes.get("kakao_account"); - Map profile = (Map) kakaoAccount.get("profile"); - - /* - Kakao API의 경우, 허용한 사용자 정보(nickname, profile_image, email 등)를 profile Map 으로 묶어서 전달. - 즉, 필요한 값을 추출하기 위해선 attributes 에서 profile을 가져오고, profile 에서 필요한 값을 추출해야 함. - */ - - String name = (String) profile.get("nickname"); - String profileImage = (String) profile.get("profile_image_url"); - - return memberRepository.findByProviderAndProviderKey(Provider.KAKAO,kakaoId) - .orElseGet(() -> memberService.createMember(name, profileImage, kakaoId, Provider.KAKAO)); - } -} \ No newline at end of file diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/auth/service/oauth2/OAuth2UserInfoService.java b/src/main/java/org/tuna/zoopzoop/backend/domain/auth/service/oauth2/OAuth2UserInfoService.java deleted file mode 100644 index 358f0b50..00000000 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/auth/service/oauth2/OAuth2UserInfoService.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.tuna.zoopzoop.backend.domain.auth.service.oauth2; - -import org.tuna.zoopzoop.backend.domain.member.entity.Member; - -import java.util.Map; - -public interface OAuth2UserInfoService { - boolean supports(String registrationId); // 이 서비스가 해당 provider(Google, Kakao)를 처리하는지 - Member processUser(Map attributes); // 받아온 정보를 바탕으로 Member 엔티티 생성 or 가져오기 -} \ No newline at end of file diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/auth/service/refresh/RefreshTokenCleanupService.java b/src/main/java/org/tuna/zoopzoop/backend/domain/auth/service/refresh/RefreshTokenCleanupService.java deleted file mode 100644 index a2bf3f22..00000000 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/auth/service/refresh/RefreshTokenCleanupService.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.tuna.zoopzoop.backend.domain.auth.service.refresh; - -import lombok.RequiredArgsConstructor; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Service; -import org.tuna.zoopzoop.backend.domain.auth.entity.RefreshToken; -import org.tuna.zoopzoop.backend.domain.auth.repository.RefreshTokenRepository; - -import java.time.LocalDateTime; -import java.util.List; - -@Service -@RequiredArgsConstructor -public class RefreshTokenCleanupService { - private final RefreshTokenRepository refreshTokenRepository; - - @Scheduled(fixedRate = 60 * 60 * 1000) // 1시간마다 실행 - public void deleteExpiredTokens() { - List expiredTokens = refreshTokenRepository.findAllByExpiredAtBefore(LocalDateTime.now()); - if (!expiredTokens.isEmpty()) { - refreshTokenRepository.deleteAll(expiredTokens); - } - } -} \ No newline at end of file diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/auth/service/refresh/RefreshTokenService.java b/src/main/java/org/tuna/zoopzoop/backend/domain/auth/service/refresh/RefreshTokenService.java deleted file mode 100644 index b34dceb0..00000000 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/auth/service/refresh/RefreshTokenService.java +++ /dev/null @@ -1,63 +0,0 @@ -package org.tuna.zoopzoop.backend.domain.auth.service.refresh; - -import lombok.RequiredArgsConstructor; -import org.springframework.security.authentication.BadCredentialsException; -import org.springframework.stereotype.Service; -import org.tuna.zoopzoop.backend.domain.auth.entity.RefreshToken; -import org.tuna.zoopzoop.backend.domain.auth.repository.RefreshTokenRepository; -import org.tuna.zoopzoop.backend.domain.member.entity.Member; -import org.tuna.zoopzoop.backend.global.security.jwt.JwtUtil; - -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.util.Date; -import java.util.List; -import java.util.UUID; - -@Service -@RequiredArgsConstructor -public class RefreshTokenService { - private final RefreshTokenRepository refreshTokenRepository; - private final JwtUtil jwtUtil; - - private LocalDateTime getExpirationLocalDateTimeFromToken(String token) { - Date expirationDate = jwtUtil.getExpirationDateFromToken(token); // 기존 메서드 - if (expirationDate == null) return null; - - return LocalDateTime.ofInstant(expirationDate.toInstant(), ZoneId.systemDefault()); - } - - public String saveSession(Member member, String refreshToken) { - String sessionId = UUID.randomUUID().toString(); - - refreshTokenRepository.findByMember(member).ifPresent(refreshTokenRepository::delete); - - RefreshToken token = RefreshToken.builder() - .member(member) - .refreshToken(refreshToken) - .sessionId(sessionId) - .expiredAt(getExpirationLocalDateTimeFromToken(refreshToken)) - .build(); - - refreshTokenRepository.save(token); - return sessionId; - } - - public RefreshToken getBySessionId(String sessionId) { - return refreshTokenRepository.findBySessionId(sessionId) - .orElseThrow(() -> new BadCredentialsException("세션을 찾을 수 없습니다.")); - } - - public void deleteBySessionId(String sessionId) { - RefreshToken token = refreshTokenRepository.findBySessionId(sessionId) - .orElseThrow(() -> new BadCredentialsException("잘못된 요청입니다.")); - refreshTokenRepository.delete(token); - } - - public void deleteByMember(Member member) { - List tokens = refreshTokenRepository.findAllByMember(member); - if (!tokens.isEmpty()) { - refreshTokenRepository.deleteAll(tokens); - } - } -} diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/controller/ApiV1DashboardController.java b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/controller/ApiV1DashboardController.java deleted file mode 100644 index 2196dbc4..00000000 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/controller/ApiV1DashboardController.java +++ /dev/null @@ -1,75 +0,0 @@ -package org.tuna.zoopzoop.backend.domain.dashboard.controller; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; -import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.*; -import org.tuna.zoopzoop.backend.domain.dashboard.dto.BodyForReactFlow; -import org.tuna.zoopzoop.backend.domain.dashboard.entity.Graph; -import org.tuna.zoopzoop.backend.domain.dashboard.service.DashboardService; -import org.tuna.zoopzoop.backend.domain.dashboard.service.GraphService; -import org.tuna.zoopzoop.backend.domain.member.entity.Member; -import org.tuna.zoopzoop.backend.global.rsData.RsData; -import org.tuna.zoopzoop.backend.global.security.jwt.CustomUserDetails; - -import java.nio.file.AccessDeniedException; - -@RestController -@RequiredArgsConstructor -@RequestMapping("api/v1/dashboard") -@Tag(name = "ApiV1GraphController", description = "React-flow 데이터 컨트롤러") -public class ApiV1DashboardController { - private final DashboardService dashboardService; - - /** - * React-flow 데이터 저장(갱신) API - * @param dashboardId React-flow 데이터의 dashboard 식별 id - * @param requestBody React-flow 에서 보내주는 body 전체 - * @param signature Liveblocks-Signature 헤더 값 - * @return ResponseEntity> - */ - @PutMapping("/{dashboardId}/graph") - @Operation(summary = "React-flow 데이터 저장(갱신)") - public ResponseEntity> queueGraphUpdate( - @PathVariable Integer dashboardId, - @RequestBody String requestBody, - @RequestHeader("Liveblocks-Signature") String signature - ) { - dashboardService.queueGraphUpdate(dashboardId, requestBody, signature); - - return ResponseEntity - .status(HttpStatus.ACCEPTED) - .body(new RsData<>( - "202", - "데이터 업데이트 요청이 성공적으로 접수되었습니다.", - null - )); - } - - /** - * React-flow 데이터 조회 API - * @param dashboardId React-flow 데이터의 dashboard 식별 id - */ - @GetMapping("/{dashboardId}/graph") - @Operation(summary = "React-flow 데이터 조회") - public ResponseEntity> getGraph( - @PathVariable Integer dashboardId, - @AuthenticationPrincipal CustomUserDetails userDetails - ) throws AccessDeniedException { - // TODO : 권한 체크 로직 추가 - Member member = userDetails.getMember(); - dashboardService.verifyAccessPermission(member, dashboardId); - - Graph graph = dashboardService.getGraphByDashboardId(dashboardId); - return ResponseEntity - .status(HttpStatus.OK) - .body(new RsData<>( - "200", - "ID: " + dashboardId + " 의 React-flow 데이터를 조회했습니다.", - BodyForReactFlow.from(graph) - )); - } -} diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/dto/BodyForReactFlow.java b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/dto/BodyForReactFlow.java deleted file mode 100644 index 037b8882..00000000 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/dto/BodyForReactFlow.java +++ /dev/null @@ -1,145 +0,0 @@ -package org.tuna.zoopzoop.backend.domain.dashboard.dto; - -import com.fasterxml.jackson.annotation.JsonProperty; -import org.tuna.zoopzoop.backend.domain.dashboard.entity.Edge; -import org.tuna.zoopzoop.backend.domain.dashboard.entity.Graph; -import org.tuna.zoopzoop.backend.domain.dashboard.entity.Node; -import org.tuna.zoopzoop.backend.domain.dashboard.enums.EdgeType; -import org.tuna.zoopzoop.backend.domain.dashboard.enums.NodeType; - -import java.util.List; -import java.util.Map; - -// Request, Response 범용 Dto -public record BodyForReactFlow( - List nodes, - List edges -) { - - public record NodeDto( - @JsonProperty("id") String nodeKey, - @JsonProperty("type") String nodeType, - Map data, - @JsonProperty("position") PositionDto positionDto - ) { - public record PositionDto( - @JsonProperty("x") double x, - @JsonProperty("y") double y - ) {} - } - - public record EdgeDto( - @JsonProperty("id") String edgeKey, - @JsonProperty("source") String sourceNodeKey, - @JsonProperty("target") String targetNodeKey, - @JsonProperty("type") String edgeType, - @JsonProperty("animated") boolean isAnimated, - @JsonProperty("style") StyleDto styleDto - ) { - public record StyleDto( - String stroke, - Double strokeWidth - ) {} - } - - // DTO -> Entity, BodyForReactFlow를 Graph 엔티티로 변환 - public Graph toEntity() { - Graph graph = new Graph(); - - List nodeEntities = this.nodes().stream() - .map(dto -> { - Node node = new Node(); - node.setNodeKey(dto.nodeKey()); - node.setNodeType(NodeType.valueOf(dto.nodeType().toUpperCase())); - node.setData(dto.data()); - node.setPositonX(dto.positionDto().x()); - node.setPositonY(dto.positionDto().y()); - node.setGraph(graph); // 연관관계 설정 - return node; - }) - .toList(); - - List edgeEntities = this.edges().stream() - .map(dto -> { - Edge edge = new Edge(); - edge.setEdgeKey(dto.edgeKey()); - edge.setSourceNodeKey(dto.sourceNodeKey()); - edge.setTargetNodeKey(dto.targetNodeKey()); - edge.setEdgeType(EdgeType.valueOf(dto.edgeType().toUpperCase())); - edge.setAnimated(dto.isAnimated()); - if (dto.styleDto() != null) { - edge.setStroke(dto.styleDto().stroke()); - edge.setStrokeWidth(dto.styleDto().strokeWidth()); - } - edge.setGraph(graph); // 연관관계 설정 - return edge; - }) - .toList(); - - graph.getNodes().addAll(nodeEntities); - graph.getEdges().addAll(edgeEntities); - - return graph; - } - - // Entity -> DTO, Graph 엔티티를 ResBodyForReactFlow로 변환 - public static BodyForReactFlow from(Graph graph) { - List nodeDtos = graph.getNodes().stream() - .map(n -> new NodeDto( - n.getNodeKey(), - n.getNodeType().name().toUpperCase(), - n.getData(), - new NodeDto.PositionDto(n.getPositonX(), n.getPositonY()) - )) - .toList(); - - List edgeDtos = graph.getEdges().stream() - .map(e -> new EdgeDto( - e.getEdgeKey(), - e.getSourceNodeKey(), - e.getTargetNodeKey(), - e.getEdgeType().name().toUpperCase(), - e.isAnimated(), - new EdgeDto.StyleDto(e.getStroke(), e.getStrokeWidth()) - )) - .toList(); - - return new BodyForReactFlow(nodeDtos, edgeDtos); - } - - public List toNodeEntities(Graph graph) { - return this.nodes().stream() - .map(dto -> { - Node node = new Node(); - node.setNodeKey(dto.nodeKey()); - node.setNodeType(NodeType.valueOf(dto.nodeType().toUpperCase())); - node.setData(dto.data()); - node.setPositonX(dto.positionDto().x()); - node.setPositonY(dto.positionDto().y()); - node.setGraph(graph); // 연관관계 설정 - return node; - }) - .toList(); - } - - public List toEdgeEntities(Graph graph) { - return this.edges().stream() - .map(dto -> { - Edge edge = new Edge(); - edge.setEdgeKey(dto.edgeKey()); - edge.setSourceNodeKey(dto.sourceNodeKey()); - edge.setTargetNodeKey(dto.targetNodeKey()); - edge.setEdgeType(EdgeType.valueOf(dto.edgeType().toUpperCase())); - edge.setAnimated(dto.isAnimated()); - if (dto.styleDto() != null) { - edge.setStroke(dto.styleDto().stroke()); - edge.setStrokeWidth(dto.styleDto().strokeWidth()); - } - edge.setGraph(graph); // 연관관계 설정 - return edge; - }) - .toList(); - } - - -} diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/dto/GraphUpdateMessage.java b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/dto/GraphUpdateMessage.java deleted file mode 100644 index 6ea10ede..00000000 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/dto/GraphUpdateMessage.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.tuna.zoopzoop.backend.domain.dashboard.dto; - -public record GraphUpdateMessage( - Integer dashboardId, - String requestBody -){ -} diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/dto/ReqBodyForLiveblocksAuth.java b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/dto/ReqBodyForLiveblocksAuth.java deleted file mode 100644 index 8a6fd606..00000000 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/dto/ReqBodyForLiveblocksAuth.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.tuna.zoopzoop.backend.domain.dashboard.dto; - -import java.util.List; -import java.util.Map; - -public record ReqBodyForLiveblocksAuth( - String userId, - UserInfo userInfo, - Map> permissions -) { - public record UserInfo( - String name, - String avatar - ) {} -} diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/dto/ResBodyForAuthToken.java b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/dto/ResBodyForAuthToken.java deleted file mode 100644 index c7e29e22..00000000 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/dto/ResBodyForAuthToken.java +++ /dev/null @@ -1,5 +0,0 @@ -package org.tuna.zoopzoop.backend.domain.dashboard.dto; - -public record ResBodyForAuthToken( - String token -){ } diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/entity/Dashboard.java b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/entity/Dashboard.java deleted file mode 100644 index b13006c4..00000000 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/entity/Dashboard.java +++ /dev/null @@ -1,36 +0,0 @@ -package org.tuna.zoopzoop.backend.domain.dashboard.entity; - -import jakarta.persistence.*; -import lombok.Getter; -import lombok.Setter; -import org.tuna.zoopzoop.backend.domain.space.space.entity.Space; -import org.tuna.zoopzoop.backend.global.jpa.entity.BaseEntity; - -@Entity -@Getter -@Setter -public class Dashboard extends BaseEntity { - // 대시보드의 이름 - @Column(nullable = false) - private String name; - - // 이 대시보드가 속한 스페이스 - @OneToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "space_id") - private Space space; - - // 이 대시보드가 담고 있는 그래프 콘텐츠 (1:1 관계) - // Cascade 설정을 통해 Dashboard 저장 시 Graph도 함께 저장되도록 함 - @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) - @JoinColumn(name = "graph_id") - private Graph graph; - - // Dashboard 생성 시 비어있는 Graph를 함께 생성하는 편의 메서드 - public static Dashboard create(String name, Space space) { - Dashboard dashboard = new Dashboard(); - dashboard.setName(name); - dashboard.setSpace(space); - dashboard.setGraph(new Graph()); // 비어있는 Graph 생성 및 연결 - return dashboard; - } -} diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/entity/Edge.java b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/entity/Edge.java deleted file mode 100644 index 65538cc1..00000000 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/entity/Edge.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.tuna.zoopzoop.backend.domain.dashboard.entity; - -import jakarta.persistence.*; -import lombok.Getter; -import lombok.Setter; -import org.tuna.zoopzoop.backend.domain.dashboard.enums.EdgeType; -import org.tuna.zoopzoop.backend.global.jpa.entity.BaseEntity; - -@Getter -@Setter -@Entity -public class Edge extends BaseEntity { - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "graph_id") - private Graph graph; - - @Column - private String edgeKey; - - @Column - private String sourceNodeKey; - - @Column - private String targetNodeKey; - - @Column - @Enumerated(EnumType.STRING) - private EdgeType edgeType; - - @Column - boolean isAnimated; - - @Column - private String stroke; - - @Column - private Double strokeWidth; -} diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/entity/Graph.java b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/entity/Graph.java deleted file mode 100644 index 7715d2d6..00000000 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/entity/Graph.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.tuna.zoopzoop.backend.domain.dashboard.entity; - -import jakarta.persistence.CascadeType; -import jakarta.persistence.Entity; -import jakarta.persistence.OneToMany; -import jakarta.persistence.Version; -import lombok.Getter; -import lombok.Setter; -import org.tuna.zoopzoop.backend.global.jpa.entity.BaseEntity; - -import java.util.ArrayList; -import java.util.List; - -@Getter -@Setter -@Entity -public class Graph extends BaseEntity { - - @Version - private Long version; - - @OneToMany(mappedBy = "graph", cascade = CascadeType.ALL, orphanRemoval = true) - private List nodes = new ArrayList<>(); - - @OneToMany(mappedBy = "graph", cascade = CascadeType.ALL, orphanRemoval = true) - private List edges = new ArrayList<>(); -} diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/entity/Node.java b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/entity/Node.java deleted file mode 100644 index 64a3b3a8..00000000 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/entity/Node.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.tuna.zoopzoop.backend.domain.dashboard.entity; - -import jakarta.persistence.*; -import lombok.Getter; -import lombok.Setter; -import org.tuna.zoopzoop.backend.domain.dashboard.enums.NodeType; -import org.tuna.zoopzoop.backend.global.jpa.entity.BaseEntity; - -import java.util.HashMap; -import java.util.Map; - -@Getter -@Setter -@Entity -public class Node extends BaseEntity { - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "graph_id") - private Graph graph; - - @Column - private String nodeKey; - - @Column - @Enumerated(EnumType.STRING) - private NodeType nodeType; - - @ElementCollection - @CollectionTable(name = "node_data", joinColumns = @JoinColumn(name = "node_id")) - @MapKeyColumn(name = "data_key") - @Column(name = "data_value") - private Map data = new HashMap<>(); - - @Column - private double positonX; - - @Column - private double positonY; -} diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/enums/EdgeType.java b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/enums/EdgeType.java deleted file mode 100644 index 7d90eaa8..00000000 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/enums/EdgeType.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.tuna.zoopzoop.backend.domain.dashboard.enums; - -public enum EdgeType { - DEFAULT, - STRAIGHT, - STEP, - SMOOTHSTEP -} diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/enums/NodeType.java b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/enums/NodeType.java deleted file mode 100644 index 0111ba3b..00000000 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/enums/NodeType.java +++ /dev/null @@ -1,5 +0,0 @@ -package org.tuna.zoopzoop.backend.domain.dashboard.enums; - -public enum NodeType { - CUSTOM -} diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/extraComponent/GraphUpdateConsumer.java b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/extraComponent/GraphUpdateConsumer.java deleted file mode 100644 index fffdb834..00000000 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/extraComponent/GraphUpdateConsumer.java +++ /dev/null @@ -1,39 +0,0 @@ -package org.tuna.zoopzoop.backend.domain.dashboard.extraComponent; - -import com.fasterxml.jackson.databind.ObjectMapper; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.amqp.rabbit.annotation.RabbitListener; -import org.springframework.orm.ObjectOptimisticLockingFailureException; -import org.springframework.stereotype.Component; -import org.tuna.zoopzoop.backend.domain.dashboard.dto.BodyForReactFlow; -import org.tuna.zoopzoop.backend.domain.dashboard.dto.GraphUpdateMessage; -import org.tuna.zoopzoop.backend.domain.dashboard.service.DashboardService; - -@Slf4j -@Component -@RequiredArgsConstructor -public class GraphUpdateConsumer { - private final DashboardService dashboardService; - private final ObjectMapper objectMapper; - - @RabbitListener(queues = "graph.update.queue") - public void handleGraphUpdate(GraphUpdateMessage message) { - log.info("Received graph update message for dashboardId: {}", message.dashboardId()); - try { - BodyForReactFlow dto = objectMapper.readValue(message.requestBody(), BodyForReactFlow.class); - dashboardService.updateGraph(message.dashboardId(), dto); - log.info("Successfully updated graph for dashboardId: {}", message.dashboardId()); - } catch (ObjectOptimisticLockingFailureException e) { - // Optimistic Lock 충돌 발생! - // 내가 처리하려던 메시지는 이미 구버전 데이터에 대한 요청이었음. - // 따라서 이 메시지는 무시하고 정상 처리된 것으로 간주. - log.warn("Stale update attempt for dashboardId: {}. A newer version already exists. Discarding message.", message.dashboardId()); - // 예외를 다시 던지지 않으므로, 메시지는 큐에서 정상적으로 제거(ACK)됩니다. - } catch (Exception e) { - // 실제 운영에서는 메시지를 재시도하거나, 실패 큐(Dead Letter Queue)로 보내는 등의 - // 정교한 에러 처리 로직이 필요합니다. - log.error("Failed to process graph update for dashboardId: {}", message.dashboardId(), e); - } - } -} diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/repository/DashboardRepository.java b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/repository/DashboardRepository.java deleted file mode 100644 index 0b163042..00000000 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/repository/DashboardRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.tuna.zoopzoop.backend.domain.dashboard.repository; - -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; -import org.tuna.zoopzoop.backend.domain.dashboard.entity.Dashboard; - -@Repository -public interface DashboardRepository extends JpaRepository { -} diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/repository/EdgeRepository.java b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/repository/EdgeRepository.java deleted file mode 100644 index 1d7823ab..00000000 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/repository/EdgeRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.tuna.zoopzoop.backend.domain.dashboard.repository; - -import org.springframework.data.jpa.repository.JpaRepository; -import org.tuna.zoopzoop.backend.domain.dashboard.entity.Edge; - -public interface EdgeRepository extends JpaRepository { -} diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/repository/GraphRepository.java b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/repository/GraphRepository.java deleted file mode 100644 index e75b7682..00000000 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/repository/GraphRepository.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.tuna.zoopzoop.backend.domain.dashboard.repository; - -import org.springframework.data.jpa.repository.JpaRepository; -import org.tuna.zoopzoop.backend.domain.dashboard.entity.Graph; - -import java.util.Optional; - -public interface GraphRepository extends JpaRepository { - Optional findGraphById(Integer id); -} diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/repository/NodeRepository.java b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/repository/NodeRepository.java deleted file mode 100644 index 6342c6a2..00000000 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/repository/NodeRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.tuna.zoopzoop.backend.domain.dashboard.repository; - -import org.springframework.data.jpa.repository.JpaRepository; -import org.tuna.zoopzoop.backend.domain.dashboard.entity.Node; - -public interface NodeRepository extends JpaRepository { -} diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/service/DashboardService.java b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/service/DashboardService.java deleted file mode 100644 index 46cabef7..00000000 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/service/DashboardService.java +++ /dev/null @@ -1,201 +0,0 @@ -package org.tuna.zoopzoop.backend.domain.dashboard.service; - -import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.persistence.NoResultException; -import lombok.RequiredArgsConstructor; -import org.apache.commons.codec.binary.Hex; -import org.springframework.amqp.rabbit.core.RabbitTemplate; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.tuna.zoopzoop.backend.domain.dashboard.dto.BodyForReactFlow; -import org.tuna.zoopzoop.backend.domain.dashboard.dto.GraphUpdateMessage; -import org.tuna.zoopzoop.backend.domain.dashboard.dto.ReqBodyForLiveblocksAuth; -import org.tuna.zoopzoop.backend.domain.dashboard.entity.Dashboard; -import org.tuna.zoopzoop.backend.domain.dashboard.entity.Edge; -import org.tuna.zoopzoop.backend.domain.dashboard.entity.Graph; -import org.tuna.zoopzoop.backend.domain.dashboard.entity.Node; -import org.tuna.zoopzoop.backend.domain.dashboard.repository.DashboardRepository; -import org.tuna.zoopzoop.backend.domain.member.entity.Member; -import org.tuna.zoopzoop.backend.domain.space.membership.entity.Membership; -import org.tuna.zoopzoop.backend.domain.space.membership.enums.Authority; -import org.tuna.zoopzoop.backend.domain.space.membership.service.MembershipService; -import org.tuna.zoopzoop.backend.domain.space.space.entity.Space; -import org.tuna.zoopzoop.backend.domain.space.space.service.SpaceService; -import org.tuna.zoopzoop.backend.global.clients.liveblocks.LiveblocksClient; - -import java.nio.file.AccessDeniedException; -import java.util.Collections; -import java.util.List; -import java.util.Map; - -@Service -@RequiredArgsConstructor -@Transactional -public class DashboardService { - private final DashboardRepository dashboardRepository; - private final MembershipService membershipService; - private final ObjectMapper objectMapper; - private final SignatureService signatureService; - private final RabbitTemplate rabbitTemplate; - private final SpaceService spaceService; - private final LiveblocksClient liveblocksClient; - - - // =========================== Graph 관련 메서드 =========================== - - /** - * 대시보드 ID를 통해 Graph 데이터를 조회하는 메서드 - */ - @Transactional(readOnly = true) - public Graph getGraphByDashboardId(Integer dashboardId) { - Dashboard dashboard = dashboardRepository.findById(dashboardId) - .orElseThrow(() -> new NoResultException(dashboardId + " ID를 가진 대시보드를 찾을 수 없습니다.")); - - return dashboard.getGraph(); - } - - /** - * 특정 대시보드의 Graph 데이터를 덮어쓰는(수정) 메서드 - */ - public void updateGraph(Integer dashboardId, BodyForReactFlow dto) { - Graph graph = getGraphByDashboardId(dashboardId); - - // 기존 Graph의 노드와 엣지를 모두 삭제 - graph.getNodes().clear(); - graph.getEdges().clear(); - - // DTO로부터 새로운 노드와 엣지 Entity 리스트를 생성 - List newNodes = dto.toNodeEntities(graph); // DTO에 변환 로직이 있다고 가정 - List newEdges = dto.toEdgeEntities(graph); - - // Graph에 새로운 리스트를 추가 - graph.getNodes().addAll(newNodes); - graph.getEdges().addAll(newEdges); - - } - - /** - * 서명 검증 후 Graph 업데이트를 수행하는 메서드 - * @param dashboardId 대시보드 ID - * @param requestBody 요청 바디 - * @param signatureHeader 서명 헤더 - */ - public void verifyAndUpdateGraph(Integer dashboardId, String requestBody, String signatureHeader) { - // 1. 서명 검증 - if (!signatureService.isValidSignature(requestBody, signatureHeader)) { - throw new SecurityException("Invalid webhook signature."); - } - - // 2. 검증 통과 후, 기존 업데이트 로직 실행 - try { - BodyForReactFlow dto = objectMapper.readValue(requestBody, BodyForReactFlow.class); - updateGraph(dashboardId, dto); - } catch (NoResultException e) { - throw new NoResultException(dashboardId + " ID를 가진 대시보드를 찾을 수 없습니다."); - } - catch (Exception e) { - throw new RuntimeException("Failed to process request body.", e); - } - } - - // =========================== 권한 관련 메서드 =========================== - - /** - * 대시보드 접근 권한을 검증하는 메서드 - * @param member 접근을 시도하는 멤버 - * @param dashboardId 접근하려는 대시보드 ID - */ - public void verifyAccessPermission(Member member, Integer dashboardId) throws AccessDeniedException { - Dashboard dashboard = dashboardRepository.findById(dashboardId) - .orElseThrow(() -> new NoResultException(dashboardId + " ID를 가진 대시보드를 찾을 수 없습니다.")); - - try { - membershipService.findByMemberAndSpace(member, dashboard.getSpace()); - } catch (NoResultException e) { - throw new AccessDeniedException("대시보드의 접근 권한이 없습니다."); - } - } - - // =========================== message 관리 메서드 =========================== - - /** - * Graph 업데이트 요청을 RabbitMQ 큐에 비동기적으로 발행하는 메서드 - * @param dashboardId 대시보드 ID - * @param requestBody 요청 바디 - * @param signatureHeader 서명 헤더 - */ - public void queueGraphUpdate(Integer dashboardId, String requestBody, String signatureHeader){ - // 서명 검증은 동기적으로 즉시 처리 - if (!signatureService.isValidSignature(requestBody, signatureHeader)) { - throw new SecurityException("Invalid webhook signature."); - } - - // 대시보드 존재 여부 확인 - if (!dashboardRepository.existsById(dashboardId)) { - throw new NoResultException(dashboardId + " ID를 가진 대시보드를 찾을 수 없습니다."); - } - - // 큐에 보낼 메시지 생성 - GraphUpdateMessage message = new GraphUpdateMessage(dashboardId, requestBody); - - // RabbitMQ에 메시지 발행 - rabbitTemplate.convertAndSend("zoopzoop.exchange", "graph.update.rk", message); - } - - // =========================== 기타 메서드 =========================== - - /** - * 특정 스페이스에 대한 Liveblocks 접속 토큰(JWT)을 발급합니다. - * @param spaceId 스페이스 ID - * @param member 토큰을 요청하는 멤버 - * @return 발급된 JWT 문자열 - * @throws AccessDeniedException 멤버가 해당 스페이스에 속해있지 않거나 권한이 없는 경우 - */ - @Transactional(readOnly = true) - public String getAuthTokenForSpace(Integer spaceId, Member member) throws AccessDeniedException { - Space space = spaceService.findById(spaceId); - - // 해당 스페이스에 멤버가 속해있는지, PENDING 상태는 아닌지 확인 - Membership membership = membershipService.findByMemberAndSpace(member, space); - if (membership.getAuthority().equals(Authority.PENDING)) { - throw new AccessDeniedException("스페이스에 가입된 멤버가 아닙니다."); - } - - // Liveblocks Room ID 생성 - String roomId = "space_" + space.getId(); - - // Liveblocks에 전달할 사용자 정보 생성 - String userId = String.valueOf(member.getId()); - ReqBodyForLiveblocksAuth.UserInfo userInfo = new ReqBodyForLiveblocksAuth.UserInfo( - member.getName(), - member.getProfileImageUrl() - ); - - // Liveblocks 권한 설정 (내 서비스의 Authority -> Liveblocks 권한으로 변환) - List permissions; - switch (membership.getAuthority()) { - case ADMIN, READ_WRITE: - permissions = List.of("room:write"); - break; - case READ_ONLY: - permissions = Collections.emptyList(); // 빈 리스트는 읽기 전용을 의미 - break; - default: - // PENDING 등 다른 상태는 위에서 이미 필터링됨 - throw new AccessDeniedException("유효하지 않은 권한입니다."); - } - - // Liveblocks Client에 전달할 요청 객체 생성 - ReqBodyForLiveblocksAuth authRequest = new ReqBodyForLiveblocksAuth( - userId, - userInfo, - Map.of(roomId, permissions) - ); - - // LiveblocksClient를 통해 토큰 발급 요청 - return liveblocksClient.getAuthToken(authRequest); - } - - -} diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/service/GraphService.java b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/service/GraphService.java deleted file mode 100644 index d6069ffd..00000000 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/service/GraphService.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.tuna.zoopzoop.backend.domain.dashboard.service; - -import jakarta.persistence.NoResultException; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.tuna.zoopzoop.backend.domain.dashboard.entity.Graph; -import org.tuna.zoopzoop.backend.domain.dashboard.repository.GraphRepository; - -@Service -@RequiredArgsConstructor -public class GraphService { - private final GraphRepository graphRepository; - - @Transactional - public Graph saveGraph(Graph graph) { - return graphRepository.save(graph); - } - - public Graph getGraph(Integer id) { - return graphRepository.findGraphById(id).orElseThrow(() -> - new NoResultException(id + " id를 가진 그래프를 찾을 수 없습니다.") - ); - } -} - diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/service/SignatureService.java b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/service/SignatureService.java deleted file mode 100644 index 242e925b..00000000 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/service/SignatureService.java +++ /dev/null @@ -1,77 +0,0 @@ -package org.tuna.zoopzoop.backend.domain.dashboard.service; - -import lombok.RequiredArgsConstructor; -import org.apache.commons.codec.binary.Hex; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; - -import javax.crypto.Mac; -import javax.crypto.spec.SecretKeySpec; -import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; - -@Service -@RequiredArgsConstructor -public class SignatureService { - @Value("${liveblocks.secret-key}") - private String liveblocksSecretKey; - - // 5분 (밀리초 단위) - private static final long TOLERANCE_IN_MILLIS = 5 * 60 * 1000; - - /** - * LiveBlocks Webhook 요청의 유효성을 검증하는 메서드 - * @param requestBody 요청 바디 - * @param signatureHeader LiveBlocks가 제공하는 서명 헤더 - * @return 서명이 유효하면 true, 그렇지 않으면 false - */ - public boolean isValidSignature(String requestBody, String signatureHeader) { - // [임시 코드] 로컬 테스트를 위해 무조건 true 반환 -// if ("true".equals(System.getProperty("local.test.skip.signature"))) { -// return true; -// } - - try { - // 1. 헤더 파싱 - String[] parts = signatureHeader.split(","); - long timestamp = -1; - String signatureHashFromHeader = null; - - for (String part : parts) { - String[] pair = part.split("=", 2); - if (pair.length == 2) { - if ("t".equals(pair[0])) { - timestamp = Long.parseLong(pair[1]); - } else if ("v1".equals(pair[0])) { - signatureHashFromHeader = pair[1]; - } - } - } - - if (timestamp == -1 || signatureHashFromHeader == null) { - return false; // 헤더 형식이 잘못됨 - } - - // 2. 리플레이 공격 방지를 위한 타임스탬프 검증 (선택사항) - long now = System.currentTimeMillis(); - if (now - timestamp > TOLERANCE_IN_MILLIS) { - return false; // 너무 오래된 요청 - } - - // 3. 서명 재생성 - String payload = timestamp + "." + requestBody; - Mac mac = Mac.getInstance("HmacSHA256"); - SecretKeySpec secretKeySpec = new SecretKeySpec(liveblocksSecretKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256"); - mac.init(secretKeySpec); - byte[] expectedHashBytes = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8)); - - // 4. 서명 비교 (타이밍 공격 방지를 위해 MessageDigest.isEqual 사용) - byte[] signatureHashBytesFromHeader = Hex.decodeHex(signatureHashFromHeader); - return MessageDigest.isEqual(expectedHashBytes, signatureHashBytesFromHeader); - - } catch (Exception e) { - // 파싱 실패, 디코딩 실패 등 모든 예외는 검증 실패로 간주 - return false; - } - } -} diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/ai/dto/AiExtractorDto.java b/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/ai/dto/AiExtractorDto.java deleted file mode 100644 index 3d3fd308..00000000 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/ai/dto/AiExtractorDto.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.tuna.zoopzoop.backend.domain.datasource.ai.dto; - -import java.time.LocalDate; - -public record AiExtractorDto( - String title, // 제목 - LocalDate dataCreatedDate, // 작성일자 - String content, // ai한테 줘야할 내용 - String imageUrl, // 썸네일 이미지 url - String source // 출처 -) { -} diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/ai/dto/AnalyzeContentDto.java b/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/ai/dto/AnalyzeContentDto.java deleted file mode 100644 index f56f1fc4..00000000 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/ai/dto/AnalyzeContentDto.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.tuna.zoopzoop.backend.domain.datasource.ai.dto; - -import org.tuna.zoopzoop.backend.domain.datasource.entity.Category; - -import java.util.List; - -public record AnalyzeContentDto( - String summary, - Category category, // ENUM 그대로 매핑 (AI 출력도 ENUM 이름으로) - List tags -) { -} diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/ai/prompt/AiPrompt.java b/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/ai/prompt/AiPrompt.java deleted file mode 100644 index 5e9268b5..00000000 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/ai/prompt/AiPrompt.java +++ /dev/null @@ -1,57 +0,0 @@ -package org.tuna.zoopzoop.backend.domain.datasource.ai.prompt; - -public class AiPrompt { - // 불특정 사이트 메타데이터 추출 프롬프트 - public static final String EXTRACTION = """ - 아래 HTML 전문에서 필요한 정보를 JSON 형식으로 추출해 주세요. - 반환 JSON 구조: - { - "title": "제목", - "datacreatedDate": "작성일자 (YYYY-MM-DD)", - "content": "본문 내용", - "imageUrl": "썸네일 이미지 URL", - "source": "출판사 이름 or 서비스 이름 or 도메인 이름" - } - - HTML 전문: - %s - - - 반드시 JSON 형식으로만 출력해 주세요. - - 해당정보가 없으면 반드시 빈 문자열로 출력해 주세요. - """; - - // 내용 요약, 태그 추출, 카테고리 선정 프롬프트 - public static final String SUMMARY_TAG_CATEGORY = """ - 너는 뉴스, 블로그 등 내용 요약 및 분류 AI야. 아래의 규칙에 따라 답변해. - - [규칙] - 1. 주어진 content를 50자 이상 100자 이하로 간단히 요약해라. - 2. 아래 Category 목록 중에서 content와 가장 적절한 카테고리 하나를 정확히 선택해라. - - POLITICS("정치") - - ECONOMY("경제") - - SOCIETY("사회") - - IT("IT") - - SCIENCE("과학") - - CULTURE("문화") - - SPORTS("스포츠") - - ENVIRONMENT("환경") - - HISTORY("역사") - - WORLD("세계") - 3. 내가 제공하는 태그 목록을 참고해서, content와 관련된 태그를 3~5개 생성해라. - - 제공된 태그와 중복 가능하다. - - 필요하면 새로운 태그를 만들어도 된다. - 4. 출력은 반드시 아래 JSON 형식으로 해라. Markdown 문법(```)은 쓰지 마라. - - 해당정보가 없을 시 summary는 빈 문자열, category는 null, tags는 빈 리스트로 출력해줘라. - - [출력 JSON 형식] - { - "summary": "내용 요약 (50~100자)", - "category": "선택된 카테고리 ENUM 이름", - "tags": ["태그1", "태그2", "태그3", ...] - } - - [입력 데이터] - content: %s - existingTags: %s - """; -} diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/ai/service/AiService.java b/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/ai/service/AiService.java deleted file mode 100644 index fa94e7aa..00000000 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/ai/service/AiService.java +++ /dev/null @@ -1,79 +0,0 @@ -package org.tuna.zoopzoop.backend.domain.datasource.ai.service; - -import com.fasterxml.jackson.core.JsonParseException; -import com.fasterxml.jackson.core.JsonProcessingException; -import lombok.RequiredArgsConstructor; -import org.springframework.ai.chat.client.ChatClient; -import org.springframework.retry.annotation.Backoff; -import org.springframework.retry.annotation.Recover; -import org.springframework.retry.annotation.Retryable; -import org.springframework.stereotype.Service; -import org.tuna.zoopzoop.backend.domain.datasource.ai.dto.AiExtractorDto; -import org.tuna.zoopzoop.backend.domain.datasource.ai.dto.AnalyzeContentDto; -import org.tuna.zoopzoop.backend.domain.datasource.ai.prompt.AiPrompt; -import org.tuna.zoopzoop.backend.domain.datasource.entity.Tag; - -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; - -@Service -@RequiredArgsConstructor -public class AiService { - private final ChatClient chatClient; - - @Retryable( - maxAttempts = 3, - backoff = @Backoff(delay = 500), - retryFor = {JsonParseException.class, JsonProcessingException.class} - ) - public AiExtractorDto extract(String rawHtml) { - AiExtractorDto response = chatClient.prompt() - .user(AiPrompt.EXTRACTION.formatted(rawHtml)) - .call() - .entity(AiExtractorDto.class); - - return response; - } - - @Recover - public AiExtractorDto extractRecover(Exception e, String rawHtml) { - return new AiExtractorDto( - "", - null, - "", - "", - "" - ); - } - - @Retryable( - maxAttempts = 3, - backoff = @Backoff(delay = 500), - retryFor = {JsonParseException.class, JsonProcessingException.class} - ) - public AnalyzeContentDto analyzeContent(String content, List tagList) { - // JSON 배열 문자열로 변환 - String tags = tagList.stream() - .map(Tag::getTagName) // 태그명만 추출 - .map(tagName -> "\"" + tagName + "\"") - .collect(Collectors.joining(", ", "[", "]")); - - AnalyzeContentDto response = chatClient.prompt() - .user(AiPrompt.SUMMARY_TAG_CATEGORY.formatted(content, tags)) - .call() - .entity(AnalyzeContentDto.class); - - return response; - } - - @Recover - public AnalyzeContentDto analyzeContentRecover(Exception e, String content, List tagList) { - return new AnalyzeContentDto( - "", - null, - new ArrayList<>() - ); - } - -} \ No newline at end of file diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/controller/DataSourceController.java b/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/controller/DataSourceController.java deleted file mode 100644 index 58e2e32a..00000000 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/controller/DataSourceController.java +++ /dev/null @@ -1,232 +0,0 @@ -package org.tuna.zoopzoop.backend.domain.datasource.controller; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.data.web.PageableDefault; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.*; -import org.springframework.web.multipart.MultipartFile; -import org.tuna.zoopzoop.backend.domain.datasource.dto.*; -import org.tuna.zoopzoop.backend.domain.datasource.entity.Category; -import org.tuna.zoopzoop.backend.domain.datasource.service.DataSourceService; -import org.tuna.zoopzoop.backend.domain.datasource.service.PersonalDataSourceService; -import org.tuna.zoopzoop.backend.global.rsData.RsData; -import org.tuna.zoopzoop.backend.global.security.jwt.CustomUserDetails; - -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; - -@RestController -@RequestMapping("/api/v1/archive") -@RequiredArgsConstructor -@Tag(name = "ApiV1DataSource(Personal)", description = "개인 아카이브 자료 API") -public class DataSourceController { - - private final PersonalDataSourceService personalApp; - - // ===== 등록 (개인만) ===== - // DataSourceController - - @Operation(summary = "자료 등록", description = "내 PersonalArchive 안에 자료를 등록합니다.") - @PostMapping("") - public ResponseEntity>> createDataSource( - @Valid @RequestBody reqBodyForCreateDataSource rq, - @AuthenticationPrincipal CustomUserDetails user - ) throws IOException { - int id = personalApp.create( - user.getMember().getId(), - rq.sourceUrl(), - rq.folderId(), - DataSourceService.CreateCmd.builder().build() - ); - return ResponseEntity.ok( - new RsData<>("200", "새로운 자료가 등록됐습니다.", Map.of("dataSourceId", id)) - ); - } - - - // ===== 단건 삭제 ===== - @Operation(summary = "자료 단건 삭제", description = "내 PersonalArchive 안에 자료를 단건 삭제합니다.") - @DeleteMapping("/{dataSourceId}") - public ResponseEntity>> delete( - @PathVariable Integer dataSourceId, - @AuthenticationPrincipal CustomUserDetails user - ) { - int deletedId = personalApp.deleteOne(user.getMember().getId(), dataSourceId); - return ResponseEntity.ok( - new RsData<>("200", deletedId + "번 자료가 삭제됐습니다.", Map.of("dataSourceId", deletedId)) - ); - } - - // ===== 다건 삭제 ===== - @Operation(summary = "자료 다건 삭제", description = "내 PersonalArchive 안에 자료를 다건 삭제합니다.") - @DeleteMapping("/delete") - public ResponseEntity> deleteMany( - @Valid @RequestBody reqBodyForDeleteMany rq, - @AuthenticationPrincipal CustomUserDetails user - ) { - personalApp.deleteMany(user.getMember().getId(), rq.dataSourceId()); - return ResponseEntity.ok(new RsData<>("200", "복수개의 자료가 삭제됐습니다.", null)); - } - - // ===== 소프트 삭제/복원 ===== - @Operation(summary = "자료 다건 임시 삭제", description = "내 PersonalArchive 안에 자료들을 임시 삭제합니다.") - @PatchMapping("/soft-delete") - public ResponseEntity> softDelete(@RequestBody @Valid IdsRequest rq, - @AuthenticationPrincipal CustomUserDetails user) { - personalApp.softDelete(user.getMember().getId(), rq.dataSourceId()); - return ResponseEntity.ok(new RsData<>("200", "자료들이 임시 삭제됐습니다.", null)); - } - - @Operation(summary = "자료 다건 복원", description = "내 PersonalArchive 안에 자료들을 복원합니다.") - @PatchMapping("/restore") - public ResponseEntity> restore(@RequestBody @Valid IdsRequest rq, - @AuthenticationPrincipal CustomUserDetails user) { - personalApp.restore(user.getMember().getId(), rq.dataSourceId()); - return ResponseEntity.ok(new RsData<>("200", "자료들이 복구됐습니다.", null)); - } - - // ===== 이동 ===== - @Operation(summary = "자료 단건 이동", description = "내 PersonalArchive 안에 자료를 단건 이동합니다.") - @PatchMapping("/{dataSourceId}/move") - public ResponseEntity>> moveDataSource( - @PathVariable Integer dataSourceId, - @Valid @RequestBody reqBodyForMoveDataSource rq, - @AuthenticationPrincipal CustomUserDetails user - ) { - var result = personalApp.moveOne(user.getMember().getId(), dataSourceId, rq.folderId()); - String msg = result.dataSourceId() + "번 자료가 " + result.folderId() + "번 폴더로 이동했습니다."; - return ResponseEntity.ok( - new RsData<>("200", msg, - Map.of("folderId", result.folderId(), "dataSourceId", result.dataSourceId())) - ); - } - - @Operation(summary = "자료 다건 이동", description = "내 PersonalArchive 안에 자료들을 다건 이동합니다.") - @PatchMapping("/move") - public ResponseEntity> moveMany( - @Valid @RequestBody reqBodyForMoveMany rq, - @AuthenticationPrincipal CustomUserDetails user - ) { - personalApp.moveMany(user.getMember().getId(), rq.folderId(), rq.dataSourceId()); - return ResponseEntity.ok(new RsData<>("200", "복수 개의 자료를 이동했습니다.", null)); - } - - // ===== 수정 ===== - // JSON만 수정 - @Operation(summary = "자료 수정", description = "내 PersonalArchive 안에 자료를 수정합니다.") - @PatchMapping(path = "/{dataSourceId}", consumes = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity>> updateDataSourceJson( - @PathVariable Integer dataSourceId, - @RequestBody @Valid reqBodyForUpdateDataSource body, - @AuthenticationPrincipal CustomUserDetails user - ) { - boolean anyPresent = - (body.title() != null && body.title().isPresent()) || - (body.summary() != null && body.summary().isPresent()) || - (body.sourceUrl() != null && body.sourceUrl().isPresent()) || - (body.imageUrl() != null && body.imageUrl().isPresent()) || - (body.source() != null && body.source().isPresent()) || - (body.tags() != null && body.tags().isPresent()) || - (body.category() != null && body.category().isPresent()); - if (!anyPresent) throw new IllegalArgumentException("변경할 값이 없습니다."); - - int updatedId = personalApp.update( - user.getMember().getId(), - dataSourceId, - DataSourceService.UpdateCmd.builder() - .title(body.title()) - .summary(body.summary()) - .source(body.source()) - .sourceUrl(body.sourceUrl()) - .imageUrl(body.imageUrl()) - .category(body.category()) - .tags(body.tags()) - .build() - ); - - return ResponseEntity.ok( - new RsData<>("200", updatedId + "번 자료가 수정됐습니다.", Map.of("dataSourceId", updatedId)) - ); - } - - // 이미지 포함 수정 - @Operation(summary = "자료 수정(이미지+JSON)", description = "내 PersonalArchive 안에 자료를 수정합니다") - @PatchMapping(path = "/{dataSourceId}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - public ResponseEntity>> updateDataSourceMultipart( - @PathVariable Integer dataSourceId, - @RequestPart("payload") @Valid reqBodyForUpdateDataSource body, // JSON 파트 - @RequestPart(value = "image", required = false) MultipartFile image, // 파일 파트 - @AuthenticationPrincipal CustomUserDetails user - ) { - boolean anyPresent = - (body.title() != null && body.title().isPresent()) || - (body.summary() != null && body.summary().isPresent()) || - (body.sourceUrl() != null && body.sourceUrl().isPresent()) || - (body.imageUrl() != null && body.imageUrl().isPresent()) || - (body.source() != null && body.source().isPresent()) || - (body.tags() != null && body.tags().isPresent()) || - (body.category() != null && body.category().isPresent()) || - (image != null && !image.isEmpty()); - if (!anyPresent) throw new IllegalArgumentException("변경할 값이 없습니다."); - - var baseCmd = DataSourceService.UpdateCmd.builder() - .title(body.title()) - .summary(body.summary()) - .source(body.source()) - .sourceUrl(body.sourceUrl()) - .imageUrl(body.imageUrl()) - .category(body.category()) - .tags(body.tags()) - .build(); - - var outcome = personalApp.updateWithImage( - user.getMember().getId(), dataSourceId, baseCmd, image - ); - - Map data = new HashMap<>(); - data.put("dataSourceId", outcome.dataSourceId()); - data.put("imageUrl", outcome.imageUrl()); - - return ResponseEntity.ok(new RsData<>("200", "자료가 수정됐습니다.", data)); - } - - // ===== 검색 ===== - @Operation(summary = "자료 검색", description = "내 PersonalArchive 안에 자료들을 검색합니다.") - @GetMapping("") - public ResponseEntity>> search( - @RequestParam(required = false) String title, - @RequestParam(required = false) String summary, - @RequestParam(required = false) String category, - @RequestParam(required = false) String keyword, - @RequestParam(required = false) Integer folderId, - @RequestParam(required = false) String folderName, - @RequestParam(required = false, defaultValue = "true") Boolean isActive, - @PageableDefault(size = 8, sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable, - @AuthenticationPrincipal CustomUserDetails user - ) { - Category categoryEnum = category != null ? Category.from(category) : null; - var cond = DataSourceSearchCondition.builder() - .title(title).summary(summary).category(categoryEnum).folderId(folderId) - .folderName(folderName).isActive(isActive).keyword(keyword).build(); - - Page page = personalApp.search(user.getMember().getId(), cond, pageable); - String sorted = pageable.getSort().toString().replace(": ", ","); - - var pageInfo = new PageInfo( - page.getNumber(), page.getSize(), page.getTotalElements(), page.getTotalPages(), - page.isFirst(), page.isLast(), sorted - ); - var body = new SearchResponse<>(page.getContent(), pageInfo); - - return ResponseEntity.ok(new RsData<>("200", "복수개의 자료가 조회됐습니다.", body)); - } -} \ No newline at end of file diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/crawler/controller/CrawlerTestController.java b/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/crawler/controller/CrawlerTestController.java deleted file mode 100644 index e6c28f50..00000000 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/crawler/controller/CrawlerTestController.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.tuna.zoopzoop.backend.domain.datasource.crawler.controller; - -import lombok.RequiredArgsConstructor; -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; -import org.tuna.zoopzoop.backend.domain.datasource.crawler.service.CrawlerManagerService; -import org.tuna.zoopzoop.backend.domain.datasource.dataprocessor.service.DataProcessorService; -import org.tuna.zoopzoop.backend.domain.datasource.dto.DataSourceDto; -import org.tuna.zoopzoop.backend.domain.datasource.repository.TagRepository; - -import java.util.ArrayList; - -@RestController -@RequestMapping("api/v1") -@RequiredArgsConstructor -public class CrawlerTestController { - private final CrawlerManagerService crawlerManagerService; - private final DataProcessorService dataProcessorService; - private final TagRepository tagRepository; - - @GetMapping("/crawl") - public DataSourceDto crawl(@RequestParam String url) throws Exception { - return dataProcessorService.process(url, new ArrayList<>()); - } -} diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/crawler/dto/CrawlerResult.java b/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/crawler/dto/CrawlerResult.java deleted file mode 100644 index c65a760b..00000000 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/crawler/dto/CrawlerResult.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.tuna.zoopzoop.backend.domain.datasource.crawler.dto; - -public record CrawlerResult( - CrawlerType type, // SPECIFIC or UNSPECIFIC - T data -) { - public enum CrawlerType { - SPECIFIC, UNSPECIFIC - } -} diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/crawler/dto/SpecificSiteDto.java b/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/crawler/dto/SpecificSiteDto.java deleted file mode 100644 index db524006..00000000 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/crawler/dto/SpecificSiteDto.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.tuna.zoopzoop.backend.domain.datasource.crawler.dto; - -import java.time.LocalDate; - -public record SpecificSiteDto( - String title, // 제목 - LocalDate dataCreatedDate, // 작성일자 - String content, // ai한테 줘야할 내용 - String imageUrl, // 썸네일 이미지 url - String source // 출처 -) { - @Override - public String toString() { - return "SpecificSiteDto {\n" + - " title='" + title + "',\n" + - " dataCreatedDate=" + dataCreatedDate + ",\n" + - " content='" + content + "',\n" + - " imageUrl='" + imageUrl + "',\n" + - " source='" + source + "'\n" + - "}"; - } -} diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/crawler/dto/UnspecificSiteDto.java b/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/crawler/dto/UnspecificSiteDto.java deleted file mode 100644 index 0f4200e0..00000000 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/crawler/dto/UnspecificSiteDto.java +++ /dev/null @@ -1,6 +0,0 @@ -package org.tuna.zoopzoop.backend.domain.datasource.crawler.dto; - -public record UnspecificSiteDto( - String rawHtml // 불특정 사이트의 html 전문 -) { -} diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/crawler/service/Crawler.java b/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/crawler/service/Crawler.java deleted file mode 100644 index d100093e..00000000 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/crawler/service/Crawler.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.tuna.zoopzoop.backend.domain.datasource.crawler.service; - -import org.jsoup.nodes.Document; -import org.tuna.zoopzoop.backend.domain.datasource.crawler.dto.CrawlerResult; - -import java.time.LocalDate; - -public interface Crawler { - - // 작성 일자가 null일 경우 기본값 설정 - // LocalDate.EPOCH(1970-01-01 - 시간이 없는 값 표현할 때 사용되는 관용적 기준점) - // 이 값이 사용되면 작성 일자가 없는 것으로 간주 - LocalDate DEFAULT_DATE = LocalDate.EPOCH; - - boolean supports(String domain); - CrawlerResult extract(Document doc); - LocalDate transLocalDate(String rawDate); -} diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/crawler/service/CrawlerManagerService.java b/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/crawler/service/CrawlerManagerService.java deleted file mode 100644 index 4b9a472f..00000000 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/crawler/service/CrawlerManagerService.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.tuna.zoopzoop.backend.domain.datasource.crawler.service; - -import lombok.RequiredArgsConstructor; -import org.jsoup.nodes.Document; -import org.springframework.stereotype.Service; -import org.tuna.zoopzoop.backend.domain.datasource.crawler.dto.CrawlerResult; - -import java.util.List; - -@Service -@RequiredArgsConstructor -public class CrawlerManagerService { - private final List crawlers; - - public CrawlerResult extractContent(String url, Document doc) { - for (Crawler crawler : crawlers) { - if (crawler.supports(url)) { - return crawler.extract(doc); - } - } - - return null; - } -} diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/crawler/service/GenericCrawler.java b/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/crawler/service/GenericCrawler.java deleted file mode 100644 index 95d9cf67..00000000 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/crawler/service/GenericCrawler.java +++ /dev/null @@ -1,57 +0,0 @@ -package org.tuna.zoopzoop.backend.domain.datasource.crawler.service; - -import org.jsoup.nodes.Document; -import org.springframework.core.Ordered; -import org.springframework.core.annotation.Order; -import org.springframework.stereotype.Component; -import org.tuna.zoopzoop.backend.domain.datasource.crawler.dto.CrawlerResult; -import org.tuna.zoopzoop.backend.domain.datasource.crawler.dto.UnspecificSiteDto; - -import java.time.LocalDate; - -@Component -@Order(Ordered.LOWEST_PRECEDENCE) // 모든 URL 대응 (우선순위 맨 뒤) -public class GenericCrawler implements Crawler { - @Override - public boolean supports(String url) { - return true; - } - - @Override - public CrawlerResult extract(Document doc) { - // img 태그 - doc.select("img[src]").forEach(el -> - el.attr("src", el.absUrl("src")) - ); - - // meta 태그 (Open Graph, Twitter Card 등) - doc.select("meta[content]").forEach(meta -> { - String absUrl = meta.absUrl("content"); - if (!absUrl.isEmpty() && !absUrl.equals(meta.attr("content"))) { - meta.attr("content", absUrl); - } - }); - - // 본문만 가져오기 (HTML) - String cleanHtml = doc.body().html() - .replaceAll("]*>.*?", "") - .replaceAll("]*>.*?", "") - // 주석 제거 - .replaceAll("", "") - // 연속된 공백 제거 - .replaceAll("\\s+", " ") - // 불필요한 속성 제거 - .replaceAll("(class|id|style|onclick|onload)=\"[^\"]*\"", "") - .trim(); - - return new CrawlerResult<>( - CrawlerResult.CrawlerType.UNSPECIFIC, - new UnspecificSiteDto(cleanHtml) - ); - } - - @Override - public LocalDate transLocalDate(String rawDate) { - return null; - } -} diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/crawler/service/NaverBlogCrawler.java b/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/crawler/service/NaverBlogCrawler.java deleted file mode 100644 index aa9f920d..00000000 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/crawler/service/NaverBlogCrawler.java +++ /dev/null @@ -1,140 +0,0 @@ -package org.tuna.zoopzoop.backend.domain.datasource.crawler.service; - -import org.jsoup.Connection; -import org.jsoup.Jsoup; -import org.jsoup.nodes.Document; -import org.jsoup.nodes.Element; -import org.jsoup.select.Elements; -import org.springframework.core.Ordered; -import org.springframework.core.annotation.Order; -import org.springframework.stereotype.Component; -import org.tuna.zoopzoop.backend.domain.datasource.crawler.dto.CrawlerResult; -import org.tuna.zoopzoop.backend.domain.datasource.crawler.dto.SpecificSiteDto; -import org.tuna.zoopzoop.backend.domain.datasource.exception.ServiceException; - -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.util.Optional; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -@Component -@Order(Ordered.HIGHEST_PRECEDENCE) -public class NaverBlogCrawler implements Crawler { - private static final SupportedDomain DOMAIN = SupportedDomain.NAVERBLOG; - private static final DateTimeFormatter NAVERBLOG_FORMATTER = - DateTimeFormatter.ofPattern("yyyy. M. d. HH:mm"); - - @Override - public boolean supports(String domain) { - return domain.contains(DOMAIN.getDomain()); - } - - @Override - public CrawlerResult extract(Document doc) { - /* - 블로그 본문은