Conversation
- .gitignore: /.cursor 추가 - dev 포털: 스티키 헤더·섹션 네비, 동아리 목록 페이지네이션·ID 복사, 토스트·저장 피드백, 스타일·접근성 정리 Co-authored-by: Cursor <cursoragent@cursor.com>
- edit.html: 동아리 수정 전용 페이지 추가 (약력/모집정보 폼)\n- index.html: 수정 UI 제거, 목록 클릭 시 openEditWindow로 수정 창 오픈 Co-authored-by: Cursor <cursoragent@cursor.com>
- dict-edit.html: 단어사전 수정 전용 페이지 추가\n- index.html: 단어사전 목록·페이지네이션, 단일/CSV 추가, 수정 창 연동, 삭제, 캐시 새로고침 Co-authored-by: Cursor <cursoragent@cursor.com>
…on-MOA-630 [feat] 네비게이션용 데이터 추가
[featuer] 백엔드 스케쥴러 ShedLock 도입하여 중복 스케쥴링 방지
…OA-638 [feature] 동아리 마감전 dday 알림표시
…A-620 [feature] 개발자 페이지 만들어서 관리 쉽게 ㄱㄱ
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning
|
| Cohort / File(s) | Summary |
|---|---|
의존성 및 설정 backend/build.gradle, backend/src/main/java/moadong/global/config/MongoConfig.java, backend/src/main/java/moadong/global/config/SecurityConfig.java |
ShedLock 라이브러리 의존성 추가, MongoDB를 통한 분산 잠금 제공자 구성, /api/admin/** 경로에 DEVELOPER 역할 요구 설정 |
모집 마감 D-day 알림 backend/src/main/java/moadong/club/service/RecruitmentDdayNotifier.java, backend/src/main/java/moadong/club/util/RecruitmentDdayNotificationBuilder.java, backend/src/main/java/moadong/club/service/RecruitmentStateChecker.java, backend/src/main/java/moadong/club/util/RecruitmentStateCalculator.java, backend/src/main/java/moadong/fcm/enums/FcmAction.java |
매일 08:00에 실행되는 D-day 알림 스케줄러 추가, 7일/3일/1일 남았을 때 FCM 알림 발송, 분산 잠금 적용, RecruitmentStateChecker 스케줄 주기를 1시간에서 10분으로 변경 |
클럽 관리 API backend/src/main/java/moadong/club/controller/ClubAdminController.java, backend/src/main/java/moadong/club/payload/response/ClubListResponse.java, backend/src/main/java/moadong/club/repository/ClubRepository.java, backend/src/main/java/moadong/club/service/ClubProfileService.java |
관리자용 REST API 컨트롤러 추가 (전체 클럽 조회, 클럽 정보/모집정보 업데이트), 응답 DTO 추가, 모집 상태 기반 조회 메서드 추가 |
사용자 역할 및 인증 backend/src/main/java/moadong/user/entity/User.java, backend/src/main/java/moadong/user/entity/enums/UserRole.java, backend/src/main/java/moadong/user/controller/DevAuthController.java, backend/src/main/java/moadong/user/payload/request/DevRegisterRequest.java, backend/src/main/java/moadong/user/service/UserCommandService.java, backend/src/main/java/moadong/user/payload/request/UserRegisterRequest.java |
사용자 역할 열거형 추가 (CLUB_ADMIN, DEVELOPER), 개발자 등록 엔드포인트 및 요청 DTO 추가, User 엔티티에 role 필드 추가, registerDeveloper 메서드로 보안 토큰 기반 개발자 등록 지원 |
개발자 포털 정적 페이지 backend/src/main/resources/static/dev/index.html, backend/src/main/resources/static/dev/edit.html, backend/src/main/resources/static/dev/dict-edit.html, backend/src/main/java/moadong/global/controller/DevPortalController.java |
개발자 포털 메인 페이지 추가 (로그인, 클럽 관리, 단어사전 관리), 클럽 편집 페이지 추가, 단어사전 항목 편집 페이지 추가, /dev 경로 리다이렉트 컨트롤러 추가 |
기타 backend/.gitignore |
firebase.json 재추가, /.cursor 무시 규칙 추가 |
Sequence Diagram(s)
sequenceDiagram
participant Scheduler as Scheduler
participant RDN as RecruitmentDdayNotifier
participant Repo as ClubRepository
participant Builder as RecruitmentDdayNotificationBuilder
participant FCM as Firebase Cloud Messaging
Scheduler->>RDN: sendDdayNotifications() at 08:00 KST
RDN->>Repo: findAllByClubRecruitmentInformation_ClubRecruitmentStatus(OPEN)
Repo-->>RDN: List<Club>
loop for each Club with recruitmentEnd
RDN->>RDN: Calculate daysLeft (7, 3, or 1)
alt daysLeft matches (7, 3, 1)
RDN->>Builder: build(club, daysLeft)
Builder-->>RDN: Firebase Message
RDN->>FCM: Send Message
FCM-->>RDN: Success
end
end
RDN-->>Scheduler: Notification Summary
sequenceDiagram
participant Client as Client
participant DevAuth as DevAuthController
participant UserCmd as UserCommandService
participant UserRepo as UserRepository
participant ClubCmd as ClubCommandService
participant DB as Database
Client->>DevAuth: POST /auth/dev/register with DevRegisterRequest & secret
DevAuth->>UserCmd: registerDeveloper(request)
UserCmd->>UserCmd: Validate devRegistrationSecret
UserCmd->>UserRepo: Check existing userId
alt User exists
UserCmd-->>DevAuth: Error - Duplicate user
else User not found
UserCmd->>UserCmd: Create User with DEVELOPER role
UserCmd->>DB: Save User
UserCmd->>ClubCmd: Create associated Club
UserCmd->>DB: Save Club
UserCmd-->>DevAuth: Success
DevAuth-->>Client: Success Response
end
Estimated code review effort
🎯 3 (Moderate) | ⏱️ ~25 minutes
Possibly related PRs
- [feature] 동아리 마감전 dday 알림표시 #1188: D-day 알림 기능 구현 — ShedLock 의존성, MongoDB 잠금 제공자, RecruitmentDdayNotifier, 스케줄 변경 등이 동일
- [feature] 개발자 페이지 만들어서 관리 쉽게 ㄱㄱ #1164: 개발자 포털 및 관리자 기능 구현 — ClubAdminController, 정적 개발자 페이지, 사용자 역할 관리 등이 동일
- [fix] 스케줄러는 db를 참조하여 모집현황을 갱신한다 #401: 스케줄 관련 변경 — RecruitmentStateChecker 스케줄 주기 및 잠금 설정 관련
Suggested labels
✨ Feature
Suggested reviewers
- Zepelown
🚥 Pre-merge checks | ✅ 1 | ❌ 3
❌ Failed checks (2 warnings, 1 inconclusive)
| Check name | Status | Explanation | Resolution |
|---|---|---|---|
| Docstring Coverage | Docstring coverage is 3.03% which is insufficient. The required threshold is 80.00%. | Write docstrings for the functions missing them to satisfy the coverage threshold. | |
| Merge Conflict Detection | ❌ Merge conflicts detected (112 files): ⚔️ .github/ISSUE_TEMPLATE/jira-issue-form.yml (content)⚔️ .gitignore (content)⚔️ README.md (content)⚔️ backend/.gitignore (content)⚔️ backend/build.gradle (content)⚔️ backend/src/main/java/moadong/club/repository/ClubRepository.java (content)⚔️ backend/src/main/java/moadong/club/service/ClubProfileService.java (content)⚔️ backend/src/main/java/moadong/club/service/RecruitmentStateChecker.java (content)⚔️ backend/src/main/java/moadong/club/util/RecruitmentStateCalculator.java (content)⚔️ backend/src/main/java/moadong/global/config/MongoConfig.java (content)⚔️ backend/src/main/java/moadong/global/config/SecurityConfig.java (content)⚔️ backend/src/main/java/moadong/user/entity/User.java (content)⚔️ backend/src/main/java/moadong/user/payload/request/UserRegisterRequest.java (content)⚔️ backend/src/main/java/moadong/user/service/UserCommandService.java (content)⚔️ frontend/.gitignore (content)⚔️ frontend/.prettierrc (content)⚔️ frontend/.storybook/main.ts (content)⚔️ frontend/codecov.yml (content)⚔️ frontend/eslint.config.mjs (content)⚔️ frontend/jest.setup.ts (content)⚔️ frontend/package-lock.json (content)⚔️ frontend/package.json (content)⚔️ frontend/public/index.html (content)⚔️ frontend/public/mockServiceWorker.js (content)⚔️ frontend/src/App.tsx (content)⚔️ frontend/src/apis/auth/refreshAccessToken.ts (content)⚔️ frontend/src/assets/images/banners/banner_desktop1.png (content)⚔️ frontend/src/assets/images/banners/banner_desktop2.png (content)⚔️ frontend/src/assets/images/banners/banner_mobile1.png (content)⚔️ frontend/src/assets/images/banners/banner_mobile2.png (content)⚔️ frontend/src/assets/images/icons/category_button/category_all_button_icon.svg (content)⚔️ frontend/src/assets/images/icons/category_button/category_hobby_button_icon.svg (content)⚔️ frontend/src/assets/images/icons/category_button/category_performance_button_icon.svg (content)⚔️ frontend/src/assets/images/icons/category_button/category_religion_button_icon.svg (content)⚔️ frontend/src/assets/images/icons/category_button/category_sport_button_icon.svg (content)⚔️ frontend/src/assets/images/icons/category_button/category_study_button_icon.svg (content)⚔️ frontend/src/assets/images/icons/category_button/category_volunteer_button_icon.svg (content)⚔️ frontend/src/assets/images/icons/next_button_icon.svg (content)⚔️ frontend/src/assets/images/icons/prev_button_icon.svg (content)⚔️ frontend/src/assets/images/icons/sns/youtube_icon.svg (content)⚔️ frontend/src/components/ClubStateBox/ClubStateBox.tsx (content)⚔️ frontend/src/components/ClubTag/ClubTag.tsx (content)⚔️ frontend/src/components/common/Button/Button.tsx (content)⚔️ frontend/src/components/common/CustomDropDown/CustomDropDown.styles.ts (content)⚔️ frontend/src/components/common/CustomDropDown/CustomDropDown.tsx (content)⚔️ frontend/src/components/common/CustomTextArea/CustomTextArea.styles.ts (content)⚔️ frontend/src/components/common/CustomTextArea/CustomTextArea.tsx (content)⚔️ frontend/src/components/common/Footer/Footer.stories.tsx (content)⚔️ frontend/src/components/common/Footer/Footer.styles.ts (content)⚔️ frontend/src/components/common/Footer/Footer.tsx (content)⚔️ frontend/src/components/common/Header/Header.stories.tsx (content)⚔️ frontend/src/components/common/Header/Header.styles.ts (content)⚔️ frontend/src/components/common/Header/Header.tsx (content)⚔️ frontend/src/components/common/InputField/InputField.styles.ts (content)⚔️ frontend/src/components/common/InputField/InputField.tsx (content)⚔️ frontend/src/components/common/Spinner/Spinner.tsx (content)⚔️ frontend/src/constants/api.ts (content)⚔️ frontend/src/constants/snsConfig.ts (content)⚔️ frontend/src/constants/uploadLimit.ts (content)⚔️ frontend/src/context/AdminClubContext.tsx (content)⚔️ frontend/src/hooks/useAuth.ts (content)⚔️ frontend/src/index.tsx (content)⚔️ frontend/src/pages/AdminPage/AdminPage.styles.ts (content)⚔️ frontend/src/pages/AdminPage/AdminPage.tsx (content)⚔️ frontend/src/pages/AdminPage/auth/LoginTab/LoginTab.styles.ts (content)⚔️ frontend/src/pages/AdminPage/auth/LoginTab/LoginTab.tsx (content)⚔️ frontend/src/pages/AdminPage/auth/PrivateRoute/PrivateRoute.tsx (content)⚔️ frontend/src/pages/AdminPage/components/ClubLogoEditor/ClubLogoEditor.styles.ts (content)⚔️ frontend/src/pages/AdminPage/components/ClubLogoEditor/ClubLogoEditor.tsx (content)⚔️ frontend/src/pages/AdminPage/components/SideBar/SideBar.styles.ts (content)⚔️ frontend/src/pages/AdminPage/components/SideBar/SideBar.tsx (content)⚔️ frontend/src/pages/AdminPage/tabs/AccountEditTab/AccountEditTab.styles.ts (content)⚔️ frontend/src/pages/AdminPage/tabs/AccountEditTab/AccountEditTab.tsx (content)⚔️ frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/ClubInfoEditTab.styles.ts (content)⚔️ frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/ClubInfoEditTab.tsx (content)⚔️ frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/MakeTags/MakeTags.styles.ts (content)⚔️ frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/MakeTags/MakeTags.tsx (content)⚔️ frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/SelectTags/SelectTags.styles.ts (content)⚔️ frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/SelectTags/SelectTags.tsx (content)⚔️ frontend/src/pages/AdminPage/tabs/PhotoEditTab/PhotoEditTab.styles.ts (content)⚔️ frontend/src/pages/AdminPage/tabs/PhotoEditTab/PhotoEditTab.tsx (content)⚔️ frontend/src/pages/AdminPage/tabs/PhotoEditTab/components/ImagePreview/ImagePreview.styles.ts (content)⚔️ frontend/src/pages/AdminPage/tabs/PhotoEditTab/components/ImagePreview/ImagePreview.tsx (content)⚔️ frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.styles.ts (content)⚔️ frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.tsx (content)⚔️ frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/Calendar/Calendar.styles.ts (content)⚔️ frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/Calendar/Calendar.tsx (content)⚔️ frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/MarkdownEditor/MarkdownEditor.tsx (content)⚔️ frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx (content)⚔️ frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx (content)⚔️ frontend/src/pages/ClubDetailPage/components/ClubDetailFooter/ClubDetailFooter.styles.ts (content)⚔️ frontend/src/pages/ClubDetailPage/components/ClubDetailFooter/ClubDetailFooter.tsx (content)⚔️ frontend/src/pages/IntroducePage/IntroducePage.styles.ts (content)⚔️ frontend/src/pages/IntroducePage/IntroducePage.tsx (content)⚔️ frontend/src/pages/MainPage/MainPage.styles.ts (content)⚔️ frontend/src/pages/MainPage/MainPage.tsx (content)⚔️ frontend/src/pages/MainPage/components/Banner/Banner.styles.ts (content)⚔️ frontend/src/pages/MainPage/components/Banner/Banner.tsx (content)⚔️ frontend/src/pages/MainPage/components/CategoryButtonList/CategoryButtonList.styles.ts (content)⚔️ frontend/src/pages/MainPage/components/CategoryButtonList/CategoryButtonList.tsx (content)⚔️ frontend/src/pages/MainPage/components/ClubCard/ClubCard.styles.ts (content)⚔️ frontend/src/pages/MainPage/components/ClubCard/ClubCard.tsx (content)⚔️ frontend/src/pages/MainPage/components/StatusRadioButton/StatusRadioButton.tsx (content)⚔️ frontend/src/styles/Global.styles.ts (content)⚔️ frontend/src/styles/PageContainer.styles.ts (content)⚔️ frontend/src/types/application.ts (content)⚔️ frontend/src/types/club.ts (content)⚔️ frontend/src/utils/getDeadLineText.test.ts (content)⚔️ frontend/src/utils/getDeadLineText.ts (content)⚔️ frontend/src/utils/initSDK.ts (content)⚔️ frontend/src/utils/validateSocialLink.test.ts (content)⚔️ frontend/tsconfig.json (content)These conflicts must be resolved before merging into main. |
Resolve conflicts locally and push changes to this branch. | |
| Title check | ❓ Inconclusive | PR 제목 '[Release] BE'는 매우 모호하고 구체적이지 않습니다. 실제 변경 사항(개발자 인증, FCM 알림, ShedLock 통합, 관리자 포털 등)을 설명하지 않습니다. | 제목을 더 구체적으로 변경하세요. 예: '[Release] Add developer auth, admin portal, and recruitment D-day notifications' 같은 주요 변경 사항을 반영하는 제목이 좋습니다. |
✅ Passed checks (1 passed)
| Check name | Status | Explanation |
|---|---|---|
| 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
🧪 Generate unit tests (beta)
- Create PR with unit tests
- Post copyable unit tests in a comment
- Commit unit tests in branch
develop/be
⚔️ Resolve merge conflicts (beta)
- Auto-commit resolved conflicts to branch
develop/be - Create stacked PR with resolved conflicts
- Post resolved changes as copyable diffs in a comment
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.
Comment @coderabbitai help to get the list of available commands and usage tips.
There was a problem hiding this comment.
Actionable comments posted: 12
🤖 Fix all issues with AI agents
In `@backend/build.gradle`:
- Around line 77-78: The shedlock dependencies currently use an incompatible
7.6.0 release; update the two artifact versions for net.javacrumbs.shedlock (the
implementation entries for 'shedlock-spring' and 'shedlock-provider-mongo') to a
Spring Boot 3.3.8–compatible 6.x release (for example 6.10.0) so they align with
Spring Framework 6.x and avoid classpath conflicts; change the version strings
for the artifacts 'net.javacrumbs.shedlock:shedlock-spring' and
'net.javacrumbs.shedlock:shedlock-provider-mongo' accordingly and run a build to
confirm dependency resolution.
In `@backend/src/main/java/moadong/club/controller/ClubAdminController.java`:
- Around line 53-58: The updateClubDescription controller method is missing
request-body validation: annotate the ClubRecruitmentInfoUpdateRequest parameter
in ClubAdminController.updateClubDescription with `@Valid` (as done for
ClubInfoRequest) and add the corresponding javax.validation.Valid import so the
request body is validated before calling
clubProfileService.updateClubRecruitmentInfoByClubId.
In `@backend/src/main/java/moadong/club/service/RecruitmentDdayNotifier.java`:
- Around line 46-47: The code calls club.getClubRecruitmentInformation() and
immediately accesses info.getRecruitmentEnd(), which can throw an NPE if
getClubRecruitmentInformation() returns null; update RecruitmentDdayNotifier to
defensively check the returned ClubRecruitmentInformation (e.g., assign to a
local variable info, if info == null then skip processing this club or log a
warning) before referencing info.getRecruitmentEnd(), ensuring you handle the
null case cleanly (continue/return or fallback behavior) so recruitmentEnd is
only accessed when info is non-null.
- Around line 45-60: In RecruitmentDdayNotifier, isolate failures during
individual notifications by wrapping the call to
club.sendPushNotification(notificationBuilder.build(club, daysLeft)) in a
try-catch (e.g., catch Exception or RuntimeException), so one club's exception
doesn't break the loop over openClubs; log the error with identifying info
(club.getId(), club.getName(), daysLeft) and continue, and only increment
sentCount on successful send.
In `@backend/src/main/java/moadong/club/service/RecruitmentStateChecker.java`:
- Around line 27-28: The distributed lock duration in RecruitmentStateChecker is
too short: `@SchedulerLock` currently sets lockAtMostFor = "1s"/"1m" while the
scheduled fixedRate is 10 minutes, which can allow another instance to acquire
the lock mid-run; update the `@SchedulerLock` on the RecruitmentStateChecker
scheduled method to increase lockAtMostFor to roughly the schedule length (e.g.
"10m" or slightly longer like "11m") so the lock covers the entire execution
window and prevent concurrent runs; keep or minimally adjust lockAtLeastFor if
you need a minimum hold, but ensure lockAtMostFor >= scheduled fixedRate.
In `@backend/src/main/java/moadong/global/config/MongoConfig.java`:
- Around line 21-24: The lockProvider currently calls
mongoClient.getDatabase("shedLock") which hardcodes and creates a separate DB;
change lockProvider in MongoConfig to use the application's configured database
instead (e.g., obtain the DB name from configuration or reuse an existing
MongoDatabase/MongoTemplate bean). Concretely: replace
mongoClient.getDatabase("shedLock") with
mongoClient.getDatabase(applicationDatabaseName) where applicationDatabaseName
is injected from properties (e.g., `@Value`("${spring.data.mongodb.database}") or
from a MongoProperties/MongoTemplate/MongoDatabase bean) and pass that into new
MongoLockProvider(...); ensure the property or bean is added to the
constructor/parameters of lockProvider so environments reuse the app DB rather
than creating "shedLock".
In `@backend/src/main/java/moadong/global/config/SecurityConfig.java`:
- Around line 42-52: SecurityFilterChain securityFilterChain currently
configures CSRF and request authorization but omits stateless session
management—add the import for SessionCreationPolicy and configure the
HttpSecurity inside securityFilterChain to use sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) so Spring
Security does not create sessions when using JwtAuthenticationFilter
(constructed with jwtProvider and userDetailsService); ensure this call is
chained before adding the JwtAuthenticationFilter.
In `@backend/src/main/java/moadong/global/controller/DevPortalController.java`:
- Around line 6-12: The /dev and /dev/ endpoints (DevPortalController.devPortal)
currently allow public access and expose static resources; update the
SecurityConfig to explicitly protect the /dev/** path (including static assets
like /dev/index.html and /dev/edit.html) by adding a request matcher such as
requestMatchers("/dev/**") (or antMatchers("/dev/**") depending on your Spring
Security version) and require an appropriate authority (e.g.,
.hasRole("DEVELOPER") or authenticated()) so only authorized developer users can
reach DevPortalController and its resources; ensure the rule precedes any
permissive matchers so it is enforced and keep existing protections like
`@PreAuthorize`("isAuthenticated()") for API endpoints intact.
In `@backend/src/main/resources/static/dev/dict-edit.html`:
- Around line 61-64: The splitter in parseInputWordsStr currently uses
`/[,,\s]+/`, which treats whitespace as a delimiter and breaks multi-word
entries like "동아리 활동"; update parseInputWordsStr to split only on commas (both
ASCII and full-width) by replacing the regex with one that matches only commas
(e.g., `/[,,]+/`) and preserve trimming/filtering logic; also apply the same
change to the identical implementation in index.html (the occurrence referenced
in the review) so both files behave consistently.
In `@backend/src/main/resources/static/dev/edit.html`:
- Line 108: The code constructs fetch URLs by concatenating '/api/club/' +
clubId without encoding, which can break for IDs with special characters; update
each occurrence that builds the club API URL (the fetch calls using API_BASE +
'/api/club/' + clubId) to wrap clubId with encodeURIComponent(clubId) — mirror
the existing approach used for dictId in dict-edit.html — so all fetches
(including the ones that currently call headers()) use encoded club IDs for
safe, consistent URLs.
In `@backend/src/main/resources/static/dev/index.html`:
- Around line 599-606: The upload sends an empty "Bearer " header because
getToken() is concatenated unconditionally in the fetch headers; change the
fetch header construction (the object passed to fetch in the POST to API_BASE +
'/api/admin/word-dictionary/csv') to only include Authorization when getToken()
returns a non-empty value (reuse the existing headers() behavior or call
headers() instead of building h = { 'Authorization': 'Bearer ' + getToken() });
ensure getToken() is checked and omit the Authorization property when it is
falsy so no empty Bearer header is sent.
- Around line 415-418: Replace the direct innerHTML insertion of the error
message with safe DOM text assignment: instead of setting tbody.innerHTML =
'<tr><td colspan="3">' + (e.message || '오류') + '</td></tr>' (and the similar
occurrence at the other spot), create a tr and td element, set td.colSpan = 3
(or 4 for the second occurrence as suggested), set td.textContent = (e.message
|| '오류'), append td to tr, then append tr to tbody; keep using
banner.textContent and banner.classList manipulation as-is.
🧹 Nitpick comments (12)
backend/src/main/java/moadong/global/config/SecurityConfig.java (1)
33-34: 주석 처리된 코드가 남아있습니다.GCP Storage 관련 주석 처리된 코드(credentials, Storage bean)가 여전히 남아 있습니다. 더 이상 필요 없다면 제거하여 코드 가독성을 높이는 것을 권장합니다.
Also applies to: 55-62
backend/src/main/java/moadong/user/entity/User.java (1)
54-55:role필드에@NotNull어노테이션 누락 검토 필요.다른 필드(
status,emailVerified등)는@Builder.Default와@NotNull을 함께 사용하고 있으나,role에는@NotNull이 없습니다. 기존 MongoDB 문서와의 호환성을 위한 의도적인 설계라면 괜찮지만, 신규 생성 시 null 방지를 위해 일관성 있게@NotNull을 추가하는 것을 권장합니다.backend/src/main/java/moadong/user/service/UserCommandService.java (2)
67-88: 시크릿 비교 시 타이밍 공격에 취약할 수 있습니다.Line 70의
devRegistrationSecret.equals(request.secret())는 일반 문자열 비교로, 타이밍 사이드 채널 공격에 이론적으로 취약합니다.MessageDigest.isEqual()또는 Spring Security의 상수 시간 비교를 사용하는 것을 권장합니다.또한
registerUser와 로직이 상당 부분 중복됩니다. 공통 부분을 추출하는 것을 고려해 보세요.🔒 타이밍 안전한 비교로 변경
+import java.security.MessageDigest; + `@Transactional` public User registerDeveloper(DevRegisterRequest request) { if (devRegistrationSecret == null || devRegistrationSecret.isBlank() - || !devRegistrationSecret.equals(request.secret())) { + || !MessageDigest.isEqual( + devRegistrationSecret.getBytes(), + request.secret().getBytes())) { throw new RestApiException(ErrorCode.USER_UNAUTHORIZED); }
49-50:devRegistrationSecret필드가final이 아니므로 필드 주입으로 동작합니다.
@RequiredArgsConstructor는final필드만 생성자에 포함하므로,@Value어노테이션이 붙은 이 필드는 필드 주입으로 처리됩니다. 동작에는 문제가 없지만, 생성자 주입 일관성을 위해final로 선언하고 생성자 매개변수로 받는 방식을 고려해 보세요.♻️ 생성자 주입으로 변경
- `@Value`("${app.dev-registration-secret:}") - private String devRegistrationSecret; // set by Spring after construction (field injection) + private final String devRegistrationSecret;그리고 생성자에
@Value를 적용하거나, 별도의@Configuration클래스에서 주입하도록 변경합니다.@RequiredArgsConstructor를 사용 중이므로, Lombok의lombok.config에lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Value를 추가하면 생성자 파라미터에@Value가 자동 복사됩니다.backend/src/main/java/moadong/user/controller/DevAuthController.java (1)
16-29: 공개 엔드포인트에 대한 rate limiting 부재.
/auth/dev/register는 인증 없이 접근 가능한 엔드포인트입니다. 시크릿 키가 유일한 보호 수단이므로, 무차별 대입 공격(brute-force)에 대비한 rate limiting 또는 IP 기반 제한을 추가하는 것을 권장합니다.backend/src/main/resources/static/dev/edit.html (1)
65-76:getToken,headers,showResult등 유틸리티 함수가 3개 파일에 중복됩니다.
edit.html,dict-edit.html,index.html모두에getToken(),headers(),showResult()등의 동일한 헬퍼 함수가 복사되어 있습니다. 공통 JS 파일(예:/dev/common.js)로 추출하면 유지보수가 쉬워집니다.backend/src/main/resources/static/dev/index.html (2)
302-310:navigator.clipboard.writeText의 Promise 거부가 처리되지 않습니다.Line 305, 355, 448에서
navigator.clipboard.writeText()를 호출하지만 반환된 Promise를 처리하지 않습니다. HTTPS가 아닌 환경이나 권한이 없는 경우 실패할 수 있으며, 이때 unhandled rejection이 발생합니다.🔧 수정 제안 (Line 305 예시)
- navigator.clipboard.writeText(t); + navigator.clipboard.writeText(t).catch(() => {});
243-257:showLogin함수의 매개변수명이 의미와 반대로 읽힐 수 있습니다.
showLogin(show)함수에서show가true일 때 로그인 폼을 숨기고 토큰 영역을 보여줍니다. 매개변수명이isLoggedIn이면 가독성이 더 좋겠습니다.backend/src/main/java/moadong/club/service/RecruitmentStateChecker.java (1)
30-41: 모든 클럽을 로드 후 개별 저장하는 비효율적인 패턴입니다.
findAll()로 전체 클럽을 로드하고,ALWAYS상태인 클럽도 포함하여 모두 가져온 뒤 개별적으로save()를 호출합니다. 두 가지 개선이 가능합니다:
ALWAYS가 아닌 클럽만 조회하도록 리포지토리 쿼리를 추가하거나, 새로 추가된findAllByClubRecruitmentInformation_ClubRecruitmentStatus를 활용- 변경된 클럽만 모아서
saveAll()로 일괄 저장backend/src/main/java/moadong/club/service/ClubProfileService.java (2)
61-71:getAllClubsForAdmin에 페이지네이션이 없습니다.현재
findAll()로 전체 클럽을 한 번에 로드합니다. 클럽 수가 증가하면 메모리와 응답 시간 문제가 발생할 수 있습니다.Pageable을 사용한 페이지네이션 도입을 권장합니다.
84-109:updateClubInfoByClubId와updateClubRecruitmentInfoByClubId는 기존 메서드와 거의 동일한 중복 코드입니다.Lines 38-44의
updateClubInfo와 Lines 84-92의updateClubInfoByClubId는 클럽 조회 방식만 다르고 나머지 로직이 동일합니다.updateClubRecruitmentInfo/updateClubRecruitmentInfoByClubId도 마찬가지입니다. 클럽 조회를 공통 private 메서드로 추출하면 중복을 줄일 수 있습니다.예시
private Club findClubByIdOrThrow(String clubId) { ObjectId objectId = ObjectIdConverter.convertString(clubId); return clubRepository.findClubById(objectId) .orElseThrow(() -> new RestApiException(ErrorCode.CLUB_NOT_FOUND)); }backend/src/main/java/moadong/club/util/RecruitmentStateCalculator.java (1)
89-95:buildNotificationData와RecruitmentDdayNotificationBuilder.buildData가 중복됩니다.
RecruitmentDdayNotificationBuilder.java(lines 41-46)의buildData메서드와 동일한 로직입니다. 경로/액션/클럽ID 맵 생성을 하나의 공유 유틸리티로 추출하면 향후 경로 변경 시 한 곳만 수정하면 됩니다.
| implementation 'net.javacrumbs.shedlock:shedlock-spring:7.6.0' | ||
| implementation 'net.javacrumbs.shedlock:shedlock-provider-mongo:7.6.0' |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
ShedLock 7.6.0 compatibility Spring Boot 3.3.8 Java 17
💡 Result:
- Java 17: OK — ShedLock 7.x requires JVM 17+. [1]
- Spring Boot 3.3.8: Not a good match for ShedLock 7.6.0. ShedLock 7.x is tested with Spring Boot 3.4/3.5/4.x and Spring Framework 6.2/7.0 (not 3.3 / Spring 6.1). [1]
- Why:
shedlock-spring:7.6.0pulls in Spring Frameworkspring-context7.0.3, which aligns with Spring Framework 7 (Boot 4 line), not Boot 3.3.x’s Spring Framework 6.1 line. [2][3] - Recommended for Spring Boot 3.3.8: Use ShedLock 6.x (tested with Spring Boot 3.3 and Spring 6.1/6.2). [1] The latest 6.x line shown on Maven Repository is 6.10.0. [4]
Sources:
[1] ShedLock GitHub compatibility matrix https://github.com/lukas-krecan/ShedLock
[2] shedlock-spring:7.6.0 dependencies (shows spring-context 7.0.3) https://mvnrepository.com/artifact/net.javacrumbs.shedlock/shedlock-spring/7.6.0/dependencies
[3] Spring Boot 3.3 system requirements (Spring Framework 6.1.x line) https://docs.enterprise.spring.io/spring-boot/system-requirements.html
[4] Maven Repository versions list for shedlock-spring (shows 6.10.0 as latest 6.x) https://mvnrepository.com/artifact/net.javacrumbs.shedlock/shedlock-spring
ShedLock 7.6.0은 Spring Boot 3.3.8과 호환되지 않습니다.
ShedLock 7.6.0은 Spring Framework 7.x (Spring Boot 4.x)를 기준으로 개발되었으나, Spring Boot 3.3.8은 Spring Framework 6.1.x를 사용합니다. shedlock-spring:7.6.0이 의존하는 spring-context 7.0.3이 프로젝트의 Spring Framework 버전과 충돌합니다. Spring Boot 3.3.8과 호환되는 **ShedLock 6.x 버전(예: 6.10.0)**을 사용하세요.
🤖 Prompt for AI Agents
In `@backend/build.gradle` around lines 77 - 78, The shedlock dependencies
currently use an incompatible 7.6.0 release; update the two artifact versions
for net.javacrumbs.shedlock (the implementation entries for 'shedlock-spring'
and 'shedlock-provider-mongo') to a Spring Boot 3.3.8–compatible 6.x release
(for example 6.10.0) so they align with Spring Framework 6.x and avoid classpath
conflicts; change the version strings for the artifacts
'net.javacrumbs.shedlock:shedlock-spring' and
'net.javacrumbs.shedlock:shedlock-provider-mongo' accordingly and run a build to
confirm dependency resolution.
| public ResponseEntity<?> updateClubDescription( | ||
| @CurrentUser CustomUserDetails user, | ||
| @PathVariable String clubId, | ||
| @RequestBody ClubRecruitmentInfoUpdateRequest request) { | ||
| clubProfileService.updateClubRecruitmentInfoByClubId(clubId, request, user); | ||
| return Response.ok("success update club description"); |
There was a problem hiding this comment.
ClubRecruitmentInfoUpdateRequest에 @Valid 어노테이션이 누락되었습니다.
Line 45의 ClubInfoRequest에는 @Valid가 적용되어 있지만, Line 56의 ClubRecruitmentInfoUpdateRequest에는 빠져 있어 요청 본문 검증이 수행되지 않습니다.
🔧 제안 수정
- `@RequestBody` ClubRecruitmentInfoUpdateRequest request) {
+ `@RequestBody` `@Valid` ClubRecruitmentInfoUpdateRequest request) {📝 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.
| public ResponseEntity<?> updateClubDescription( | |
| @CurrentUser CustomUserDetails user, | |
| @PathVariable String clubId, | |
| @RequestBody ClubRecruitmentInfoUpdateRequest request) { | |
| clubProfileService.updateClubRecruitmentInfoByClubId(clubId, request, user); | |
| return Response.ok("success update club description"); | |
| public ResponseEntity<?> updateClubDescription( | |
| `@CurrentUser` CustomUserDetails user, | |
| `@PathVariable` String clubId, | |
| `@RequestBody` `@Valid` ClubRecruitmentInfoUpdateRequest request) { | |
| clubProfileService.updateClubRecruitmentInfoByClubId(clubId, request, user); | |
| return Response.ok("success update club description"); |
🤖 Prompt for AI Agents
In `@backend/src/main/java/moadong/club/controller/ClubAdminController.java`
around lines 53 - 58, The updateClubDescription controller method is missing
request-body validation: annotate the ClubRecruitmentInfoUpdateRequest parameter
in ClubAdminController.updateClubDescription with `@Valid` (as done for
ClubInfoRequest) and add the corresponding javax.validation.Valid import so the
request body is validated before calling
clubProfileService.updateClubRecruitmentInfoByClubId.
| for (Club club : openClubs) { | ||
| ClubRecruitmentInformation info = club.getClubRecruitmentInformation(); | ||
| ZonedDateTime recruitmentEnd = info.getRecruitmentEnd(); | ||
|
|
||
| if (recruitmentEnd == null) { | ||
| continue; | ||
| } | ||
|
|
||
| long daysLeft = ChronoUnit.DAYS.between(today, recruitmentEnd.toLocalDate()); | ||
|
|
||
| if (NOTIFICATION_DAYS.contains(daysLeft)) { | ||
| log.info("D-Day 알림 전송 - clubId: {}, clubName: {}, D-{}", club.getId(), club.getName(), daysLeft); | ||
| club.sendPushNotification(notificationBuilder.build(club, daysLeft)); | ||
| sentCount++; | ||
| } | ||
| } |
There was a problem hiding this comment.
개별 클럽 알림 전송 실패 시 나머지 클럽이 알림을 받지 못합니다.
club.sendPushNotification()에서 예외가 발생하면 루프 전체가 중단되어 이후 클럽들이 알림을 받지 못하게 됩니다. 개별 전송을 try-catch로 감싸서 실패를 격리해야 합니다.
🛡️ 제안 수정
for (Club club : openClubs) {
ClubRecruitmentInformation info = club.getClubRecruitmentInformation();
ZonedDateTime recruitmentEnd = info.getRecruitmentEnd();
if (recruitmentEnd == null) {
continue;
}
long daysLeft = ChronoUnit.DAYS.between(today, recruitmentEnd.toLocalDate());
if (NOTIFICATION_DAYS.contains(daysLeft)) {
- log.info("D-Day 알림 전송 - clubId: {}, clubName: {}, D-{}", club.getId(), club.getName(), daysLeft);
- club.sendPushNotification(notificationBuilder.build(club, daysLeft));
- sentCount++;
+ try {
+ log.info("D-Day 알림 전송 - clubId: {}, clubName: {}, D-{}", club.getId(), club.getName(), daysLeft);
+ club.sendPushNotification(notificationBuilder.build(club, daysLeft));
+ sentCount++;
+ } catch (Exception e) {
+ log.error("D-Day 알림 전송 실패 - clubId: {}, clubName: {}", club.getId(), club.getName(), e);
+ }
}
}📝 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.
| for (Club club : openClubs) { | |
| ClubRecruitmentInformation info = club.getClubRecruitmentInformation(); | |
| ZonedDateTime recruitmentEnd = info.getRecruitmentEnd(); | |
| if (recruitmentEnd == null) { | |
| continue; | |
| } | |
| long daysLeft = ChronoUnit.DAYS.between(today, recruitmentEnd.toLocalDate()); | |
| if (NOTIFICATION_DAYS.contains(daysLeft)) { | |
| log.info("D-Day 알림 전송 - clubId: {}, clubName: {}, D-{}", club.getId(), club.getName(), daysLeft); | |
| club.sendPushNotification(notificationBuilder.build(club, daysLeft)); | |
| sentCount++; | |
| } | |
| } | |
| for (Club club : openClubs) { | |
| ClubRecruitmentInformation info = club.getClubRecruitmentInformation(); | |
| ZonedDateTime recruitmentEnd = info.getRecruitmentEnd(); | |
| if (recruitmentEnd == null) { | |
| continue; | |
| } | |
| long daysLeft = ChronoUnit.DAYS.between(today, recruitmentEnd.toLocalDate()); | |
| if (NOTIFICATION_DAYS.contains(daysLeft)) { | |
| try { | |
| log.info("D-Day 알림 전송 - clubId: {}, clubName: {}, D-{}", club.getId(), club.getName(), daysLeft); | |
| club.sendPushNotification(notificationBuilder.build(club, daysLeft)); | |
| sentCount++; | |
| } catch (Exception e) { | |
| log.error("D-Day 알림 전송 실패 - clubId: {}, clubName: {}", club.getId(), club.getName(), e); | |
| } | |
| } | |
| } |
🤖 Prompt for AI Agents
In `@backend/src/main/java/moadong/club/service/RecruitmentDdayNotifier.java`
around lines 45 - 60, In RecruitmentDdayNotifier, isolate failures during
individual notifications by wrapping the call to
club.sendPushNotification(notificationBuilder.build(club, daysLeft)) in a
try-catch (e.g., catch Exception or RuntimeException), so one club's exception
doesn't break the loop over openClubs; log the error with identifying info
(club.getId(), club.getName(), daysLeft) and continue, and only increment
sentCount on successful send.
| ClubRecruitmentInformation info = club.getClubRecruitmentInformation(); | ||
| ZonedDateTime recruitmentEnd = info.getRecruitmentEnd(); |
There was a problem hiding this comment.
getClubRecruitmentInformation()이 null을 반환할 경우 NPE 가능성.
OPEN 상태인 클럽이라면 일반적으로 모집 정보가 존재하겠지만, 방어적으로 null 체크를 추가하면 더 안전합니다.
🤖 Prompt for AI Agents
In `@backend/src/main/java/moadong/club/service/RecruitmentDdayNotifier.java`
around lines 46 - 47, The code calls club.getClubRecruitmentInformation() and
immediately accesses info.getRecruitmentEnd(), which can throw an NPE if
getClubRecruitmentInformation() returns null; update RecruitmentDdayNotifier to
defensively check the returned ClubRecruitmentInformation (e.g., assign to a
local variable info, if info == null then skip processing this club or log a
warning) before referencing info.getRecruitmentEnd(), ensuring you handle the
null case cleanly (continue/return or fallback behavior) so recruitmentEnd is
only accessed when info is non-null.
| @Scheduled(fixedRate = 10 * 60 * 1000) // 10분마다 실행 | ||
| @SchedulerLock(name="RecruitmentStateChecker", lockAtMostFor = "1m", lockAtLeastFor = "1s") |
There was a problem hiding this comment.
lockAtMostFor가 실행 주기에 비해 너무 짧을 수 있습니다.
스케줄 주기는 10분이지만 lockAtMostFor는 1분입니다. 클럽 수가 많아질 경우 findAll() + 개별 save() 루프가 1분을 초과할 수 있으며, 이 경우 락이 해제되어 다른 인스턴스가 동시에 같은 작업을 실행할 수 있습니다. lockAtMostFor를 실행 주기(10분)에 가깝게 설정하는 것이 안전합니다.
제안
- `@SchedulerLock`(name="RecruitmentStateChecker", lockAtMostFor = "1m", lockAtLeastFor = "1s")
+ `@SchedulerLock`(name="RecruitmentStateChecker", lockAtMostFor = "9m", lockAtLeastFor = "30s")🤖 Prompt for AI Agents
In `@backend/src/main/java/moadong/club/service/RecruitmentStateChecker.java`
around lines 27 - 28, The distributed lock duration in RecruitmentStateChecker
is too short: `@SchedulerLock` currently sets lockAtMostFor = "1s"/"1m" while the
scheduled fixedRate is 10 minutes, which can allow another instance to acquire
the lock mid-run; update the `@SchedulerLock` on the RecruitmentStateChecker
scheduled method to increase lockAtMostFor to roughly the schedule length (e.g.
"10m" or slightly longer like "11m") so the lock covers the entire execution
window and prevent concurrent runs; keep or minimally adjust lockAtLeastFor if
you need a minimum hold, but ensure lockAtMostFor >= scheduled fixedRate.
| @Controller | ||
| public class DevPortalController { | ||
|
|
||
| @GetMapping({"/dev", "/dev/"}) | ||
| public String devPortal() { | ||
| return "redirect:/dev/index.html"; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check for Spring Security configuration files and classes
find . -type f -name "*.java" -o -name "*.yml" -o -name "*.yaml" -o -name "*.properties" | head -20
# Search for Spring Security related configurations
echo "=== Searching for Spring Security configurations ==="
rg -l "SecurityFilterChain|WebSecurityConfigurerAdapter|@EnableWebSecurity" --type=javaRepository: Moadong/moadong
Length of output: 1298
🏁 Script executed:
#!/bin/bash
# Search for /dev path restrictions in security configs
echo "=== Searching for /dev path configuration ==="
rg -n "/dev" --type=java --type=yml --type=yaml -B 3 -A 3 | grep -E "(Security|Config|@Bean|@Configuration)" -A 5 -B 5 | head -100
# Search for antMatchers or permitAll configurations
echo "=== Searching for security matchers ==="
rg "antMatchers|mvcMatchers|permitAll" --type=java -B 2 -A 2Repository: Moadong/moadong
Length of output: 696
🏁 Script executed:
#!/bin/bash
# Look for the main security configuration class
echo "=== Looking for security configuration ==="
fd -e java | xargs grep -l "WebSecurityConfigurerAdapter\|SecurityFilterChain" 2>/dev/null
# Check application configuration files
echo "=== Checking application configuration files ==="
fd -e yml -o -e yaml -o -e properties | xargs cat 2>/dev/null | grep -A 10 -B 2 "dev\|security" | head -50Repository: Moadong/moadong
Length of output: 348
🏁 Script executed:
#!/bin/bash
# Read the SecurityConfig file to see full security configuration
cat -n backend/src/main/java/moadong/global/config/SecurityConfig.javaRepository: Moadong/moadong
Length of output: 3555
🏁 Script executed:
#!/bin/bash
# Check if there are static resource security configurations
rg -n "addResourceHandlers|static|dev" backend/src/main/java/moadong/global/config/ --type=java
# Check for any interceptors or other security mechanisms
rg -n "Interceptor|Filter|Security" backend/src/main/java/moadong/global/config/ --type=java | head -30
# Check if /dev is mentioned elsewhere in security-related code
rg -n '"/dev"' backend/src/main/java/moadong --type=javaRepository: Moadong/moadong
Length of output: 3068
🏁 Script executed:
#!/bin/bash
# Check if there are static resources under /dev
echo "=== Looking for /dev static resources ==="
find . -path "*/src/main/resources/static/dev*" -o -path "*/public/dev*" -o -path "*/frontend/*dev*" 2>/dev/null
# Check the structure of frontend/public directories
echo "=== Checking frontend/public structure ==="
ls -la frontend/public 2>/dev/null || ls -la frontend 2>/dev/null || echo "No frontend directory found"
# Check if dev resources are in resources/static
echo "=== Checking resources/static ==="
find backend/src/main/resources -type d -name "dev" -o -type f -name "*dev*" 2>/dev/null | head -20
# Check for any frontend build output that might serve /dev
echo "=== Checking for dev portal files ==="
fd -e html -e jsx -e js | xargs grep -l "dev\|Dev" 2>/dev/null | head -10Repository: Moadong/moadong
Length of output: 1251
🏁 Script executed:
#!/bin/bash
# Check the content of dev portal HTML files to see if they have any authentication
echo "=== Checking dev/index.html ==="
cat -n backend/src/main/resources/static/dev/index.html | head -50
echo "=== Checking dev/edit.html ==="
head -30 backend/src/main/resources/static/dev/edit.html
# Check if there's any client-side auth or redirect logic
echo "=== Searching for authentication checks in dev files ==="
grep -n "auth\|login\|token" backend/src/main/resources/static/dev/*.html 2>/dev/null || echo "No auth checks found in HTML"Repository: Moadong/moadong
Length of output: 8838
🏁 Script executed:
#!/bin/bash
# Check what operations require authentication in the dev portal
echo "=== Checking dev/index.html for API calls and auth requirements ==="
grep -n "fetch\|API_BASE\|token\|auth" backend/src/main/resources/static/dev/index.html | head -40
# Check if there are any `@PreAuthorize` or similar annotations on the APIs that /dev might call
echo "=== Checking for API authorization ==="
rg -n "@PreAuthorize|@Secured|@RolesAllowed" backend/src/main/java --type=java | head -20Repository: Moadong/moadong
Length of output: 3853
개발자 포털 엔드포인트는 Spring Security 레벨에서 보호되어야 합니다.
/dev 및 /dev/ 엔드포인트가 공개적으로 접근 가능하며, 정적 리소스(/dev/index.html, /dev/edit.html 등)도 노출됩니다. 실제 API 작업(/api/admin/**)은 hasRole("DEVELOPER")와 @PreAuthorize("isAuthenticated()")로 보호되지만, 엔드포인트 자체에 대한 접근 제어가 없으면 불필요한 정보 노출이나 공격 표면이 증가합니다. SecurityConfig에서 /dev/** 경로를 명시적으로 보호하거나 Spring Security의 requestMatchers를 통해 접근을 제한해야 합니다.
🤖 Prompt for AI Agents
In `@backend/src/main/java/moadong/global/controller/DevPortalController.java`
around lines 6 - 12, The /dev and /dev/ endpoints
(DevPortalController.devPortal) currently allow public access and expose static
resources; update the SecurityConfig to explicitly protect the /dev/** path
(including static assets like /dev/index.html and /dev/edit.html) by adding a
request matcher such as requestMatchers("/dev/**") (or antMatchers("/dev/**")
depending on your Spring Security version) and require an appropriate authority
(e.g., .hasRole("DEVELOPER") or authenticated()) so only authorized developer
users can reach DevPortalController and its resources; ensure the rule precedes
any permissive matchers so it is enforced and keep existing protections like
`@PreAuthorize`("isAuthenticated()") for API endpoints intact.
| function parseInputWordsStr(str) { | ||
| if (!str || !str.trim()) return []; | ||
| return str.split(/[,,\s]+/).map(s => s.trim()).filter(Boolean); | ||
| } |
There was a problem hiding this comment.
공백으로도 단어가 분리되어 의도치 않은 결과가 발생할 수 있습니다.
parseInputWordsStr의 정규식 /[,,\s]+/가 공백도 구분자로 사용합니다. 만약 "동아리 활동"처럼 공백이 포함된 단어를 입력해야 한다면 의도대로 동작하지 않습니다. 쉼표만 구분자로 사용하는 것이 더 안전할 수 있습니다. (index.html의 Line 551도 동일)
🔧 쉼표만 구분자로 사용하는 수정안
- return str.split(/[,,\s]+/).map(s => s.trim()).filter(Boolean);
+ return str.split(/[,,]+/).map(s => s.trim()).filter(Boolean);📝 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.
| function parseInputWordsStr(str) { | |
| if (!str || !str.trim()) return []; | |
| return str.split(/[,,\s]+/).map(s => s.trim()).filter(Boolean); | |
| } | |
| function parseInputWordsStr(str) { | |
| if (!str || !str.trim()) return []; | |
| return str.split(/[,,]+/).map(s => s.trim()).filter(Boolean); | |
| } |
🤖 Prompt for AI Agents
In `@backend/src/main/resources/static/dev/dict-edit.html` around lines 61 - 64,
The splitter in parseInputWordsStr currently uses `/[,,\s]+/`, which treats
whitespace as a delimiter and breaks multi-word entries like "동아리 활동"; update
parseInputWordsStr to split only on commas (both ASCII and full-width) by
replacing the regex with one that matches only commas (e.g., `/[,,]+/`) and
preserve trimming/filtering logic; also apply the same change to the identical
implementation in index.html (the occurrence referenced in the review) so both
files behave consistently.
| document.getElementById('pageTitle').textContent = '동아리 수정 (' + clubId + ')'; | ||
| (async function() { | ||
| try { | ||
| const res = await fetch(API_BASE + '/api/club/' + clubId, { headers: headers() }); |
There was a problem hiding this comment.
clubId에 encodeURIComponent가 누락되었습니다.
Line 108, 162, 190에서 clubId를 URL에 직접 삽입하고 있습니다. dict-edit.html에서는 encodeURIComponent(dictId)를 사용하고 있으므로 일관성을 위해, 그리고 특수문자가 포함된 ID에 대한 안전을 위해 인코딩을 추가해 주세요.
🔧 수정 제안
- const res = await fetch(API_BASE + '/api/club/' + clubId, { headers: headers() });
+ const res = await fetch(API_BASE + '/api/club/' + encodeURIComponent(clubId), { headers: headers() });Line 162, 190도 동일하게 적용:
- const res = await fetch(API_BASE + '/api/admin/club/' + clubId + '/info', { method: 'PUT', headers: headers(), body: JSON.stringify(body) });
+ const res = await fetch(API_BASE + '/api/admin/club/' + encodeURIComponent(clubId) + '/info', { method: 'PUT', headers: headers(), body: JSON.stringify(body) });- const res = await fetch(API_BASE + '/api/admin/club/' + clubId + '/description', { method: 'PUT', headers: headers(), body: JSON.stringify(body) });
+ const res = await fetch(API_BASE + '/api/admin/club/' + encodeURIComponent(clubId) + '/description', { method: 'PUT', headers: headers(), body: JSON.stringify(body) });📝 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.
| const res = await fetch(API_BASE + '/api/club/' + clubId, { headers: headers() }); | |
| const res = await fetch(API_BASE + '/api/club/' + encodeURIComponent(clubId), { headers: headers() }); |
🤖 Prompt for AI Agents
In `@backend/src/main/resources/static/dev/edit.html` at line 108, The code
constructs fetch URLs by concatenating '/api/club/' + clubId without encoding,
which can break for IDs with special characters; update each occurrence that
builds the club API URL (the fetch calls using API_BASE + '/api/club/' + clubId)
to wrap clubId with encodeURIComponent(clubId) — mirror the existing approach
used for dictId in dict-edit.html — so all fetches (including the ones that
currently call headers()) use encoded club IDs for safe, consistent URLs.
| banner.textContent = '요청 실패: ' + (e.message || ''); | ||
| banner.className = 'banner error'; | ||
| banner.classList.remove('hidden'); | ||
| tbody.innerHTML = '<tr><td colspan="3">' + (e.message || '오류') + '</td></tr>'; |
There was a problem hiding this comment.
innerHTML에 에러 메시지를 직접 삽입하면 XSS 위험이 있습니다.
Line 418과 525에서 e.message를 innerHTML로 삽입하고 있습니다. 현재 fetch 에러 메시지는 일반적으로 안전하지만, 방어적으로 textContent를 사용하는 것이 좋습니다.
🔧 수정 제안
- tbody.innerHTML = '<tr><td colspan="3">' + (e.message || '오류') + '</td></tr>';
+ const errRow = document.createElement('tr');
+ const errCell = document.createElement('td');
+ errCell.colSpan = 3;
+ errCell.textContent = e.message || '오류';
+ errRow.appendChild(errCell);
+ tbody.innerHTML = '';
+ tbody.appendChild(errRow);Line 525도 동일하게 수정 (colspan을 4로).
📝 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.
| banner.textContent = '요청 실패: ' + (e.message || ''); | |
| banner.className = 'banner error'; | |
| banner.classList.remove('hidden'); | |
| tbody.innerHTML = '<tr><td colspan="3">' + (e.message || '오류') + '</td></tr>'; | |
| banner.textContent = '요청 실패: ' + (e.message || ''); | |
| banner.className = 'banner error'; | |
| banner.classList.remove('hidden'); | |
| const errRow = document.createElement('tr'); | |
| const errCell = document.createElement('td'); | |
| errCell.colSpan = 3; | |
| errCell.textContent = e.message || '오류'; | |
| errRow.appendChild(errCell); | |
| tbody.innerHTML = ''; | |
| tbody.appendChild(errRow); |
🤖 Prompt for AI Agents
In `@backend/src/main/resources/static/dev/index.html` around lines 415 - 418,
Replace the direct innerHTML insertion of the error message with safe DOM text
assignment: instead of setting tbody.innerHTML = '<tr><td colspan="3">' +
(e.message || '오류') + '</td></tr>' (and the similar occurrence at the other
spot), create a tr and td element, set td.colSpan = 3 (or 4 for the second
occurrence as suggested), set td.textContent = (e.message || '오류'), append td to
tr, then append tr to tbody; keep using banner.textContent and banner.classList
manipulation as-is.
| btn.disabled = true; | ||
| try { | ||
| const h = { 'Authorization': 'Bearer ' + getToken() }; | ||
| const res = await fetch(API_BASE + '/api/admin/word-dictionary/csv', { | ||
| method: 'POST', | ||
| headers: h, | ||
| body: formData | ||
| }); |
There was a problem hiding this comment.
CSV 업로드 시 토큰이 없을 때 빈 Bearer 헤더가 전송됩니다.
headers() 함수는 토큰이 비어있으면 Authorization 헤더를 생략하지만, Line 601에서는 getToken() 결과를 확인하지 않고 항상 'Bearer ' + getToken()을 설정합니다. 토큰이 없을 때 "Bearer "가 전송되어 서버에서 예기치 않은 인증 오류가 발생할 수 있습니다.
🔧 수정 제안
- const h = { 'Authorization': 'Bearer ' + getToken() };
+ const h = {};
+ const t = getToken();
+ if (t) h['Authorization'] = 'Bearer ' + t;🤖 Prompt for AI Agents
In `@backend/src/main/resources/static/dev/index.html` around lines 599 - 606, The
upload sends an empty "Bearer " header because getToken() is concatenated
unconditionally in the fetch headers; change the fetch header construction (the
object passed to fetch in the POST to API_BASE +
'/api/admin/word-dictionary/csv') to only include Authorization when getToken()
returns a non-empty value (reuse the existing headers() behavior or call
headers() instead of building h = { 'Authorization': 'Bearer ' + getToken() });
ensure getToken() is checked and omit the Authorization property when it is
falsy so no empty Bearer header is sent.
v 1.1.5
Summary by CodeRabbit
릴리스 노트
새로운 기능
보안 개선