diff --git a/.github/auto_assign.yml b/.github/auto_assign.yml new file mode 100644 index 0000000000..59bfd7cd4d --- /dev/null +++ b/.github/auto_assign.yml @@ -0,0 +1,12 @@ +addAssignees: author +numberOfReviewers: 2 +addReviewers: true +reviewers: + - BaeJinho4028 + - Soundbar91 + - ImTotem + - kih1015 + - DHkimgit + - taejinn + - dh2906 + - JanooGwan diff --git a/.github/auto_assign_team_business.yml b/.github/auto_assign_team_business.yml deleted file mode 100644 index f2789c3f3e..0000000000 --- a/.github/auto_assign_team_business.yml +++ /dev/null @@ -1,7 +0,0 @@ -addAssignees: author # 작성자를 assignee로 지정 -numberOfReviewers: 0 # 전원 리뷰어로 지정 -addReviewers: true -reviewers: - - ImTotem - - DHkimgit - - Soundbar91 diff --git a/.github/auto_assign_team_campus.yml b/.github/auto_assign_team_campus.yml deleted file mode 100644 index 7c4c5eb78e..0000000000 --- a/.github/auto_assign_team_campus.yml +++ /dev/null @@ -1,7 +0,0 @@ -addAssignees: author # 작성자를 assignee로 지정 -numberOfReviewers: 0 # 전원 리뷰어로 지정 -addReviewers: true -reviewers: - - krSeonghyeon - - Soundbar91 - - duehee diff --git a/.github/auto_assign_team_user.yml b/.github/auto_assign_team_user.yml deleted file mode 100644 index c4ded5e347..0000000000 --- a/.github/auto_assign_team_user.yml +++ /dev/null @@ -1,7 +0,0 @@ -addAssignees: author # 작성자를 assignee로 지정 -numberOfReviewers: 0 # 전원 리뷰어로 지정 -addReviewers: true -reviewers: - - Choon0414 - - Baejinho4028 - - kih1015 diff --git a/.github/workflows/flyway-dangerous-sql-validator.yml b/.github/workflows/flyway-dangerous-sql-validator.yml index a784970fb1..c85a0c2862 100644 --- a/.github/workflows/flyway-dangerous-sql-validator.yml +++ b/.github/workflows/flyway-dangerous-sql-validator.yml @@ -32,7 +32,7 @@ jobs: for file in $(git diff --name-only "$BASE" "$HEAD" -- src/main/resources/db/migration/*.sql); do ADDED_SQL=$(git diff "$BASE" "$HEAD" -- "$file" \ - | grep -i -wE "^\+.*\b(DROP|TRUNCATE|RENAME|CHANGE)\b" || true) + | grep -i -wE "^\+.*\b(DROP COLUMN|TRUNCATE|RENAME|CHANGE)\b" || true) if [[ -n "$ADDED_SQL" ]]; then echo "::error::⚠️ 위험 SQL 감지됨" diff --git a/.github/workflows/pr-label-reviewer-assigner.yml b/.github/workflows/pr-label-reviewer-assigner.yml index f840839042..a8c43c2bd3 100644 --- a/.github/workflows/pr-label-reviewer-assigner.yml +++ b/.github/workflows/pr-label-reviewer-assigner.yml @@ -2,7 +2,7 @@ name: "PR Label & Assign Reviewers" on: pull_request: - types: [opened, labeled] + types: [opened, labeled, ready_for_review] jobs: label_and_assign: @@ -20,38 +20,10 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # 3. 최신 PR 라벨 목록 조회 - - name: Fetch current PR labels - id: get_labels - uses: actions/github-script@v6 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const { data: labels } = await github.rest.issues.listLabelsOnIssue({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.payload.pull_request.number - }); - return labels.map(l => l.name); - - # 4. 라벨별 리뷰어 할당 - - name: Assign Team Business - if: contains(steps.get_labels.outputs.result, 'Team Business') - uses: kentaro-m/auto-assign-action@v2.0.0 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - configuration-path: .github/auto_assign_team_business.yml - - - name: Assign Team Campus - if: contains(steps.get_labels.outputs.result, 'Team Campus') - uses: kentaro-m/auto-assign-action@v2.0.0 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - configuration-path: .github/auto_assign_team_campus.yml - - - name: Assign Team User - if: contains(steps.get_labels.outputs.result, 'Team User') + # 3. Team Members에서 랜덤 2명 리뷰어 할당 (Draft 제외) + - name: Assign Random Reviewers + if: github.event.pull_request.draft == false uses: kentaro-m/auto-assign-action@v2.0.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} - configuration-path: .github/auto_assign_team_user.yml + configuration-path: .github/auto_assign.yml diff --git a/.github/workflows/pr-reviewers-assigner.yml b/.github/workflows/pr-reviewers-assigner.yml deleted file mode 100644 index ac88e8f14b..0000000000 --- a/.github/workflows/pr-reviewers-assigner.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: Assign Reviewers by Label -on: - pull_request: - types: [opened, labeled, unlabeled] - -jobs: - assign: - runs-on: ubuntu-latest - steps: - - name: Check if Team Business - if: contains(toJson(github.event.pull_request.labels), 'Team Business') - uses: kentaro-m/auto-assign-action@v2.0.0 - with: - configuration-path: .github/auto_assign_team_business.yml - - - name: Check if Team Campus - if: contains(toJson(github.event.pull_request.labels), 'Team Campus') - uses: kentaro-m/auto-assign-action@v2.0.0 - with: - configuration-path: .github/auto_assign_team_campus.yml - - - name: Check if Team User - if: contains(toJson(github.event.pull_request.labels), 'Team User') - uses: kentaro-m/auto-assign-action@v2.0.0 - with: - configuration-path: .github/auto_assign_team_user.yml diff --git a/README.md b/README.md index e7e45dc050..34f094c494 100644 --- a/README.md +++ b/README.md @@ -97,19 +97,40 @@ src --- -### 🧑‍🧑‍🧒‍🧒 Team Members +
+

🧑‍🧑‍🧒‍🧒 Team Members

+ +
+ +#### Active Members + +| | | | +| :--: | :--: | :--: | +| [관규](https://github.com/Soundbar91) | [동훈](https://github.com/dh2906) | [두현](https://github.com/DHkimgit) | -| | | | | +| | | | +| :--: | :--: | :--: | +| [성빈](https://github.com/ImTotem) | [진호](https://github.com/BaeJinho4028) | [태진](https://github.com/taejinn) | + +#### Former Members + +| | | | | | :--: | :--: | :--: | :--: | -| [진호](https://github.com/BaeJinho4028) | [성현](https://github.com/krSeonghyeon) | [관규](https://github.com/Soundbar91) | [성빈](https://github.com/ImTotem) | +| [다희](https://github.com/daheeParkk) | [선권](https://github.com/songsunkook) | [성재](https://github.com/seongjae6751) | [성현](https://github.com/krSeonghyeon) | -| | | | | +| | | | | | :--: | :--: | :--: | :--: | -| [정빈](https://github.com/duehee) | [현식](https://github.com/Choon0414) | [인화](https://github.com/kih1015) | [두현](https://github.com/DHkimgit) | +| [원경](https://github.com/kwoo28) | [인화](https://github.com/kih1015) | [정빈](https://github.com/duehee) | [준기](https://github.com/dradnats1012) | -| | | | | +| | | | | | :--: | :--: | :--: | :--: | -| [박종범](https://github.com/asa9874) | [박태진](https://github.com/taejinn) | [이동훈](https://github.com/dh2906) | [임아리](https://github.com/dldb-chamchi) | +| [준영](https://github.com/johnny19991006) | [준호](https://github.com/Choi-JJunho) | [한수](https://github.com/Invidam) | [현수](https://github.com/20HyeonsuLee) | + +| | +| :--: | +| [현식](https://github.com/Choon0414) | + +
--- diff --git a/src/main/java/in/koreatech/koin/admin/keyword/controller/AdminKeywordApi.java b/src/main/java/in/koreatech/koin/admin/keyword/controller/AdminKeywordApi.java index cf3d4e2cab..b39076ed6b 100644 --- a/src/main/java/in/koreatech/koin/admin/keyword/controller/AdminKeywordApi.java +++ b/src/main/java/in/koreatech/koin/admin/keyword/controller/AdminKeywordApi.java @@ -7,10 +7,12 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; import in.koreatech.koin.admin.history.aop.AdminActivityLogging; import in.koreatech.koin.admin.keyword.dto.AdminFilteredKeywordsResponse; import in.koreatech.koin.admin.keyword.dto.AdminKeywordFilterRequest; +import in.koreatech.koin.domain.community.keyword.enums.KeywordCategory; import in.koreatech.koin.global.auth.Auth; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; @@ -36,6 +38,7 @@ public interface AdminKeywordApi { @AdminActivityLogging(domain = KEYWORDS) ResponseEntity toggleKeywordFilter( @Valid @RequestBody AdminKeywordFilterRequest request, + @RequestParam(value = "type", required = false, defaultValue = "KOREATECH") KeywordCategory keywordCategory, @Auth(permit = {ADMIN}) Integer adminId ); @@ -50,6 +53,7 @@ ResponseEntity toggleKeywordFilter( @Operation(summary = "필터링 된 키워드 조회") @GetMapping("/admin/articles/keyword/filtered") ResponseEntity getFilteredKeywords( + @RequestParam(value = "type", required = false, defaultValue = "KOREATECH") KeywordCategory keywordCategory, @Auth(permit = {ADMIN}) Integer adminId ); } diff --git a/src/main/java/in/koreatech/koin/admin/keyword/controller/AdminKeywordController.java b/src/main/java/in/koreatech/koin/admin/keyword/controller/AdminKeywordController.java index 836097621a..0fb3a377e4 100644 --- a/src/main/java/in/koreatech/koin/admin/keyword/controller/AdminKeywordController.java +++ b/src/main/java/in/koreatech/koin/admin/keyword/controller/AdminKeywordController.java @@ -8,12 +8,14 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import in.koreatech.koin.admin.history.aop.AdminActivityLogging; import in.koreatech.koin.admin.keyword.dto.AdminFilteredKeywordsResponse; import in.koreatech.koin.admin.keyword.dto.AdminKeywordFilterRequest; import in.koreatech.koin.admin.keyword.service.AdminKeywordService; +import in.koreatech.koin.domain.community.keyword.enums.KeywordCategory; import in.koreatech.koin.global.auth.Auth; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @@ -29,17 +31,19 @@ public class AdminKeywordController implements AdminKeywordApi { @AdminActivityLogging(domain = KEYWORDS) public ResponseEntity toggleKeywordFilter( @Valid @RequestBody AdminKeywordFilterRequest request, + @RequestParam(value = "type", required = false, defaultValue = "KOREATECH") KeywordCategory keywordCategory, @Auth(permit = {ADMIN}) Integer adminId ) { - adminKeywordService.filterKeyword(request.keyword(), request.isFiltered()); + adminKeywordService.filterKeyword(request.keyword(), request.isFiltered(), keywordCategory); return ResponseEntity.ok().build(); } @GetMapping("/filtered") public ResponseEntity getFilteredKeywords( + @RequestParam(value = "type", required = false, defaultValue = "KOREATECH") KeywordCategory keywordCategory, @Auth(permit = {ADMIN}) Integer adminId ) { - AdminFilteredKeywordsResponse response = adminKeywordService.getFilteredKeywords(); + AdminFilteredKeywordsResponse response = adminKeywordService.getFilteredKeywords(keywordCategory); return ResponseEntity.ok(response); } } diff --git a/src/main/java/in/koreatech/koin/admin/keyword/repository/AdminArticleKeywordRepository.java b/src/main/java/in/koreatech/koin/admin/keyword/repository/AdminArticleKeywordRepository.java index cf64af684e..993013447e 100644 --- a/src/main/java/in/koreatech/koin/admin/keyword/repository/AdminArticleKeywordRepository.java +++ b/src/main/java/in/koreatech/koin/admin/keyword/repository/AdminArticleKeywordRepository.java @@ -6,16 +6,19 @@ import org.springframework.data.repository.Repository; import in.koreatech.koin.domain.community.keyword.exception.ArticleKeywordNotFoundException; +import in.koreatech.koin.domain.community.keyword.enums.KeywordCategory; import in.koreatech.koin.domain.community.keyword.model.ArticleKeyword; public interface AdminArticleKeywordRepository extends Repository { - Optional findByKeyword(String keyword); + Optional findByKeywordAndCategory(String keyword, KeywordCategory category); - default ArticleKeyword getByKeyword(String keyword) { - return findByKeyword(keyword) - .orElseThrow(() -> ArticleKeywordNotFoundException.withDetail("keyword : " + keyword)); + default ArticleKeyword getByKeywordAndCategory(String keyword, KeywordCategory category) { + return findByKeywordAndCategory(keyword, category) + .orElseThrow(() -> ArticleKeywordNotFoundException.withDetail( + "keyword : " + keyword + ", category : " + category + )); } - List findByIsFiltered(boolean isFiltered); + List findByIsFilteredAndCategory(boolean isFiltered, KeywordCategory category); } diff --git a/src/main/java/in/koreatech/koin/admin/keyword/service/AdminKeywordService.java b/src/main/java/in/koreatech/koin/admin/keyword/service/AdminKeywordService.java index 6ad1dac487..96a428d1f2 100644 --- a/src/main/java/in/koreatech/koin/admin/keyword/service/AdminKeywordService.java +++ b/src/main/java/in/koreatech/koin/admin/keyword/service/AdminKeywordService.java @@ -8,6 +8,7 @@ import in.koreatech.koin.admin.keyword.repository.AdminArticleKeywordRepository; import in.koreatech.koin.admin.keyword.dto.AdminFilteredKeywordsResponse; +import in.koreatech.koin.domain.community.keyword.enums.KeywordCategory; import in.koreatech.koin.domain.community.keyword.model.ArticleKeyword; import in.koreatech.koin.global.exception.custom.KoinIllegalArgumentException; import lombok.RequiredArgsConstructor; @@ -20,19 +21,22 @@ public class AdminKeywordService { private final AdminArticleKeywordRepository adminArticleKeywordRepository; @Transactional - public void filterKeyword(String keyword, Boolean isFiltered) { - ArticleKeyword articleKeyword = adminArticleKeywordRepository.getByKeyword(keyword); + public void filterKeyword(String keyword, Boolean isFiltered, KeywordCategory category) { + ArticleKeyword articleKeyword = adminArticleKeywordRepository.getByKeywordAndCategory(keyword, category); if (Objects.equals(articleKeyword.getIsFiltered(), isFiltered)) { String action = isFiltered ? "필터링 된" : "필터링이 취소된"; - throw new KoinIllegalArgumentException("이미 " + action + " 키워드입니다: " + keyword); + throw new KoinIllegalArgumentException("이미 " + action + " 키워드입니다: " + keyword + ", category: " + category); } articleKeyword.applyFiltered(isFiltered); } - public AdminFilteredKeywordsResponse getFilteredKeywords() { - List filteredKeywords = adminArticleKeywordRepository.findByIsFiltered(true); + public AdminFilteredKeywordsResponse getFilteredKeywords(KeywordCategory category) { + List filteredKeywords = adminArticleKeywordRepository.findByIsFilteredAndCategory( + true, + category + ); return AdminFilteredKeywordsResponse.from(filteredKeywords); } } diff --git a/src/main/java/in/koreatech/koin/admin/member/controller/AdminMemberApi.java b/src/main/java/in/koreatech/koin/admin/member/controller/AdminMemberApi.java deleted file mode 100644 index 332f91a0e7..0000000000 --- a/src/main/java/in/koreatech/koin/admin/member/controller/AdminMemberApi.java +++ /dev/null @@ -1,132 +0,0 @@ -package in.koreatech.koin.admin.member.controller; - -import static in.koreatech.koin.admin.history.enums.DomainType.MEMBERS; -import static in.koreatech.koin.domain.user.model.UserType.ADMIN; - -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestParam; - -import in.koreatech.koin.admin.history.aop.AdminActivityLogging; -import in.koreatech.koin.admin.member.dto.AdminMemberRequest; -import in.koreatech.koin.admin.member.dto.AdminMemberResponse; -import in.koreatech.koin.admin.member.dto.AdminMembersResponse; -import in.koreatech.koin.admin.member.enums.TrackTag; -import in.koreatech.koin.global.auth.Auth; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.security.SecurityRequirement; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; - -@Tag(name = "(Admin) AdminMember: BCSDLab 회원", description = "관리자 권한으로 BCSDLab 회원 정보를 관리한다") -public interface AdminMemberApi { - - @ApiResponses( - value = { - @ApiResponse(responseCode = "200"), - @ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true))), - } - ) - @Operation(summary = "페이지별 BCSDLab 회원 리스트 조회") - @SecurityRequirement(name = "Jwt Authentication") - @GetMapping("/admin/members") - ResponseEntity getMembers( - @RequestParam(name = "page", defaultValue = "1") Integer page, - @RequestParam(name = "limit", defaultValue = "50", required = false) Integer limit, - @RequestParam(name = "track") TrackTag track, - @RequestParam(name = "is_deleted", defaultValue = "false") Boolean isDeleted, - @Auth(permit = {ADMIN}) Integer adminId - ); - - @ApiResponses( - value = { - @ApiResponse(responseCode = "201"), - @ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true))), - } - ) - @Operation(summary = "BCSDLab 회원 생성") - @SecurityRequirement(name = "Jwt Authentication") - @PostMapping("/admin/members") - @AdminActivityLogging(domain = MEMBERS) - ResponseEntity createMember( - @RequestBody @Valid AdminMemberRequest request, - @Auth(permit = {ADMIN}) Integer adminId - ); - - @ApiResponses( - value = { - @ApiResponse(responseCode = "200"), - @ApiResponse(responseCode = "401", content = @Content(schema = @Schema(hidden = true))), - @ApiResponse(responseCode = "403", content = @Content(schema = @Schema(hidden = true))), - @ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true))), - } - ) - @Operation(summary = "BCSDLab 회원 조회") - @SecurityRequirement(name = "Jwt Authentication") - @GetMapping("/admin/members/{id}") - ResponseEntity getMember( - @PathVariable("id") Integer memberId, - @Auth(permit = {ADMIN}) Integer adminId - ); - - @ApiResponses( - value = { - @ApiResponse(responseCode = "200"), - @ApiResponse(responseCode = "401", content = @Content(schema = @Schema(hidden = true))), - @ApiResponse(responseCode = "403", content = @Content(schema = @Schema(hidden = true))), - @ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true))), - } - ) - @Operation(summary = "BCSDLab 회원 삭제") - @SecurityRequirement(name = "Jwt Authentication") - @DeleteMapping("/admin/members/{id}") - @AdminActivityLogging(domain = MEMBERS, domainIdParam = "memberId") - ResponseEntity deleteMember( - @PathVariable("id") Integer memberId, - @Auth(permit = {ADMIN}) Integer adminId - ); - - @ApiResponses( - value = { - @ApiResponse(responseCode = "200"), - @ApiResponse(responseCode = "401", content = @Content(schema = @Schema(hidden = true))), - @ApiResponse(responseCode = "403", content = @Content(schema = @Schema(hidden = true))), - @ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true))), - } - ) - @Operation(summary = "BCSDLab 회원 수정") - @SecurityRequirement(name = "Jwt Authentication") - @PutMapping("/admin/members/{id}") - @AdminActivityLogging(domain = MEMBERS, domainIdParam = "memberId") - ResponseEntity updateMember( - @PathVariable("id") Integer memberId, - @RequestBody @Valid AdminMemberRequest request, - @Auth(permit = {ADMIN}) Integer adminId - ); - - @ApiResponses( - value = { - @ApiResponse(responseCode = "200"), - @ApiResponse(responseCode = "401", content = @Content(schema = @Schema(hidden = true))), - @ApiResponse(responseCode = "403", content = @Content(schema = @Schema(hidden = true))), - @ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true))), - } - ) - @Operation(summary = "BCSDLab 회원 삭제 취소") - @SecurityRequirement(name = "Jwt Authentication") - @PostMapping("/admin/members/{id}/undelete") - @AdminActivityLogging(domain = MEMBERS, domainIdParam = "memberId") - ResponseEntity undeleteMember( - @PathVariable("id") Integer memberId, - @Auth(permit = {ADMIN}) Integer adminId - ); -} diff --git a/src/main/java/in/koreatech/koin/admin/member/controller/AdminMemberController.java b/src/main/java/in/koreatech/koin/admin/member/controller/AdminMemberController.java deleted file mode 100644 index b715d7eee5..0000000000 --- a/src/main/java/in/koreatech/koin/admin/member/controller/AdminMemberController.java +++ /dev/null @@ -1,92 +0,0 @@ -package in.koreatech.koin.admin.member.controller; - -import static in.koreatech.koin.admin.history.enums.DomainType.MEMBERS; -import static in.koreatech.koin.domain.user.model.UserType.ADMIN; - -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -import in.koreatech.koin.admin.history.aop.AdminActivityLogging; -import in.koreatech.koin.admin.member.dto.AdminMemberRequest; -import in.koreatech.koin.admin.member.dto.AdminMemberResponse; -import in.koreatech.koin.admin.member.dto.AdminMembersResponse; -import in.koreatech.koin.admin.member.enums.TrackTag; -import in.koreatech.koin.admin.member.service.AdminMemberService; -import in.koreatech.koin.global.auth.Auth; -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; - -@RestController -@RequiredArgsConstructor -public class AdminMemberController implements AdminMemberApi { - - private final AdminMemberService adminMemberService; - - @GetMapping("/admin/members") - public ResponseEntity getMembers( - @RequestParam(name = "page", defaultValue = "1") Integer page, - @RequestParam(name = "limit", defaultValue = "50", required = false) Integer limit, - @RequestParam(name = "track") TrackTag track, - @RequestParam(name = "is_deleted", defaultValue = "false") Boolean isDeleted, - @Auth(permit = {ADMIN}) Integer adminId - ) { - return ResponseEntity.ok().body(adminMemberService.getMembers(page, limit, track, isDeleted)); - } - - @PostMapping("/admin/members") - @AdminActivityLogging(domain = MEMBERS) - public ResponseEntity createMember( - @RequestBody @Valid AdminMemberRequest request, - @Auth(permit = {ADMIN}) Integer adminId - ) { - adminMemberService.createMember(request); - return ResponseEntity.status(HttpStatus.CREATED).build(); - } - - @GetMapping("/admin/members/{id}") - public ResponseEntity getMember( - @PathVariable("id") Integer memberId, - @Auth(permit = {ADMIN}) Integer adminId - ) { - return ResponseEntity.ok().body(adminMemberService.getMember(memberId)); - } - - @DeleteMapping("/admin/members/{id}") - @AdminActivityLogging(domain = MEMBERS, domainIdParam = "memberId") - public ResponseEntity deleteMember( - @PathVariable("id") Integer memberId, - @Auth(permit = {ADMIN}) Integer adminId - ) { - adminMemberService.deleteMember(memberId); - return ResponseEntity.ok().build(); - } - - @PutMapping("/admin/members/{id}") - @AdminActivityLogging(domain = MEMBERS, domainIdParam = "memberId") - public ResponseEntity updateMember( - @PathVariable("id") Integer memberId, - @RequestBody @Valid AdminMemberRequest request, - @Auth(permit = {ADMIN}) Integer adminId - ) { - adminMemberService.updateMember(memberId, request); - return ResponseEntity.ok().build(); - } - - @PostMapping("/admin/members/{id}/undelete") - @AdminActivityLogging(domain = MEMBERS, domainIdParam = "memberId") - public ResponseEntity undeleteMember( - @PathVariable("id") Integer memberId, - @Auth(permit = {ADMIN}) Integer adminId - ) { - adminMemberService.undeleteMember(memberId); - return ResponseEntity.ok().build(); - } -} diff --git a/src/main/java/in/koreatech/koin/admin/member/controller/AdminTrackApi.java b/src/main/java/in/koreatech/koin/admin/member/controller/AdminTrackApi.java deleted file mode 100644 index 94101dfdea..0000000000 --- a/src/main/java/in/koreatech/koin/admin/member/controller/AdminTrackApi.java +++ /dev/null @@ -1,175 +0,0 @@ -package in.koreatech.koin.admin.member.controller; - -import static in.koreatech.koin.admin.history.enums.DomainType.TECHSTACKS; -import static in.koreatech.koin.admin.history.enums.DomainType.TRACKS; -import static in.koreatech.koin.domain.user.model.UserType.ADMIN; - -import java.util.List; - -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestParam; - -import in.koreatech.koin.admin.history.aop.AdminActivityLogging; -import in.koreatech.koin.admin.member.dto.AdminTechStackRequest; -import in.koreatech.koin.admin.member.dto.AdminTechStackResponse; -import in.koreatech.koin.admin.member.dto.AdminTrackRequest; -import in.koreatech.koin.admin.member.dto.AdminTrackResponse; -import in.koreatech.koin.admin.member.dto.AdminTrackSingleResponse; -import in.koreatech.koin.global.auth.Auth; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.security.SecurityRequirement; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; - -@Tag(name = "(Admin) Track: BCSDLab 트랙", description = "관리자 권한으로 BCSDLab 트랙 정보를 관리한다") -public interface AdminTrackApi { - - @ApiResponses( - value = { - @ApiResponse(responseCode = "200"), - @ApiResponse(responseCode = "401", content = @Content(schema = @Schema(hidden = true))), - @ApiResponse(responseCode = "403", content = @Content(schema = @Schema(hidden = true))), - @ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true))), - } - ) - @Operation(summary = "트랙 목록 조회") - @SecurityRequirement(name = "Jwt Authentication") - @GetMapping("/admin/tracks") - ResponseEntity> getTracks( - @Auth(permit = {ADMIN}) Integer adminId - ); - - @ApiResponses( - value = { - @ApiResponse(responseCode = "200"), - @ApiResponse(responseCode = "401", content = @Content(schema = @Schema(hidden = true))), - @ApiResponse(responseCode = "403", content = @Content(schema = @Schema(hidden = true))), - @ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true))), - @ApiResponse(responseCode = "409", content = @Content(schema = @Schema(hidden = true))), - } - ) - @Operation(summary = "트랙 생성") - @SecurityRequirement(name = "Jwt Authentication") - @PostMapping("/admin/tracks") - @AdminActivityLogging(domain = TRACKS) - ResponseEntity createTrack( - @RequestBody @Valid AdminTrackRequest request, - @Auth(permit = {ADMIN}) Integer adminId - ); - - @ApiResponses( - value = { - @ApiResponse(responseCode = "200"), - @ApiResponse(responseCode = "401", content = @Content(schema = @Schema(hidden = true))), - @ApiResponse(responseCode = "403", content = @Content(schema = @Schema(hidden = true))), - @ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true))), - } - ) - @Operation(summary = "트랙 단건 조회") - @SecurityRequirement(name = "Jwt Authentication") - @GetMapping("/admin/tracks/{id}") - ResponseEntity getTrack( - @PathVariable("id") Integer trackId, - @Auth(permit = {ADMIN}) Integer adminId - ); - - @ApiResponses( - value = { - @ApiResponse(responseCode = "200"), - @ApiResponse(responseCode = "401", content = @Content(schema = @Schema(hidden = true))), - @ApiResponse(responseCode = "403", content = @Content(schema = @Schema(hidden = true))), - @ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true))), - @ApiResponse(responseCode = "409", content = @Content(schema = @Schema(hidden = true))), - } - ) - @Operation(summary = "트랙 수정") - @SecurityRequirement(name = "Jwt Authentication") - @PutMapping("/admin/tracks/{id}") - @AdminActivityLogging(domain = TRACKS, domainIdParam = "trackId") - ResponseEntity updateTrack( - @PathVariable("id") Integer trackId, - @RequestBody @Valid AdminTrackRequest request, - @Auth(permit = {ADMIN}) Integer adminId - ); - - @ApiResponses( - value = { - @ApiResponse(responseCode = "200"), - @ApiResponse(responseCode = "204", content = @Content(schema = @Schema(hidden = true))), - @ApiResponse(responseCode = "401", content = @Content(schema = @Schema(hidden = true))), - @ApiResponse(responseCode = "403", content = @Content(schema = @Schema(hidden = true))), - } - ) - @Operation(summary = "트랙 삭제") - @SecurityRequirement(name = "Jwt Authentication") - @DeleteMapping("/admin/tracks/{id}") - @AdminActivityLogging(domain = TRACKS, domainIdParam = "trackId") - ResponseEntity deleteTrack( - @PathVariable("id") Integer trackId, - @Auth(permit = {ADMIN}) Integer adminId - ); - - @ApiResponses( - value = { - @ApiResponse(responseCode = "200"), - @ApiResponse(responseCode = "401", content = @Content(schema = @Schema(hidden = true))), - @ApiResponse(responseCode = "403", content = @Content(schema = @Schema(hidden = true))), - @ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true))), - } - ) - @Operation(summary = "기술스택 생성") - @SecurityRequirement(name = "Jwt Authentication") - @PostMapping("/admin/techStacks") - @AdminActivityLogging(domain = TECHSTACKS) - ResponseEntity createTechStack( - @RequestBody @Valid AdminTechStackRequest request, - @RequestParam(value = "trackName") String trackName, - @Auth(permit = {ADMIN}) Integer adminId - ); - - @ApiResponses( - value = { - @ApiResponse(responseCode = "200"), - @ApiResponse(responseCode = "401", content = @Content(schema = @Schema(hidden = true))), - @ApiResponse(responseCode = "403", content = @Content(schema = @Schema(hidden = true))), - @ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true))), - } - ) - @Operation(summary = "기술스택 수정") - @SecurityRequirement(name = "Jwt Authentication") - @PutMapping("/admin/techStacks/{id}") - @AdminActivityLogging(domain = TECHSTACKS, domainIdParam = "techStackId") - ResponseEntity updateTechStack( - @RequestBody @Valid AdminTechStackRequest request, - @RequestParam(value = "trackName") String trackName, - @PathVariable("id") Integer techStackId, - @Auth(permit = {ADMIN}) Integer adminId - ); - - @ApiResponses( - value = { - @ApiResponse(responseCode = "200"), - @ApiResponse(responseCode = "204", content = @Content(schema = @Schema(hidden = true))), - @ApiResponse(responseCode = "401", content = @Content(schema = @Schema(hidden = true))), - @ApiResponse(responseCode = "403", content = @Content(schema = @Schema(hidden = true))), - } - ) - @Operation(summary = "기술스택 삭제") - @SecurityRequirement(name = "Jwt Authentication") - @DeleteMapping("/admin/techStacks/{id}") - @AdminActivityLogging(domain = TECHSTACKS, domainIdParam = "techStackId") - ResponseEntity deleteTechStack( - @PathVariable("id") Integer techStackId, - @Auth(permit = {ADMIN}) Integer adminId - ); -} diff --git a/src/main/java/in/koreatech/koin/admin/member/controller/AdminTrackController.java b/src/main/java/in/koreatech/koin/admin/member/controller/AdminTrackController.java deleted file mode 100644 index fb3324a319..0000000000 --- a/src/main/java/in/koreatech/koin/admin/member/controller/AdminTrackController.java +++ /dev/null @@ -1,116 +0,0 @@ -package in.koreatech.koin.admin.member.controller; - -import static in.koreatech.koin.admin.history.enums.DomainType.TECHSTACKS; -import static in.koreatech.koin.admin.history.enums.DomainType.TRACKS; -import static in.koreatech.koin.domain.user.model.UserType.ADMIN; - -import java.util.List; - -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -import in.koreatech.koin.admin.history.aop.AdminActivityLogging; -import in.koreatech.koin.admin.member.dto.AdminTechStackRequest; -import in.koreatech.koin.admin.member.dto.AdminTechStackResponse; -import in.koreatech.koin.admin.member.dto.AdminTrackRequest; -import in.koreatech.koin.admin.member.dto.AdminTrackResponse; -import in.koreatech.koin.admin.member.dto.AdminTrackSingleResponse; -import in.koreatech.koin.admin.member.service.AdminTrackService; -import in.koreatech.koin.global.auth.Auth; -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; - -@RestController -@RequiredArgsConstructor -public class AdminTrackController implements AdminTrackApi { - - private final AdminTrackService adminTrackService; - - @GetMapping("/admin/tracks") - public ResponseEntity> getTracks( - @Auth(permit = {ADMIN}) Integer adminId - ) { - var response = adminTrackService.getTracks(); - return ResponseEntity.ok(response); - } - - @PostMapping("/admin/tracks") - @AdminActivityLogging(domain = TRACKS) - public ResponseEntity createTrack( - @RequestBody @Valid AdminTrackRequest request, - @Auth(permit = {ADMIN}) Integer adminId - ) { - var response = adminTrackService.createTrack(request); - return ResponseEntity.ok(response); - } - - @GetMapping("/admin/tracks/{id}") - public ResponseEntity getTrack( - @PathVariable("id") Integer trackId, - @Auth(permit = {ADMIN}) Integer adminId - ) { - var response = adminTrackService.getTrack(trackId); - return ResponseEntity.ok(response); - } - - @PutMapping("/admin/tracks/{id}") - @AdminActivityLogging(domain = TRACKS, domainIdParam = "trackId") - public ResponseEntity updateTrack( - @PathVariable("id") Integer trackId, - @RequestBody @Valid AdminTrackRequest request, - @Auth(permit = {ADMIN}) Integer adminId - ) { - var response = adminTrackService.updateTrack(trackId, request); - return ResponseEntity.ok(response); - } - - @DeleteMapping("/admin/tracks/{id}") - @AdminActivityLogging(domain = TRACKS, domainIdParam = "trackId") - public ResponseEntity deleteTrack( - @PathVariable("id") Integer trackId, - @Auth(permit = {ADMIN}) Integer adminId - ) { - adminTrackService.deleteTrack(trackId); - return ResponseEntity.ok().build(); - } - - @PostMapping("/admin/techStacks") - @AdminActivityLogging(domain = TECHSTACKS) - public ResponseEntity createTechStack( - @RequestBody @Valid AdminTechStackRequest request, - @RequestParam String trackName, - @Auth(permit = {ADMIN}) Integer adminId - ) { - var response = adminTrackService.createTechStack(request, trackName); - return ResponseEntity.ok(response); - } - - @PutMapping("/admin/techStacks/{id}") - @AdminActivityLogging(domain = TECHSTACKS, domainIdParam = "techStackId") - public ResponseEntity updateTechStack( - @RequestBody @Valid AdminTechStackRequest request, - @RequestParam String trackName, - @PathVariable("id") Integer techStackId, - @Auth(permit = {ADMIN}) Integer adminId - ) { - var response = adminTrackService.updateTechStack(request, trackName, techStackId); - return ResponseEntity.ok(response); - } - - @DeleteMapping("/admin/techStacks/{id}") - @AdminActivityLogging(domain = TECHSTACKS, domainIdParam = "techStackId") - public ResponseEntity deleteTechStack( - @PathVariable("id") Integer techStackId, - @Auth(permit = {ADMIN}) Integer adminId - ) { - adminTrackService.deleteTechStack(techStackId); - return ResponseEntity.ok().build(); - } -} diff --git a/src/main/java/in/koreatech/koin/admin/member/dto/AdminMemberRequest.java b/src/main/java/in/koreatech/koin/admin/member/dto/AdminMemberRequest.java deleted file mode 100644 index 015a9aa0b4..0000000000 --- a/src/main/java/in/koreatech/koin/admin/member/dto/AdminMemberRequest.java +++ /dev/null @@ -1,55 +0,0 @@ -package in.koreatech.koin.admin.member.dto; - -import static com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; -import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; - -import com.fasterxml.jackson.databind.annotation.JsonNaming; - -import in.koreatech.koin.domain.member.model.Member; -import in.koreatech.koin.domain.member.model.Track; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Pattern; -import jakarta.validation.constraints.Size; - -@JsonNaming(value = SnakeCaseStrategy.class) -public record AdminMemberRequest( - @NotNull(message = "이름은 필수입니다.") - @Size(max = 50, message = "이름의 길이는 최대 50자입니다.") - @Schema(description = "이름", example = "김주원", requiredMode = REQUIRED) - String name, - - @Size(max = 10, message = "학번의 길이는 최대 10자입니다.") - @Schema(description = "소속 트랙", example = "2019136037") - String studentNumber, - - @NotNull(message = "트랙은 필수입니다.") - @Pattern(regexp = "^(Android|BackEnd|FrontEnd|Game|UI\\/UX|HR|iOS|P&M)$", message = "트랙의 형식이 올바르지 않습니다.") - @Schema(description = "고유 id", example = "BackEnd", requiredMode = REQUIRED) - String track, - - @NotNull(message = "직급은 필수입니다.") - @Pattern(regexp = "^(Mentor|Regular)$", message = "직급의 형식이 올바르지 않습니다.") - @Schema(description = "직급", example = "Regular", requiredMode = REQUIRED) - String position, - - @Email(message = "이메일의 길이는 최대 100자입니다.") - @Schema(description = "이메일", example = "damiano102777@naver.com") - String email, - - @Size(max = 65535, message = "이미지 링크의 길이는 최대 65535자입니다.") - @Schema(description = "이미지 링크", example = "https://example.com") - String imageUrl -) { - public Member toMember(Track track) { - return Member.builder() - .name(name) - .studentNumber(studentNumber) - .track(track) - .position(position) - .email(email) - .imageUrl(imageUrl) - .build(); - } -} diff --git a/src/main/java/in/koreatech/koin/admin/member/dto/AdminMemberResponse.java b/src/main/java/in/koreatech/koin/admin/member/dto/AdminMemberResponse.java deleted file mode 100644 index b73a371acf..0000000000 --- a/src/main/java/in/koreatech/koin/admin/member/dto/AdminMemberResponse.java +++ /dev/null @@ -1,49 +0,0 @@ -package in.koreatech.koin.admin.member.dto; - -import static com.fasterxml.jackson.databind.PropertyNamingStrategies.*; -import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; - -import com.fasterxml.jackson.databind.annotation.JsonNaming; - -import in.koreatech.koin.domain.member.model.Member; -import io.swagger.v3.oas.annotations.media.Schema; - -@JsonNaming(value = SnakeCaseStrategy.class) -public record AdminMemberResponse( - @Schema(description = "고유 id", example = "1", requiredMode = REQUIRED) - Integer id, - - @Schema(description = "이름", example = "김주원", requiredMode = REQUIRED) - String name, - - @Schema(description = "학번", example = "2019136037") - String studentNumber, - - @Schema(description = "트랙 이름", example = "BackEnd", requiredMode = REQUIRED) - String track, - - @Schema(description = "직책", example = "Regular", requiredMode = REQUIRED) - String position, - - @Schema(description = "이메일", example = "damiano102777@naver.com") - String email, - - @Schema(description = "이미지 url", example = "https://static.koreatech.in/test.png") - String imageUrl, - - @Schema(description = "삭제(soft delete) 여부", example = "false", requiredMode = REQUIRED) - boolean isDeleted -) { - public static AdminMemberResponse from(Member member) { - return new AdminMemberResponse( - member.getId(), - member.getName(), - member.getStudentNumber(), - member.getTrack() != null ? member.getTrack().getName() : null, - member.getPosition(), - member.getEmail(), - member.getImageUrl(), - member.isDeleted() - ); - } -} diff --git a/src/main/java/in/koreatech/koin/admin/member/dto/AdminMembersResponse.java b/src/main/java/in/koreatech/koin/admin/member/dto/AdminMembersResponse.java deleted file mode 100644 index 8fd942268b..0000000000 --- a/src/main/java/in/koreatech/koin/admin/member/dto/AdminMembersResponse.java +++ /dev/null @@ -1,86 +0,0 @@ -package in.koreatech.koin.admin.member.dto; - -import static com.fasterxml.jackson.databind.PropertyNamingStrategies.*; -import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; - -import java.util.List; -import java.util.stream.Collectors; - -import org.springframework.data.domain.Page; - -import com.fasterxml.jackson.databind.annotation.JsonNaming; - -import in.koreatech.koin.domain.member.model.Member; -import in.koreatech.koin.common.model.Criteria; -import io.swagger.v3.oas.annotations.media.Schema; - -@JsonNaming(value = SnakeCaseStrategy.class) -public record AdminMembersResponse( - @Schema(description = "조건에 해당하는 총 회원의 수", example = "57", requiredMode = REQUIRED) - Long totalCount, - - @Schema(description = "조건에 해당하는 회원 중에 현재 페이지에서 조회된 수", example = "10", requiredMode = REQUIRED) - Integer currentCount, - - @Schema(description = "조건에 해당하는 회원들을 조회할 수 있는 최대 페이지", example = "6", requiredMode = REQUIRED) - Integer totalPage, - - @Schema(description = "현재 페이지", example = "2", requiredMode = REQUIRED) - Integer currentPage, - - @Schema(description = "회원 정보 리스트", requiredMode = REQUIRED) - List members -) { - public static AdminMembersResponse of(Page pagedResult, Criteria criteria) { - return new AdminMembersResponse( - pagedResult.getTotalElements(), - pagedResult.getContent().size(), - pagedResult.getTotalPages(), - criteria.getPage() + 1, - pagedResult.getContent() - .stream() - .map(SimpleMemberInformation::from) - .collect(Collectors.toList()) - ); - } - @JsonNaming(value = SnakeCaseStrategy.class) - private record SimpleMemberInformation( - @Schema(description = "고유 id", example = "1", requiredMode = REQUIRED) - Integer id, - - @Schema(description = "이름", example = "김주원", requiredMode = REQUIRED) - String name, - - @Schema(description = "학번", example = "2019136037") - String studentNumber, - - @Schema(description = "트랙 이름", example = "BackEnd", requiredMode = REQUIRED) - String track, - - @Schema(description = "직책", example = "Regular", requiredMode = REQUIRED) - String position, - - @Schema(description = "이메일", example = "damiano102777@naver.com") - String email, - - @Schema(description = "이미지 url", example = "https://static.koreatech.in/test.png") - String imageUrl, - - @Schema(description = "삭제(soft delete) 여부", example = "false", requiredMode = REQUIRED) - boolean isDeleted - ) { - - public static SimpleMemberInformation from(Member member) { - return new SimpleMemberInformation( - member.getId(), - member.getName(), - member.getStudentNumber(), - member.getTrack().getName(), - member.getPosition(), - member.getEmail(), - member.getImageUrl(), - member.isDeleted() - ); - } - } -} diff --git a/src/main/java/in/koreatech/koin/admin/member/dto/AdminTechStackRequest.java b/src/main/java/in/koreatech/koin/admin/member/dto/AdminTechStackRequest.java deleted file mode 100644 index 1643ef943e..0000000000 --- a/src/main/java/in/koreatech/koin/admin/member/dto/AdminTechStackRequest.java +++ /dev/null @@ -1,37 +0,0 @@ -package in.koreatech.koin.admin.member.dto; - -import static com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; -import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; - -import com.fasterxml.jackson.databind.annotation.JsonNaming; - -import in.koreatech.koin.domain.member.model.TechStack; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotBlank; - -@JsonNaming(SnakeCaseStrategy.class) -public record AdminTechStackRequest( - @Schema(description = "이미지 링크", example = "http://url.com") - String imageUrl, - - @Schema(description = "기술 스택명", example = "Spring", requiredMode = REQUIRED) - @NotBlank(message = "기술 스택명은 비워둘 수 없습니다.") - String name, - - @Schema(description = "기술 스택 설명", example = "스프링은 웹 프레임워크이다") - String description, - - @Schema(description = "삭제 여부", example = "false") - boolean isDeleted -) { - - public TechStack toEntity(Integer trackId) { - return TechStack.builder() - .imageUrl(imageUrl) - .trackId(trackId) - .name(name) - .description(description) - .isDeleted(isDeleted) - .build(); - } -} diff --git a/src/main/java/in/koreatech/koin/admin/member/dto/AdminTechStackResponse.java b/src/main/java/in/koreatech/koin/admin/member/dto/AdminTechStackResponse.java deleted file mode 100644 index 3562d76354..0000000000 --- a/src/main/java/in/koreatech/koin/admin/member/dto/AdminTechStackResponse.java +++ /dev/null @@ -1,56 +0,0 @@ -package in.koreatech.koin.admin.member.dto; - -import static com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; -import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; - -import java.time.LocalDateTime; - -import com.fasterxml.jackson.annotation.JsonFormat; -import com.fasterxml.jackson.databind.annotation.JsonNaming; - -import in.koreatech.koin.domain.member.model.TechStack; -import io.swagger.v3.oas.annotations.media.Schema; - -@JsonNaming(SnakeCaseStrategy.class) -public record AdminTechStackResponse( - - @Schema(description = "기술스택 고유 ID", example = "1", requiredMode = REQUIRED) - Integer id, - - @Schema(description = "이미지 링크", example = "http://url.com", requiredMode = REQUIRED) - String imageUrl, - - @Schema(description = "기술 스택명", example = "Spring", requiredMode = REQUIRED) - String name, - - @Schema(description = "기술 스택 설명", example = "스프링은 웹 프레임워크이다", requiredMode = REQUIRED) - String description, - - @Schema(description = "트랙 고유 ID", example = "2", requiredMode = REQUIRED) - Integer trackId, - - @Schema(description = "삭제 여부", example = "false", requiredMode = REQUIRED) - Boolean isDeleted, - - @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") - @Schema(description = "생성 일자", example = "2024-01-15 12:00:00", requiredMode = REQUIRED) - LocalDateTime createdAt, - - @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") - @Schema(description = "수정 일자", example = "2024-01-15 12:00:00", requiredMode = REQUIRED) - LocalDateTime updatedAt -) { - - public static AdminTechStackResponse from(TechStack techStack) { - return new AdminTechStackResponse( - techStack.getId(), - techStack.getImageUrl(), - techStack.getName(), - techStack.getDescription(), - techStack.getTrackId(), - techStack.isDeleted(), - techStack.getCreatedAt(), - techStack.getUpdatedAt() - ); - } -} diff --git a/src/main/java/in/koreatech/koin/admin/member/dto/AdminTrackRequest.java b/src/main/java/in/koreatech/koin/admin/member/dto/AdminTrackRequest.java deleted file mode 100644 index cbe9b6e1a0..0000000000 --- a/src/main/java/in/koreatech/koin/admin/member/dto/AdminTrackRequest.java +++ /dev/null @@ -1,34 +0,0 @@ -package in.koreatech.koin.admin.member.dto; - -import static com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; -import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; - -import com.fasterxml.jackson.databind.annotation.JsonNaming; - -import in.koreatech.koin.domain.member.model.Track; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; - -@JsonNaming(SnakeCaseStrategy.class) -public record AdminTrackRequest( - @Schema(description = "트랙 명", example = "Backend", requiredMode = REQUIRED) - @NotBlank(message = "트랙명은 비워둘 수 없습니다.") - String name, - - @Schema(description = "인원 수", example = "15") - @NotNull(message = "인원 수는 비워둘 수 없습니다.") - Integer headcount, - - @Schema(description = "삭제 여부", example = "false") - boolean isDeleted -) { - - public Track toEntity() { - return Track.builder() - .name(name) - .headcount(headcount) - .isDeleted(isDeleted) - .build(); - } -} diff --git a/src/main/java/in/koreatech/koin/admin/member/dto/AdminTrackResponse.java b/src/main/java/in/koreatech/koin/admin/member/dto/AdminTrackResponse.java deleted file mode 100644 index db90979c45..0000000000 --- a/src/main/java/in/koreatech/koin/admin/member/dto/AdminTrackResponse.java +++ /dev/null @@ -1,47 +0,0 @@ -package in.koreatech.koin.admin.member.dto; - -import static com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; -import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; - -import java.time.LocalDateTime; - -import com.fasterxml.jackson.annotation.JsonFormat; -import com.fasterxml.jackson.databind.annotation.JsonNaming; - -import in.koreatech.koin.domain.member.model.Track; -import io.swagger.v3.oas.annotations.media.Schema; - -@JsonNaming(SnakeCaseStrategy.class) -public record AdminTrackResponse( - @Schema(description = "트랙 고유 ID", example = "1", requiredMode = REQUIRED) - Integer id, - - @Schema(description = "트랙 명", example = "Backend", requiredMode = REQUIRED) - String name, - - @Schema(description = "인원 수", example = "15", requiredMode = REQUIRED) - Integer headcount, - - @Schema(description = "삭제 여부", example = "false", requiredMode = REQUIRED) - Boolean isDeleted, - - @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") - @Schema(description = "생성 일자", example = "2023-01-04 12:00:01", requiredMode = REQUIRED) - LocalDateTime createdAt, - - @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") - @Schema(description = "수정 일자", example = "2023-01-04 12:00:01", requiredMode = REQUIRED) - LocalDateTime updatedAt -) { - - public static AdminTrackResponse from(Track track) { - return new AdminTrackResponse( - track.getId(), - track.getName(), - track.getHeadcount(), - track.isDeleted(), - track.getCreatedAt(), - track.getUpdatedAt() - ); - } -} diff --git a/src/main/java/in/koreatech/koin/admin/member/dto/AdminTrackSingleResponse.java b/src/main/java/in/koreatech/koin/admin/member/dto/AdminTrackSingleResponse.java deleted file mode 100644 index 13329280e6..0000000000 --- a/src/main/java/in/koreatech/koin/admin/member/dto/AdminTrackSingleResponse.java +++ /dev/null @@ -1,136 +0,0 @@ -package in.koreatech.koin.admin.member.dto; - -import static com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; -import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED; -import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; - -import java.time.LocalDateTime; -import java.util.List; - -import com.fasterxml.jackson.annotation.JsonFormat; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonNaming; - -import in.koreatech.koin.domain.member.model.Member; -import in.koreatech.koin.domain.member.model.TechStack; -import in.koreatech.koin.domain.member.model.Track; -import io.swagger.v3.oas.annotations.media.Schema; - -@JsonNaming(SnakeCaseStrategy.class) -public record AdminTrackSingleResponse( - @JsonProperty("TrackName") - @Schema(description = "트랙 명", example = "Backend", requiredMode = REQUIRED) - String trackName, - - @JsonProperty("Members") - List innerMemberResponses, - - @JsonProperty("TechStacks") - List innerTechStackResponses -) { - - public static AdminTrackSingleResponse of(Track track, List members, List techStacks) { - return new AdminTrackSingleResponse( - track.getName(), - members.stream() - .map(member -> InnerMemberResponse.from(member, track.getName())) - .toList(), - techStacks.stream() - .map(InnerTechStackResponse::from) - .toList() - ); - } - - @JsonNaming(value = SnakeCaseStrategy.class) - private record InnerMemberResponse( - @Schema(description = "BCSD 회원 고유 ID", example = "1", requiredMode = REQUIRED) - Integer id, - - @Schema(description = "이름", example = "최준호", requiredMode = REQUIRED) - String name, - - @Schema(description = "학번", example = "2019136135", requiredMode = NOT_REQUIRED) - String studentNumber, - - @Schema(description = "동아리 포지션 `Beginner`, `Regular`, `Mentor`", example = "Regular", requiredMode = REQUIRED) - String position, - - @Schema(description = "트랙 명", example = "Backend", requiredMode = REQUIRED) - String track, - - @Schema(description = "이메일", example = "koin123@koreatech.ac.kr", requiredMode = NOT_REQUIRED) - String email, - - @Schema(description = "이미지 Url", example = "https://static.koreatech.in/example/image.png", requiredMode = NOT_REQUIRED) - String imageUrl, - - @Schema(description = "삭제 여부", example = "false", requiredMode = REQUIRED) - Boolean isDeleted, - - @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") - @Schema(description = "생성 일자", example = "2023-01-04 12:00:01", requiredMode = REQUIRED) - LocalDateTime createdAt, - - @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") - @Schema(description = "수정 일자", example = "2023-01-04 12:00:01", requiredMode = REQUIRED) - LocalDateTime updatedAt - ) { - - public static InnerMemberResponse from(Member member, String trackName) { - return new InnerMemberResponse( - member.getId(), - member.getName(), - member.getStudentNumber(), - member.getPosition(), - trackName, - member.getEmail(), - member.getImageUrl(), - member.isDeleted(), - member.getCreatedAt(), - member.getUpdatedAt() - ); - } - } - - @JsonNaming(value = SnakeCaseStrategy.class) - private record InnerTechStackResponse( - @Schema(description = "기술 스택 고유 ID", example = "1", requiredMode = REQUIRED) - Integer id, - - @Schema(description = "기술 이름", example = "Backend") - String name, - - @Schema(description = "기술 설명", example = "15") - String description, - - @Schema(description = "이미지 Url", example = "https://static.koreatech.in/example/image.png") - String imageUrl, - - @Schema(description = "트랙 ID", example = "1") - Integer trackId, - - @Schema(description = "삭제 여부", example = "false") - Boolean isDeleted, - - @Schema(description = "생성 일자", example = "2023-01-04 12:00:01") - @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime createdAt, - - @Schema(description = "수정 일자", example = "2023-01-04 12:00:01") - @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime updatedAt - ) { - - public static InnerTechStackResponse from(TechStack techStack) { - return new InnerTechStackResponse( - techStack.getId(), - techStack.getName(), - techStack.getDescription(), - techStack.getImageUrl(), - techStack.getTrackId(), - techStack.isDeleted(), - techStack.getCreatedAt(), - techStack.getUpdatedAt() - ); - } - } -} - diff --git a/src/main/java/in/koreatech/koin/admin/member/enums/TrackTag.java b/src/main/java/in/koreatech/koin/admin/member/enums/TrackTag.java deleted file mode 100644 index 62324c16c5..0000000000 --- a/src/main/java/in/koreatech/koin/admin/member/enums/TrackTag.java +++ /dev/null @@ -1,22 +0,0 @@ -package in.koreatech.koin.admin.member.enums; - -import lombok.Getter; - -@Getter -public enum TrackTag { - ANDROID("Android"), - BACKEND("BackEnd"), - FRONTEND("FrontEnd"), - GAME("Game"), - PM("P&M"), - HR("HR"), - UI_UX("UI/UX"), - IOS("iOS"), - ; - - private final String tag; - - TrackTag(String tag) { - this.tag = tag; - } -} diff --git a/src/main/java/in/koreatech/koin/admin/member/exception/TechStackNotFoundException.java b/src/main/java/in/koreatech/koin/admin/member/exception/TechStackNotFoundException.java deleted file mode 100644 index a9856c6dfe..0000000000 --- a/src/main/java/in/koreatech/koin/admin/member/exception/TechStackNotFoundException.java +++ /dev/null @@ -1,20 +0,0 @@ -package in.koreatech.koin.admin.member.exception; - -import in.koreatech.koin.global.exception.custom.DataNotFoundException; - -public class TechStackNotFoundException extends DataNotFoundException { - - private static final String DEFAULT_MESSAGE = "존재하지 않는 기술스택입니다."; - - protected TechStackNotFoundException(String message) { - super(message); - } - - protected TechStackNotFoundException(String message, String detail) { - super(message, detail); - } - - public static TechStackNotFoundException withDetail(String detail) { - return new TechStackNotFoundException(DEFAULT_MESSAGE, detail); - } -} diff --git a/src/main/java/in/koreatech/koin/admin/member/exception/TrackNameDuplicationException.java b/src/main/java/in/koreatech/koin/admin/member/exception/TrackNameDuplicationException.java deleted file mode 100644 index d47d2e49d6..0000000000 --- a/src/main/java/in/koreatech/koin/admin/member/exception/TrackNameDuplicationException.java +++ /dev/null @@ -1,20 +0,0 @@ -package in.koreatech.koin.admin.member.exception; - -import in.koreatech.koin.global.exception.custom.DuplicationException; - -public class TrackNameDuplicationException extends DuplicationException { - - private static final String DEFAULT_MESSAGE = "트랙명이 이미 존재합니다"; - - protected TrackNameDuplicationException(String message) { - super(message); - } - - protected TrackNameDuplicationException(String message, String detail) { - super(message, detail); - } - - public static TrackNameDuplicationException withDetail(String name) { - return new TrackNameDuplicationException(DEFAULT_MESSAGE, "name: " + name); - } -} diff --git a/src/main/java/in/koreatech/koin/admin/member/repository/AdminMemberRepository.java b/src/main/java/in/koreatech/koin/admin/member/repository/AdminMemberRepository.java deleted file mode 100644 index 4bd51b56fa..0000000000 --- a/src/main/java/in/koreatech/koin/admin/member/repository/AdminMemberRepository.java +++ /dev/null @@ -1,50 +0,0 @@ -package in.koreatech.koin.admin.member.repository; - -import java.util.List; -import java.util.Optional; - -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.repository.EntityGraph; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.Repository; - -import in.koreatech.koin.domain.member.exception.MemberNotFoundException; -import in.koreatech.koin.domain.member.model.Member; -import org.springframework.data.repository.query.Param; - -public interface AdminMemberRepository extends Repository { - - @EntityGraph(attributePaths = {"track"}) - @Query("select m from Member m where m.track.name = :trackName and m.isDeleted = :isDeleted") - Page findAllByTrackAndIsDeleted( - @Param("trackName") String trackName, - @Param("isDeleted") Boolean isDeleted, - Pageable pageable); - - @EntityGraph(attributePaths = {"track"}) - @Query("select count(m) from Member m where m.track.name = :trackName and m.isDeleted = :isDeleted") - Integer countAllByTrackAndIsDeleted( - @Param("trackName") String trackName, - @Param("isDeleted") Boolean isDeleted); - - Member save(Member member); - - List findByTrackId(Integer id); - - @EntityGraph(attributePaths = {"track"}) - Optional findByName(String name); - - default Member getByName(String name) { - return findByName(name) - .orElseThrow(() -> MemberNotFoundException.withDetail("name: " + name)); - } - - @EntityGraph(attributePaths = {"track"}) - Optional findById(Integer id); - - default Member getById(Integer id) { - return findById(id) - .orElseThrow(() -> MemberNotFoundException.withDetail("id: " + id)); - } -} diff --git a/src/main/java/in/koreatech/koin/admin/member/repository/AdminTechStackRepository.java b/src/main/java/in/koreatech/koin/admin/member/repository/AdminTechStackRepository.java deleted file mode 100644 index 9ae8143173..0000000000 --- a/src/main/java/in/koreatech/koin/admin/member/repository/AdminTechStackRepository.java +++ /dev/null @@ -1,23 +0,0 @@ -package in.koreatech.koin.admin.member.repository; - -import java.util.List; -import java.util.Optional; - -import org.springframework.data.repository.Repository; - -import in.koreatech.koin.admin.member.exception.TechStackNotFoundException; -import in.koreatech.koin.domain.member.model.TechStack; - -public interface AdminTechStackRepository extends Repository { - - TechStack save(TechStack techStack); - - Optional findById(Integer id); - - List findAllByTrackId(Integer id); - - default TechStack getById(Integer id) { - return findById(id) - .orElseThrow(() -> TechStackNotFoundException.withDetail("id : " + id)); - } -} diff --git a/src/main/java/in/koreatech/koin/admin/member/repository/AdminTrackRepository.java b/src/main/java/in/koreatech/koin/admin/member/repository/AdminTrackRepository.java deleted file mode 100644 index 4477a2bbb7..0000000000 --- a/src/main/java/in/koreatech/koin/admin/member/repository/AdminTrackRepository.java +++ /dev/null @@ -1,30 +0,0 @@ -package in.koreatech.koin.admin.member.repository; - -import java.util.List; -import java.util.Optional; - -import org.springframework.data.repository.Repository; - -import in.koreatech.koin.domain.member.exception.TrackNotFoundException; -import in.koreatech.koin.domain.member.model.Track; - -public interface AdminTrackRepository extends Repository { - - Track save(Track track); - - List findAll(); - - Optional findById(Integer trackId); - - Optional findByName(String trackName); - - default Track getById(Integer trackId) { - return findById(trackId) - .orElseThrow(() -> TrackNotFoundException.withDetail("trackId: " + trackId)); - } - - default Track getByName(String trackName) { - return findByName(trackName) - .orElseThrow(() -> TrackNotFoundException.withDetail("name: " + trackName)); - } -} diff --git a/src/main/java/in/koreatech/koin/admin/member/service/AdminMemberService.java b/src/main/java/in/koreatech/koin/admin/member/service/AdminMemberService.java deleted file mode 100644 index b9740f8499..0000000000 --- a/src/main/java/in/koreatech/koin/admin/member/service/AdminMemberService.java +++ /dev/null @@ -1,76 +0,0 @@ -package in.koreatech.koin.admin.member.service; - -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Sort; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import in.koreatech.koin.admin.member.dto.AdminMemberRequest; -import in.koreatech.koin.admin.member.dto.AdminMemberResponse; -import in.koreatech.koin.admin.member.dto.AdminMembersResponse; -import in.koreatech.koin.admin.member.enums.TrackTag; -import in.koreatech.koin.admin.member.repository.AdminMemberRepository; -import in.koreatech.koin.admin.member.repository.AdminTrackRepository; -import in.koreatech.koin.domain.member.model.Member; -import in.koreatech.koin.domain.member.model.Track; -import in.koreatech.koin.common.model.Criteria; -import lombok.RequiredArgsConstructor; - -@Service -@RequiredArgsConstructor -@Transactional(readOnly = true) -public class AdminMemberService { - - private final AdminMemberRepository adminMemberRepository; - private final AdminTrackRepository adminTrackRepository; - - public AdminMembersResponse getMembers(Integer page, Integer limit, TrackTag track, Boolean isDeleted) { - Integer total = adminMemberRepository.countAllByTrackAndIsDeleted(track.getTag(), isDeleted); - - Criteria criteria = Criteria.of(page, limit, total); - PageRequest pageRequest = PageRequest.of(criteria.getPage(), criteria.getLimit(), - Sort.by(Sort.Direction.ASC, "id")); - - Page result = adminMemberRepository.findAllByTrackAndIsDeleted(track.getTag(), isDeleted, pageRequest); - - return AdminMembersResponse.of(result, criteria); - } - - @Transactional - public void createMember(AdminMemberRequest request) { - Track track = adminTrackRepository.getByName(request.track()); - Member member = request.toMember(track); - adminMemberRepository.save(member); - } - - public AdminMemberResponse getMember(Integer memberId) { - Member member = adminMemberRepository.getById(memberId); - return AdminMemberResponse.from(member); - } - - @Transactional - public void deleteMember(Integer memberId) { - Member member = adminMemberRepository.getById(memberId); - member.delete(); - } - - @Transactional - public void updateMember(Integer memberId, AdminMemberRequest request) { - Member member = adminMemberRepository.getById(memberId); - - String currentTrackName = member.getTrack().getName(); - String changedTrackName = request.track(); - if (!currentTrackName.equals(changedTrackName)) { - member.updateTrack(adminTrackRepository.getByName(request.track())); - } - - member.update(request.name(), request.studentNumber(), request.position(), request.email(), request.imageUrl()); - } - - @Transactional - public void undeleteMember(Integer memberId) { - Member member = adminMemberRepository.getById(memberId); - member.undelete(); - } -} diff --git a/src/main/java/in/koreatech/koin/admin/member/service/AdminTrackService.java b/src/main/java/in/koreatech/koin/admin/member/service/AdminTrackService.java deleted file mode 100644 index 57af6b991c..0000000000 --- a/src/main/java/in/koreatech/koin/admin/member/service/AdminTrackService.java +++ /dev/null @@ -1,99 +0,0 @@ -package in.koreatech.koin.admin.member.service; - -import java.util.List; - -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import in.koreatech.koin.admin.member.dto.AdminTechStackRequest; -import in.koreatech.koin.admin.member.dto.AdminTechStackResponse; -import in.koreatech.koin.admin.member.dto.AdminTrackRequest; -import in.koreatech.koin.admin.member.dto.AdminTrackResponse; -import in.koreatech.koin.admin.member.dto.AdminTrackSingleResponse; -import in.koreatech.koin.admin.member.exception.TrackNameDuplicationException; -import in.koreatech.koin.admin.member.repository.AdminMemberRepository; -import in.koreatech.koin.admin.member.repository.AdminTechStackRepository; -import in.koreatech.koin.admin.member.repository.AdminTrackRepository; -import in.koreatech.koin.domain.member.model.Member; -import in.koreatech.koin.domain.member.model.TechStack; -import in.koreatech.koin.domain.member.model.Track; -import lombok.RequiredArgsConstructor; - -@Service -@RequiredArgsConstructor -@Transactional(readOnly = true) -public class AdminTrackService { - - private final AdminTrackRepository adminTrackRepository; - private final AdminMemberRepository adminMemberRepository; - private final AdminTechStackRepository adminTechStackRepository; - - public List getTracks() { - return adminTrackRepository.findAll().stream() - .map(AdminTrackResponse::from) - .toList(); - } - - @Transactional - public AdminTrackResponse createTrack(AdminTrackRequest request) { - if (adminTrackRepository.findByName(request.name()).isPresent()) { - throw TrackNameDuplicationException.withDetail("name: " + request.name()); - } - Track track = request.toEntity(); - Track savedTrack = adminTrackRepository.save(track); - return AdminTrackResponse.from(savedTrack); - } - - public AdminTrackSingleResponse getTrack(Integer trackId) { - Track track = adminTrackRepository.getById(trackId); - List members = adminMemberRepository.findByTrackId(trackId); - List techStacks = adminTechStackRepository.findAllByTrackId(trackId); - return AdminTrackSingleResponse.of(track, members, techStacks); - } - - @Transactional - public AdminTrackResponse updateTrack(Integer trackId, AdminTrackRequest request) { - if (adminTrackRepository.findByName(request.name()).isPresent()) { - throw TrackNameDuplicationException.withDetail("name: " + request.name()); - } - Track track = adminTrackRepository.getById(trackId); - track.update(request.name(), request.headcount(), request.isDeleted()); - return AdminTrackResponse.from(track); - } - - @Transactional - public void deleteTrack(Integer trackId) { - Track track = adminTrackRepository.getById(trackId); - track.delete(); - } - - @Transactional - public AdminTechStackResponse createTechStack(AdminTechStackRequest request, String trackName) { - Track track = adminTrackRepository.getByName(trackName); - TechStack techStack = request.toEntity(track.getId()); - TechStack savedTechStack = adminTechStackRepository.save(techStack); - return AdminTechStackResponse.from(savedTechStack); - } - - @Transactional - public AdminTechStackResponse updateTechStack( - AdminTechStackRequest request, - String trackName, - Integer techStackId - ) { - TechStack techStack = adminTechStackRepository.getById(techStackId); - Integer id = techStack.getTrackId(); - if (trackName != null) { - Track track = adminTrackRepository.getByName(trackName); - id = track.getId(); - } - techStack.update(id, request.imageUrl(), request.name(), request.description(), request.isDeleted()); - return AdminTechStackResponse.from(techStack); - } - - @Transactional - public void deleteTechStack(Integer techStackId) { - TechStack techStack = adminTechStackRepository.getById(techStackId); - techStack.delete(); - } -} diff --git a/src/main/java/in/koreatech/koin/common/event/ArticleKeywordEvent.java b/src/main/java/in/koreatech/koin/common/event/ArticleKeywordEvent.java index 7dafab30f5..4ba7ea1f4f 100644 --- a/src/main/java/in/koreatech/koin/common/event/ArticleKeywordEvent.java +++ b/src/main/java/in/koreatech/koin/common/event/ArticleKeywordEvent.java @@ -4,9 +4,12 @@ import java.util.LinkedHashMap; import java.util.Map; +import in.koreatech.koin.domain.community.keyword.enums.KeywordCategory; + public record ArticleKeywordEvent( Integer articleId, Integer authorId, + KeywordCategory category, Map matchedKeywordByUserId ) { diff --git a/src/main/java/in/koreatech/koin/common/model/MobileAppPath.java b/src/main/java/in/koreatech/koin/common/model/MobileAppPath.java index f36541adbc..7362261503 100644 --- a/src/main/java/in/koreatech/koin/common/model/MobileAppPath.java +++ b/src/main/java/in/koreatech/koin/common/model/MobileAppPath.java @@ -9,6 +9,7 @@ public enum MobileAppPath { SHOP("shop"), DINING("dining"), KEYWORD("keyword"), + LOST_ITEM("lost-item"), CHAT("chat"), CALLVAN("callvan"), CALLVAN_CHAT("callvan-chat"), diff --git a/src/main/java/in/koreatech/koin/domain/activity/controller/ActivityApi.java b/src/main/java/in/koreatech/koin/domain/activity/controller/ActivityApi.java deleted file mode 100644 index bd9176f1df..0000000000 --- a/src/main/java/in/koreatech/koin/domain/activity/controller/ActivityApi.java +++ /dev/null @@ -1,29 +0,0 @@ -package in.koreatech.koin.domain.activity.controller; - -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestParam; - -import in.koreatech.koin.domain.activity.dto.ActivitiesResponse; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; - -@Tag(name = "(Normal) Activity", description = "BCSDLab 활동") -public interface ActivityApi { - - @ApiResponses( - value = { - @ApiResponse(responseCode = "200", description = "성공적으로 활동 목록을 조회함"), - @ApiResponse(responseCode = "404", description = "해당하는 활동이 없음", content = @Content(schema = @Schema(hidden = true))), - } - ) - @Operation(summary = "BCSD Lab 활동 조회") - @GetMapping("/activities") - ResponseEntity getActivities( - @RequestParam(required = false) String year - ); -} diff --git a/src/main/java/in/koreatech/koin/domain/activity/controller/ActivityController.java b/src/main/java/in/koreatech/koin/domain/activity/controller/ActivityController.java deleted file mode 100644 index e356f83858..0000000000 --- a/src/main/java/in/koreatech/koin/domain/activity/controller/ActivityController.java +++ /dev/null @@ -1,25 +0,0 @@ -package in.koreatech.koin.domain.activity.controller; - -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -import in.koreatech.koin.domain.activity.dto.ActivitiesResponse; -import in.koreatech.koin.domain.activity.service.ActivityService; -import lombok.RequiredArgsConstructor; - -@RestController -@RequiredArgsConstructor -public class ActivityController implements ActivityApi { - - private final ActivityService activityService; - - @GetMapping("/activities") - public ResponseEntity getActivities( - @RequestParam(required = false) String year - ) { - ActivitiesResponse response = activityService.getActivities(year); - return ResponseEntity.ok(response); - } -} diff --git a/src/main/java/in/koreatech/koin/domain/activity/dto/ActivitiesResponse.java b/src/main/java/in/koreatech/koin/domain/activity/dto/ActivitiesResponse.java deleted file mode 100644 index 46835c0889..0000000000 --- a/src/main/java/in/koreatech/koin/domain/activity/dto/ActivitiesResponse.java +++ /dev/null @@ -1,14 +0,0 @@ -package in.koreatech.koin.domain.activity.dto; - -import java.util.List; - -import com.fasterxml.jackson.annotation.JsonProperty; - -public record ActivitiesResponse( - @JsonProperty("Activities") - List activities -) { - public ActivitiesResponse(List activities) { - this.activities = activities; - } -} diff --git a/src/main/java/in/koreatech/koin/domain/activity/dto/ActivityResponse.java b/src/main/java/in/koreatech/koin/domain/activity/dto/ActivityResponse.java deleted file mode 100644 index 40c0be1b1c..0000000000 --- a/src/main/java/in/koreatech/koin/domain/activity/dto/ActivityResponse.java +++ /dev/null @@ -1,61 +0,0 @@ -package in.koreatech.koin.domain.activity.dto; - -import static com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; -import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED; -import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; - -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.util.List; - -import com.fasterxml.jackson.annotation.JsonFormat; -import com.fasterxml.jackson.databind.annotation.JsonNaming; - -import in.koreatech.koin.domain.activity.model.Activity; -import io.swagger.v3.oas.annotations.media.Schema; - -@JsonNaming(value = SnakeCaseStrategy.class) -public record ActivityResponse( - @Schema(description = "활동 날짜", example = "2019-07-29", requiredMode = REQUIRED) - @JsonFormat(pattern = "yyyy-MM-dd") - LocalDate date, - - @Schema(description = "삭제 여부", example = "false", requiredMode = REQUIRED) - Boolean isDeleted, - - @Schema(description = "최근 업데이트 일시", example = "2019-08-16 23:01:52", requiredMode = REQUIRED) - @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") - LocalDateTime updatedAt, - - @Schema(description = "초기 생성 일시", example = "2019-08-16 23:01:52", requiredMode = REQUIRED) - @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") - LocalDateTime createdAt, - - @Schema(description = "활동 설명", example = "더 편리한 서비스 제공을 위해 시간표 기능을 추가했습니다.", requiredMode = REQUIRED) - String description, - - @Schema(description = "이미지 URL 목록", example = """ - ["https://test2.com.png", "https://test3.com.png"] - """, requiredMode = NOT_REQUIRED) - List imageUrls, - - @Schema(description = "고유 식별자", example = "1", requiredMode = REQUIRED) - Integer id, - - @Schema(description = "제목", example = "코인 시간표 기능 추가", requiredMode = REQUIRED) - String title -) { - - public static ActivityResponse of(Activity activity, List imageUrls) { - return new ActivityResponse( - activity.getDate(), - activity.isDeleted(), - activity.getUpdatedAt(), - activity.getCreatedAt(), - activity.getDescription(), - imageUrls, - activity.getId(), - activity.getTitle() - ); - } -} diff --git a/src/main/java/in/koreatech/koin/domain/activity/model/Activity.java b/src/main/java/in/koreatech/koin/domain/activity/model/Activity.java deleted file mode 100644 index e06b42e82d..0000000000 --- a/src/main/java/in/koreatech/koin/domain/activity/model/Activity.java +++ /dev/null @@ -1,58 +0,0 @@ -package in.koreatech.koin.domain.activity.model; - -import static jakarta.persistence.GenerationType.IDENTITY; -import static lombok.AccessLevel.PROTECTED; - -import java.time.LocalDate; - -import in.koreatech.koin.common.model.BaseEntity; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.Id; -import jakarta.persistence.Table; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@Entity -@Table(name = "activities") -@NoArgsConstructor(access = PROTECTED) -public class Activity extends BaseEntity { - - @Id - @GeneratedValue(strategy = IDENTITY) - @Column(name = "id") - private Integer id; - - @Column(name = "title", nullable = false) - private String title; - - @Column(name = "description") - private String description; - - @Column(name = "image_urls") - private String imageUrls; - - @Column(name = "date") - private LocalDate date; - - @Column(name = "is_deleted") - private boolean isDeleted = false; - - @Builder - private Activity( - String title, - String description, - String imageUrls, - LocalDate date, - boolean isDeleted - ) { - this.title = title; - this.description = description; - this.imageUrls = imageUrls; - this.date = date; - this.isDeleted = isDeleted; - } -} diff --git a/src/main/java/in/koreatech/koin/domain/activity/repository/ActivityRepository.java b/src/main/java/in/koreatech/koin/domain/activity/repository/ActivityRepository.java deleted file mode 100644 index df6696ea00..0000000000 --- a/src/main/java/in/koreatech/koin/domain/activity/repository/ActivityRepository.java +++ /dev/null @@ -1,19 +0,0 @@ -package in.koreatech.koin.domain.activity.repository; - -import java.util.List; - -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.Repository; -import org.springframework.data.repository.query.Param; - -import in.koreatech.koin.domain.activity.model.Activity; - -public interface ActivityRepository extends Repository { - - @Query(value = "SELECT * FROM activities WHERE is_deleted = 0 AND YEAR(date) = :year", nativeQuery = true) - List getActivitiesByYear(@Param("year") String year); - - List findAllByIsDeleted(boolean isDeleted); - - Activity save(Activity activity); -} diff --git a/src/main/java/in/koreatech/koin/domain/activity/service/ActivityService.java b/src/main/java/in/koreatech/koin/domain/activity/service/ActivityService.java deleted file mode 100644 index 076a3fad40..0000000000 --- a/src/main/java/in/koreatech/koin/domain/activity/service/ActivityService.java +++ /dev/null @@ -1,52 +0,0 @@ -package in.koreatech.koin.domain.activity.service; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import in.koreatech.koin.domain.activity.dto.ActivitiesResponse; -import in.koreatech.koin.domain.activity.dto.ActivityResponse; -import in.koreatech.koin.domain.activity.model.Activity; -import in.koreatech.koin.domain.activity.repository.ActivityRepository; -import lombok.RequiredArgsConstructor; - -@Service -@RequiredArgsConstructor -@Transactional(readOnly = true) -public class ActivityService { - - private final ActivityRepository activityRepository; - - public ActivitiesResponse getActivities(String year) { - List activities; - - if (year == null) { - activities = activityRepository.findAllByIsDeleted(false); - } else { - activities = activityRepository.getActivitiesByYear(year); - } - - List activityResponseList = activities.stream() - .map(activity -> { - List imageUrlsList = parseImageUrls(activity.getImageUrls()); - return ActivityResponse.of(activity, imageUrlsList); - }) - .toList(); - - return new ActivitiesResponse(activityResponseList); - } - - private List parseImageUrls(String imageUrls) { - if (imageUrls == null || imageUrls.trim().isEmpty()) { - return Collections.emptyList(); - } - - return Arrays.stream(imageUrls.split(",")) - .map(String::strip) - .map(url -> url.replace("\n", "").replace("\r", "")) // 개행 문자 제거 - .toList(); - } -} diff --git a/src/main/java/in/koreatech/koin/domain/bus/service/model/route/CityBusRouteStrategy.java b/src/main/java/in/koreatech/koin/domain/bus/service/model/route/CityBusRouteStrategy.java index a593ecb572..bf7081adc0 100644 --- a/src/main/java/in/koreatech/koin/domain/bus/service/model/route/CityBusRouteStrategy.java +++ b/src/main/java/in/koreatech/koin/domain/bus/service/model/route/CityBusRouteStrategy.java @@ -38,11 +38,10 @@ public List findSchedule(BusRouteCommand command) { } private CityBusTimetable getScheduleForRoute(Long busNumber, BusStation depart, CityBusDirection cityBusInfo) { - if (depart == BusStation.TERMINAL) { - return cityBusTimetableRepository.getByBusInfoNumberAndBusInfoDepart(busNumber, "종합터미널"); - } else { + if (depart == BusStation.KOREATECH) { return cityBusTimetableRepository.getByBusInfoNumberAndBusInfoDepart(busNumber, cityBusInfo.getName()); } + return cityBusTimetableRepository.getByBusInfoNumberAndBusInfoDepart(busNumber, "종합터미널"); } @Override diff --git a/src/main/java/in/koreatech/koin/domain/community/article/dto/BusArticleProjection.java b/src/main/java/in/koreatech/koin/domain/community/article/dto/BusArticleProjection.java new file mode 100644 index 0000000000..cb14cb86af --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/community/article/dto/BusArticleProjection.java @@ -0,0 +1,11 @@ +package in.koreatech.koin.domain.community.article.dto; + +import java.time.LocalDateTime; + +public record BusArticleProjection( + Integer id, + String title, + LocalDateTime createdAt +) { + +} diff --git a/src/main/java/in/koreatech/koin/domain/community/article/model/redis/BusNoticeArticle.java b/src/main/java/in/koreatech/koin/domain/community/article/model/redis/BusNoticeArticle.java index 4c3e062d5c..7453fe0e2a 100644 --- a/src/main/java/in/koreatech/koin/domain/community/article/model/redis/BusNoticeArticle.java +++ b/src/main/java/in/koreatech/koin/domain/community/article/model/redis/BusNoticeArticle.java @@ -1,10 +1,10 @@ package in.koreatech.koin.domain.community.article.model.redis; -import in.koreatech.koin.domain.community.article.model.Article; import org.springframework.data.annotation.Id; +import org.springframework.data.redis.core.RedisHash; + import lombok.Builder; import lombok.Getter; -import org.springframework.data.redis.core.RedisHash; @Getter @RedisHash(value = "busNoticeArticle") @@ -21,10 +21,10 @@ private BusNoticeArticle(Integer id, String title) { this.title = title; } - public static BusNoticeArticle from(Article article) { + public static BusNoticeArticle of(int id, String title) { return BusNoticeArticle.builder() - .id(article.getId()) - .title(article.getTitle()) - .build(); + .id(id) + .title(title) + .build(); } } diff --git a/src/main/java/in/koreatech/koin/domain/community/article/repository/ArticleRepository.java b/src/main/java/in/koreatech/koin/domain/community/article/repository/ArticleRepository.java index daabf61dc3..1733dc9973 100644 --- a/src/main/java/in/koreatech/koin/domain/community/article/repository/ArticleRepository.java +++ b/src/main/java/in/koreatech/koin/domain/community/article/repository/ArticleRepository.java @@ -14,6 +14,7 @@ import org.springframework.data.repository.Repository; import org.springframework.data.repository.query.Param; +import in.koreatech.koin.domain.community.article.dto.BusArticleProjection; import in.koreatech.koin.domain.community.article.exception.ArticleNotFoundException; import in.koreatech.koin.domain.community.article.exception.BoardNotFoundException; import in.koreatech.koin.domain.community.article.model.Article; @@ -184,8 +185,19 @@ List
findAllByRegisteredAtIsAfterExcludingBoardId(@Param("registeredAt" @Query("SELECT a.title FROM Article a WHERE a.id = :id") String getTitleById(@Param("id") Integer id); - @Query(value = "SELECT * FROM new_articles a " - + "WHERE a.title REGEXP '통학버스|등교버스|셔틀버스|하교버스' AND a.is_notice = true " - + "ORDER BY a.created_at DESC LIMIT 5", nativeQuery = true) - List
findBusArticlesTop5OrderByCreatedAtDesc(); + @Query(""" + SELECT new in.koreatech.koin.domain.community.article.dto.BusArticleProjection( + a.id, a.title, a.createdAt + ) + FROM Article a + WHERE ( + a.title LIKE '%통학버스%' + OR a.title LIKE '%등교버스%' + OR a.title LIKE '%셔틀버스%' + OR a.title LIKE '%하교버스%' + ) + AND a.isNotice = true + ORDER BY a.createdAt DESC + """) + List findBusArticlesTop5OrderByCreatedAtDesc(Pageable pageable); } diff --git a/src/main/java/in/koreatech/koin/domain/community/article/service/ArticleSyncService.java b/src/main/java/in/koreatech/koin/domain/community/article/service/ArticleSyncService.java index 28ebcf226d..f4422c2e03 100644 --- a/src/main/java/in/koreatech/koin/domain/community/article/service/ArticleSyncService.java +++ b/src/main/java/in/koreatech/koin/domain/community/article/service/ArticleSyncService.java @@ -1,5 +1,21 @@ package in.koreatech.koin.domain.community.article.service; +import java.time.Clock; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import org.springframework.data.domain.PageRequest; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ZSetOperations.TypedTuple; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import in.koreatech.koin.domain.community.article.dto.BusArticleProjection; import in.koreatech.koin.domain.community.article.model.Article; import in.koreatech.koin.domain.community.article.model.ArticleSearchKeyword; import in.koreatech.koin.domain.community.article.model.ArticleSearchKeywordIpMap; @@ -14,20 +30,6 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.data.redis.core.ZSetOperations.TypedTuple; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.time.Clock; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; - @Slf4j @Service @RequiredArgsConstructor @@ -75,10 +77,11 @@ public void updateHotArticles() { @Transactional public void updateBusNoticeArticle() { - List
articles = articleRepository.findBusArticlesTop5OrderByCreatedAtDesc(); - LocalDate latestDate = articles.get(0).getCreatedAt().toLocalDate(); - List
latestArticles = articles.stream() - .filter(article -> article.getCreatedAt().toLocalDate().isEqual(latestDate)) + List articles = articleRepository.findBusArticlesTop5OrderByCreatedAtDesc( + PageRequest.of(0, 5)); + LocalDate latestDate = articles.get(0).createdAt().toLocalDate(); + List latestArticles = articles.stream() + .filter(article -> article.createdAt().toLocalDate().isEqual(latestDate)) .toList(); if (latestArticles.size() >= 2) { @@ -88,21 +91,23 @@ public void updateBusNoticeArticle() { int secondWeight = 0; // 제목(title)에 "사과"가 들어가면 후순위, "긴급"이 포함되면 우선순위 - if (first.getTitle().contains("사과")) + if (first.title().contains("사과")) firstWeight++; - if (first.getTitle().contains("긴급")) + if (first.title().contains("긴급")) firstWeight--; - if (second.getTitle().contains("사과")) + if (second.title().contains("사과")) secondWeight++; - if (second.getTitle().contains("긴급")) + if (second.title().contains("긴급")) secondWeight--; return Integer.compare(firstWeight, secondWeight); }) .toList(); } - busArticleRepository.save(BusNoticeArticle.from(latestArticles.get(0))); + + BusArticleProjection latestArticle = latestArticles.get(0); + busArticleRepository.save(BusNoticeArticle.of(latestArticle.id(), latestArticle.title())); } @Transactional @@ -130,9 +135,10 @@ private void syncAllIpSearchCounts() { String ipAddress = ipKey.replace(IP_SEARCH_COUNT_PREFIX, ""); for (Map.Entry entry : keywordSearchCounts.entrySet()) { - String searchedKeyword = (String) entry.getKey(); + String searchedKeyword = (String)entry.getKey(); int searchCount = Integer.parseInt(entry.getValue().toString()); - if (searchCount <= 0) continue; + if (searchCount <= 0) + continue; articleSearchKeywordRepository.findByKeyword(searchedKeyword).ifPresent(keywordEntity -> { ipMapRepository.findByArticleSearchKeywordAndIpAddress(keywordEntity, ipAddress) diff --git a/src/main/java/in/koreatech/koin/domain/community/article/service/LostItemArticleService.java b/src/main/java/in/koreatech/koin/domain/community/article/service/LostItemArticleService.java index bb9c9601f5..505722cafd 100644 --- a/src/main/java/in/koreatech/koin/domain/community/article/service/LostItemArticleService.java +++ b/src/main/java/in/koreatech/koin/domain/community/article/service/LostItemArticleService.java @@ -34,6 +34,7 @@ import in.koreatech.koin.domain.community.article.repository.ArticleRepository; import in.koreatech.koin.domain.community.article.repository.BoardRepository; import in.koreatech.koin.domain.community.article.repository.LostItemArticleRepository; +import in.koreatech.koin.domain.community.keyword.enums.KeywordCategory; import in.koreatech.koin.domain.community.util.KeywordExtractor; import in.koreatech.koin.domain.organization.model.Organization; import in.koreatech.koin.domain.organization.repository.OrganizationRepository; @@ -252,7 +253,7 @@ private Board getBoard(Integer boardId, Article article) { } private void sendKeywordNotification(List
articles, Integer authorId) { - List keywordEvents = keywordExtractor.matchKeyword(articles, authorId); + List keywordEvents = keywordExtractor.matchKeyword(articles, authorId, KeywordCategory.LOST_ITEM); if (!keywordEvents.isEmpty()) { for (ArticleKeywordEvent event : keywordEvents) { eventPublisher.publishEvent(event); diff --git a/src/main/java/in/koreatech/koin/domain/community/keyword/controller/KeywordApi.java b/src/main/java/in/koreatech/koin/domain/community/keyword/controller/KeywordApi.java index 0969cfc6dc..ebc68692e1 100644 --- a/src/main/java/in/koreatech/koin/domain/community/keyword/controller/KeywordApi.java +++ b/src/main/java/in/koreatech/koin/domain/community/keyword/controller/KeywordApi.java @@ -8,12 +8,14 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; import in.koreatech.koin.domain.community.keyword.dto.ArticleKeywordCreateRequest; import in.koreatech.koin.domain.community.keyword.dto.ArticleKeywordResponse; import in.koreatech.koin.domain.community.keyword.dto.ArticleKeywordsResponse; import in.koreatech.koin.domain.community.keyword.dto.ArticleKeywordsSuggestionResponse; import in.koreatech.koin.domain.community.keyword.dto.KeywordNotificationRequest; +import in.koreatech.koin.domain.community.keyword.enums.KeywordCategory; import in.koreatech.koin.global.auth.Auth; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; @@ -39,6 +41,7 @@ public interface KeywordApi { @PostMapping("/articles/keyword") ResponseEntity createKeyword( @Valid @RequestBody ArticleKeywordCreateRequest request, + @RequestParam(value = "type", required = false, defaultValue = "KOREATECH") KeywordCategory keywordCategory, @Auth(permit = {GENERAL, STUDENT, COUNCIL}) Integer userId ); @@ -67,6 +70,7 @@ ResponseEntity deleteKeyword( @Operation(summary = "자신의 알림 키워드 전체 조회") @GetMapping("/articles/keyword/me") ResponseEntity getMyKeywords( + @RequestParam(value = "type", required = false, defaultValue = "KOREATECH") KeywordCategory keywordCategory, @Auth(permit = {GENERAL, STUDENT, COUNCIL}) Integer userId ); @@ -79,6 +83,7 @@ ResponseEntity getMyKeywords( @Operation(summary = "알림 키워드 추천") @GetMapping("/articles/keyword/suggestions") ResponseEntity suggestKeywords( + @RequestParam(value = "type", required = false, defaultValue = "KOREATECH") KeywordCategory keywordCategory ); @Operation(summary = "키워드 알림 전송", hidden = true) diff --git a/src/main/java/in/koreatech/koin/domain/community/keyword/controller/KeywordController.java b/src/main/java/in/koreatech/koin/domain/community/keyword/controller/KeywordController.java index b34f845898..f98dc1c3ce 100644 --- a/src/main/java/in/koreatech/koin/domain/community/keyword/controller/KeywordController.java +++ b/src/main/java/in/koreatech/koin/domain/community/keyword/controller/KeywordController.java @@ -9,6 +9,7 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import in.koreatech.koin.domain.community.keyword.dto.ArticleKeywordCreateRequest; @@ -16,6 +17,7 @@ import in.koreatech.koin.domain.community.keyword.dto.ArticleKeywordsResponse; import in.koreatech.koin.domain.community.keyword.dto.ArticleKeywordsSuggestionResponse; import in.koreatech.koin.domain.community.keyword.dto.KeywordNotificationRequest; +import in.koreatech.koin.domain.community.keyword.enums.KeywordCategory; import in.koreatech.koin.domain.community.keyword.service.KeywordService; import in.koreatech.koin.global.auth.Auth; import jakarta.validation.Valid; @@ -24,16 +26,17 @@ @RestController @RequiredArgsConstructor @RequestMapping("/articles/keyword") -public class KeywordController implements KeywordApi{ +public class KeywordController implements KeywordApi { private final KeywordService keywordService; @PostMapping() public ResponseEntity createKeyword( @Valid @RequestBody ArticleKeywordCreateRequest request, + @RequestParam(value = "type", required = false, defaultValue = "KOREATECH") KeywordCategory keywordCategory, @Auth(permit = {STUDENT, COUNCIL}) Integer userId ) { - ArticleKeywordResponse response = keywordService.createKeyword(userId, request); + ArticleKeywordResponse response = keywordService.createKeyword(userId, request, keywordCategory); return ResponseEntity.ok(response); } @@ -48,16 +51,18 @@ public ResponseEntity deleteKeyword( @GetMapping("/me") public ResponseEntity getMyKeywords( + @RequestParam(value = "type", required = false, defaultValue = "KOREATECH") KeywordCategory keywordCategory, @Auth(permit = {GENERAL, STUDENT, COUNCIL}) Integer userId ) { - ArticleKeywordsResponse response = keywordService.getMyKeywords(userId); + ArticleKeywordsResponse response = keywordService.getMyKeywords(userId, keywordCategory); return ResponseEntity.ok(response); } @GetMapping("/suggestions") public ResponseEntity suggestKeywords( + @RequestParam(value = "type", required = false, defaultValue = "KOREATECH") KeywordCategory keywordCategory ) { - ArticleKeywordsSuggestionResponse response = keywordService.suggestKeywords(); + ArticleKeywordsSuggestionResponse response = keywordService.suggestKeywords(keywordCategory); return ResponseEntity.ok(response); } diff --git a/src/main/java/in/koreatech/koin/domain/community/keyword/enums/KeywordCategory.java b/src/main/java/in/koreatech/koin/domain/community/keyword/enums/KeywordCategory.java new file mode 100644 index 0000000000..c17e0392c0 --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/community/keyword/enums/KeywordCategory.java @@ -0,0 +1,7 @@ +package in.koreatech.koin.domain.community.keyword.enums; + +public enum KeywordCategory { + KOREATECH, + LOST_ITEM, + ; +} diff --git a/src/main/java/in/koreatech/koin/domain/community/keyword/model/ArticleKeyword.java b/src/main/java/in/koreatech/koin/domain/community/keyword/model/ArticleKeyword.java index 5f77f1c427..5502207ede 100644 --- a/src/main/java/in/koreatech/koin/domain/community/keyword/model/ArticleKeyword.java +++ b/src/main/java/in/koreatech/koin/domain/community/keyword/model/ArticleKeyword.java @@ -10,13 +10,17 @@ import org.hibernate.annotations.Where; import in.koreatech.koin.common.model.BaseEntity; +import in.koreatech.koin.domain.community.keyword.enums.KeywordCategory; import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; import jakarta.persistence.OneToMany; import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -24,7 +28,9 @@ @Getter @Entity @Where(clause = "is_deleted = 0") -@Table(name = "article_keywords") +@Table(name = "article_keywords", uniqueConstraints = { + @UniqueConstraint(columnNames = {"keyword", "category"}) +}) @NoArgsConstructor(access = PROTECTED) public class ArticleKeyword extends BaseEntity { @@ -32,9 +38,13 @@ public class ArticleKeyword extends BaseEntity { @GeneratedValue(strategy = IDENTITY) private Integer id; - @Column(nullable = false, unique = true, length = 50) + @Column(nullable = false, length = 50) private String keyword; + @Enumerated(EnumType.STRING) + @Column(nullable = false, length = 20) + private KeywordCategory category; + @Column(name = "last_used_at", columnDefinition = "TIMESTAMP") private LocalDateTime lastUsedAt; @@ -48,8 +58,9 @@ public class ArticleKeyword extends BaseEntity { private List articleKeywordUserMaps = new ArrayList<>(); @Builder - private ArticleKeyword(String keyword, LocalDateTime lastUsedAt, Boolean isFiltered) { + private ArticleKeyword(String keyword, KeywordCategory category, LocalDateTime lastUsedAt, Boolean isFiltered) { this.keyword = keyword; + this.category = category != null ? category : KeywordCategory.KOREATECH; this.lastUsedAt = lastUsedAt; this.isFiltered = isFiltered != null ? isFiltered : false; } diff --git a/src/main/java/in/koreatech/koin/domain/community/keyword/model/ArticleKeywordSuggestCache.java b/src/main/java/in/koreatech/koin/domain/community/keyword/model/ArticleKeywordSuggestCache.java index 92d6a298a4..2c10505b7a 100644 --- a/src/main/java/in/koreatech/koin/domain/community/keyword/model/ArticleKeywordSuggestCache.java +++ b/src/main/java/in/koreatech/koin/domain/community/keyword/model/ArticleKeywordSuggestCache.java @@ -1,7 +1,9 @@ package in.koreatech.koin.domain.community.keyword.model; +import in.koreatech.koin.domain.community.keyword.enums.KeywordCategory; import org.springframework.data.annotation.Id; import org.springframework.data.redis.core.RedisHash; +import org.springframework.data.redis.core.index.Indexed; import lombok.Builder; import lombok.Getter; @@ -13,12 +15,20 @@ public class ArticleKeywordSuggestCache { @Id private Integer hotKeywordId; private String keyword; + @Indexed + private KeywordCategory category; private Integer count; @Builder - private ArticleKeywordSuggestCache(Integer hotKeywordId, String keyword, Integer count) { + private ArticleKeywordSuggestCache( + Integer hotKeywordId, + String keyword, + KeywordCategory category, + Integer count + ) { this.hotKeywordId = hotKeywordId; this.keyword = keyword; + this.category = category != null ? category : KeywordCategory.KOREATECH; this.count = count; } } diff --git a/src/main/java/in/koreatech/koin/domain/community/keyword/repository/ArticleKeywordRepository.java b/src/main/java/in/koreatech/koin/domain/community/keyword/repository/ArticleKeywordRepository.java index b1e3866011..d7a46d5b1f 100644 --- a/src/main/java/in/koreatech/koin/domain/community/keyword/repository/ArticleKeywordRepository.java +++ b/src/main/java/in/koreatech/koin/domain/community/keyword/repository/ArticleKeywordRepository.java @@ -10,17 +10,22 @@ import org.springframework.data.repository.query.Param; import in.koreatech.koin.domain.community.article.dto.ArticleKeywordResult; +import in.koreatech.koin.domain.community.keyword.enums.KeywordCategory; import in.koreatech.koin.domain.community.keyword.model.ArticleKeyword; public interface ArticleKeywordRepository extends Repository { - Optional findByKeyword(String keyword); + Optional findByKeywordAndCategory(String keyword, KeywordCategory category); @Query(value = """ SELECT * FROM article_keywords ak WHERE ak.keyword = :keyword + AND ak.category = :category """, nativeQuery = true) - Optional findByKeywordIncludingDeleted(@Param("keyword") String keyword); + Optional findByKeywordAndCategoryIncludingDeleted( + @Param("keyword") String keyword, + @Param("category") String category + ); ArticleKeyword save(ArticleKeyword articleKeyword); @@ -28,25 +33,32 @@ public interface ArticleKeywordRepository extends Repository findById(Integer id); - List findAll(Pageable pageable); + List findAllByCategory(KeywordCategory category, Pageable pageable); @Query(""" SELECT new in.koreatech.koin.domain.community.article.dto.ArticleKeywordResult(k.id, k.keyword, COUNT(u)) FROM ArticleKeywordUserMap u JOIN u.articleKeyword k - WHERE k.lastUsedAt >= :oneWeekAgo AND k.isFiltered = false + WHERE k.lastUsedAt >= :oneWeekAgo + AND k.isFiltered = false + AND k.category = :category GROUP BY k.id, k.keyword ORDER BY COUNT(u) DESC """) - List findTopKeywordsInLastWeekExcludingFiltered(LocalDateTime oneWeekAgo, Pageable top15); + List findTopKeywordsInLastWeekExcludingFiltered( + LocalDateTime oneWeekAgo, + KeywordCategory category, + Pageable top15 + ); @Query(""" SELECT new in.koreatech.koin.domain.community.article.dto.ArticleKeywordResult(k.id, k.keyword, COUNT(u)) FROM ArticleKeyword k LEFT JOIN k.articleKeywordUserMaps u WHERE k.isFiltered = false + AND k.category = :category GROUP BY k.id, k.keyword ORDER BY k.createdAt DESC """) - List findTop15KeywordsExcludingFiltered(Pageable top15); + List findTop15KeywordsExcludingFiltered(KeywordCategory category, Pageable top15); } diff --git a/src/main/java/in/koreatech/koin/domain/community/keyword/repository/ArticleKeywordSuggestRepository.java b/src/main/java/in/koreatech/koin/domain/community/keyword/repository/ArticleKeywordSuggestRepository.java index 6a83adfd1e..c603f8ff0c 100644 --- a/src/main/java/in/koreatech/koin/domain/community/keyword/repository/ArticleKeywordSuggestRepository.java +++ b/src/main/java/in/koreatech/koin/domain/community/keyword/repository/ArticleKeywordSuggestRepository.java @@ -4,12 +4,15 @@ import org.springframework.data.repository.Repository; +import in.koreatech.koin.domain.community.keyword.enums.KeywordCategory; import in.koreatech.koin.domain.community.keyword.model.ArticleKeywordSuggestCache; public interface ArticleKeywordSuggestRepository extends Repository { List findTop15ByOrderByCountDesc(); + List findTop15ByCategoryOrderByCountDesc(KeywordCategory category); + void deleteAll(); void save(ArticleKeywordSuggestCache hotKeyword); diff --git a/src/main/java/in/koreatech/koin/domain/community/keyword/repository/ArticleKeywordUserMapRepository.java b/src/main/java/in/koreatech/koin/domain/community/keyword/repository/ArticleKeywordUserMapRepository.java index c1c00bc818..b1e325319c 100644 --- a/src/main/java/in/koreatech/koin/domain/community/keyword/repository/ArticleKeywordUserMapRepository.java +++ b/src/main/java/in/koreatech/koin/domain/community/keyword/repository/ArticleKeywordUserMapRepository.java @@ -7,6 +7,7 @@ import org.springframework.data.repository.Repository; import org.springframework.data.repository.query.Param; +import in.koreatech.koin.domain.community.keyword.enums.KeywordCategory; import in.koreatech.koin.domain.community.keyword.exception.ArticleKeywordUserMapNotFoundException; import in.koreatech.koin.domain.community.keyword.model.ArticleKeywordUserMap; @@ -14,7 +15,7 @@ public interface ArticleKeywordUserMapRepository extends Repository ArticleKeywordUserMapNotFoundException.withDetail("keywordUserMapId: " + keywordUserMapId)); } - List findAllByUserId(Integer userId); + List findAllByUserIdAndArticleKeywordCategory(Integer userId, KeywordCategory category); @Query(""" SELECT akw.keyword FROM ArticleKeywordUserMap akum JOIN akum.articleKeyword akw WHERE akum.user.id = :userId + AND akw.category = :category """) - List findAllKeywordByUserId(@Param("userId") Integer userId); + List findAllKeywordByUserIdAndCategory( + @Param("userId") Integer userId, + @Param("category") KeywordCategory category + ); @Query(value = """ SELECT * FROM article_keyword_user_map akum diff --git a/src/main/java/in/koreatech/koin/domain/community/keyword/service/KeywordService.java b/src/main/java/in/koreatech/koin/domain/community/keyword/service/KeywordService.java index a87498e7cf..f7ef91dc72 100644 --- a/src/main/java/in/koreatech/koin/domain/community/keyword/service/KeywordService.java +++ b/src/main/java/in/koreatech/koin/domain/community/keyword/service/KeywordService.java @@ -21,6 +21,7 @@ import in.koreatech.koin.domain.community.keyword.dto.ArticleKeywordsResponse; import in.koreatech.koin.domain.community.keyword.dto.ArticleKeywordsSuggestionResponse; import in.koreatech.koin.domain.community.keyword.dto.KeywordNotificationRequest; +import in.koreatech.koin.domain.community.keyword.enums.KeywordCategory; import in.koreatech.koin.domain.community.keyword.exception.KeywordDuplicationException; import in.koreatech.koin.domain.community.keyword.exception.KeywordLimitExceededException; import in.koreatech.koin.domain.community.keyword.model.ArticleKeyword; @@ -56,40 +57,43 @@ public class KeywordService { private final KeywordExtractor keywordExtractor; @Transactional - public ArticleKeywordResponse createKeyword(Integer userId, ArticleKeywordCreateRequest request) { + public ArticleKeywordResponse createKeyword( + Integer userId, ArticleKeywordCreateRequest request, KeywordCategory category + ) { String keyword = validateAndGetKeyword(request.keyword()); - validateKeywordLimit(userId); + validateKeywordLimit(userId, category); - ArticleKeyword existingKeyword = findOrRestoreKeyword(keyword); + ArticleKeyword existingKeyword = findOrRestoreKeyword(keyword, category); ArticleKeywordUserMap userMap = findOrCreateKeywordMapping(existingKeyword, userId); return new ArticleKeywordResponse(userMap.getId(), existingKeyword.getKeyword()); } - private void validateKeywordLimit(Integer userId) { - if (articleKeywordUserMapRepository.countByUserId(userId) >= ARTICLE_KEYWORD_LIMIT) { - throw KeywordLimitExceededException.withDetail("userId: " + userId); + private void validateKeywordLimit(Integer userId, KeywordCategory category) { + if (articleKeywordUserMapRepository.countByUserIdAndArticleKeywordCategory(userId, category) >= ARTICLE_KEYWORD_LIMIT) { + throw KeywordLimitExceededException.withDetail("userId: " + userId + ", category: " + category); } } @ConcurrencyGuard(lockName = "createKeywordManagement") - private ArticleKeyword findOrRestoreKeyword(String keyword) { - return articleKeywordRepository.findByKeywordIncludingDeleted(keyword) + private ArticleKeyword findOrRestoreKeyword(String keyword, KeywordCategory category) { + return articleKeywordRepository.findByKeywordAndCategoryIncludingDeleted(keyword, category.name()) .map(keywordEntity -> { if (keywordEntity.getIsDeleted()) { keywordEntity.restore(); } return keywordEntity; }) - .orElseGet(() -> createNewKeyword(keyword)); + .orElseGet(() -> createNewKeyword(keyword, category)); } - private ArticleKeyword createNewKeyword(String keyword) { + private ArticleKeyword createNewKeyword(String keyword, KeywordCategory category) { return articleKeywordRepository.save( ArticleKeyword.builder() .keyword(keyword) + .category(category) .lastUsedAt(LocalDateTime.now()) .build() ); @@ -133,13 +137,14 @@ public void deleteKeyword(Integer userId, Integer keywordUserMapId) { } } - public ArticleKeywordsResponse getMyKeywords(Integer userId) { - List articleKeywordUserMaps = articleKeywordUserMapRepository.findAllByUserId(userId); + public ArticleKeywordsResponse getMyKeywords(Integer userId, KeywordCategory category) { + List articleKeywordUserMaps = articleKeywordUserMapRepository + .findAllByUserIdAndArticleKeywordCategory(userId, category); return ArticleKeywordsResponse.from(articleKeywordUserMaps); } - public ArticleKeywordsSuggestionResponse suggestKeywords() { - List suggestions = articleKeywordSuggestRepository.findTop15ByOrderByCountDesc() + public ArticleKeywordsSuggestionResponse suggestKeywords(KeywordCategory category) { + List suggestions = articleKeywordSuggestRepository.findTop15ByCategoryOrderByCountDesc(category) .stream() .map(ArticleKeywordSuggestCache::getKeyword) .collect(Collectors.toList()); @@ -169,7 +174,7 @@ public void sendKeywordNotification(KeywordNotificationRequest request) { }) .toList(); - List keywordEvents = keywordExtractor.matchKeyword(articles, null); + List keywordEvents = keywordExtractor.matchKeyword(articles, null, KeywordCategory.KOREATECH); for (ArticleKeywordEvent event : keywordEvents) { eventPublisher.publishEvent(event); } @@ -187,23 +192,30 @@ public void fetchTopKeywordsFromLastWeek() { Pageable top15 = PageRequest.of(0, 15); LocalDateTime oneWeekAgo = LocalDateTime.now().minusWeeks(1); - List topKeywords = articleKeywordRepository.findTopKeywordsInLastWeekExcludingFiltered(oneWeekAgo, top15); - - if (topKeywords.size() < 15) { - topKeywords = articleKeywordRepository.findTop15KeywordsExcludingFiltered(top15); - } - - List hotKeywords = topKeywords.stream() - .map(result -> ArticleKeywordSuggestCache.builder() - .hotKeywordId(result.hotKeywordId()) - .keyword(result.keyword()) - .count(result.count().intValue()) - .build()) - .toList(); - articleKeywordSuggestRepository.deleteAll(); - for (ArticleKeywordSuggestCache hotKeyword : hotKeywords) { - articleKeywordSuggestRepository.save(hotKeyword); + for (KeywordCategory category : KeywordCategory.values()) { + List topKeywords = articleKeywordRepository.findTopKeywordsInLastWeekExcludingFiltered( + oneWeekAgo, + category, + top15 + ); + + if (topKeywords.size() < 15) { + topKeywords = articleKeywordRepository.findTop15KeywordsExcludingFiltered(category, top15); + } + + List hotKeywords = topKeywords.stream() + .map(result -> ArticleKeywordSuggestCache.builder() + .hotKeywordId(result.hotKeywordId()) + .keyword(result.keyword()) + .category(category) + .count(result.count().intValue()) + .build()) + .toList(); + + for (ArticleKeywordSuggestCache hotKeyword : hotKeywords) { + articleKeywordSuggestRepository.save(hotKeyword); + } } } diff --git a/src/main/java/in/koreatech/koin/domain/community/util/KeywordExtractor.java b/src/main/java/in/koreatech/koin/domain/community/util/KeywordExtractor.java index 2fce59b974..f6affc0ad6 100644 --- a/src/main/java/in/koreatech/koin/domain/community/util/KeywordExtractor.java +++ b/src/main/java/in/koreatech/koin/domain/community/util/KeywordExtractor.java @@ -12,6 +12,7 @@ import org.springframework.transaction.annotation.Transactional; import in.koreatech.koin.domain.community.article.model.Article; +import in.koreatech.koin.domain.community.keyword.enums.KeywordCategory; import in.koreatech.koin.domain.community.keyword.model.ArticleKeyword; import in.koreatech.koin.domain.community.keyword.model.ArticleKeywordUserMap; import in.koreatech.koin.common.event.ArticleKeywordEvent; @@ -29,13 +30,13 @@ public class KeywordExtractor { private final ArticleKeywordRepository articleKeywordRepository; private final ArticleKeywordUserMapRepository articleKeywordUserMapRepository; - public List matchKeyword(List
articles, Integer authorId) { + public List matchKeyword(List
articles, Integer authorId, KeywordCategory category) { Map> matchedKeywordByUserIdByArticleId = new LinkedHashMap<>(); int offset = 0; while (true) { Pageable pageable = PageRequest.of(offset / KEYWORD_BATCH_SIZE, KEYWORD_BATCH_SIZE); - List keywords = articleKeywordRepository.findAll(pageable); + List keywords = articleKeywordRepository.findAllByCategory(category, pageable); if (keywords.isEmpty()) { break; @@ -80,7 +81,7 @@ public List matchKeyword(List
articles, Integer au for (Article article : articles) { Map matchedKeywordByUserId = matchedKeywordByUserIdByArticleId.get(article.getId()); if (matchedKeywordByUserId != null && !matchedKeywordByUserId.isEmpty()) { - keywordEvents.add(new ArticleKeywordEvent(article.getId(), authorId, matchedKeywordByUserId)); + keywordEvents.add(new ArticleKeywordEvent(article.getId(), authorId, category, matchedKeywordByUserId)); } } diff --git a/src/main/java/in/koreatech/koin/domain/member/controller/MemberApi.java b/src/main/java/in/koreatech/koin/domain/member/controller/MemberApi.java deleted file mode 100644 index ddd2725cc9..0000000000 --- a/src/main/java/in/koreatech/koin/domain/member/controller/MemberApi.java +++ /dev/null @@ -1,40 +0,0 @@ -package in.koreatech.koin.domain.member.controller; - -import java.util.List; - -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; - -import in.koreatech.koin.domain.member.dto.MemberResponse; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; - -@Tag(name = "(Normal) Member : BCSDLab 회원", description = "BCSDLab 회원 정보를 관리한다") -public interface MemberApi { - - @ApiResponses( - value = { - @ApiResponse(responseCode = "200"), - } - ) - @Operation(summary = "회원 목록 조회") - @GetMapping("/members") - ResponseEntity> getMembers(); - - @ApiResponses( - value = { - @ApiResponse(responseCode = "200"), - @ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true))), - } - ) - @Operation(summary = "특정 회원 조회") - @GetMapping("/members/{id}") - ResponseEntity getMember( - @PathVariable Integer id - ); -} diff --git a/src/main/java/in/koreatech/koin/domain/member/controller/MemberController.java b/src/main/java/in/koreatech/koin/domain/member/controller/MemberController.java deleted file mode 100644 index ea8176c3f9..0000000000 --- a/src/main/java/in/koreatech/koin/domain/member/controller/MemberController.java +++ /dev/null @@ -1,33 +0,0 @@ -package in.koreatech.koin.domain.member.controller; - -import java.util.List; - -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RestController; - -import in.koreatech.koin.domain.member.dto.MemberResponse; -import in.koreatech.koin.domain.member.service.MemberService; -import lombok.RequiredArgsConstructor; - -@RestController -@RequiredArgsConstructor -public class MemberController implements MemberApi { - - private final MemberService memberService; - - @GetMapping("/members") - public ResponseEntity> getMembers() { - var response = memberService.getMembers(); - return ResponseEntity.ok(response); - } - - @GetMapping("/members/{id}") - public ResponseEntity getMember( - @PathVariable Integer id - ) { - var response = memberService.getMember(id); - return ResponseEntity.ok(response); - } -} diff --git a/src/main/java/in/koreatech/koin/domain/member/controller/TrackApi.java b/src/main/java/in/koreatech/koin/domain/member/controller/TrackApi.java deleted file mode 100644 index 489b41697a..0000000000 --- a/src/main/java/in/koreatech/koin/domain/member/controller/TrackApi.java +++ /dev/null @@ -1,45 +0,0 @@ -package in.koreatech.koin.domain.member.controller; - -import static io.swagger.v3.oas.annotations.enums.ParameterIn.PATH; - -import java.util.List; - -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; - -import in.koreatech.koin.domain.member.dto.TrackResponse; -import in.koreatech.koin.domain.member.dto.TrackSingleResponse; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; - -@Tag(name = "(BCSDLab) Track: BCSDLab 트랙", description = "BCSDLab 트랙 정보를 관리한다") -public interface TrackApi { - - @ApiResponses( - value = { - @ApiResponse(responseCode = "200"), - @ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true))), - } - ) - @Operation(summary = "트랙 목록 조회") - @GetMapping("/tracks") - ResponseEntity> getTracks(); - - @ApiResponses( - value = { - @ApiResponse(responseCode = "200"), - @ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true))), - } - ) - @Operation(summary = "트랙 단건 조회") - @GetMapping("/tracks/{id}") - ResponseEntity getTrack( - @Parameter(in = PATH) @PathVariable Integer id - ); -} diff --git a/src/main/java/in/koreatech/koin/domain/member/controller/TrackController.java b/src/main/java/in/koreatech/koin/domain/member/controller/TrackController.java deleted file mode 100644 index 6b9d8cfad4..0000000000 --- a/src/main/java/in/koreatech/koin/domain/member/controller/TrackController.java +++ /dev/null @@ -1,34 +0,0 @@ -package in.koreatech.koin.domain.member.controller; - -import java.util.List; - -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RestController; - -import in.koreatech.koin.domain.member.dto.TrackResponse; -import in.koreatech.koin.domain.member.dto.TrackSingleResponse; -import in.koreatech.koin.domain.member.service.TrackService; -import lombok.RequiredArgsConstructor; - -@RestController -@RequiredArgsConstructor -public class TrackController implements TrackApi { - - private final TrackService trackService; - - @GetMapping("/tracks") - public ResponseEntity> getTracks() { - List tracksResponse = trackService.getTracks(); - return ResponseEntity.ok(tracksResponse); - } - - @GetMapping("/tracks/{id}") - public ResponseEntity getTrack( - @PathVariable Integer id - ) { - TrackSingleResponse response = trackService.getTrack(id); - return ResponseEntity.ok(response); - } -} diff --git a/src/main/java/in/koreatech/koin/domain/member/dto/MemberResponse.java b/src/main/java/in/koreatech/koin/domain/member/dto/MemberResponse.java deleted file mode 100644 index d1082ce869..0000000000 --- a/src/main/java/in/koreatech/koin/domain/member/dto/MemberResponse.java +++ /dev/null @@ -1,64 +0,0 @@ -package in.koreatech.koin.domain.member.dto; - -import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED; -import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; - -import java.time.LocalDateTime; - -import com.fasterxml.jackson.annotation.JsonFormat; -import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; -import com.fasterxml.jackson.databind.annotation.JsonNaming; - -import in.koreatech.koin.domain.member.model.Member; -import io.swagger.v3.oas.annotations.media.Schema; - -@JsonNaming(SnakeCaseStrategy.class) -public record MemberResponse( - @Schema(example = "1", requiredMode = REQUIRED) - Integer id, - - @Schema(example = "최준호", requiredMode = REQUIRED) - String name, - - @Schema(example = "2019136135", requiredMode = NOT_REQUIRED) - String studentNumber, - - @Schema(example = "Backend", requiredMode = NOT_REQUIRED) - String track, - - @Schema(example = "Regular", requiredMode = REQUIRED) - String position, - - @Schema(example = "juno@gmail.com", requiredMode = NOT_REQUIRED) - String email, - - @Schema(example = "https://static.koreatech.in/bcsdlab_page_assets/img/people/juno.jpg", requiredMode = NOT_REQUIRED) - String imageUrl, - - @Schema(example = "false", requiredMode = REQUIRED) - boolean isDeleted, - - @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") - @Schema(description = "생성 일자", example = "2020-08-14 16:26:35", requiredMode = REQUIRED) - LocalDateTime createdAt, - - @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") - @Schema(description = "수정 일자", example = "2021-08-16 06:42:44", requiredMode = REQUIRED) - LocalDateTime updatedAt -) { - - public static MemberResponse from(Member member) { - return new MemberResponse( - member.getId(), - member.getName(), - member.getStudentNumber(), - member.getTrack().getName(), - member.getPosition(), - member.getEmail(), - member.getImageUrl(), - member.isDeleted(), - member.getCreatedAt(), - member.getUpdatedAt() - ); - } -} diff --git a/src/main/java/in/koreatech/koin/domain/member/dto/TrackResponse.java b/src/main/java/in/koreatech/koin/domain/member/dto/TrackResponse.java deleted file mode 100644 index 7cf6265203..0000000000 --- a/src/main/java/in/koreatech/koin/domain/member/dto/TrackResponse.java +++ /dev/null @@ -1,47 +0,0 @@ -package in.koreatech.koin.domain.member.dto; - -import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; - -import java.time.LocalDateTime; - -import com.fasterxml.jackson.annotation.JsonFormat; -import com.fasterxml.jackson.databind.PropertyNamingStrategies; -import com.fasterxml.jackson.databind.annotation.JsonNaming; - -import in.koreatech.koin.domain.member.model.Track; -import io.swagger.v3.oas.annotations.media.Schema; - -@JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy.class) -public record TrackResponse( - @Schema(description = "트랙 고유 ID", example = "1", requiredMode = REQUIRED) - Integer id, - - @Schema(description = "트랙 명", example = "Backend", requiredMode = REQUIRED) - String name, - - @Schema(description = "인원 수", example = "15", requiredMode = REQUIRED) - Integer headcount, - - @Schema(description = "삭제 여부", example = "false", requiredMode = REQUIRED) - Boolean isDeleted, - - @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") - @Schema(description = "생성 일자", example = "2023-01-04 12:00:01", requiredMode = REQUIRED) - LocalDateTime createdAt, - - @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") - @Schema(description = "수정 일자", example = "2023-01-04 12:00:01", requiredMode = REQUIRED) - LocalDateTime updatedAt -) { - - public static TrackResponse from(Track track) { - return new TrackResponse( - track.getId(), - track.getName(), - track.getHeadcount(), - track.isDeleted(), - track.getCreatedAt(), - track.getUpdatedAt() - ); - } -} diff --git a/src/main/java/in/koreatech/koin/domain/member/dto/TrackSingleResponse.java b/src/main/java/in/koreatech/koin/domain/member/dto/TrackSingleResponse.java deleted file mode 100644 index 31286f1f76..0000000000 --- a/src/main/java/in/koreatech/koin/domain/member/dto/TrackSingleResponse.java +++ /dev/null @@ -1,134 +0,0 @@ -package in.koreatech.koin.domain.member.dto; - -import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED; -import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; - -import java.time.LocalDateTime; -import java.util.List; - -import com.fasterxml.jackson.annotation.JsonFormat; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.PropertyNamingStrategies; -import com.fasterxml.jackson.databind.annotation.JsonNaming; - -import in.koreatech.koin.domain.member.model.Member; -import in.koreatech.koin.domain.member.model.TechStack; -import in.koreatech.koin.domain.member.model.Track; -import io.swagger.v3.oas.annotations.media.Schema; - -public record TrackSingleResponse( - @JsonProperty("TrackName") - @Schema(description = "트랙 명", example = "Backend", requiredMode = REQUIRED) - String trackName, - - @JsonProperty("TechStacks") - List innerTechStackResponses, - - @JsonProperty("Members") - List innerMemberResponses -) { - - public static TrackSingleResponse of(Track track, List members, List techStacks) { - return new TrackSingleResponse( - track.getName(), - techStacks.stream() - .map(InnerTechStackResponse::from) - .toList(), - members.stream() - .map(member -> InnerMemberResponse.from(member, track.getName())) - .toList() - ); - } - - @JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy.class) - private record InnerTechStackResponse( - @Schema(description = "기술 스택 고유 ID", example = "1", requiredMode = REQUIRED) - Integer id, - - @Schema(description = "기술 이름", example = "Backend") - String name, - - @Schema(description = "기술 설명", example = "15") - String description, - - @Schema(description = "이미지 Url", example = "https://static.koreatech.in/example/image.png") - String imageUrl, - - @Schema(description = "트랙 ID", example = "1") - Integer trackId, - - @Schema(description = "삭제 여부", example = "false") - Boolean isDeleted, - - @Schema(description = "생성 일자", example = "2023-01-04 12:00:01") - @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime createdAt, - - @Schema(description = "수정 일자", example = "2023-01-04 12:00:01") - @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime updatedAt - ) { - - public static InnerTechStackResponse from(TechStack techStack) { - return new InnerTechStackResponse( - techStack.getId(), - techStack.getName(), - techStack.getDescription(), - techStack.getImageUrl(), - techStack.getTrackId(), - techStack.isDeleted(), - techStack.getCreatedAt(), - techStack.getUpdatedAt() - ); - } - } - - @JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy.class) - private record InnerMemberResponse( - @Schema(description = "BCSD 회원 고유 ID", example = "1", requiredMode = REQUIRED) - Integer id, - - @Schema(description = "이름", example = "최준호", requiredMode = REQUIRED) - String name, - - @Schema(description = "학번", example = "2019136135", requiredMode = NOT_REQUIRED) - String studentNumber, - - @Schema(description = "동아리 포지션 `Beginner`, `Regular`, `Mentor`", example = "Regular", requiredMode = REQUIRED) - String position, - - @Schema(description = "트랙 명", example = "Backend", requiredMode = REQUIRED) - String track, - - @Schema(description = "이메일", example = "koin123@koreatech.ac.kr", requiredMode = NOT_REQUIRED) - String email, - - @Schema(description = "이미지 Url", example = "https://static.koreatech.in/example/image.png", requiredMode = NOT_REQUIRED) - String imageUrl, - - @Schema(description = "삭제 여부", example = "false", requiredMode = REQUIRED) - Boolean isDeleted, - - @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") - @Schema(description = "생성 일자", example = "2023-01-04 12:00:01", requiredMode = REQUIRED) - LocalDateTime createdAt, - - @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") - @Schema(description = "수정 일자", example = "2023-01-04 12:00:01", requiredMode = REQUIRED) - LocalDateTime updatedAt - ) { - - public static InnerMemberResponse from(Member member, String trackName) { - return new InnerMemberResponse( - member.getId(), - member.getName(), - member.getStudentNumber(), - member.getPosition(), - trackName, - member.getEmail(), - member.getImageUrl(), - member.isDeleted(), - member.getCreatedAt(), - member.getUpdatedAt() - ); - } - } -} diff --git a/src/main/java/in/koreatech/koin/domain/member/exception/MemberNotFoundException.java b/src/main/java/in/koreatech/koin/domain/member/exception/MemberNotFoundException.java deleted file mode 100644 index c809812358..0000000000 --- a/src/main/java/in/koreatech/koin/domain/member/exception/MemberNotFoundException.java +++ /dev/null @@ -1,20 +0,0 @@ -package in.koreatech.koin.domain.member.exception; - -import in.koreatech.koin.global.exception.custom.DataNotFoundException; - -public class MemberNotFoundException extends DataNotFoundException { - - private static final String DEFAULT_MESSAGE = "존재하지 않는 BCSD 회원입니다."; - - public MemberNotFoundException(String message) { - super(message); - } - - public MemberNotFoundException(String message, String detail) { - super(message, detail); - } - - public static MemberNotFoundException withDetail(String detail) { - return new MemberNotFoundException(DEFAULT_MESSAGE, detail); - } -} diff --git a/src/main/java/in/koreatech/koin/domain/member/exception/TrackNotFoundException.java b/src/main/java/in/koreatech/koin/domain/member/exception/TrackNotFoundException.java deleted file mode 100644 index 2e8bfba790..0000000000 --- a/src/main/java/in/koreatech/koin/domain/member/exception/TrackNotFoundException.java +++ /dev/null @@ -1,20 +0,0 @@ -package in.koreatech.koin.domain.member.exception; - -import in.koreatech.koin.global.exception.custom.DataNotFoundException; - -public class TrackNotFoundException extends DataNotFoundException { - - private static final String DEFAULT_MESSAGE = "존재하지 않는 트랙입니다."; - - public TrackNotFoundException(String message) { - super(message); - } - - public TrackNotFoundException(String message, String detail) { - super(message, detail); - } - - public static TrackNotFoundException withDetail(String detail) { - return new TrackNotFoundException(DEFAULT_MESSAGE, detail); - } -} diff --git a/src/main/java/in/koreatech/koin/domain/member/model/Member.java b/src/main/java/in/koreatech/koin/domain/member/model/Member.java deleted file mode 100644 index 8123778dac..0000000000 --- a/src/main/java/in/koreatech/koin/domain/member/model/Member.java +++ /dev/null @@ -1,102 +0,0 @@ -package in.koreatech.koin.domain.member.model; - -import static jakarta.persistence.FetchType.LAZY; -import static jakarta.persistence.GenerationType.IDENTITY; -import static lombok.AccessLevel.PROTECTED; - -import in.koreatech.koin.common.model.BaseEntity; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.Table; -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Size; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -/** - * BCSDLab 회원에 대한 정보를 다루는 엔티티 - */ -@Getter -@Entity -@Table(name = "members") -@NoArgsConstructor(access = PROTECTED) -public class Member extends BaseEntity { - - @Id - @GeneratedValue(strategy = IDENTITY) - private Integer id; - - @Size(max = 50) - @NotNull - @Column(name = "name", nullable = false, length = 50) - private String name; - - @Size(max = 20) - @Column(name = "student_number", length = 20) - private String studentNumber; - - @NotNull - @ManyToOne(fetch = LAZY) - @JoinColumn(name = "track_id") - private Track track; - - @Size(max = 20) - @NotNull - @Column(name = "position", nullable = false, length = 20) - private String position; - - @Size(max = 100) - @Column(name = "email", length = 100) - private String email; - - @Column(name = "image_url") - private String imageUrl; - - @NotNull - @Column(name = "is_deleted", nullable = false) - private boolean isDeleted = false; - - @Builder - private Member( - String name, - String studentNumber, - Track track, - String position, - String email, - String imageUrl, - boolean isDeleted - ) { - this.name = name; - this.studentNumber = studentNumber; - this.track = track; - this.position = position; - this.email = email; - this.imageUrl = imageUrl; - this.isDeleted = isDeleted; - } - - public void delete() { - this.isDeleted = true; - } - - public void undelete() { - this.isDeleted = false; - } - - public void update(String name, String studentNumber, String position, String email, String imageUrl) { - this.name = name; - this.studentNumber = studentNumber; - this.position = position; - this.email = email; - this.imageUrl = imageUrl; - } - - public void updateTrack(Track track) { - this.track = track; - } -} diff --git a/src/main/java/in/koreatech/koin/domain/member/model/TechStack.java b/src/main/java/in/koreatech/koin/domain/member/model/TechStack.java deleted file mode 100644 index c678a3c5ed..0000000000 --- a/src/main/java/in/koreatech/koin/domain/member/model/TechStack.java +++ /dev/null @@ -1,73 +0,0 @@ -package in.koreatech.koin.domain.member.model; - -import static jakarta.persistence.GenerationType.IDENTITY; -import static lombok.AccessLevel.PROTECTED; - -import in.koreatech.koin.common.model.BaseEntity; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.Id; -import jakarta.persistence.Table; -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Size; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@Entity -@Table(name = "tech_stacks") -@NoArgsConstructor(access = PROTECTED) -public class TechStack extends BaseEntity { - - @Id - @GeneratedValue(strategy = IDENTITY) - private Integer id; - - @Column(name = "image_url") - private String imageUrl; - - @Size(max = 50) - @NotNull - @Column(name = "name", nullable = false, length = 50) - private String name; - - @Size(max = 100) - @Column(name = "description", length = 100) - private String description; - - @Column(name = "track_id") - private Integer trackId; - - @NotNull - @Column(name = "is_deleted", nullable = false) - private boolean isDeleted = false; - - @Builder - private TechStack( - String imageUrl, - String name, - String description, - Integer trackId, - boolean isDeleted - ) { - this.imageUrl = imageUrl; - this.trackId = trackId; - this.name = name; - this.description = description; - this.isDeleted = isDeleted; - } - - public void update(Integer trackId, String imageUrl, String name, String description, boolean isDeleted) { - this.imageUrl = imageUrl; - this.trackId = trackId; - this.name = name; - this.description = description; - this.isDeleted = isDeleted; - } - - public void delete() { - this.isDeleted = true; - } -} diff --git a/src/main/java/in/koreatech/koin/domain/member/model/Track.java b/src/main/java/in/koreatech/koin/domain/member/model/Track.java deleted file mode 100644 index 046c231292..0000000000 --- a/src/main/java/in/koreatech/koin/domain/member/model/Track.java +++ /dev/null @@ -1,57 +0,0 @@ -package in.koreatech.koin.domain.member.model; - -import static lombok.AccessLevel.PROTECTED; - -import in.koreatech.koin.common.model.BaseEntity; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.Table; -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Size; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Entity -@Getter -@Table(name = "tracks") -@NoArgsConstructor(access = PROTECTED) -public class Track extends BaseEntity { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Integer id; - - @Size(max = 50) - @NotNull - @Column(name = "name", nullable = false, length = 50) - private String name; - - @NotNull - @Column(name = "headcount", nullable = false) - private Integer headcount; - - @NotNull - @Column(name = "is_deleted", nullable = false) - private boolean isDeleted = false; - - @Builder - private Track(String name, Integer headcount, boolean isDeleted) { - this.name = name; - this.headcount = headcount != null ? headcount : 0; - this.isDeleted = isDeleted; - } - - public void update(String name, Integer headcount, boolean isDeleted) { - this.name = name; - this.headcount = headcount; - this.isDeleted = isDeleted; - } - - public void delete() { - this.isDeleted = true; - } -} diff --git a/src/main/java/in/koreatech/koin/domain/member/repository/MemberRepository.java b/src/main/java/in/koreatech/koin/domain/member/repository/MemberRepository.java deleted file mode 100644 index cde93f1cf5..0000000000 --- a/src/main/java/in/koreatech/koin/domain/member/repository/MemberRepository.java +++ /dev/null @@ -1,29 +0,0 @@ -package in.koreatech.koin.domain.member.repository; - -import java.util.List; -import java.util.Optional; - -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.repository.EntityGraph; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.Repository; - -import in.koreatech.koin.domain.member.exception.MemberNotFoundException; -import in.koreatech.koin.domain.member.model.Member; - -public interface MemberRepository extends Repository { - - Member save(Member member); - - List findAllByTrackIdAndIsDeletedFalse(Integer id); - - List findAll(); - - Optional findById(Integer id); - - default Member getById(Integer id) { - return findById(id) - .orElseThrow(() -> MemberNotFoundException.withDetail("id: " + id)); - } -} diff --git a/src/main/java/in/koreatech/koin/domain/member/repository/TechStackRepository.java b/src/main/java/in/koreatech/koin/domain/member/repository/TechStackRepository.java deleted file mode 100644 index 018e02ee66..0000000000 --- a/src/main/java/in/koreatech/koin/domain/member/repository/TechStackRepository.java +++ /dev/null @@ -1,14 +0,0 @@ -package in.koreatech.koin.domain.member.repository; - -import java.util.List; - -import org.springframework.data.repository.Repository; - -import in.koreatech.koin.domain.member.model.TechStack; - -public interface TechStackRepository extends Repository { - - TechStack save(TechStack techStack); - - List findAllByTrackId(Integer id); -} diff --git a/src/main/java/in/koreatech/koin/domain/member/repository/TrackRepository.java b/src/main/java/in/koreatech/koin/domain/member/repository/TrackRepository.java deleted file mode 100644 index aa310cdb11..0000000000 --- a/src/main/java/in/koreatech/koin/domain/member/repository/TrackRepository.java +++ /dev/null @@ -1,23 +0,0 @@ -package in.koreatech.koin.domain.member.repository; - -import java.util.List; -import java.util.Optional; - -import org.springframework.data.repository.Repository; - -import in.koreatech.koin.domain.member.exception.TrackNotFoundException; -import in.koreatech.koin.domain.member.model.Track; - -public interface TrackRepository extends Repository { - - Track save(Track track); - - List findAll(); - - Optional findById(Integer trackId); - - default Track getById(Integer trackId) { - return findById(trackId) - .orElseThrow(() -> TrackNotFoundException.withDetail("trackId: " + trackId)); - } -} diff --git a/src/main/java/in/koreatech/koin/domain/member/service/MemberService.java b/src/main/java/in/koreatech/koin/domain/member/service/MemberService.java deleted file mode 100644 index c2f3c33065..0000000000 --- a/src/main/java/in/koreatech/koin/domain/member/service/MemberService.java +++ /dev/null @@ -1,30 +0,0 @@ -package in.koreatech.koin.domain.member.service; - -import java.util.List; - -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import in.koreatech.koin.domain.member.dto.MemberResponse; -import in.koreatech.koin.domain.member.repository.MemberRepository; -import lombok.RequiredArgsConstructor; - -@Service -@RequiredArgsConstructor -@Transactional(readOnly = true) -public class MemberService { - - private final MemberRepository memberRepository; - - public List getMembers() { - var members = memberRepository.findAll(); - return members.stream() - .map(MemberResponse::from) - .toList(); - } - - public MemberResponse getMember(Integer id) { - var member = memberRepository.getById(id); - return MemberResponse.from(member); - } -} diff --git a/src/main/java/in/koreatech/koin/domain/member/service/TrackService.java b/src/main/java/in/koreatech/koin/domain/member/service/TrackService.java deleted file mode 100644 index fe23ba50ab..0000000000 --- a/src/main/java/in/koreatech/koin/domain/member/service/TrackService.java +++ /dev/null @@ -1,38 +0,0 @@ -package in.koreatech.koin.domain.member.service; - -import java.util.List; - -import org.springframework.stereotype.Service; - -import in.koreatech.koin.domain.member.dto.TrackResponse; -import in.koreatech.koin.domain.member.dto.TrackSingleResponse; -import in.koreatech.koin.domain.member.model.Member; -import in.koreatech.koin.domain.member.model.TechStack; -import in.koreatech.koin.domain.member.model.Track; -import in.koreatech.koin.domain.member.repository.MemberRepository; -import in.koreatech.koin.domain.member.repository.TechStackRepository; -import in.koreatech.koin.domain.member.repository.TrackRepository; -import lombok.RequiredArgsConstructor; - -@Service -@RequiredArgsConstructor -public class TrackService { - - private final TrackRepository trackRepository; - private final MemberRepository memberRepository; - private final TechStackRepository techStackRepository; - - public List getTracks() { - return trackRepository.findAll().stream() - .map(TrackResponse::from) - .toList(); - } - - public TrackSingleResponse getTrack(Integer id) { - Track track = trackRepository.getById(id); - List member = memberRepository.findAllByTrackIdAndIsDeletedFalse(id); - List techStacks = techStackRepository.findAllByTrackId(id); - - return TrackSingleResponse.of(track, member, techStacks); - } -} diff --git a/src/main/java/in/koreatech/koin/domain/notification/eventlistener/ArticleKeywordEventListener.java b/src/main/java/in/koreatech/koin/domain/notification/eventlistener/ArticleKeywordEventListener.java index 53f985fe77..5bed9890ba 100644 --- a/src/main/java/in/koreatech/koin/domain/notification/eventlistener/ArticleKeywordEventListener.java +++ b/src/main/java/in/koreatech/koin/domain/notification/eventlistener/ArticleKeywordEventListener.java @@ -1,7 +1,11 @@ package in.koreatech.koin.domain.notification.eventlistener; import static in.koreatech.koin.common.model.MobileAppPath.KEYWORD; +import static in.koreatech.koin.domain.community.keyword.enums.KeywordCategory.KOREATECH; +import static in.koreatech.koin.domain.community.keyword.enums.KeywordCategory.LOST_ITEM; import static in.koreatech.koin.domain.notification.model.NotificationSubscribeType.ARTICLE_KEYWORD; +import static in.koreatech.koin.domain.notification.model.NotificationSubscribeType.LOST_ITEM_KEYWORD; +import static org.springframework.transaction.event.TransactionPhase.AFTER_COMMIT; import java.util.HashSet; import java.util.LinkedHashMap; @@ -13,11 +17,13 @@ import java.util.stream.Collectors; import org.springframework.context.annotation.Profile; +import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; -import org.springframework.util.StringUtils; import org.springframework.transaction.event.TransactionalEventListener; +import org.springframework.util.StringUtils; import in.koreatech.koin.common.event.ArticleKeywordEvent; +import in.koreatech.koin.common.model.MobileAppPath; import in.koreatech.koin.domain.community.article.model.Article; import in.koreatech.koin.domain.community.article.model.Board; import in.koreatech.koin.domain.community.article.repository.ArticleRepository; @@ -26,6 +32,7 @@ import in.koreatech.koin.domain.notification.model.Notification; import in.koreatech.koin.domain.notification.model.NotificationFactory; import in.koreatech.koin.domain.notification.model.NotificationSubscribe; +import in.koreatech.koin.domain.notification.model.NotificationSubscribeType; import in.koreatech.koin.domain.notification.repository.NotificationSubscribeRepository; import in.koreatech.koin.domain.notification.service.NotificationService; import lombok.RequiredArgsConstructor; @@ -42,7 +49,8 @@ public class ArticleKeywordEventListener { // TODO : 리팩터링 필요 (비즈 private final KeywordService keywordService; private final ArticleRepository articleRepository; - @TransactionalEventListener + @Async(value = "keywordNotificationTaskExecutor") + @TransactionalEventListener(phase = AFTER_COMMIT) public void onKeywordRequest(ArticleKeywordEvent event) { Map matchedKeywordByUserId = event.matchedKeywordByUserId(); @@ -52,9 +60,10 @@ public void onKeywordRequest(ArticleKeywordEvent event) { Article article = articleRepository.getById(event.articleId()); Board board = article.getBoard(); + MobileAppPath appPath = getAppPath(event); Map keywordSubscribersByUserId = notificationSubscribeRepository - .findAllBySubscribeTypeAndDetailTypeIsNullWithUser(ARTICLE_KEYWORD) + .findAllBySubscribeTypeAndDetailTypeIsNullWithUser(getSubscribeType(event)) .stream() .filter(this::hasDeviceToken) .collect(Collectors.toMap( @@ -80,6 +89,8 @@ public void onKeywordRequest(ArticleKeywordEvent event) { .map(subscribe -> createNotification( article, board, + appPath, + event, matchedKeywordByUserId.get(subscribe.getUser().getId()), subscribe )) @@ -89,7 +100,8 @@ public void onKeywordRequest(ArticleKeywordEvent event) { notificationService.pushNotificationsWithResult(notifications); for (NotificationService.NotificationDeliveryResult deliveryResult : deliveryResults) { if (deliveryResult.delivered()) { - keywordService.createNotifiedArticleStatus(deliveryResult.notification().getUser().getId(), article.getId()); + keywordService.createNotifiedArticleStatus(deliveryResult.notification().getUser().getId(), + article.getId()); } } } @@ -98,6 +110,26 @@ private boolean hasDeviceToken(NotificationSubscribe subscribe) { return StringUtils.hasText(subscribe.getUser().getDeviceToken()); } + private NotificationSubscribeType getSubscribeType(ArticleKeywordEvent event) { + if (event.category() == KOREATECH) { + return ARTICLE_KEYWORD; + } + if (event.category() == LOST_ITEM) { + return LOST_ITEM_KEYWORD; + } + throw new IllegalArgumentException("지원하지 않는 키워드 카테고리입니다: " + event.category()); + } + + private MobileAppPath getAppPath(ArticleKeywordEvent event) { + if (event.category() == KOREATECH) { + return KEYWORD; + } + if (event.category() == LOST_ITEM) { + return MobileAppPath.LOST_ITEM; + } + throw new IllegalArgumentException("지원하지 않는 키워드 카테고리입니다: " + event.category()); + } + private Set getAlreadyNotifiedUserIds(Integer articleId, Set subscriberUserIds) { if (subscriberUserIds.isEmpty()) { return Set.of(); @@ -116,13 +148,15 @@ private boolean isMyArticle(ArticleKeywordEvent event, NotificationSubscribe sub private Notification createNotification( Article article, Board board, + MobileAppPath appPath, + ArticleKeywordEvent event, String keyword, NotificationSubscribe subscribe ) { - String description = generateDescription(keyword); + String description = generateDescription(event, keyword); - Notification notification = notificationFactory.generateKeywordNotification( - KEYWORD, + return notificationFactory.generateKeywordNotification( + appPath, article.getId(), keyword, article.getTitle(), @@ -130,10 +164,12 @@ private Notification createNotification( description, subscribe.getUser() ); - return notification; } - private String generateDescription(String keyword) { + private String generateDescription(ArticleKeywordEvent event, String keyword) { + if (event.category() == LOST_ITEM) { + return "방금 등록된 %s 분실물 게시물을 확인해보세요!".formatted(keyword); + } return "방금 등록된 %s 공지를 확인해보세요!".formatted(keyword); } } diff --git a/src/main/java/in/koreatech/koin/domain/notification/model/NotificationFactory.java b/src/main/java/in/koreatech/koin/domain/notification/model/NotificationFactory.java index aec0612727..7979789dc5 100644 --- a/src/main/java/in/koreatech/koin/domain/notification/model/NotificationFactory.java +++ b/src/main/java/in/koreatech/koin/domain/notification/model/NotificationFactory.java @@ -212,6 +212,9 @@ private String generateKeywordSchemeUri(MobileAppPath path, Integer eventId, Str if (keyword == null) { return generateSchemeUri(path, eventId); } + if (path == MobileAppPath.LOST_ITEM) { + return String.format("%s?id=%d&keyword=%s", path.getPath(), eventId, keyword); + } return String.format("%s?id=%d&keyword=%s&board-id=%s", path.getPath(), eventId, keyword, boardId); } diff --git a/src/main/java/in/koreatech/koin/domain/notification/model/NotificationSubscribeType.java b/src/main/java/in/koreatech/koin/domain/notification/model/NotificationSubscribeType.java index ff4e50cf9c..c2b6820c87 100644 --- a/src/main/java/in/koreatech/koin/domain/notification/model/NotificationSubscribeType.java +++ b/src/main/java/in/koreatech/koin/domain/notification/model/NotificationSubscribeType.java @@ -20,6 +20,7 @@ public enum NotificationSubscribeType { LOST_ITEM_CHAT(List.of()), CALLVAN(List.of()), MARKETING(List.of()), + LOST_ITEM_KEYWORD(List.of()), ; private final List detailTypes; diff --git a/src/main/java/in/koreatech/koin/global/config/AsyncConfig.java b/src/main/java/in/koreatech/koin/global/config/AsyncConfig.java index e9f4094a83..30025f295d 100644 --- a/src/main/java/in/koreatech/koin/global/config/AsyncConfig.java +++ b/src/main/java/in/koreatech/koin/global/config/AsyncConfig.java @@ -1,11 +1,15 @@ package in.koreatech.koin.global.config; +import static java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy; + import java.util.concurrent.Executor; import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.AsyncConfigurer; import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import lombok.extern.slf4j.Slf4j; @@ -19,6 +23,21 @@ public Executor getAsyncExecutor() { return null; } + @Bean(name = "keywordNotificationTaskExecutor") + public ThreadPoolTaskExecutor keywordNotificationTaskExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(2); + executor.setMaxPoolSize(6); + executor.setQueueCapacity(5); + executor.setKeepAliveSeconds(60); + executor.setThreadNamePrefix("keyword-notification-"); + executor.setRejectedExecutionHandler(new CallerRunsPolicy()); + executor.setWaitForTasksToCompleteOnShutdown(true); + executor.setAwaitTerminationSeconds(30); + executor.initialize(); + return executor; + } + @Override public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { return (throwable, method, params) -> { diff --git a/src/main/resources/db/migration/V235__add_category_to_article_keywords.sql b/src/main/resources/db/migration/V235__add_category_to_article_keywords.sql new file mode 100644 index 0000000000..6fcce431d5 --- /dev/null +++ b/src/main/resources/db/migration/V235__add_category_to_article_keywords.sql @@ -0,0 +1,9 @@ +ALTER TABLE `article_keywords` + ADD COLUMN `category` VARCHAR(20) NOT NULL DEFAULT 'KOREATECH' AFTER `keyword`; + +UPDATE `article_keywords` +SET `category` = 'KOREATECH'; + +ALTER TABLE `article_keywords` + DROP INDEX `keyword`, + ADD UNIQUE KEY `uk_article_keywords_keyword_category` (`keyword`, `category`); diff --git a/src/test/java/in/koreatech/koin/acceptance/admin/AdminMemberApiTest.java b/src/test/java/in/koreatech/koin/acceptance/admin/AdminMemberApiTest.java deleted file mode 100644 index 2ef9ccc58c..0000000000 --- a/src/test/java/in/koreatech/koin/acceptance/admin/AdminMemberApiTest.java +++ /dev/null @@ -1,276 +0,0 @@ -package in.koreatech.koin.acceptance.admin; - -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import org.assertj.core.api.SoftAssertions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.MediaType; - -import in.koreatech.koin.acceptance.AcceptanceTest; -import in.koreatech.koin.acceptance.fixture.MemberAcceptanceFixture; -import in.koreatech.koin.acceptance.fixture.TrackAcceptanceFixture; -import in.koreatech.koin.acceptance.fixture.UserAcceptanceFixture; -import in.koreatech.koin.admin.member.repository.AdminMemberRepository; -import in.koreatech.koin.admin.manager.model.Admin; -import in.koreatech.koin.domain.member.model.Member; - -public class AdminMemberApiTest extends AcceptanceTest { - - @Autowired - private MemberAcceptanceFixture memberFixture; - - @Autowired - private TrackAcceptanceFixture trackFixture; - - @Autowired - private UserAcceptanceFixture userFixture; - - @Autowired - private AdminMemberRepository adminMemberRepository; - - @BeforeAll - void setup() { - clear(); - } - - @Test - void BCSDLab_회원들의_정보를_조회한다() throws Exception { - memberFixture.최준호(trackFixture.backend()); - - Admin adminUser = userFixture.코인_운영자(); - String token = userFixture.getToken(adminUser.getUser()); - - mockMvc.perform( - get("/admin/members") - .header("Authorization", "Bearer " + token) - .param("page", "1") - .param("track", "BACKEND") - .param("is_deleted", "false") - ) - .andExpect(status().isOk()) - .andExpect(content().json(""" - { - "total_count": 1, - "current_count": 1, - "total_page": 1, - "current_page": 1, - "members": [ - { - "id": 1, - "name": "최준호", - "student_number": "2019136135", - "track": "BackEnd", - "position": "Regular", - "email": "testjuno@gmail.com", - "image_url": "https://imagetest.com/juno.jpg", - "is_deleted": false - } - ] - } - """)); - } - - @Test - void 관리자_권한으로_BCSDLab_회원을_추가한다() throws Exception { - trackFixture.backend(); - - Admin adminUser = userFixture.코인_운영자(); - String token = userFixture.getToken(adminUser.getUser()); - - String jsonBody = """ - { - "name": "최준호", - "student_number": "2019136135", - "track": "BackEnd", - "position": "Regular", - "email": "testjuno@gmail.com", - "image_url": "https://imagetest.com/juno.jpg" - } - """; - - mockMvc.perform( - post("/admin/members") - .header("Authorization", "Bearer " + token) - .contentType(MediaType.APPLICATION_JSON) - .content(jsonBody) - ) - .andExpect(status().isCreated()); - - Member savedMember = adminMemberRepository.getByName("최준호"); - - SoftAssertions.assertSoftly(softly -> { - softly.assertThat(savedMember.getName()).isEqualTo("최준호"); - softly.assertThat(savedMember.getStudentNumber()).isEqualTo("2019136135"); - softly.assertThat(savedMember.getTrack().getName()).isEqualTo("BackEnd"); - softly.assertThat(savedMember.getPosition()).isEqualTo("Regular"); - softly.assertThat(savedMember.getEmail()).isEqualTo("testjuno@gmail.com"); - softly.assertThat(savedMember.getImageUrl()).isEqualTo("https://imagetest.com/juno.jpg"); - softly.assertThat(savedMember.isDeleted()).isEqualTo(false); - }); - } - - @Test - void BCSDLab_회원_정보를_조회한다() throws Exception { - memberFixture.최준호(trackFixture.backend()); - - Admin adminUser = userFixture.코인_운영자(); - String token = userFixture.getToken(adminUser.getUser()); - - mockMvc.perform( - get("/admin/members/{id}", 1) - .header("Authorization", "Bearer " + token) - ) - .andExpect(status().isOk()) - .andExpect(content().json(""" - { - "id": 1, - "name": "최준호", - "student_number": "2019136135", - "track": "BackEnd", - "position": "Regular", - "email": "testjuno@gmail.com", - "image_url": "https://imagetest.com/juno.jpg", - "is_deleted": false - } - """)); - } - - @Test - void BCSDLab_회원_정보를_삭제한다() throws Exception { - Member member = memberFixture.최준호(trackFixture.backend()); - Integer memberId = member.getId(); - - Admin adminUser = userFixture.코인_운영자(); - String token = userFixture.getToken(adminUser.getUser()); - - mockMvc.perform( - delete("/admin/members/{id}", memberId) - .header("Authorization", "Bearer " + token) - ) - .andExpect(status().isOk()); - - Member savedMember = adminMemberRepository.getById(memberId); - - SoftAssertions.assertSoftly(softly -> { - softly.assertThat(savedMember.getName()).isEqualTo("최준호"); - softly.assertThat(savedMember.getStudentNumber()).isEqualTo("2019136135"); - softly.assertThat(savedMember.getTrack().getName()).isEqualTo("BackEnd"); - softly.assertThat(savedMember.getPosition()).isEqualTo("Regular"); - softly.assertThat(savedMember.getEmail()).isEqualTo("testjuno@gmail.com"); - softly.assertThat(savedMember.getImageUrl()).isEqualTo("https://imagetest.com/juno.jpg"); - softly.assertThat(savedMember.isDeleted()).isEqualTo(true); - }); - } - - @Test - void BCSDLab_회원_정보를_수정한다() throws Exception { - Member member = memberFixture.최준호(trackFixture.backend()); - Integer memberId = member.getId(); - - Admin adminUser = userFixture.코인_운영자(); - String token = userFixture.getToken(adminUser.getUser()); - - String jsonBody = """ - { - "name": "최준호", - "student_number": "2019136135", - "track": "BackEnd", - "position": "Mentor", - "email": "testjuno@gmail.com", - "image_url": "https://imagetest.com/juno.jpg" - } - """; - - mockMvc.perform( - put("/admin/members/{id}", memberId) - .header("Authorization", "Bearer " + token) - .contentType(MediaType.APPLICATION_JSON) - .content(jsonBody) - ) - .andExpect(status().isOk()); - - Member updatedMember = adminMemberRepository.getById(memberId); - - SoftAssertions.assertSoftly(softly -> { - softly.assertThat(updatedMember.getName()).isEqualTo("최준호"); - softly.assertThat(updatedMember.getStudentNumber()).isEqualTo("2019136135"); - softly.assertThat(updatedMember.getTrack().getName()).isEqualTo("BackEnd"); - softly.assertThat(updatedMember.getPosition()).isEqualTo("Mentor"); - softly.assertThat(updatedMember.getEmail()).isEqualTo("testjuno@gmail.com"); - softly.assertThat(updatedMember.getImageUrl()).isEqualTo("https://imagetest.com/juno.jpg"); - softly.assertThat(updatedMember.isDeleted()).isEqualTo(false); - }); - } - - @Test - void BCSDLab_회원_정보를_트랙과_함께_수정한다() throws Exception { - Member member = memberFixture.최준호(trackFixture.backend()); - trackFixture.frontend(); - Integer memberId = member.getId(); - - Admin adminUser = userFixture.코인_운영자(); - String token = userFixture.getToken(adminUser.getUser()); - - String jsonBody = """ - { - "name": "최준호", - "student_number": "2019136135", - "track": "FrontEnd", - "position": "Mentor", - "email": "testjuno@gmail.com", - "image_url": "https://imagetest.com/juno.jpg" - } - """; - - mockMvc.perform( - put("/admin/members/{id}", memberId) - .header("Authorization", "Bearer " + token) - .contentType(MediaType.APPLICATION_JSON) - .content(jsonBody) - ) - .andExpect(status().isOk()); - - Member updatedMember = adminMemberRepository.getById(memberId); - - SoftAssertions.assertSoftly(softly -> { - softly.assertThat(updatedMember.getName()).isEqualTo("최준호"); - softly.assertThat(updatedMember.getStudentNumber()).isEqualTo("2019136135"); - softly.assertThat(updatedMember.getTrack().getName()).isEqualTo("FrontEnd"); - softly.assertThat(updatedMember.getPosition()).isEqualTo("Mentor"); - softly.assertThat(updatedMember.getEmail()).isEqualTo("testjuno@gmail.com"); - softly.assertThat(updatedMember.getImageUrl()).isEqualTo("https://imagetest.com/juno.jpg"); - softly.assertThat(updatedMember.isDeleted()).isEqualTo(false); - }); - } - - @Test - void BCSDLab_회원_정보를_삭제를_취소한다() throws Exception { - Member member = memberFixture.최준호_삭제(trackFixture.backend()); - Integer memberId = member.getId(); - - Admin adminUser = userFixture.코인_운영자(); - String token = userFixture.getToken(adminUser.getUser()); - - mockMvc.perform( - post("/admin/members/{id}/undelete", memberId) - .header("Authorization", "Bearer " + token) - ) - .andExpect(status().isOk()); - - Member savedMember = adminMemberRepository.getById(memberId); - - SoftAssertions.assertSoftly(softly -> { - softly.assertThat(savedMember.getName()).isEqualTo("최준호"); - softly.assertThat(savedMember.getStudentNumber()).isEqualTo("2019136135"); - softly.assertThat(savedMember.getTrack().getName()).isEqualTo("BackEnd"); - softly.assertThat(savedMember.getPosition()).isEqualTo("Regular"); - softly.assertThat(savedMember.getEmail()).isEqualTo("testjuno@gmail.com"); - softly.assertThat(savedMember.getImageUrl()).isEqualTo("https://imagetest.com/juno.jpg"); - softly.assertThat(savedMember.isDeleted()).isEqualTo(false); - }); - } -} diff --git a/src/test/java/in/koreatech/koin/acceptance/admin/AdminTrackApiTest.java b/src/test/java/in/koreatech/koin/acceptance/admin/AdminTrackApiTest.java deleted file mode 100644 index 057f6bb37a..0000000000 --- a/src/test/java/in/koreatech/koin/acceptance/admin/AdminTrackApiTest.java +++ /dev/null @@ -1,406 +0,0 @@ -package in.koreatech.koin.acceptance.admin; - -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import org.assertj.core.api.SoftAssertions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.MediaType; - -import in.koreatech.koin.acceptance.AcceptanceTest; -import in.koreatech.koin.acceptance.fixture.DepartmentAcceptanceFixture; -import in.koreatech.koin.acceptance.fixture.MemberAcceptanceFixture; -import in.koreatech.koin.acceptance.fixture.TechStackAcceptanceFixture; -import in.koreatech.koin.acceptance.fixture.TrackAcceptanceFixture; -import in.koreatech.koin.acceptance.fixture.UserAcceptanceFixture; -import in.koreatech.koin.admin.member.repository.AdminTechStackRepository; -import in.koreatech.koin.admin.member.repository.AdminTrackRepository; -import in.koreatech.koin.admin.manager.model.Admin; -import in.koreatech.koin.domain.member.model.TechStack; -import in.koreatech.koin.domain.member.model.Track; -import in.koreatech.koin.domain.student.model.Department; -import in.koreatech.koin.domain.student.model.Student; - -public class AdminTrackApiTest extends AcceptanceTest { - - @Autowired - private TrackAcceptanceFixture trackFixture; - - @Autowired - private MemberAcceptanceFixture memberFixture; - - @Autowired - private TechStackAcceptanceFixture techStackFixture; - - @Autowired - private UserAcceptanceFixture userFixture; - - @Autowired - private DepartmentAcceptanceFixture departmentFixture; - - @Autowired - private AdminTrackRepository adminTrackRepository; - - @Autowired - private AdminTechStackRepository adminTechStackRepository; - - @BeforeAll - void setup() { - clear(); - } - - @Test - void 관리자가_BCSDLab_트랙_정보를_조회한다_관리자가_아니면_403_반환() throws Exception { - Department department = departmentFixture.컴퓨터공학부(); - Student student = userFixture.준호_학생(department, null); - String token = userFixture.getToken(student.getUser()); - - mockMvc.perform( - get("/admin/tracks") - .header("Authorization", "Bearer " + token) - ) - .andExpect(status().isForbidden()); - } - - @Test - void 관리자가_BCSDLab_트랙_정보를_조회한다() throws Exception { - Admin adminUser = userFixture.코인_운영자(); - String token = userFixture.getToken(adminUser.getUser()); - - trackFixture.backend(); - trackFixture.frontend(); - trackFixture.ios(); - - mockMvc.perform( - get("/admin/tracks") - .header("Authorization", "Bearer " + token) - ) - .andExpect(status().isOk()) - .andExpect(content().json(""" - [ - { - "id": 1, - "name": "BackEnd", - "headcount": 0, - "is_deleted": false, - "created_at": "2024-01-15 12:00:00", - "updated_at": "2024-01-15 12:00:00" - }, - { - "id": 2, - "name": "FrontEnd", - "headcount": 0, - "is_deleted": false, - "created_at": "2024-01-15 12:00:00", - "updated_at": "2024-01-15 12:00:00" - }, - { - "id": 3, - "name": "iOS", - "headcount": 0, - "is_deleted": false, - "created_at": "2024-01-15 12:00:00", - "updated_at": "2024-01-15 12:00:00" - } - ] - """)); - } - - @Test - void 관리자가_BCSDLab_트랙_정보를_생성한다() throws Exception { - Admin adminUser = userFixture.코인_운영자(); - String token = userFixture.getToken(adminUser.getUser()); - - mockMvc.perform( - post("/admin/tracks") - .header("Authorization", "Bearer " + token) - .contentType(MediaType.APPLICATION_JSON) - .content(""" - { - "name": "BackEnd", - "headcount": 20 - } - """) - ) - .andExpect(status().isOk()) - .andExpect(content().json(""" - { - "id": 1, - "name": "BackEnd", - "headcount": 20, - "is_deleted": false, - "created_at": "2024-01-15 12:00:00", - "updated_at": "2024-01-15 12:00:00" - } - """)); - } - - @Test - void 관리자가_BCSDLab_트랙_정보를_생성한다_이미_있는_트랙명이면_409_반환() throws Exception { - Admin adminUser = userFixture.코인_운영자(); - String token = userFixture.getToken(adminUser.getUser()); - - trackFixture.backend(); - - mockMvc.perform( - post("/admin/tracks") - .header("Authorization", "Bearer " + token) - .contentType(MediaType.APPLICATION_JSON) - .content(""" - { - "name": "BackEnd", - "headcount": 20 - } - """) - ) - .andExpect(status().isConflict()); - } - - @Test - void 관리자가_BCSDLab_트랙_단거_정보를_조회한다() throws Exception { - Admin adminUser = userFixture.코인_운영자(); - String token = userFixture.getToken(adminUser.getUser()); - - Track backend = trackFixture.backend(); - trackFixture.ai(); // 삭제된 트랙 - memberFixture.배진호(backend); // 삭제된 멤버 - memberFixture.최준호(backend); - techStackFixture.java(backend); - techStackFixture.adobeFlash(backend); //삭제된 기술스택 - - mockMvc.perform( - get("/admin/tracks/{id}", backend.getId()) - .header("Authorization", "Bearer " + token) - ) - .andExpect(status().isOk()) - .andExpect(content().json(""" - { - "TrackName": "BackEnd", - "Members": [ - { - "id": 1, - "name": "배진호", - "student_number": "2020136061", - "position": "Regular", - "track": "BackEnd", - "email": "testjhb@gmail.com", - "image_url": "https://imagetest.com/jino.jpg", - "is_deleted": true, - "created_at": "2024-01-15 12:00:00", - "updated_at": "2024-01-15 12:00:00" - }, - { - "id": 2, - "name": "최준호", - "student_number": "2019136135", - "position": "Regular", - "track": "BackEnd", - "email": "testjuno@gmail.com", - "image_url": "https://imagetest.com/juno.jpg", - "is_deleted": false, - "created_at": "2024-01-15 12:00:00", - "updated_at": "2024-01-15 12:00:00" - } - ], - "TechStacks": [ - { - "id": 1, - "name": "Java", - "description": "Language", - "image_url": "https://testimageurl.com", - "track_id": 1, - "is_deleted": false, - "created_at": "2024-01-15 12:00:00", - "updated_at": "2024-01-15 12:00:00" - }, - { - "id": 2, - "name": "AdobeFlash", - "description": "deleted", - "image_url": "https://testimageurl.com", - "track_id": 1, - "is_deleted": true, - "created_at": "2024-01-15 12:00:00", - "updated_at": "2024-01-15 12:00:00" - } - ] - } - """)); - } - - @Test - void 관리자가_BCSDLab_트랙_정보를_수정한다() throws Exception { - Admin adminUser = userFixture.코인_운영자(); - String token = userFixture.getToken(adminUser.getUser()); - - Track backEnd = trackFixture.backend(); - - mockMvc.perform( - put("/admin/tracks/{id}", backEnd.getId()) - .header("Authorization", "Bearer " + token) - .contentType(MediaType.APPLICATION_JSON) - .content(""" - { - "name": "frontEnd", - "headcount": 20 - } - """) - ) - .andExpect(status().isOk()) - .andExpect(content().json(""" - { - "id": 1, - "name": "frontEnd", - "headcount": 20, - "is_deleted": false, - "created_at": "2024-01-15 12:00:00", - "updated_at": "2024-01-15 12:00:00" - } - """)); - } - - @Test - void 관리자가_BCSDLab_트랙_정보를_수정한다_이미_있는_트랙명이면_409_반환() throws Exception { - Admin adminUser = userFixture.코인_운영자(); - String token = userFixture.getToken(adminUser.getUser()); - - Track backEnd = trackFixture.backend(); - - mockMvc.perform( - put("/admin/tracks/{id}", backEnd.getId()) - .header("Authorization", "Bearer " + token) - .contentType(MediaType.APPLICATION_JSON) - .content(""" - { - "name": "BackEnd", - "headcount": 20 - } - """) - ) - .andExpect(status().isConflict()); - } - - @Test - void 관리자가_BCSDLab_트랙_정보를_삭제한다() throws Exception { - Admin adminUser = userFixture.코인_운영자(); - String token = userFixture.getToken(adminUser.getUser()); - - Track backEnd = trackFixture.backend(); - - mockMvc.perform( - delete("/admin/tracks/{id}", backEnd.getId()) - .header("Authorization", "Bearer " + token) - ) - .andExpect(status().isOk()); - - Track updatedTrack = adminTrackRepository.getById(backEnd.getId()); - - SoftAssertions.assertSoftly(softly -> { - softly.assertThat(updatedTrack.getName()).isEqualTo(backEnd.getName()); - softly.assertThat(updatedTrack.getHeadcount()).isEqualTo(backEnd.getHeadcount()); - softly.assertThat(updatedTrack.isDeleted()).isEqualTo(true); - }); - } - - @Test - void 관리자가_BCSDLab_기술스택_정보를_생성한다() throws Exception { - Admin adminUser = userFixture.코인_운영자(); - String token = userFixture.getToken(adminUser.getUser()); - - trackFixture.frontend(); - Track backEnd = trackFixture.backend(); - - mockMvc.perform( - post("/admin/techStacks") - .header("Authorization", "Bearer " + token) - .contentType(MediaType.APPLICATION_JSON) - .content(""" - { - "image_url": "https://url.com", - "name": "Spring", - "description": "스프링은 웹 프레임워크이다" - } - """) - .param("trackName", backEnd.getName()) - ) - .andExpect(status().isOk()) - .andExpect(content().json(""" - { - "id": 1, - "image_url": "https://url.com", - "name": "Spring", - "description": "스프링은 웹 프레임워크이다", - "track_id": 2, - "is_deleted": false, - "created_at": "2024-01-15 12:00:00", - "updated_at": "2024-01-15 12:00:00" - } - """)); - } - - @Test - void 관리자가_BCSDLab_기술스택_정보를_수정한다() throws Exception { - Admin adminUser = userFixture.코인_운영자(); - String token = userFixture.getToken(adminUser.getUser()); - - TechStack java = techStackFixture.java(trackFixture.frontend()); - Track backEnd = trackFixture.backend(); - - mockMvc.perform( - put("/admin/techStacks/{id}", java.getId()) - .header("Authorization", "Bearer " + token) - .contentType(MediaType.APPLICATION_JSON) - .content(""" - { - "image_url": "https://java.com", - "name": "JAVA", - "description": "java의 TrackID를 BackEnd로 수정한다.", - "is_deleted": true - } - """) - .param("trackName", backEnd.getName()) - ) - .andExpect(status().isOk()) - .andExpect(content().json(""" - { - "id": 1, - "image_url": "https://java.com", - "name": "JAVA", - "description": "java의 TrackID를 BackEnd로 수정한다.", - "track_id": 2, - "is_deleted": true, - "created_at": "2024-01-15 12:00:00", - "updated_at": "2024-01-15 12:00:00" - } - """)); - } - - @Test - void 관리자가_기술스택_정보를_삭제한다() throws Exception { - Admin adminUser = userFixture.코인_운영자(); - String token = userFixture.getToken(adminUser.getUser()); - - Track backEnd = trackFixture.backend(); - TechStack java = techStackFixture.java(backEnd); - - mockMvc.perform( - delete("/admin/techStacks/{id}", java.getId()) - .header("Authorization", "Bearer " + token) - - ) - .andExpect(status().isOk()); - - TechStack updatedtechStack = adminTechStackRepository.getById(java.getId()); - - SoftAssertions.assertSoftly(softly -> { - softly.assertThat(updatedtechStack.getImageUrl()).isEqualTo(java.getImageUrl()); - softly.assertThat(updatedtechStack.getName()).isEqualTo(java.getName()); - softly.assertThat(updatedtechStack.getDescription()).isEqualTo(java.getDescription()); - softly.assertThat(updatedtechStack.getTrackId()).isEqualTo(backEnd.getId()); - softly.assertThat(updatedtechStack.isDeleted()).isEqualTo(true); - }); - } -} diff --git a/src/test/java/in/koreatech/koin/acceptance/domain/ActivityApiTest.java b/src/test/java/in/koreatech/koin/acceptance/domain/ActivityApiTest.java deleted file mode 100644 index a1d98cfbf7..0000000000 --- a/src/test/java/in/koreatech/koin/acceptance/domain/ActivityApiTest.java +++ /dev/null @@ -1,167 +0,0 @@ -package in.koreatech.koin.acceptance.domain; - -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import java.time.LocalDate; - -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.MediaType; - -import in.koreatech.koin.acceptance.AcceptanceTest; -import in.koreatech.koin.acceptance.fixture.ActivityAcceptanceFixture; - -class ActivityApiTest extends AcceptanceTest { - - @Autowired - protected ActivityAcceptanceFixture activityFixture; - - @Test - void BCSDLab_활동_내역을_조회한다() throws Exception { - activityFixture.builder() - .title("BCSD/KAP 통합") - .description("BCSD와 KAP가 통합되었습니다.") - .imageUrls("https://test.com.png") - .date(LocalDate.of(2018, 9, 12)) - .isDeleted(false) - .build(); - - activityFixture.builder() - .title("19-3기 모집") - .description("BCSD Lab과 함께 성장해나갈 인재를 모집했습니다.") - .imageUrls(""" - https://test2.com.png, - https://test3.com.png - """) - .date(LocalDate.of(2019, 7, 29)) - .isDeleted(false) - .build(); - - activityFixture.builder() - .title("코인 시간표 기능 추가") - .description("더 편리한 서비스 제공을 위해 시간표 기능을 추가했습니다") - .imageUrls("https://test4.com.png") - .date(LocalDate.of(2019, 8, 20)) - .isDeleted(false) - .build(); - - mockMvc.perform( - get("/activities") - .param("year", "2019") - .contentType(MediaType.APPLICATION_JSON) - ) - .andExpect(status().isOk()) - .andExpect(content().json(""" - { - "Activities": [ - { - "date": "2019-07-29", - "is_deleted": false, - "updated_at": "2024-01-15 12:00:00", - "created_at": "2024-01-15 12:00:00", - "description": "BCSD Lab과 함께 성장해나갈 인재를 모집했습니다.", - "image_urls": [ - "https://test2.com.png", - "https://test3.com.png" - ], - "id": 2, - "title": "19-3기 모집" - }, - { - "date": "2019-08-20", - "is_deleted": false, - "updated_at": "2024-01-15 12:00:00", - "created_at": "2024-01-15 12:00:00", - "description": "더 편리한 서비스 제공을 위해 시간표 기능을 추가했습니다", - "image_urls": [ - "https://test4.com.png" - ], - "id": 3, - "title": "코인 시간표 기능 추가" - } - ] - } - """)); - } - - @Test - void BCSDLab_활동_내역을_조회한다_파라미터가_없는_경우_전체조회() throws Exception { - activityFixture.builder() - .title("BCSD/KAP 통합") - .description("BCSD와 KAP가 통합되었습니다.") - .imageUrls("https://test.com.png") - .date(LocalDate.of(2018, 9, 12)) - .isDeleted(false) - .build(); - - activityFixture.builder() - .title("19-3기 모집") - .description("BCSD Lab과 함께 성장해나갈 인재를 모집했습니다.") - .imageUrls(""" - https://test2.com.png, - https://test3.com.png - """) - .date(LocalDate.of(2019, 7, 29)) - .isDeleted(false) - .build(); - - activityFixture.builder() - .title("코인 시간표 기능 추가") - .description("더 편리한 서비스 제공을 위해 시간표 기능을 추가했습니다") - .imageUrls("https://test4.com.png") - .date(LocalDate.of(2019, 8, 20)) - .isDeleted(false) - .build(); - - mockMvc.perform( - get("/activities") - .contentType(MediaType.APPLICATION_JSON) - ) - .andExpect(status().isOk()) - .andExpect(content().json(""" - { - "Activities": [ - { - "date": "2018-09-12", - "is_deleted": false, - "updated_at": "2024-01-15 12:00:00", - "created_at": "2024-01-15 12:00:00", - "description": "BCSD와 KAP가 통합되었습니다.", - "image_urls": [ - "https://test.com.png" - ], - "id": 1, - "title": "BCSD/KAP 통합" - }, - { - "date": "2019-07-29", - "is_deleted": false, - "updated_at": "2024-01-15 12:00:00", - "created_at": "2024-01-15 12:00:00", - "description": "BCSD Lab과 함께 성장해나갈 인재를 모집했습니다.", - "image_urls": [ - "https://test2.com.png", - "https://test3.com.png" - ], - "id": 2, - "title": "19-3기 모집" - }, - { - "date": "2019-08-20", - "is_deleted": false, - "updated_at": "2024-01-15 12:00:00", - "created_at": "2024-01-15 12:00:00", - "description": "더 편리한 서비스 제공을 위해 시간표 기능을 추가했습니다", - "image_urls": [ - "https://test4.com.png" - ], - "id": 3, - "title": "코인 시간표 기능 추가" - } - ] - } - """)); - } -} diff --git a/src/test/java/in/koreatech/koin/acceptance/domain/MemberApiTest.java b/src/test/java/in/koreatech/koin/acceptance/domain/MemberApiTest.java deleted file mode 100644 index e5397eb259..0000000000 --- a/src/test/java/in/koreatech/koin/acceptance/domain/MemberApiTest.java +++ /dev/null @@ -1,113 +0,0 @@ -package in.koreatech.koin.acceptance.domain; - -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.MediaType; -import org.springframework.test.web.servlet.MvcResult; - -import in.koreatech.koin.acceptance.AcceptanceTest; -import in.koreatech.koin.acceptance.fixture.MemberAcceptanceFixture; -import in.koreatech.koin.acceptance.support.JsonAssertions; -import in.koreatech.koin.domain.member.model.Member; -import in.koreatech.koin.domain.member.model.Track; -import in.koreatech.koin.domain.member.repository.TrackRepository; - -class MemberApiTest extends AcceptanceTest { - - @Autowired - private TrackRepository trackRepository; - - @Autowired - private MemberAcceptanceFixture memberFixture; - - Track backend; - Track frontend; - - @BeforeAll - void setUp() { - clear(); - backend = trackRepository.save( - Track.builder() - .name("BackEnd") - .build() - ); - frontend = trackRepository.save( - Track.builder() - .name("FrontEnd") - .build() - ); - } - - @Test - void BCSDLab_회원의_정보를_조회한다() throws Exception { - Member member = memberFixture.최준호(backend); - - MvcResult result = mockMvc.perform( - get("/members/{id}", member.getId()) - .contentType(MediaType.APPLICATION_JSON) - ) - .andExpect(status().isOk()) - .andReturn(); - - JsonAssertions.assertThat(result.getResponse().getContentAsString()) - .isEqualTo(""" - { - "id": 1, - "name": "최준호", - "student_number": "2019136135", - "track": "BackEnd", - "position": "Regular", - "email": "testjuno@gmail.com", - "image_url": "https://imagetest.com/juno.jpg", - "is_deleted": false, - "created_at": "2024-01-15 12:00:00", - "updated_at": "2024-01-15 12:00:00" - }""" - ); - } - - @Test - void BCSDLab_회원들의_정보를_조회한다() throws Exception { - memberFixture.최준호(backend); - memberFixture.박한수(frontend); - - mockMvc.perform( - get("/members") - .contentType(MediaType.APPLICATION_JSON) - ) - .andExpect(status().isOk()) - .andExpect(content().json(""" - [ - { - "id": 1, - "name": "최준호", - "student_number": "2019136135", - "track": "BackEnd", - "position": "Regular", - "email": "testjuno@gmail.com", - "image_url": "https://imagetest.com/juno.jpg", - "is_deleted": false, - "created_at": "2024-01-15 12:00:00", - "updated_at": "2024-01-15 12:00:00" - }, - { - "id": 2, - "name": "박한수", - "student_number": "2019136064", - "track": "FrontEnd", - "position": "Regular", - "email": "testhsp@gmail.com", - "image_url": "https://imagetest.com/juno.jpg", - "is_deleted": false, - "created_at": "2024-01-15 12:00:00", - "updated_at": "2024-01-15 12:00:00" - } - ] - """)); - } -} diff --git a/src/test/java/in/koreatech/koin/acceptance/domain/NotificationApiTest.java b/src/test/java/in/koreatech/koin/acceptance/domain/NotificationApiTest.java index b526a0bbbf..2a1c31cda7 100644 --- a/src/test/java/in/koreatech/koin/acceptance/domain/NotificationApiTest.java +++ b/src/test/java/in/koreatech/koin/acceptance/domain/NotificationApiTest.java @@ -139,6 +139,13 @@ void setUp() { "detail_subscribes": [ \s ] + }, + { + "type": "LOST_ITEM_KEYWORD", + "is_permit": false, + "detail_subscribes": [ + \s + ] } ] } @@ -271,6 +278,13 @@ void setUp() { "detail_subscribes": [ \s ] + }, + { + "type": "LOST_ITEM_KEYWORD", + "is_permit": false, + "detail_subscribes": [ + \s + ] } ] } @@ -403,6 +417,13 @@ void setUp() { "detail_subscribes": [ \s ] + }, + { + "type": "LOST_ITEM_KEYWORD", + "is_permit": false, + "detail_subscribes": [ + \s + ] } ] } @@ -543,6 +564,13 @@ void setUp() { "detail_subscribes": [ \s ] + }, + { + "type": "LOST_ITEM_KEYWORD", + "is_permit": false, + "detail_subscribes": [ + \s + ] } ] } @@ -680,6 +708,13 @@ void setUp() { "detail_subscribes": [ \s ] + }, + { + "type": "LOST_ITEM_KEYWORD", + "is_permit": false, + "detail_subscribes": [ + \s + ] } ] } diff --git a/src/test/java/in/koreatech/koin/acceptance/domain/TrackApiTest.java b/src/test/java/in/koreatech/koin/acceptance/domain/TrackApiTest.java deleted file mode 100644 index b279be368e..0000000000 --- a/src/test/java/in/koreatech/koin/acceptance/domain/TrackApiTest.java +++ /dev/null @@ -1,185 +0,0 @@ -package in.koreatech.koin.acceptance.domain; - -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.MediaType; - -import in.koreatech.koin.acceptance.AcceptanceTest; -import in.koreatech.koin.acceptance.fixture.MemberAcceptanceFixture; -import in.koreatech.koin.acceptance.fixture.TechStackAcceptanceFixture; -import in.koreatech.koin.acceptance.fixture.TrackAcceptanceFixture; -import in.koreatech.koin.domain.member.model.Track; - -class TrackApiTest extends AcceptanceTest { - - @Autowired - private TrackAcceptanceFixture trackFixture; - - @Autowired - private MemberAcceptanceFixture memberFixture; - - @Autowired - private TechStackAcceptanceFixture techStackFixture; - - @BeforeAll - void setup() { - clear(); - } - - @Test - void BCSDLab_트랙_정보를_조회한다() throws Exception { - trackFixture.backend(); - trackFixture.frontend(); - trackFixture.ios(); - - mockMvc.perform( - get("/tracks") - .contentType(MediaType.APPLICATION_JSON) - ) - .andExpect(status().isOk()) - .andExpect(content().json(""" - [ - { - "id": 1, - "name": "BackEnd", - "headcount": 0, - "is_deleted": false, - "created_at": "2024-01-15 12:00:00", - "updated_at": "2024-01-15 12:00:00" - }, - { - "id": 2, - "name": "FrontEnd", - "headcount": 0, - "is_deleted": false, - "created_at": "2024-01-15 12:00:00", - "updated_at": "2024-01-15 12:00:00" - }, - { - "id": 3, - "name": "iOS", - "headcount": 0, - "is_deleted": false, - "created_at": "2024-01-15 12:00:00", - "updated_at": "2024-01-15 12:00:00" - } - ] - """)); - } - - @Test - void BCSDLab_트랙_정보_단건_조회_삭제된_멤버는_조회하지_않는다() throws Exception { - Track track = trackFixture.backend(); - memberFixture.배진호(track); // 삭제된 멤버 - memberFixture.최준호(track); - techStackFixture.java(track); - - mockMvc.perform( - get("/tracks/{id}", track.getId()) - .contentType(MediaType.APPLICATION_JSON) - ) - .andExpect(status().isOk()) - .andExpect(content().json(""" - { - "TrackName": "BackEnd", - "TechStacks": [ - { - "id": 1, - "name": "Java", - "description": "Language", - "image_url": "https://testimageurl.com", - "track_id": 1, - "is_deleted": false, - "created_at": "2024-01-15 12:00:00", - "updated_at": "2024-01-15 12:00:00" - } - ], - "Members": [ - { - "id": 2, - "name": "최준호", - "student_number": "2019136135", - "position": "Regular", - "track": "BackEnd", - "email": "testjuno@gmail.com", - "image_url": "https://imagetest.com/juno.jpg", - "is_deleted": false, - "created_at": "2024-01-15 12:00:00", - "updated_at": "2024-01-15 12:00:00" - } - ] - } - """)); - } - - @Test - void BCSDLab_트랙_정보_단건_조회() throws Exception { - Track track = trackFixture.backend(); - memberFixture.최준호(track); - techStackFixture.java(track); - - mockMvc.perform( - get("/tracks/{id}", track.getId()) - .contentType(MediaType.APPLICATION_JSON) - ) - .andExpect(status().isOk()) - .andExpect(content().json(""" - { - "TrackName": "BackEnd", - "TechStacks": [ - { - "id": 1, - "name": "Java", - "description": "Language", - "image_url": "https://testimageurl.com", - "track_id": 1, - "is_deleted": false, - "created_at": "2024-01-15 12:00:00", - "updated_at": "2024-01-15 12:00:00" - } - ], - "Members": [ - { - "id": 1, - "name": "최준호", - "student_number": "2019136135", - "position": "Regular", - "track": "BackEnd", - "email": "testjuno@gmail.com", - "image_url": "https://imagetest.com/juno.jpg", - "is_deleted": false, - "created_at": "2024-01-15 12:00:00", - "updated_at": "2024-01-15 12:00:00" - } - ] - } - """)); - } - - @Test - void BCSDLab_트랙_정보_단건_조회_트랙에_속한_멤버와_기술스택이_없을_때() throws Exception { - Track track = trackFixture.frontend(); - - mockMvc.perform( - get("/tracks/{id}", track.getId()) - .contentType(MediaType.APPLICATION_JSON) - ) - .andExpect(status().isOk()) - .andExpect(content().json(""" - { - "TrackName": "FrontEnd", - "TechStacks": [ - - ], - "Members": [ - - ] - } - """)); - } -} diff --git a/src/test/java/in/koreatech/koin/acceptance/fixture/ActivityAcceptanceFixture.java b/src/test/java/in/koreatech/koin/acceptance/fixture/ActivityAcceptanceFixture.java deleted file mode 100644 index 067f6e0db1..0000000000 --- a/src/test/java/in/koreatech/koin/acceptance/fixture/ActivityAcceptanceFixture.java +++ /dev/null @@ -1,70 +0,0 @@ -package in.koreatech.koin.acceptance.fixture; - -import java.time.LocalDate; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -import in.koreatech.koin.domain.activity.model.Activity; -import in.koreatech.koin.domain.activity.repository.ActivityRepository; - -@Component -public final class ActivityAcceptanceFixture { - - private final ActivityRepository activityRepository; - - @Autowired - public ActivityAcceptanceFixture(ActivityRepository activityRepository) { - this.activityRepository = activityRepository; - } - - public ActivityFixtureBuilder builder() { - return new ActivityFixtureBuilder(); - } - - public final class ActivityFixtureBuilder { - - private String title; - private String description; - private String imageUrls; - private LocalDate date; - private boolean isDeleted; - - public ActivityFixtureBuilder title(String title) { - this.title = title; - return this; - } - - public ActivityFixtureBuilder description(String description) { - this.description = description; - return this; - } - - public ActivityFixtureBuilder imageUrls(String imageUrls) { - this.imageUrls = imageUrls; - return this; - } - - public ActivityFixtureBuilder date(LocalDate date) { - this.date = date; - return this; - } - - public ActivityFixtureBuilder isDeleted(boolean isDeleted) { - this.isDeleted = isDeleted; - return this; - } - - public Activity build() { - return activityRepository.save( - Activity.builder() - .title(title) - .description(description) - .imageUrls(imageUrls) - .date(date) - .isDeleted(isDeleted) - .build() - ); - } - } -} diff --git a/src/test/java/in/koreatech/koin/acceptance/fixture/MemberAcceptanceFixture.java b/src/test/java/in/koreatech/koin/acceptance/fixture/MemberAcceptanceFixture.java deleted file mode 100644 index bc86da6a5f..0000000000 --- a/src/test/java/in/koreatech/koin/acceptance/fixture/MemberAcceptanceFixture.java +++ /dev/null @@ -1,80 +0,0 @@ -package in.koreatech.koin.acceptance.fixture; - -import org.springframework.stereotype.Component; - -import in.koreatech.koin.domain.member.model.Member; -import in.koreatech.koin.domain.member.model.Track; -import in.koreatech.koin.domain.member.repository.MemberRepository; -import in.koreatech.koin.domain.member.repository.TrackRepository; - -@Component -@SuppressWarnings("NonAsciiCharacters") -public class MemberAcceptanceFixture { - - private final MemberRepository memberRepository; - private final TrackRepository trackRepository; - - public MemberAcceptanceFixture( - MemberRepository memberRepository, - TrackRepository trackRepository - ) { - this.memberRepository = memberRepository; - this.trackRepository = trackRepository; - } - - public Member 최준호(Track track) { - return memberRepository.save( - Member.builder() - .isDeleted(false) - .studentNumber("2019136135") - .imageUrl("https://imagetest.com/juno.jpg") - .name("최준호") - .position("Regular") - .track(track) - .email("testjuno@gmail.com") - .build() - ); - } - - public Member 박한수(Track track) { - return memberRepository.save( - Member.builder() - .isDeleted(false) - .studentNumber("2019136064") - .imageUrl("https://imagetest.com/juno.jpg") - .name("박한수") - .position("Regular") - .track(track) - .email("testhsp@gmail.com") - .build() - ); - } - - public Member 배진호(Track track) { - return memberRepository.save( - Member.builder() - .isDeleted(true) - .studentNumber("2020136061") - .imageUrl("https://imagetest.com/jino.jpg") - .name("배진호") - .position("Regular") - .track(track) - .email("testjhb@gmail.com") - .build() - ); - } - - public Member 최준호_삭제(Track track) { - return memberRepository.save( - Member.builder() - .isDeleted(true) - .studentNumber("2019136135") - .imageUrl("https://imagetest.com/juno.jpg") - .name("최준호") - .position("Regular") - .track(track) - .email("testjuno@gmail.com") - .build() - ); - } -} diff --git a/src/test/java/in/koreatech/koin/acceptance/fixture/TechStackAcceptanceFixture.java b/src/test/java/in/koreatech/koin/acceptance/fixture/TechStackAcceptanceFixture.java deleted file mode 100644 index 8453745462..0000000000 --- a/src/test/java/in/koreatech/koin/acceptance/fixture/TechStackAcceptanceFixture.java +++ /dev/null @@ -1,40 +0,0 @@ -package in.koreatech.koin.acceptance.fixture; - -import org.springframework.stereotype.Component; - -import in.koreatech.koin.domain.member.model.TechStack; -import in.koreatech.koin.domain.member.model.Track; -import in.koreatech.koin.domain.member.repository.TechStackRepository; - -@Component -public class TechStackAcceptanceFixture { - - private final TechStackRepository techStackRepository; - - public TechStackAcceptanceFixture(TechStackRepository techStackRepository) { - this.techStackRepository = techStackRepository; - } - - public TechStack java(Track track) { - return techStackRepository.save( - TechStack.builder() - .imageUrl("https://testimageurl.com") - .trackId(track.getId()) - .name("Java") - .description("Language") - .build() - ); - } - - public TechStack adobeFlash(Track track) { - return techStackRepository.save( - TechStack.builder() - .imageUrl("https://testimageurl.com") - .trackId(track.getId()) - .name("AdobeFlash") - .description("deleted") - .isDeleted(true) - .build() - ); - } -} diff --git a/src/test/java/in/koreatech/koin/acceptance/fixture/TrackAcceptanceFixture.java b/src/test/java/in/koreatech/koin/acceptance/fixture/TrackAcceptanceFixture.java deleted file mode 100644 index 5265afb6e7..0000000000 --- a/src/test/java/in/koreatech/koin/acceptance/fixture/TrackAcceptanceFixture.java +++ /dev/null @@ -1,49 +0,0 @@ -package in.koreatech.koin.acceptance.fixture; - -import org.springframework.stereotype.Component; - -import in.koreatech.koin.domain.member.model.Track; -import in.koreatech.koin.domain.member.repository.TrackRepository; - -@Component -public class TrackAcceptanceFixture { - - private final TrackRepository trackRepository; - - public TrackAcceptanceFixture(TrackRepository trackRepository) { - this.trackRepository = trackRepository; - } - - public Track backend() { - return trackRepository.save( - Track.builder() - .name("BackEnd") - .build() - ); - } - - public Track frontend() { - return trackRepository.save( - Track.builder() - .name("FrontEnd") - .build() - ); - } - - public Track ios() { - return trackRepository.save( - Track.builder() - .name("iOS") - .build() - ); - } - - public Track ai() { - return trackRepository.save( - Track.builder() - .name("AI") - .isDeleted(true) - .build() - ); - } -} diff --git a/src/test/java/in/koreatech/koin/unit/domain/bus/CityBusRouteStrategyTest.java b/src/test/java/in/koreatech/koin/unit/domain/bus/CityBusRouteStrategyTest.java new file mode 100644 index 0000000000..ccac825a3f --- /dev/null +++ b/src/test/java/in/koreatech/koin/unit/domain/bus/CityBusRouteStrategyTest.java @@ -0,0 +1,239 @@ +package in.koreatech.koin.unit.domain.bus; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import in.koreatech.koin.domain.bus.dto.BusRouteCommand; +import in.koreatech.koin.domain.bus.dto.BusScheduleResponse.ScheduleInfo; +import in.koreatech.koin.domain.bus.enums.BusRouteType; +import in.koreatech.koin.domain.bus.enums.BusStation; +import in.koreatech.koin.domain.bus.service.city.model.CityBusTimetable; +import in.koreatech.koin.domain.bus.service.city.repository.CityBusTimetableRepository; +import in.koreatech.koin.domain.bus.service.model.route.CityBusRouteStrategy; + +@ExtendWith(MockitoExtension.class) +class CityBusRouteStrategyTest { + + @InjectMocks + private CityBusRouteStrategy cityBusRouteStrategy; + + @Mock + private CityBusTimetableRepository cityBusTimetableRepository; + + // 평일 날짜 고정 + private static final LocalDate WEEKDAY = LocalDate.of(2024, 4, 8); // 월요일 + private static final LocalTime QUERY_TIME = LocalTime.of(10, 0); + + private CityBusTimetable timetable400Terminal; + private CityBusTimetable timetable402Terminal; + private CityBusTimetable timetable405Terminal; + private CityBusTimetable timetable400Koreatech; + private CityBusTimetable timetable402Koreatech; + private CityBusTimetable timetable405Koreatech; + + @BeforeEach + void setUp() { + CityBusTimetable.BusTimetable weekdayTimetable = CityBusTimetable.BusTimetable.builder() + .dayOfWeek("평일") + .departInfo(List.of("09:00", "11:00", "13:00")) + .build(); + + // 종합터미널 출발 (TERMINAL, STATION 방면 조회 시 사용) + timetable400Terminal = CityBusTimetable.builder() + .busInfo(CityBusTimetable.BusInfo.builder().number(400L).depart("종합터미널").arrival("코리아텍").build()) + .busTimetables(List.of(weekdayTimetable)) + .updatedAt(LocalDateTime.now()) + .build(); + + timetable402Terminal = CityBusTimetable.builder() + .busInfo(CityBusTimetable.BusInfo.builder().number(402L).depart("종합터미널").arrival("코리아텍").build()) + .busTimetables(List.of(weekdayTimetable)) + .updatedAt(LocalDateTime.now()) + .build(); + + timetable405Terminal = CityBusTimetable.builder() + .busInfo(CityBusTimetable.BusInfo.builder().number(405L).depart("종합터미널").arrival("코리아텍").build()) + .busTimetables(List.of(weekdayTimetable)) + .updatedAt(LocalDateTime.now()) + .build(); + + // 코리아텍 출발 (KOREATECH 방면 조회 시 사용) + timetable400Koreatech = CityBusTimetable.builder() + .busInfo(CityBusTimetable.BusInfo.builder().number(400L).depart("병천3리").arrival("종합터미널").build()) + .busTimetables(List.of(weekdayTimetable)) + .updatedAt(LocalDateTime.now()) + .build(); + + timetable402Koreatech = CityBusTimetable.builder() + .busInfo(CityBusTimetable.BusInfo.builder().number(402L).depart("황사동").arrival("종합터미널").build()) + .busTimetables(List.of(weekdayTimetable)) + .updatedAt(LocalDateTime.now()) + .build(); + + timetable405Koreatech = CityBusTimetable.builder() + .busInfo(CityBusTimetable.BusInfo.builder().number(405L).depart("유관순열사사적지").arrival("종합터미널").build()) + .busTimetables(List.of(weekdayTimetable)) + .updatedAt(LocalDateTime.now()) + .build(); + } + + @Nested + @DisplayName("학교 방면 시내버스 시간표 조회") + class SchoolDirectionTest { + + @Test + @DisplayName("터미널 → 천안역: 종합터미널 기점 시간표를 반환한다") + void 터미널에서_천안역_시간표_조회() { + // given + BusRouteCommand command = new BusRouteCommand( + BusStation.TERMINAL, BusStation.STATION, BusRouteType.CITY, WEEKDAY, QUERY_TIME); + + when(cityBusTimetableRepository.getByBusInfoNumberAndBusInfoDepart(400L, "종합터미널")) + .thenReturn(timetable400Terminal); + when(cityBusTimetableRepository.getByBusInfoNumberAndBusInfoDepart(402L, "종합터미널")) + .thenReturn(timetable402Terminal); + when(cityBusTimetableRepository.getByBusInfoNumberAndBusInfoDepart(405L, "종합터미널")) + .thenReturn(timetable405Terminal); + + // when + List result = cityBusRouteStrategy.findSchedule(command); + + // then + assertThat(result).isNotEmpty(); + assertThat(result).hasSize(9); // 3개 버스 × 3개 시간 + assertThat(result).allMatch(info -> info.busType().equals("city")); + } + + @Test + @DisplayName("터미널 → 코리아텍: 종합터미널 기점 시간표를 반환한다") + void 터미널에서_코리아텍_시간표_조회() { + // given + BusRouteCommand command = new BusRouteCommand( + BusStation.TERMINAL, BusStation.KOREATECH, BusRouteType.CITY, WEEKDAY, QUERY_TIME); + + when(cityBusTimetableRepository.getByBusInfoNumberAndBusInfoDepart(400L, "종합터미널")) + .thenReturn(timetable400Terminal); + when(cityBusTimetableRepository.getByBusInfoNumberAndBusInfoDepart(402L, "종합터미널")) + .thenReturn(timetable402Terminal); + when(cityBusTimetableRepository.getByBusInfoNumberAndBusInfoDepart(405L, "종합터미널")) + .thenReturn(timetable405Terminal); + + // when + List result = cityBusRouteStrategy.findSchedule(command); + + // then + assertThat(result).isNotEmpty(); + assertThat(result).hasSize(9); + assertThat(result).allMatch(info -> info.busType().equals("city")); + } + + @Test + @DisplayName("천안역 → 코리아텍: 종합터미널 기점 시간표를 반환한다 (천안역 도착 시간 보정 적용)") + void 천안역에서_코리아텍_시간표_조회() { + // given + BusRouteCommand command = new BusRouteCommand( + BusStation.STATION, BusStation.KOREATECH, BusRouteType.CITY, WEEKDAY, QUERY_TIME); + + when(cityBusTimetableRepository.getByBusInfoNumberAndBusInfoDepart(400L, "종합터미널")) + .thenReturn(timetable400Terminal); + when(cityBusTimetableRepository.getByBusInfoNumberAndBusInfoDepart(402L, "종합터미널")) + .thenReturn(timetable402Terminal); + when(cityBusTimetableRepository.getByBusInfoNumberAndBusInfoDepart(405L, "종합터미널")) + .thenReturn(timetable405Terminal); + + // when + List result = cityBusRouteStrategy.findSchedule(command); + + // then + assertThat(result).isNotEmpty(); + assertThat(result).hasSize(9); + // 천안역 출발 시 +7분 보정이 적용된 시간 + assertThat(result).anyMatch(info -> info.departTime().equals(LocalTime.of(9, 7))); + } + } + + @Nested + @DisplayName("터미널 방면 시내버스 시간표 조회") + class TerminalDirectionTest { + + @Test + @DisplayName("천안역 → 터미널: 응답하지 않고 빈 목록을 반환한다") + void 천안역에서_터미널_시간표_조회_빈목록_반환() { + // given + BusRouteCommand command = new BusRouteCommand( + BusStation.STATION, BusStation.TERMINAL, BusRouteType.CITY, WEEKDAY, QUERY_TIME); + + // when + List result = cityBusRouteStrategy.findSchedule(command); + + // then + assertThat(result).isEmpty(); + } + + @Test + @DisplayName("코리아텍 → 터미널: 각 버스 방면(병천3리/황사동/유관순열사사적지) 기점 시간표를 반환한다") + void 코리아텍에서_터미널_시간표_조회() { + // given + BusRouteCommand command = new BusRouteCommand( + BusStation.KOREATECH, BusStation.TERMINAL, BusRouteType.CITY, WEEKDAY, QUERY_TIME); + + when(cityBusTimetableRepository.getByBusInfoNumberAndBusInfoDepart(400L, "병천3리")) + .thenReturn(timetable400Koreatech); + when(cityBusTimetableRepository.getByBusInfoNumberAndBusInfoDepart(402L, "황사동")) + .thenReturn(timetable402Koreatech); + when(cityBusTimetableRepository.getByBusInfoNumberAndBusInfoDepart(405L, "유관순열사사적지")) + .thenReturn(timetable405Koreatech); + + // when + List result = cityBusRouteStrategy.findSchedule(command); + + // then + assertThat(result).isNotEmpty(); + assertThat(result).hasSize(9); + // 코리아텍 출발 시 버스별 보정 시간 적용 (400: +6분, 402: +13분, 405: +7분) + assertThat(result).anyMatch(info -> + info.busName().equals("400") && info.departTime().equals(LocalTime.of(9, 6))); + assertThat(result).anyMatch(info -> + info.busName().equals("402") && info.departTime().equals(LocalTime.of(9, 13))); + assertThat(result).anyMatch(info -> + info.busName().equals("405") && info.departTime().equals(LocalTime.of(9, 7))); + } + + @Test + @DisplayName("코리아텍 → 천안역: 각 버스 방면(병천3리/황사동/유관순열사사적지) 기점 시간표를 반환한다") + void 코리아텍에서_천안역_시간표_조회() { + // given + BusRouteCommand command = new BusRouteCommand( + BusStation.KOREATECH, BusStation.STATION, BusRouteType.CITY, WEEKDAY, QUERY_TIME); + + when(cityBusTimetableRepository.getByBusInfoNumberAndBusInfoDepart(400L, "병천3리")) + .thenReturn(timetable400Koreatech); + when(cityBusTimetableRepository.getByBusInfoNumberAndBusInfoDepart(402L, "황사동")) + .thenReturn(timetable402Koreatech); + when(cityBusTimetableRepository.getByBusInfoNumberAndBusInfoDepart(405L, "유관순열사사적지")) + .thenReturn(timetable405Koreatech); + + // when + List result = cityBusRouteStrategy.findSchedule(command); + + // then + assertThat(result).isNotEmpty(); + assertThat(result).hasSize(9); + assertThat(result).allMatch(info -> info.busType().equals("city")); + } + } +} diff --git a/src/test/java/in/koreatech/koin/unit/domain/community/keyword/service/KeywordServiceTest.java b/src/test/java/in/koreatech/koin/unit/domain/community/keyword/service/KeywordServiceTest.java index 90edc4c71b..84f1750653 100644 --- a/src/test/java/in/koreatech/koin/unit/domain/community/keyword/service/KeywordServiceTest.java +++ b/src/test/java/in/koreatech/koin/unit/domain/community/keyword/service/KeywordServiceTest.java @@ -1,11 +1,8 @@ package in.koreatech.koin.unit.domain.community.keyword.service; +import static in.koreatech.koin.domain.community.keyword.enums.KeywordCategory.KOREATECH; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; -import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; import java.lang.reflect.Method; import java.util.List; @@ -25,6 +22,7 @@ import in.koreatech.koin.domain.community.article.model.Article; import in.koreatech.koin.domain.community.article.repository.ArticleRepository; import in.koreatech.koin.domain.community.keyword.dto.KeywordNotificationRequest; +import in.koreatech.koin.domain.community.keyword.model.ArticleKeyword; import in.koreatech.koin.domain.community.keyword.repository.ArticleKeywordRepository; import in.koreatech.koin.domain.community.keyword.repository.ArticleKeywordSuggestRepository; import in.koreatech.koin.domain.community.keyword.repository.ArticleKeywordUserMapRepository; @@ -76,19 +74,22 @@ void sendKeywordNotification_withDuplicatedArticleIds_publishesEventsOncePerArti ArticleKeywordEvent event10 = new ArticleKeywordEvent( 10, null, + KOREATECH, Map.of(1, "A") ); ArticleKeywordEvent event11 = new ArticleKeywordEvent( 11, null, + KOREATECH, Map.of(2, "B") ); - when(keywordExtractor.matchKeyword(List.of(article10, article11), null)).thenReturn(List.of(event10, event11)); + when(keywordExtractor.matchKeyword(List.of(article10, article11), null, KOREATECH)) + .thenReturn(List.of(event10, event11)); keywordService.sendKeywordNotification(request); verify(articleRepository).findAllByIdIn(List.of(10, 11)); - verify(keywordExtractor).matchKeyword(List.of(article10, article11), null); + verify(keywordExtractor).matchKeyword(List.of(article10, article11), null, KOREATECH); verify(eventPublisher).publishEvent(event10); verify(eventPublisher).publishEvent(event11); verifyNoMoreInteractions(eventPublisher); @@ -121,4 +122,9 @@ void createNotifiedArticleStatus_startsNewTransaction() throws NoSuchMethodExcep assertThat(transactional).isNotNull(); assertThat(transactional.propagation()).isEqualTo(Propagation.REQUIRES_NEW); } + + private ArticleKeyword argThatKeywordCategory( + in.koreatech.koin.domain.community.keyword.enums.KeywordCategory category) { + return org.mockito.ArgumentMatchers.argThat(keyword -> keyword.getCategory() == category); + } } diff --git a/src/test/java/in/koreatech/koin/unit/domain/community/util/KeywordExtractorTest.java b/src/test/java/in/koreatech/koin/unit/domain/community/util/KeywordExtractorTest.java index 0c9c4f6076..802011899d 100644 --- a/src/test/java/in/koreatech/koin/unit/domain/community/util/KeywordExtractorTest.java +++ b/src/test/java/in/koreatech/koin/unit/domain/community/util/KeywordExtractorTest.java @@ -1,10 +1,10 @@ package in.koreatech.koin.unit.domain.community.util; +import static in.koreatech.koin.domain.community.keyword.enums.KeywordCategory.KOREATECH; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verifyNoInteractions; -import static org.mockito.Mockito.when; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; import java.util.List; import java.util.Map; @@ -51,7 +51,7 @@ void matchKeyword_withMultipleMatchedKeywordsInSingleArticle_createsSingleEvent( ArticleKeyword keywordA = createKeyword(1, "근로", subscriber); ArticleKeyword keywordB = createKeyword(2, "근로장학", subscriber); - when(articleKeywordRepository.findAll(any(Pageable.class))) + when(articleKeywordRepository.findAllByCategory(eq(KOREATECH), any(Pageable.class))) .thenReturn(List.of(keywordA, keywordB)) .thenReturn(List.of()); when(articleKeywordUserMapRepository.findAllByArticleKeywordIdIn(any())) @@ -60,12 +60,13 @@ void matchKeyword_withMultipleMatchedKeywordsInSingleArticle_createsSingleEvent( keywordB.getArticleKeywordUserMaps().get(0) )); - List result = keywordExtractor.matchKeyword(List.of(article), null); + List result = keywordExtractor.matchKeyword(List.of(article), null, KOREATECH); assertThat(result).hasSize(1); ArticleKeywordEvent event = result.get(0); assertThat(event.articleId()).isEqualTo(1); assertThat(event.authorId()).isNull(); + assertThat(event.category()).isEqualTo(KOREATECH); assertThat(event.matchedKeywordByUserId()).isEqualTo(Map.of(1, "근로장학")); } @@ -79,13 +80,13 @@ void matchKeyword_whenNoKeywordsMatch_returnsEmptyResult() { User subscriber = UserFixture.id_설정_코인_유저(1); ArticleKeyword keyword = createKeyword(1, "장학금", subscriber); - when(articleKeywordRepository.findAll(any(Pageable.class))) + when(articleKeywordRepository.findAllByCategory(eq(KOREATECH), any(Pageable.class))) .thenReturn(List.of(keyword)) .thenReturn(List.of()); when(articleKeywordUserMapRepository.findAllByArticleKeywordIdIn(any())) .thenReturn(List.of(keyword.getArticleKeywordUserMaps().get(0))); - List result = keywordExtractor.matchKeyword(List.of(article), null); + List result = keywordExtractor.matchKeyword(List.of(article), null, KOREATECH); assertThat(result).isEmpty(); } @@ -106,7 +107,7 @@ void matchKeyword_withMultipleArticles_createsEventPerArticle() { ArticleKeyword firstKeyword = createKeyword(1, "근로", firstSubscriber); ArticleKeyword secondKeyword = createKeyword(2, "장학금", secondSubscriber); - when(articleKeywordRepository.findAll(any(Pageable.class))) + when(articleKeywordRepository.findAllByCategory(eq(KOREATECH), any(Pageable.class))) .thenReturn(List.of(firstKeyword, secondKeyword)) .thenReturn(List.of()); when(articleKeywordUserMapRepository.findAllByArticleKeywordIdIn(any())) @@ -115,12 +116,15 @@ void matchKeyword_withMultipleArticles_createsEventPerArticle() { secondKeyword.getArticleKeywordUserMaps().get(0) )); - List result = keywordExtractor.matchKeyword(List.of(firstArticle, secondArticle), null); + List result = keywordExtractor.matchKeyword(List.of(firstArticle, secondArticle), null, + KOREATECH); assertThat(result).hasSize(2); assertThat(result.get(0).articleId()).isEqualTo(1); + assertThat(result.get(0).category()).isEqualTo(KOREATECH); assertThat(result.get(0).matchedKeywordByUserId()).isEqualTo(Map.of(1, "근로")); assertThat(result.get(1).articleId()).isEqualTo(2); + assertThat(result.get(1).category()).isEqualTo(KOREATECH); assertThat(result.get(1).matchedKeywordByUserId()).isEqualTo(Map.of(2, "장학금")); } @@ -129,9 +133,9 @@ void matchKeyword_withMultipleArticles_createsEventPerArticle() { void matchKeyword_whenNoKeywordsExist_returnsEmptyResult() { Article article = mock(Article.class); when(article.getId()).thenReturn(1); - when(articleKeywordRepository.findAll(any(Pageable.class))).thenReturn(List.of()); + when(articleKeywordRepository.findAllByCategory(eq(KOREATECH), any(Pageable.class))).thenReturn(List.of()); - List result = keywordExtractor.matchKeyword(List.of(article), null); + List result = keywordExtractor.matchKeyword(List.of(article), null, KOREATECH); assertThat(result).isEmpty(); verifyNoInteractions(articleKeywordUserMapRepository); diff --git a/src/test/java/in/koreatech/koin/unit/domain/notification/eventlistener/ArticleKeywordEventListenerTest.java b/src/test/java/in/koreatech/koin/unit/domain/notification/eventlistener/ArticleKeywordEventListenerTest.java index 2b8b4271e3..cb0445b0fe 100644 --- a/src/test/java/in/koreatech/koin/unit/domain/notification/eventlistener/ArticleKeywordEventListenerTest.java +++ b/src/test/java/in/koreatech/koin/unit/domain/notification/eventlistener/ArticleKeywordEventListenerTest.java @@ -1,19 +1,14 @@ package in.koreatech.koin.unit.domain.notification.eventlistener; import static in.koreatech.koin.common.model.MobileAppPath.KEYWORD; +import static in.koreatech.koin.domain.community.keyword.enums.KeywordCategory.KOREATECH; import static in.koreatech.koin.domain.notification.model.NotificationSubscribeType.ARTICLE_KEYWORD; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.ArgumentMatchers.contains; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; +import static org.springframework.transaction.event.TransactionPhase.AFTER_COMMIT; +import java.lang.reflect.Method; import java.util.List; import java.util.Map; import java.util.Set; @@ -24,6 +19,8 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.scheduling.annotation.Async; +import org.springframework.transaction.event.TransactionalEventListener; import in.koreatech.koin.common.event.ArticleKeywordEvent; import in.koreatech.koin.domain.community.article.model.Article; @@ -35,6 +32,7 @@ import in.koreatech.koin.domain.notification.model.Notification; import in.koreatech.koin.domain.notification.model.NotificationFactory; import in.koreatech.koin.domain.notification.model.NotificationSubscribe; +import in.koreatech.koin.domain.notification.model.NotificationSubscribeType; import in.koreatech.koin.domain.notification.repository.NotificationSubscribeRepository; import in.koreatech.koin.domain.notification.service.NotificationService; import in.koreatech.koin.domain.user.model.User; @@ -64,6 +62,19 @@ class ArticleKeywordEventListenerTest { @Mock private ArticleRepository articleRepository; + @Test + void 키워드_알림_이벤트_리스너는_전용_executor에서_커밋_이후_비동기로_실행된다() throws NoSuchMethodException { + Method method = ArticleKeywordEventListener.class.getMethod("onKeywordRequest", ArticleKeywordEvent.class); + + Async async = method.getAnnotation(Async.class); + TransactionalEventListener transactionalEventListener = method.getAnnotation(TransactionalEventListener.class); + + assertThat(async).isNotNull(); + assertThat(async.value()).isEqualTo("keywordNotificationTaskExecutor"); + assertThat(transactionalEventListener).isNotNull(); + assertThat(transactionalEventListener.phase()).isEqualTo(AFTER_COMMIT); + } + @Test @DisplayName("중복 구독이 있어도 사용자당 알림은 한 번만 발송된다.") void onKeywordRequest_withDuplicateSubscriptions_sendsSingleNotification() { @@ -75,7 +86,7 @@ void onKeywordRequest_withDuplicateSubscriptions_sendsSingleNotification() { NotificationSubscribe subscribeA = createKeywordSubscribe(subscriber); NotificationSubscribe subscribeB = createKeywordSubscribe(subscriber); - ArticleKeywordEvent event = new ArticleKeywordEvent(articleId, 999, Map.of(userId, "근로장학")); + ArticleKeywordEvent event = new ArticleKeywordEvent(articleId, 999, KOREATECH, Map.of(userId, "근로장학")); Article article = mock(Article.class); Board board = mock(Board.class); @@ -91,7 +102,8 @@ void onKeywordRequest_withDuplicateSubscriptions_sendsSingleNotification() { Notification notification = mock(Notification.class); when(notification.getUser()).thenReturn(subscriber); - when(notificationFactory.generateKeywordNotification(any(), anyInt(), anyString(), anyString(), anyInt(), anyString(), any())) + when(notificationFactory.generateKeywordNotification(any(), anyInt(), anyString(), anyString(), anyInt(), + anyString(), any())) .thenReturn(notification); when(notificationService.pushNotificationsWithResult(any())) .thenReturn(List.of(new NotificationService.NotificationDeliveryResult(notification, true))); @@ -122,7 +134,7 @@ void onKeywordRequest_whenSubscriberIsAuthor_skipsNotification() { subscriber.permitNotification("device-token"); NotificationSubscribe subscribe = createKeywordSubscribe(subscriber); - ArticleKeywordEvent event = new ArticleKeywordEvent(articleId, userId, Map.of(userId, "A")); + ArticleKeywordEvent event = new ArticleKeywordEvent(articleId, userId, KOREATECH, Map.of(userId, "A")); Article article = mock(Article.class); Board board = mock(Board.class); @@ -158,7 +170,7 @@ void onKeywordRequest_whenAlreadyNotified_skipsNotification() { subscriber.permitNotification("device-token"); NotificationSubscribe subscribe = createKeywordSubscribe(subscriber); - ArticleKeywordEvent event = new ArticleKeywordEvent(articleId, 999, Map.of(userId, "C")); + ArticleKeywordEvent event = new ArticleKeywordEvent(articleId, 999, KOREATECH, Map.of(userId, "C")); Article article = mock(Article.class); Board board = mock(Board.class); @@ -195,7 +207,7 @@ void onKeywordRequest_whenDeliveryFails_doesNotRecordNotifiedStatus() { subscriber.permitNotification("device-token"); NotificationSubscribe subscribe = createKeywordSubscribe(subscriber); - ArticleKeywordEvent event = new ArticleKeywordEvent(articleId, 999, Map.of(userId, "근로장학")); + ArticleKeywordEvent event = new ArticleKeywordEvent(articleId, 999, KOREATECH, Map.of(userId, "근로장학")); Article article = mock(Article.class); Board board = mock(Board.class); @@ -210,7 +222,8 @@ void onKeywordRequest_whenDeliveryFails_doesNotRecordNotifiedStatus() { .thenReturn(List.of()); Notification notification = mock(Notification.class); - when(notificationFactory.generateKeywordNotification(any(), anyInt(), anyString(), anyString(), anyInt(), anyString(), any())) + when(notificationFactory.generateKeywordNotification(any(), anyInt(), anyString(), anyString(), anyInt(), + anyString(), any())) .thenReturn(notification); when(notificationService.pushNotificationsWithResult(any())) .thenReturn(List.of(new NotificationService.NotificationDeliveryResult(notification, false))); @@ -247,7 +260,7 @@ void onKeywordRequest_queriesNotifiedStatusOnlyForMatchedUsers() { NotificationSubscribe matchedSubscribe = createKeywordSubscribe(matchedUser); NotificationSubscribe unmatchedSubscribe = createKeywordSubscribe(unmatchedUser); - ArticleKeywordEvent event = new ArticleKeywordEvent(articleId, 999, Map.of(matchedUserId, "근로장학")); + ArticleKeywordEvent event = new ArticleKeywordEvent(articleId, 999, KOREATECH, Map.of(matchedUserId, "근로장학")); Article article = mock(Article.class); Board board = mock(Board.class); @@ -258,11 +271,13 @@ void onKeywordRequest_queriesNotifiedStatusOnlyForMatchedUsers() { when(board.getId()).thenReturn(boardId); when(notificationSubscribeRepository.findAllBySubscribeTypeAndDetailTypeIsNullWithUser(ARTICLE_KEYWORD)) .thenReturn(List.of(matchedSubscribe, unmatchedSubscribe)); - when(userNotificationStatusRepository.findUserIdsByNotifiedArticleIdAndUserIdIn(articleId, Set.of(matchedUserId))) + when(userNotificationStatusRepository.findUserIdsByNotifiedArticleIdAndUserIdIn(articleId, + Set.of(matchedUserId))) .thenReturn(List.of()); Notification notification = mock(Notification.class); - when(notificationFactory.generateKeywordNotification(any(), anyInt(), anyString(), anyString(), anyInt(), anyString(), any())) + when(notificationFactory.generateKeywordNotification(any(), anyInt(), anyString(), anyString(), anyInt(), + anyString(), any())) .thenReturn(notification); when(notificationService.pushNotificationsWithResult(any())).thenReturn(List.of()); @@ -273,8 +288,12 @@ void onKeywordRequest_queriesNotifiedStatusOnlyForMatchedUsers() { } private NotificationSubscribe createKeywordSubscribe(User user) { + return createSubscribe(user, ARTICLE_KEYWORD); + } + + private NotificationSubscribe createSubscribe(User user, NotificationSubscribeType subscribeType) { return NotificationSubscribe.builder() - .subscribeType(ARTICLE_KEYWORD) + .subscribeType(subscribeType) .user(user) .build(); }