Skip to content

Comments

[feature] 연관어 검색 구현#1093

Merged
Zepelown merged 9 commits intodevelop/befrom
feature/#1077-add-related-search-MOA-539
Feb 1, 2026
Merged

[feature] 연관어 검색 구현#1093
Zepelown merged 9 commits intodevelop/befrom
feature/#1077-add-related-search-MOA-539

Conversation

@Zepelown
Copy link
Member

@Zepelown Zepelown commented Jan 24, 2026

#️⃣연관된 이슈

#1077

📝작업 내용

1. 연관 검색 기능 개선: 벡터 검색 → 단어 사전 기반 검색

  • 벡터 기반 유사어 검색 로직 제거단어 사전 방식 로직 추가
  • MongoDB 기반 단어 사전 시스템 구축
  • CSV 파일을 통한 초기 데이터 로드 기능 추가
  • 동의어/유사어 매핑을 통한 키워드 확장 검색 구현

2. 단어 사전 관리 기능 추가

  • WordDictionary 엔티티 및 Repository 추가
  • WordDictionaryService: 단어 사전 로드 및 키워드 확장 서비스
  • WordDictionaryAdminService: 단어 사전 CRUD 관리 서비스
  • WordDictionaryAdminController: 관리자용 단어 사전 관리 API
    • 단어 사전 생성 (단건/일괄)
    • 단어 사전 조회 (목록/상세)
    • 단어 사전 수정
    • 단어 사전 삭제
  • CsvParser: CSV 파일 파싱 유틸리티 추가

3. 검색 기능 개선

  • ClubSearchRepository: 단어 사전 기반 키워드 확장 검색 로직 구현
  • 키값 입력 시에도 연관어와 같은 검색 결과 제공
  • 검색 결과에 관련 키워드 정보 포함 (ClubSearchResult 확장)

4. Redis 및 SSE 기능 추가

  • Redis 의존성 추가 및 설정 (RedisConfig)
  • Redis Pub/Sub 구조를 위한 모델 정의
  • 분산락을 위한 Redis 도입
  • 지원자 상태 공유를 위한 SSE 서비스 구현 (ApplicantsStatusShareSse)
  • Redis 리스너 추가

5. 기타 개선사항

  • ClubAward 엔티티: year와 semesterTerm 분리
  • ClubApplyAdminService: 코드 리팩토링 및 간소화
  • ClubProfileService: 벡터 데이터 저장 기능 추가
  • AsyncConfig: 설정 개선

변경 이유

  • 벡터 검색의 복잡성과 유지보수 어려움 해소
  • 단어 사전 기반 검색으로 더 정확하고 예측 가능한 검색 결과 제공
  • 관리자가 직접 단어 사전을 관리할 수 있는 기능 필요
  • 동시성 문제 해결을 위한 분산락 도입
  • 실시간 지원자 상태 공유를 위한 SSE 기능 필요

테스트

테스트 실행 여부

  • UnitTest 실행 완료
  • IntegrationTest 실행 완료

테스트 결과

  • ClubApplyAdminServiceTest: 지원폼 및 동아리 정보 수정 테스트 개선
    • 엔티티를 생성하고 테스트 끝나면 삭제하도록 개선
    • 동시성 테스트 추가
  • ClubProfileServiceTest: 테스트 코드 수정
    • ClubRepository 주입으로 Club을 주입하도록 수정
  • ClubSearchServiceTest: 단어 사전 기반 검색 테스트 추가

주요 테스트 케이스

  • 단어 사전 동시성 문제 수정 및 테스트
  • 키값 입력 시 연관어 검색 동작 확인
  • 단어 사전 빈값 방어 로직 테스트

Breaking Changes

  • Breaking Changes 없음
  • Breaking Changes 있음

변경 사항

  • 벡터 검색 관련 API는 제거되었으나, 검색 API 자체는 유지되며 내부 구현만 변경됨
  • 기존 검색 API와의 호환성 유지

변경된 파일 목록

신규 파일

  • src/main/java/moadong/club/entity/WordDictionary.java
  • src/main/java/moadong/club/repository/WordDictionaryRepository.java
  • src/main/java/moadong/club/service/WordDictionaryService.java
  • src/main/java/moadong/club/service/WordDictionaryAdminService.java
  • src/main/java/moadong/club/controller/WordDictionaryAdminController.java
  • src/main/java/moadong/club/util/CsvParser.java
  • src/main/java/moadong/global/config/RedisConfig.java
  • src/main/java/moadong/sse/dto/ApplicantSseDto.java
  • src/main/java/moadong/sse/enums/ApplicantEventType.java
  • src/main/java/moadong/sse/service/ApplicantsStatusShareSse.java
  • src/main/java/moadong/club/payload/request/WordDictionaryCreateRequest.java
  • src/main/java/moadong/club/payload/request/WordDictionaryBulkCreateRequest.java
  • src/main/java/moadong/club/payload/request/WordDictionaryUpdateRequest.java
  • src/main/java/moadong/club/payload/response/WordDictionaryResponse.java
  • src/main/java/moadong/club/payload/response/WordDictionaryListResponse.java

수정된 파일

  • build.gradle: Redis 의존성 추가
  • src/main/java/moadong/MoadongApplication.java: 설정 변경
  • src/main/java/moadong/club/controller/ClubApplyAdminController.java
  • src/main/java/moadong/club/controller/ClubProfileController.java
  • src/main/java/moadong/club/entity/ClubAward.java: year와 semesterTerm 분리
  • src/main/java/moadong/club/payload/dto/ClubAwardDto.java
  • src/main/java/moadong/club/payload/dto/ClubSearchResult.java: 관련 키워드 정보 추가
  • src/main/java/moadong/club/repository/ClubSearchRepository.java: 단어 사전 기반 검색 로직 구현
  • src/main/java/moadong/club/service/ClubApplyAdminService.java: 리팩토링 및 간소화
  • src/main/java/moadong/club/service/ClubProfileService.java: 벡터 데이터 저장 기능 추가
  • src/main/java/moadong/club/service/ClubSearchService.java: 단어 사전 확장 검색 적용
  • src/main/java/moadong/global/config/AsyncConfig.java: 설정 개선
  • src/test/java/moadong/club/service/ClubApplyAdminServiceTest.java: 테스트 개선
  • src/test/java/moadong/club/service/ClubProfileServiceTest.java: 테스트 수정
  • src/test/java/moadong/club/service/ClubSearchServiceTest.java: 테스트 추가

통계

  • 총 변경 파일: 30개
  • 추가된 라인: +1,302줄
  • 삭제된 라인: -279줄
  • 순 증가: +1,023줄

체크리스트

  • 코드 리뷰 준비 완료
  • UnitTest 및 IntegrationTest 통과 확인
  • 로컬 환경에서 정상 동작 확인
  • 코드 스타일 가이드 준수
  • 문서 업데이트 (API 문서, README 등)
  • Breaking Changes 문서화 (해당시)
  • 불필요한 주석 및 디버그 코드 제거
  • 커밋 메시지 규칙 준수

추가 정보

주요 커밋 내역

  1. feat: 벡터 검색 적용 - 초기 벡터 검색 기능 추가
  2. feat: 클럽 약력 수정 시에 벡터 데이터 저장되는 기능 추가
  3. feat: 벡터 기반 유사어 검색 로직 제거 후 단어 사전 방식 로직 추가 - 핵심 변경
  4. fix: 키값 입력 시에도 연관어와 같은 검색 결과가 나오도록 수정
  5. chore: 단어 사전 빈값 방어 로직 추가
  6. fix: 단어 사전 map 동시성 문제 수정 및 테스트 코드 파라미터값이 잘못되어있던 문제 수정
  7. fix: ClubProfileService 테스트 코드가 올바르게 동작하지 않던 문제 수정
  8. fix: 지원폼 및 동아리 정보 수정 테스트에서 엔티티를 생성하고 테스트 끝나면 삭제하도록 개선

중점적으로 리뷰받고 싶은 부분(선택)

리뷰어가 특별히 봐주었으면 하는 부분이 있다면 작성해주세요

ex) 메서드 XXX의 이름을 더 잘 짓고 싶은데 혹시 좋은 명칭이 있을까요?

논의하고 싶은 부분(선택)

논의하고 싶은 부분이 있다면 작성해주세요.

🫡 참고사항

Summary by CodeRabbit

  • 새로운 기능
    • 단어 사전 관리(생성·수정·삭제·조회) 및 CSV 업로드·일괄 등록 지원, 관리자용 API 제공
    • 클럽 검색에 단어 사전 기반 키워드 확장 적용으로 검색 정확도 향상
    • 검색 결과 정렬 개선(상태 우선, 카테고리 우선순위 기반, 이름 순)
    • 서비스 비동기 실행 지원 및 서버 시작 시 사전 자동 로드·갱신 기능 추가

✏️ Tip: You can customize this high-level summary in your review settings.

@vercel
Copy link

vercel bot commented Jan 24, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
moadong Ready Ready Preview, Comment Jan 25, 2026 6:37am

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 24, 2026

Warning

.coderabbit.yaml has a parsing error

The CodeRabbit configuration file in this repository has a parsing error and default settings were used instead. Please fix the error(s) in the configuration file. You can initialize chat with CodeRabbit to get help with the configuration file.

💥 Parsing errors (1)
Validation error: Invalid regex pattern for base branch. Received: "**" at "reviews.auto_review.base_branches[0]"
⚙️ Configuration instructions
  • Please see the configuration documentation for more information.
  • You can also validate your configuration using the online YAML validator.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Walkthrough

클럽 검색에 연관어(단어사전) 기능을 추가했습니다. MongoDB 엔티티·레포지토리·관리자 API·서비스(메모리 캐시 포함)·CSV 파서·DTO가 추가되었고, 검색 저장소와 검색 서비스의 키워드 처리 및 정렬 로직이 변경되었습니다.

Changes

Cohort / File(s) 변경 내용
빌드·앱 설정
backend/build.gradle, backend/src/main/java/moadong/MoadongApplication.java, backend/src/main/java/moadong/global/config/AsyncConfig.java
Maven 저장소 3곳 추가, @EnableAsync 추가, 포맷팅 미세 수정
관리자 API / 컨트롤러
backend/src/main/java/moadong/club/controller/WordDictionaryAdminController.java, backend/src/main/java/moadong/club/controller/ClubProfileController.java
단어사전 관리용 REST 컨트롤러 신규 추가(생성/벌크/CSV/조회/수정/삭제/새로고침). 기존 컨트롤러 import 정리(wildcard)
엔티티·레포지토리
backend/src/main/java/moadong/club/entity/WordDictionary.java, backend/src/main/java/moadong/club/repository/WordDictionaryRepository.java
WordDictionary Mongo 문서 엔티티 추가(인덱스 포함) 및 findByStandardWord 리포지토리 추가
DTO / 요청·응답 레코드
backend/src/main/java/moadong/club/payload/request/WordDictionaryCreateRequest.java, .../WordDictionaryBulkCreateRequest.java, .../WordDictionaryUpdateRequest.java, backend/src/main/java/moadong/club/payload/response/WordDictionaryResponse.java, .../WordDictionaryListResponse.java, backend/src/main/java/moadong/club/payload/dto/ClubSearchResult.java
생성/벌크/업데이트 요청 레코드 및 응답 레코드 추가, ClubSearchResult.of(Club) 팩토리 메서드 추가
검색 저장소 변경
backend/src/main/java/moadong/club/repository/ClubSearchRepository.java
WordDictionaryService 주입 및 키워드 확장 적용 → 확장된 키워드로 OR 정규식 결합하여 name/category/tags 검색 패턴 구성으로 대체
서비스 — 단어사전 관리/확장
backend/src/main/java/moadong/club/service/WordDictionaryAdminService.java, .../WordDictionaryService.java
관리자용 CRUD/벌크/CSV 처리 서비스 및 메모리 캐시 기반 키워드 확장 서비스 추가(초기 CSV 로드, Mongo 동기화, expandKeywords, refresh)
서비스 — 검색·프로필 변경
backend/src/main/java/moadong/club/service/ClubSearchService.java, .../ClubProfileService.java
키워드 처리 책임을 저장소로 이동, 결과 정렬 로직 재구성(모집상태 우선 → 카테고리 랜덤 우선 → 이름), @Slf4j 추가
유틸리티
backend/src/main/java/moadong/club/util/CsvParser.java
CSV를 WordDictionaryCreateRequest 목록으로 파싱하는 유틸리티 추가(헤더/주석/중복 처리)
테스트 변경
backend/src/test/java/moadong/club/service/ClubSearchServiceTest.java, .../ClubProfileServiceTest.java, .../ClubApplyAdminServiceTest.java
검색 테스트 목 설정 및 준비/정리 로직 보강(키워드 인용 제거, 클럽/폼 생성·정리, 예외 카운트 등)

Sequence Diagram(s)

sequenceDiagram
    autonumber
    actor Admin
    participant Controller as WordDictionary\r\nAdminController
    participant Service as WordDictionary\r\nAdminService
    participant Parser as CsvParser
    participant Repo as WordDictionary\r\nRepository
    participant Cache as WordDictionary\r\nService

    Admin->>Controller: POST /api/admin/word-dictionary/csv (file)
    Controller->>Service: createWordDictionariesFromCsv(file)
    Service->>Parser: parseWordDictionaryCsv(file)
    Parser-->>Service: List<WordDictionaryCreateRequest>
    loop 각 표준단어
        Service->>Repo: save(WordDictionary)
        Repo-->>Service: saved entity
    end
    Service->>Cache: refreshDictionary()
    Cache->>Repo: findAll()
    Repo-->>Cache: List<WordDictionary>
    Cache->>Cache: 메모리 딕셔너리 재구성
    Service-->>Controller: List<WordDictionaryResponse>
    Controller-->>Admin: 응답
Loading
sequenceDiagram
    autonumber
    actor User
    participant Controller as ClubSearch\r\nController
    participant Service as ClubSearch\r\nService
    participant Repo as ClubSearch\r\nRepository
    participant Dict as WordDictionary\r\nService
    participant Mongo as MongoDB

    User->>Controller: GET /api/clubs/search?keyword=키워드
    Controller->>Service: searchClubsByKeyword(키워드)
    Service->>Repo: searchClubsByKeyword(키워드)
    Repo->>Dict: expandKeywords(키워드)
    Dict-->>Repo: List<String> (확장된 키워드)
    Repo->>Mongo: find with OR regex (name/category/tags)
    Mongo-->>Repo: List<ClubSearchResult>
    Repo-->>Service: List<ClubSearchResult>
    Service->>Service: 정렬(모집상태→카테고리 랜덤 우선→이름)
    Service-->>Controller: ClubSearchResponse
    Controller-->>User: 응답
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45분

Possibly related PRs

Suggested labels

✨ Feature, 💾 BE

Suggested reviewers

  • lepitaaar
  • seongwon030
  • PororoAndFriends
🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 58.62% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목 '[feature] 연관어 검색 구현'은 변경사항의 주요 목표인 연관어(관련 단어) 검색 기능 구현을 명확하게 반영하고 있습니다.
Linked Issues check ✅ Passed PR은 연관어 검색 기능(MOA-539)의 핵심 구현으로 단어사전 엔티티, 저장소, 서비스, 컨트롤러, CSV 파싱 유틸리티를 추가하고 기존 검색 로직에 단어 확장 기능을 통합했습니다.
Out of Scope Changes check ✅ Passed 일부 변경사항(import 리팩토링, 로깅 추가, 테스트 개선)은 연관어 검색 핵심 구현 외의 영역이지만, 코드 품질 개선을 위한 합리적인 범위 내 변경입니다.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link

github-actions bot commented Jan 24, 2026

Test Results

72 tests   72 ✅  18s ⏱️
14 suites   0 💤
14 files     0 ❌

Results for commit 395ec72.

♻️ This comment has been updated with latest results.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In
`@backend/src/main/java/moadong/club/controller/WordDictionaryAdminController.java`:
- Around line 20-24: The WordDictionaryAdminController class lacks authorization
and OpenAPI security metadata; annotate the class (or each admin method) with
`@PreAuthorize`("hasRole('ADMIN')") to enforce ADMIN-only access and add
`@SecurityRequirement`(name = "bearerAuth") (from
io.swagger.v3.oas.annotations.security) to expose the security scheme in API
docs; ensure you import the annotations and that method security is enabled in
the application (e.g., `@EnableGlobalMethodSecurity`(prePostEnabled = true) on a
configuration class) so the `@PreAuthorize` checks take effect.

In
`@backend/src/main/java/moadong/club/payload/request/WordDictionaryBulkCreateRequest.java`:
- Around line 8-9: The List field wordDictionaries in class
WordDictionaryBulkCreateRequest is missing the `@Valid` annotation so nested
constraints on WordDictionaryCreateRequest elements won't be evaluated; add
`@Valid` to the wordDictionaries field declaration (in addition to the existing
`@NotEmpty`) so the validator will traverse into each WordDictionaryCreateRequest
and enforce its `@NotBlank/`@NotEmpty constraints.

In `@backend/src/main/java/moadong/club/repository/ClubSearchRepository.java`:
- Around line 44-56: The code builds regexPattern from expandedKeywords returned
by wordDictionaryService.expandKeywords(keyword) but doesn't guard against an
empty or all-blank list (which yields an empty regex matching everything); fix
by filtering expandedKeywords to remove null/blank entries (trim each entry),
then check that the filtered list is non-empty before building regexPattern and
adding the Aggregation.match(Criteria.orOperator(...)) stage; if the filtered
list is empty, skip adding the match stage (or return early) so
Criteria.where("name")/.where("category")/.where("recruitmentInformation.tags")
are not created with an empty regex.

In `@backend/src/main/java/moadong/club/service/WordDictionaryService.java`:
- Around line 155-230: The dictionary is currently mutated in place causing
corruption/race issues when refreshDictionary()/loadDictionaryFromMongo() clear
and repopulate while expandKeywords() reads it; fix by making the shared
dictionary a single atomic reference (e.g.,
AtomicReference<Map<String,List<String>>> or a volatile Map) and change
loadDictionaryFromMongo() to build a new local Map<String,List<String>> (using
HashMap/ArrayList), deduplicate and wrap with Collections.unmodifiableMap, then
atomically replace the reference (dictionary.set(...) or dictionary = newMap)
once fully built; update expandKeywords() and refreshDictionary() to read from
that atomic/volatile reference instead of mutating a shared HashMap.

In `@backend/src/main/java/moadong/club/util/CsvParser.java`:
- Around line 39-50: The first CSV line may contain a UTF-8 BOM which breaks the
header check and pollutes the first column; in CsvParser.java remove any BOM
from the read `line` before using `isFirstLine` logic and before using the value
as a key (e.g., strip '\uFEFF' or use a replaceFirst for the BOM). Update the
code around the `line = line.trim();` / `if (isFirstLine)` block so `line` is
normalized (BOM removed) prior to `line.startsWith("표준단어")` and any subsequent
key parsing to prevent corrupted keys.
🧹 Nitpick comments (5)
backend/src/main/java/moadong/MoadongApplication.java (1)

15-15: 중복된 @EnableAsync 어노테이션

@EnableAsync가 이미 AsyncConfig.java(line 12)에 선언되어 있습니다. 동일한 어노테이션을 두 곳에 선언할 필요가 없으며, 비동기 설정이 어디서 활성화되는지 혼란을 줄 수 있습니다.

AsyncConfig에서 executor 빈과 함께 @EnableAsync를 관리하는 것이 더 응집력 있는 설계입니다. 여기서는 제거하는 것을 권장합니다.

♻️ 제안된 수정
 import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
 import org.springframework.retry.annotation.EnableRetry;
-import org.springframework.scheduling.annotation.EnableAsync;
 import org.springframework.scheduling.annotation.EnableScheduling;

 `@SpringBootApplication`(exclude = {DataSourceAutoConfiguration.class})
 `@RequiredArgsConstructor`
 `@EnableScheduling`
 `@EnableRetry`
-@EnableAsync
 public class MoadongApplication {
backend/build.gradle (1)

22-29: 스냅샷/마일스톤 저장소를 제거하거나 범위를 제한하세요.

현재 프로젝트의 모든 의존성이 안정 버전(예: 2.3.0, 5.8.0)을 사용하고 있어 Spring.io의 스냅샷/마일스톤 저장소와 Central Portal Snapshots는 실제로 사용되지 않습니다. 불필요한 저장소는 빌드 설정을 복잡하게 하고 재현성을 해칠 수 있으므로:

  • 추천: 현재 사용되지 않는 이 저장소들을 제거하세요.
  • 대안: 향후 스냅샷 의존성이 필요한 경우, 반드시 mavenContent로 범위를 제한하고 snapshotsOnly()를 명시하세요.
🔧 범위 제한 예시 (필요 시)
 repositories {
 	mavenCentral()
-    maven { url 'https://repo.spring.io/milestone' }
-    maven { url 'https://repo.spring.io/snapshot' }
-    maven {
-        name = 'Central Portal Snapshots'
-        url = 'https://central.sonatype.com/repository/maven-snapshots/'
-    }
+    maven {
+        url 'https://repo.spring.io/snapshot'
+        mavenContent {
+            snapshotsOnly()
+            includeGroupByRegex 'org\\.springframework.*'
+        }
+    }
 }
backend/src/main/java/moadong/club/payload/request/WordDictionaryCreateRequest.java (1)

12-13: inputWords 리스트 요소에 대한 유효성 검사 추가를 권장합니다.

@NotEmpty는 리스트가 비어있지 않음만 검증합니다. 리스트 내 개별 문자열이 null이거나 공백일 수 있습니다.

♻️ 리스트 요소 검증 추가 제안
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotEmpty;
+
+import java.util.List;
+
 public record WordDictionaryCreateRequest(
     `@NotBlank`(message = "표준단어는 필수입니다")
     String standardWord,
     
-    `@NotEmpty`(message = "입력단어 목록은 비어있을 수 없습니다")
+    `@NotEmpty`(message = "입력단어 목록은 비어있을 수 없습니다")
+    List<@NotBlank(message = "입력단어는 공백일 수 없습니다") String> inputWords
-    List<String> inputWords
 ) {
 }
backend/src/main/java/moadong/club/payload/request/WordDictionaryUpdateRequest.java (1)

8-9: WordDictionaryCreateRequest와 동일하게 리스트 요소 검증 추가를 권장합니다.

일관성을 위해 inputWords 리스트 내 개별 문자열에 대한 유효성 검사를 추가하는 것이 좋습니다.

♻️ 리스트 요소 검증 추가 제안
+import jakarta.validation.constraints.NotBlank;
 import jakarta.validation.constraints.NotEmpty;
 
 import java.util.List;
 
 public record WordDictionaryUpdateRequest(
-    `@NotEmpty`(message = "입력단어 목록은 비어있을 수 없습니다")
-    List<String> inputWords
+    `@NotEmpty`(message = "입력단어 목록은 비어있을 수 없습니다")
+    List<@NotBlank(message = "입력단어는 공백일 수 없습니다") String> inputWords
 ) {
 }
backend/src/main/java/moadong/club/service/WordDictionaryAdminService.java (1)

62-135: 일괄 생성에서 N+1 조회로 성능 저하 가능
요청 항목마다 findByStandardWord를 호출해 대량 입력 시 DB 왕복이 급증합니다. 입력 표준단어 목록을 한 번에 조회(findByStandardWordIn)하거나 유니크 인덱스를 두고 중복키 예외 처리로 간소화하는 방식을 고려해주세요.

Comment on lines +20 to +24
@RestController
@RequestMapping("/api/admin/word-dictionary")
@AllArgsConstructor
@Tag(name = "Word_Dictionary_Admin", description = "단어사전 관리자 API")
public class WordDictionaryAdminController {
Copy link
Contributor

@coderabbitai coderabbitai bot Jan 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

관리자 API 인증/인가 누락
관리자 전용 엔드포인트인데 클래스/메서드에 권한 체크가 없습니다. 최소한 ADMIN 권한 요구(@PreAuthorize 등)와 보안 스키마를 추가해 주세요.

🔐 보안 주석 예시
 `@RestController`
 `@RequestMapping`("/api/admin/word-dictionary")
 `@AllArgsConstructor`
 `@Tag`(name = "Word_Dictionary_Admin", description = "단어사전 관리자 API")
+@PreAuthorize("hasRole('ADMIN')")
+@SecurityRequirement(name = "BearerAuth")
 public class WordDictionaryAdminController {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@RestController
@RequestMapping("/api/admin/word-dictionary")
@AllArgsConstructor
@Tag(name = "Word_Dictionary_Admin", description = "단어사전 관리자 API")
public class WordDictionaryAdminController {
`@RestController`
`@RequestMapping`("/api/admin/word-dictionary")
`@AllArgsConstructor`
`@Tag`(name = "Word_Dictionary_Admin", description = "단어사전 관리자 API")
`@PreAuthorize`("hasRole('ADMIN')")
`@SecurityRequirement`(name = "BearerAuth")
public class WordDictionaryAdminController {
🤖 Prompt for AI Agents
In
`@backend/src/main/java/moadong/club/controller/WordDictionaryAdminController.java`
around lines 20 - 24, The WordDictionaryAdminController class lacks
authorization and OpenAPI security metadata; annotate the class (or each admin
method) with `@PreAuthorize`("hasRole('ADMIN')") to enforce ADMIN-only access and
add `@SecurityRequirement`(name = "bearerAuth") (from
io.swagger.v3.oas.annotations.security) to expose the security scheme in API
docs; ensure you import the annotations and that method security is enabled in
the application (e.g., `@EnableGlobalMethodSecurity`(prePostEnabled = true) on a
configuration class) so the `@PreAuthorize` checks take effect.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

어드민용 컨트롤러라 되어있어서 권한설정필요해보이네요

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

@lepitaaar 네, 맞습니다! 관리자 전용 API인데 권한 체크가 없는 것은 보안 취약점입니다. 기존 코드베이스의 보안 설정을 확인해서 적절한 인증/인가 방식을 제안드리겠습니다.


🏁 Script executed:

#!/bin/bash

# Check for existing admin controllers and their security annotations
echo "=== Searching for existing admin controllers ==="
fd -e java -p "Controller.java" backend/src/main/java | xargs grep -l "@RequestMapping.*admin" | head -5

echo -e "\n=== Checking for `@PreAuthorize` usage ==="
rg -n "@PreAuthorize" backend/src/main/java --type java -C 2

echo -e "\n=== Checking for Spring Security configuration ==="
rg -n "@EnableGlobalMethodSecurity\|@EnableMethodSecurity" backend/src/main/java --type java -C 3

echo -e "\n=== Checking for existing SecurityRequirement annotations ==="
rg -n "@SecurityRequirement" backend/src/main/java --type java -C 2

echo -e "\n=== Looking for security configuration classes ==="
fd -e java "SecurityConfig" backend/src/main/java --exec cat {}

Length of output: 26199


기존 코드베이스의 보안 설정을 확인했습니다. 추가로 역할(Role) 기반 권한이 있는지 확인하겠습니다.


🏁 Script executed:

#!/bin/bash

echo "=== Checking for ROLE definitions ==="
rg -n "ROLE_ADMIN|ROLE_USER|hasRole|hasAuthority" backend/src/main/java --type java -C 2

echo -e "\n=== Checking CustomUserDetails implementation ==="
fd "CustomUserDetails.java" backend/src/main/java --exec cat {}

echo -e "\n=== Checking User entity for roles ==="
rg -n "enum.*Role|class.*Role" backend/src/main/java --type java -C 3

Length of output: 1178


@lepitaaar 코드베이스 분석 결과를 공유드립니다:

현재 상황

현재 프로젝트에는 역할(Role) 기반 권한 체계가 구현되어 있지 않고, 모든 보호된 엔드포인트가 인증 여부만 체크하고 있습니다.

권장사항

1단계: 즉시 적용 (기존 패턴 따르기)

ClubImageController와 동일하게 클래스 레벨에 보안 어노테이션을 추가하세요:

`@RestController`
`@RequestMapping`("/api/admin/word-dictionary")
`@AllArgsConstructor`
`@Tag`(name = "Word_Dictionary_Admin", description = "단어사전 관리자 API")
`@PreAuthorize`("isAuthenticated()")
`@SecurityRequirement`(name = "BearerAuth")
public class WordDictionaryAdminController {

필요한 import:

import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import org.springframework.security.access.prepost.PreAuthorize;

2단계: 향후 개선 (역할 기반 권한)

추후 관리자 권한이 필요하다면:

  1. User 엔티티에 role 필드 추가 (예: ROLE_ADMIN, ROLE_USER)
  2. @PreAuthorize("hasRole('ADMIN')")로 변경

참고사항

  • SecurityConfig에서 @EnableMethodSecurity(prePostEnabled = true) 활성화되어 있어 @PreAuthorize 동작 가능
  • 현재 ClubApplyAdminController도 같은 방식(isAuthenticated())으로 보호되고 있음
  • /api/admin/** 경로라도 현재는 인증된 사용자면 누구나 접근 가능함

관리자 전용 기능이라면 역할 기반 권한 체계 도입도 함께 고려해보시는 것을 추천드립니다! 🔐

Comment on lines +39 to +50
line = line.trim();

// 빈 줄 또는 주석 라인 무시
if (line.isEmpty() || line.startsWith("#")) {
continue;
}

// 헤더 라인 스킵
if (isFirstLine) {
isFirstLine = false;
if (line.startsWith("표준단어") || line.startsWith("standard")) {
continue;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

UTF-8 BOM 포함 시 헤더 스킵/키 값 오염 가능

엑셀 등에서 저장한 CSV가 BOM을 포함하면 startsWith("표준단어")가 실패하거나 첫 컬럼에 BOM이 남아 표준단어 키가 오염될 수 있습니다. 첫 줄에서 BOM 제거를 권장합니다.

🛠️ 제안 수정
-                line = line.trim();
+                line = line.trim();
+                if (lineNumber == 1) {
+                    line = line.replace("\uFEFF", "");
+                }
🤖 Prompt for AI Agents
In `@backend/src/main/java/moadong/club/util/CsvParser.java` around lines 39 - 50,
The first CSV line may contain a UTF-8 BOM which breaks the header check and
pollutes the first column; in CsvParser.java remove any BOM from the read `line`
before using `isFirstLine` logic and before using the value as a key (e.g.,
strip '\uFEFF' or use a replaceFirst for the BOM). Update the code around the
`line = line.trim();` / `if (isFirstLine)` block so `line` is normalized (BOM
removed) prior to `line.startsWith("표준단어")` and any subsequent key parsing to
prevent corrupted keys.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@backend/src/main/java/moadong/club/service/WordDictionaryService.java`:
- Around line 180-189: In WordDictionaryService, guard against
wordDict.getInputWords() returning null by replacing List<String> inputWords =
wordDict.getInputWords() with a null-safe value (e.g., List<String> inputWords =
Optional.ofNullable(wordDict.getInputWords()).orElse(Collections.emptyList()) or
new ArrayList<>()), then build expandedList from that safe inputWords and
proceed with the existing logic (this prevents the new ArrayList<>(inputWords)
NPE and safe-guards the subsequent for-loop over expandedList); also consider
handling a null standardWord similarly before adding it to expandedList.
🧹 Nitpick comments (2)
backend/src/main/java/moadong/club/service/WordDictionaryService.java (2)

107-144: 중복 코드 추출을 권장합니다.

이 딕셔너리 구축 로직(표준단어와 입력단어를 확장 리스트로 매핑하고 중복 제거하는 부분)이 loadDictionaryFromMongo() 메서드(lines 176-211)에서도 거의 동일하게 반복됩니다. 유지보수성을 위해 공통 헬퍼 메서드로 추출하는 것이 좋겠습니다.

♻️ 헬퍼 메서드 추출 예시
private Map<String, List<String>> buildDictionaryFromGroups(Map<String, ? extends Collection<String>> standardToInputs) {
    Map<String, List<String>> newDictionary = new HashMap<>();
    
    for (Map.Entry<String, ? extends Collection<String>> entry : standardToInputs.entrySet()) {
        String standardWord = entry.getKey();
        Collection<String> inputWords = entry.getValue();
        
        List<String> expandedList = new ArrayList<>(inputWords);
        if (!expandedList.contains(standardWord)) {
            expandedList.add(standardWord);
        }
        
        String normalizedStandard = standardWord.toLowerCase();
        newDictionary.computeIfAbsent(normalizedStandard, k -> new ArrayList<>())
                .addAll(expandedList);
        
        for (String inputWord : inputWords) {
            String normalizedInput = inputWord.toLowerCase();
            newDictionary.computeIfAbsent(normalizedInput, k -> new ArrayList<>())
                    .addAll(expandedList);
        }
    }
    
    newDictionary.replaceAll((k, v) -> v.stream().distinct().collect(Collectors.toList()));
    return newDictionary;
}

246-254: 내부 리스트 직접 반환으로 인한 캡슐화 위반 가능성

line 253에서 딕셔너리에 저장된 리스트를 직접 반환하면, 호출자가 이 리스트를 수정할 경우 내부 상태가 변경될 수 있습니다. 방어적 복사 또는 불변 리스트로 감싸는 것이 안전합니다.

♻️ 불변 리스트 반환
         // 단어사전에서 찾기
         List<String> expanded = currentDictionary.get(normalizedKeyword);
         
         if (expanded != null && !expanded.isEmpty()) {
-            return expanded;
+            return List.copyOf(expanded);
         }

또는 loadDictionaryFromMongo()loadFromCsvAndSaveToMongo()에서 리스트를 저장할 때 Collections.unmodifiableList()로 감싸는 방법도 있습니다.

Comment on lines +180 to +189
for (WordDictionary wordDict : allDictionaries) {
String standardWord = wordDict.getStandardWord();
List<String> inputWords = wordDict.getInputWords();

// 표준단어와 모든 입력단어를 포함한 확장 리스트 생성
List<String> expandedList = new ArrayList<>(inputWords);
// 표준단어가 inputWords에 없으면 추가
if (!expandedList.contains(standardWord)) {
expandedList.add(standardWord);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "*.java" | xargs grep -l "class WordDictionary" | head -5

Repository: Moadong/moadong

Length of output: 354


🏁 Script executed:

fd "WordDictionary.java" --type f

Repository: Moadong/moadong

Length of output: 120


🏁 Script executed:

rg "class WordDictionary" --type java -A 20

Repository: Moadong/moadong

Length of output: 8028


🏁 Script executed:

cat -n backend/src/main/java/moadong/club/entity/WordDictionary.java

Repository: Moadong/moadong

Length of output: 1014


🏁 Script executed:

rg "inputWords" backend/src/main/java/moadong/club/service/WordDictionaryAdminService.java -C 5

Repository: Moadong/moadong

Length of output: 1743


🏁 Script executed:

rg "WordDictionaryCreateRequest" --type java -A 10

Repository: Moadong/moadong

Length of output: 12277


🏁 Script executed:

cat -n backend/src/main/java/moadong/club/service/WordDictionaryService.java | sed -n '172,222p'

Repository: Moadong/moadong

Length of output: 2636


🏁 Script executed:

rg "findAll\|findByStandardWord" backend/src/main/java/moadong/club/service/WordDictionaryService.java -A 3

Repository: Moadong/moadong

Length of output: 41


🏁 Script executed:

rg "WordDictionary.builder\|new WordDictionary" --type java -B 2 -A 5 | head -50

Repository: Moadong/moadong

Length of output: 41


inputWords가 null일 경우 NPE 발생 가능성이 있습니다.

wordDict.getInputWords()가 null을 반환하면 line 185에서 new ArrayList<>(inputWords) 호출 시 NullPointerException이 발생합니다. 또한 line 197의 for-loop에서도 null 입력단어를 순회할 때 예외가 발생합니다. WordDictionary 엔티티에 null 검증이 없어 MongoDB에 저장된 데이터가 null을 포함할 수 있으므로 방어 로직이 필수입니다.

🐛 null 방어 로직 추가
 for (WordDictionary wordDict : allDictionaries) {
     String standardWord = wordDict.getStandardWord();
     List<String> inputWords = wordDict.getInputWords();
     
+    if (inputWords == null) {
+        inputWords = Collections.emptyList();
+    }
+    
     // 표준단어와 모든 입력단어를 포함한 확장 리스트 생성
     List<String> expandedList = new ArrayList<>(inputWords);
🤖 Prompt for AI Agents
In `@backend/src/main/java/moadong/club/service/WordDictionaryService.java` around
lines 180 - 189, In WordDictionaryService, guard against
wordDict.getInputWords() returning null by replacing List<String> inputWords =
wordDict.getInputWords() with a null-safe value (e.g., List<String> inputWords =
Optional.ofNullable(wordDict.getInputWords()).orElse(Collections.emptyList()) or
new ArrayList<>()), then build expandedList from that safe inputWords and
proceed with the existing logic (this prevents the new ArrayList<>(inputWords)
NPE and safe-guards the subsequent for-loop over expandedList); also consider
handling a null standardWord similarly before adding it to expandedList.

@Zepelown Zepelown changed the title feat: 클럽 약력 수정 시에 벡터 데이터 저장되는 기능 추가 [feature] 연관어 검색 구현 Jan 25, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@backend/src/test/java/moadong/club/service/ClubApplyAdminServiceTest.java`:
- Around line 49-87: The setup reuses existing Club/ClubApplicationForm and
tearDown unconditionally deletes by clubId/clubApplicationFormId which can
remove real/seed data and is brittle (also using findByClubId(...).get(0) picks
a random form). Always create fresh test entities in setUp (e.g., force-create a
new Club via new Club(user.getId()) and a new ClubApplicationForm via
ClubApplicationForm.builder()...), store boolean flags or only set
clubId/clubApplicationFormId when you created them (track e.g., createdClub =
true, createdForm = true), and in tearDown delete only those created entities
(check the flags). Replace findByClubId(...).get(0) usage with explicit creation
or a query that targets the newly created test form to avoid deleting existing
ACTIVE forms.

Comment on lines +49 to 87
if (userRepository.findUserByUserId(UserFixture.collectUserId).isEmpty()) {
userRepository.save(UserFixture.createUser(passwordEncoder));
}
User user = userRepository.findUserByUserId(UserFixture.collectUserId).get();
userDetails = new CustomUserDetails(user);
Club club = clubRepository.findClubByUserId(user.getId()).get();

// 테스트 전에 문서를 한 번만 조회하여 ID를 확보
if (clubApplicationFormsRepository.findByClubId(club.getId()).isEmpty()){
throw new NoSuchElementException("테스트를 위한 ClubApplicationForm 문서가 DB에 존재하지 않습니다. 먼저 문서를 생성해주세요.");
// Club이 없으면 생성
if (clubRepository.findClubByUserId(user.getId()).isEmpty()) {
Club club = new Club(user.getId());
clubRepository.save(club);
this.clubId = club.getId();
} else {
Club club = clubRepository.findClubByUserId(user.getId()).get();
this.clubId = club.getId();
}

// ClubApplicationForm이 없으면 생성
if (clubApplicationFormsRepository.findByClubId(clubId).isEmpty()) {
ClubApplicationForm clubApplicationForm = ClubApplicationForm.builder()
.clubId(clubId)
.build();
clubApplicationFormsRepository.save(clubApplicationForm);
this.clubApplicationFormId = clubApplicationForm.getId();
} else {
ClubApplicationForm clubApplicationForm = clubApplicationFormsRepository.findByClubId(clubId).get(0);
this.clubApplicationFormId = clubApplicationForm.getId();
}
}

@AfterEach
void tearDown() {
// 테스트용 ClubApplicationForm 삭제
if (clubApplicationFormId != null) {
clubApplicationFormsRepository.deleteById(clubApplicationFormId);
}
// 테스트용 Club 삭제
if (clubId != null) {
clubRepository.deleteById(clubId);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

기존 데이터까지 삭제될 수 있어 테스트 간 간섭 위험이 큽니다.
setUp에서 기존 Club/ClubApplicationForm을 재사용하는데 tearDown은 무조건 삭제합니다. 이 경우 다른 테스트/시드 데이터가 삭제되어 테스트가 순서 의존 및 flaky해질 수 있습니다. 또한 findByClubId(...).get(0)는 여러 문서가 있는 경우 임의 문서를 선택해 삭제할 수 있습니다. 테스트용 데이터만 생성·삭제하도록 플래그를 두거나, 매번 새 테스트 문서를 생성해 정리하는 방식이 안전합니다. Based on learnings, 동일 clubId에 여러 ACTIVE 폼이 존재할 수 있어 임의 선택/삭제는 더 위험합니다.

🔧 제안 수정안(생성 여부 추적 + 테스트용 문서만 정리)
@@
-    private String clubId;
-    private String clubApplicationFormId;
+    private String clubId;
+    private String clubApplicationFormId;
+    private boolean createdClub;
+    private boolean createdForm;
@@
-        if (clubRepository.findClubByUserId(user.getId()).isEmpty()) {
-            Club club = new Club(user.getId());
-            clubRepository.save(club);
-            this.clubId = club.getId();
-        } else {
-            Club club = clubRepository.findClubByUserId(user.getId()).get();
-            this.clubId = club.getId();
-        }
+        Club club = clubRepository.findClubByUserId(user.getId())
+                .orElseGet(() -> {
+                    createdClub = true;
+                    return clubRepository.save(new Club(user.getId()));
+                });
+        this.clubId = club.getId();
@@
-        if (clubApplicationFormsRepository.findByClubId(clubId).isEmpty()) {
-            ClubApplicationForm clubApplicationForm = ClubApplicationForm.builder()
-                    .clubId(clubId)
-                    .build();
-            clubApplicationFormsRepository.save(clubApplicationForm);
-            this.clubApplicationFormId = clubApplicationForm.getId();
-        } else {
-            ClubApplicationForm clubApplicationForm = clubApplicationFormsRepository.findByClubId(clubId).get(0);
-            this.clubApplicationFormId = clubApplicationForm.getId();
-        }
+        ClubApplicationForm clubApplicationForm = ClubApplicationForm.builder()
+                .clubId(clubId)
+                .build();
+        clubApplicationFormsRepository.save(clubApplicationForm);
+        this.clubApplicationFormId = clubApplicationForm.getId();
+        this.createdForm = true;
@@
-        if (clubApplicationFormId != null) {
+        if (createdForm && clubApplicationFormId != null) {
             clubApplicationFormsRepository.deleteById(clubApplicationFormId);
         }
@@
-        if (clubId != null) {
+        if (createdClub && clubId != null) {
             clubRepository.deleteById(clubId);
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (userRepository.findUserByUserId(UserFixture.collectUserId).isEmpty()) {
userRepository.save(UserFixture.createUser(passwordEncoder));
}
User user = userRepository.findUserByUserId(UserFixture.collectUserId).get();
userDetails = new CustomUserDetails(user);
Club club = clubRepository.findClubByUserId(user.getId()).get();
// 테스트 전에 문서를 한 번만 조회하여 ID를 확보
if (clubApplicationFormsRepository.findByClubId(club.getId()).isEmpty()){
throw new NoSuchElementException("테스트를 위한 ClubApplicationForm 문서가 DB에 존재하지 않습니다. 먼저 문서를 생성해주세요.");
// Club이 없으면 생성
if (clubRepository.findClubByUserId(user.getId()).isEmpty()) {
Club club = new Club(user.getId());
clubRepository.save(club);
this.clubId = club.getId();
} else {
Club club = clubRepository.findClubByUserId(user.getId()).get();
this.clubId = club.getId();
}
// ClubApplicationForm이 없으면 생성
if (clubApplicationFormsRepository.findByClubId(clubId).isEmpty()) {
ClubApplicationForm clubApplicationForm = ClubApplicationForm.builder()
.clubId(clubId)
.build();
clubApplicationFormsRepository.save(clubApplicationForm);
this.clubApplicationFormId = clubApplicationForm.getId();
} else {
ClubApplicationForm clubApplicationForm = clubApplicationFormsRepository.findByClubId(clubId).get(0);
this.clubApplicationFormId = clubApplicationForm.getId();
}
}
@AfterEach
void tearDown() {
// 테스트용 ClubApplicationForm 삭제
if (clubApplicationFormId != null) {
clubApplicationFormsRepository.deleteById(clubApplicationFormId);
}
// 테스트용 Club 삭제
if (clubId != null) {
clubRepository.deleteById(clubId);
}
if (userRepository.findUserByUserId(UserFixture.collectUserId).isEmpty()) {
userRepository.save(UserFixture.createUser(passwordEncoder));
}
User user = userRepository.findUserByUserId(UserFixture.collectUserId).get();
userDetails = new CustomUserDetails(user);
// Club이 없으면 생성
Club club = clubRepository.findClubByUserId(user.getId())
.orElseGet(() -> {
createdClub = true;
return clubRepository.save(new Club(user.getId()));
});
this.clubId = club.getId();
// ClubApplicationForm이 없으면 생성
ClubApplicationForm clubApplicationForm = ClubApplicationForm.builder()
.clubId(clubId)
.build();
clubApplicationFormsRepository.save(clubApplicationForm);
this.clubApplicationFormId = clubApplicationForm.getId();
this.createdForm = true;
}
`@AfterEach`
void tearDown() {
// 테스트용 ClubApplicationForm 삭제
if (createdForm && clubApplicationFormId != null) {
clubApplicationFormsRepository.deleteById(clubApplicationFormId);
}
// 테스트용 Club 삭제
if (createdClub && clubId != null) {
clubRepository.deleteById(clubId);
}
}
🤖 Prompt for AI Agents
In `@backend/src/test/java/moadong/club/service/ClubApplyAdminServiceTest.java`
around lines 49 - 87, The setup reuses existing Club/ClubApplicationForm and
tearDown unconditionally deletes by clubId/clubApplicationFormId which can
remove real/seed data and is brittle (also using findByClubId(...).get(0) picks
a random form). Always create fresh test entities in setUp (e.g., force-create a
new Club via new Club(user.getId()) and a new ClubApplicationForm via
ClubApplicationForm.builder()...), store boolean flags or only set
clubId/clubApplicationFormId when you created them (track e.g., createdClub =
true, createdForm = true), and in tearDown delete only those created entities
(check the flags). Replace findByClubId(...).get(0) usage with explicit creation
or a query that targets the newly created test form to avoid deleting existing
ACTIVE forms.

Copy link
Contributor

@lepitaaar lepitaaar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수고하셨습니다. 연관검색어 만든거 좋네요. 다만연관검색어 컨트롤러는 프로덕션에서는 제외하거나 해야할꺼같습니다!

Comment on lines +24 to +29
maven { url 'https://repo.spring.io/milestone' }
maven { url 'https://repo.spring.io/snapshot' }
maven {
name = 'Central Portal Snapshots'
url = 'https://central.sonatype.com/repository/maven-snapshots/'
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

따로 의존성이 추가된거같지않은데 레포지토리가 왜추가됐을까요?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

벡터모델 의존성 추가하면서 생겼는데 있으면 좋데요

Comment on lines +20 to +24
@RestController
@RequestMapping("/api/admin/word-dictionary")
@AllArgsConstructor
@Tag(name = "Word_Dictionary_Admin", description = "단어사전 관리자 API")
public class WordDictionaryAdminController {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

어드민용 컨트롤러라 되어있어서 권한설정필요해보이네요

@RequiredArgsConstructor
public class WordDictionaryService {

private static final String CSV_FILE_PATH = "club_keyword_dictionary_long_expanded_cleaned.csv";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

추가해야할 파일이 늘어난거같아서 나중에 설명해주시면 감사하겠습니다

@Zepelown Zepelown merged commit 36cd275 into develop/be Feb 1, 2026
5 checks passed
@Zepelown Zepelown deleted the feature/#1077-add-related-search-MOA-539 branch February 1, 2026 12:08
@coderabbitai coderabbitai bot mentioned this pull request Feb 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants