[feature] 외부 지원서 링크를 추가할 수 있게한다#881
Conversation
…feature/#846-add-external-url-to-application-MOA-345
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Rate limit exceeded@lepitaaar has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 3 minutes and 15 seconds before requesting another review. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. 📒 Files selected for processing (1)
Warning
|
| Cohort / File(s) | Summary |
|---|---|
타입 및 상수 정의 frontend/src/types/application.ts |
ApplicationFormMode 상수 및 타입 추가. ApplicationFormData에 questions?, formMode?, externalApplicationUrl? 필드 추가. |
초기값 및 목 데이터 frontend/src/constants/INITIAL_FORM_DATA.ts, frontend/src/mocks/data/mockData.ts |
ApplicationFormMode 임포트 추가. INITIAL_FORM_DATA와 mockData에 formMode: ApplicationFormMode.INTERNAL 및 externalApplicationUrl: '' 필드 추가, 일부 표현식 간소화. |
API 모듈 frontend/src/apis/application/getApplication.ts, frontend/src/apis/application/updateApplication.ts |
getApplication에 ApplicationFormData 명시적 import 및 반환형 Promise<ApplicationFormData>로 서명 정리(포맷 변경). updateApplication의 HTTP 메서드를 PUT → PATCH로 변경. |
Admin - ApplicationEditTab 스타일 frontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.styles.ts |
styled-components에서 css named import 추가. FormTitle 마진 조정 및 HeaderContainer, ChangeButtonWrapper, ApplicationFormChangeButton($active prop), ExternalApplicationFormContainer, ExternalApplicationFormTitle, ExternalApplicationFormLinkInput, ExternalApplicationFormHint 등 새 스타일 컴포넌트 추가/내보냄. |
Admin - ApplicationEditTab 컴포넌트 frontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.tsx |
ApplicationFormMode 도입, 내부/외부 모드 전환 지원, applicationFormMode·externalApplicationUrl 상태 추가. 초기 데이터 로드·질문 정규화·nextId 계산 보강, 모드별 렌더링(Internal/External 컴포넌트), 제출 시 모드에 따른 페이로드 구성 및 URL 검증 로직 추가. |
페이지 컴포넌트 및 버튼 frontend/src/pages/ApplicationFormPage/ApplicationFormPage.tsx, frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx |
ApplicationFormPage: 로딩 가드 강화(formData, clubDetail, formData.questions 필요). ClubApplyButton: ApplicationFormMode.EXTERNAL 검사 후 externalApplicationUrl이 있으면 새 탭으로 열고 내부 네비게이션을 건너뛰도록 처리; openByOption 헬퍼 추가 및 모달 라우팅 조정. |
Sequence Diagram(s)
sequenceDiagram
participant User
participant ClubApplyButton
participant API as getApplication API
participant AppForm as Application Form
User->>ClubApplyButton: 지원하기 클릭
ClubApplyButton->>API: getApplication(clubId, applicationFormId)
API-->>ClubApplyButton: applicationData (formMode, externalApplicationUrl)
alt formMode == EXTERNAL and externalApplicationUrl present
ClubApplyButton->>User: 새 탭에서 externalApplicationUrl 오픈
else
ClubApplyButton->>AppForm: 내부 지원서로 라우팅/표시
AppForm-->>User: 질문 기반 폼 렌더링
end
sequenceDiagram
participant Admin
participant EditTab as ApplicationEditTab
participant ModeToggle
participant InternalComp
participant ExternalComp
Admin->>EditTab: 지원서 편집 진입
EditTab->>ModeToggle: 현재 formMode 표시/초기화
Admin->>ModeToggle: 모드 변경
alt INTERNAL
ModeToggle->>InternalComp: 질문 및 옵션 편집 UI 표시
Admin->>InternalComp: 질문 추가/수정/삭제
else EXTERNAL
ModeToggle->>ExternalComp: 외부 URL 입력 UI 표시
Admin->>ExternalComp: URL 입력
end
Admin->>EditTab: 저장 클릭
EditTab->>EditTab: 모드에 따라 payload 구성 및 create/update 호출 (PATCH 사용)
Estimated code review effort
🎯 3 (Moderate) | ⏱️ ~25 minutes
- 주의 필요 항목:
- ApplicationEditTab.tsx: 모드 전환·상태 초기화·질문 정규화 로직의 경계 조건(questions undefined) 검증
- ClubApplyButton.tsx: 외부 URL 열기 흐름이 기존 모달/네비게이션과 충돌하지 않는지 확인
- updateApplication의 PUT → PATCH 변경이 백엔드와 호환되는지 확인
- 타입 변경으로 인한 코드베이스 내 null/undefined 처리 일관성 검사
- 새 스타일 컴포넌트에 대한 타입/prop 인터페이스 검토
Possibly related issues
- [feature] MOA-345 외부 지원서 링크를 추가할 수 있게한다 -FE #846: 외부 지원서 링크 기능 요청 — 이 PR은
ApplicationFormMode와externalApplicationUrl도입 및 UI/로직 변경으로 해당 요구사항을 구현합니다.
Possibly related PRs
- [feature]외부 지원서 링크 추가 #606 —
externalApplicationUrl필드 추가 및 외부 지원서 처리 관련 변경이 유사함. - [feature] 지원서 관리 페이지 디자인을 적용한다 #691 —
ApplicationEditTab.styles.ts변경과 중복되는 스타일/컴포넌트 수정을 포함함. - [fix] 지원서 질문 설명 색상코드 변경 #544 —
getApplication.ts관련 변경(반환형/구현)과 연관성이 있음.
Suggested labels
📬 API, 🔨 Refactor
Suggested reviewers
- seongwon030
- oesnuj
- Zepelown
Pre-merge checks and finishing touches
✅ Passed checks (5 passed)
| Check name | Status | Explanation |
|---|---|---|
| Description Check | ✅ Passed | Check skipped - CodeRabbit’s high-level summary is enabled. |
| Title check | ✅ Passed | 제목은 외부 지원서 링크 추가 기능을 명확하게 설명하며, 변경사항의 주요 목적을 정확하게 전달합니다. |
| Linked Issues check | ✅ Passed | PR의 코드 변경사항이 연결된 이슈 MOA-345의 외부 지원서 링크 추가 기능 요구사항을 충족합니다. |
| Out of Scope Changes check | ✅ Passed | 모든 변경사항이 외부 지원서 기능 구현과 관련되어 있으며, 범위를 벗어난 변경사항이 없습니다. |
| Docstring Coverage | ✅ Passed | No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. |
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: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx (1)
71-76: 지원서 옵션이 0개일 때 사용자 피드백이 없어서 다소 모호할 수 있습니다.
getApplicationOptions가 빈 배열을 반환하면 곧바로return;해서, 사용자는 “버튼을 눌렀는데 아무 반응이 없다”고 느낄 수 있습니다.예를 들어 아래 중 하나로 보완을 고려해 볼 수 있을 것 같습니다.
- alert/토스트로 “현재 신청 가능한 지원서가 없습니다.” 안내
- 이미 옵션이 0개인 상태라면 버튼 자체를 disabled 처리
비즈니스 요구사항에 맞는 쪽으로 가볍게 한 번만 논의해 보면 좋겠습니다.
🧹 Nitpick comments (6)
frontend/src/pages/ApplicationFormPage/ApplicationFormPage.tsx (2)
65-72:questions없을 때를 오류로 보는 현재 처리 기획 의도만 재확인해 주세요.
!formData || !clubDetail || !formData.questions이면 곧바로 에러 메시지를 보여주는데, EXTERNAL 모드 폼(질문 없이 외부 링크만 있는 케이스)은 이 페이지로 진입하지 않는 전제라면 괜찮습니다. 다만 사용자가 URL을 직접 치고 들어오는 등 예외 경로에서는 무조건 오류로 보이니,
- EXTERNAL 폼은 애초에 이 페이지로 안 온다
- 혹은 EXTERNAL일 때는 다른 UX를 보여준다
중 어떤 게 기획 의도인지 한 번만 확인해 보시면 좋겠습니다.
87-92:formData.questions!non-null assertion은 가드 덕분에 불필요합니다.위에서
if (!formData || !clubDetail || !formData.questions) { return ... }로 이미 좁혀줘서 이후 영역에서는formData.questions가 정의된 상태입니다.
const firstInvalidIndex = formData.questions!.findIndex(...)const invalidIds = validateAnswers(formData.questions!, getAnswersById);두 군데 모두
!없이 사용해도 타입/런타임 상 문제가 없고, 아래formData.questions.map(...)와 스타일도 맞출 수 있어 보입니다.Also applies to: 100-105
frontend/src/mocks/data/mockData.ts (1)
1-5: 새 필드 추가로 타입 정합성이 좋아졌습니다. EXTERNAL 케이스 mock도 고려해 볼 만합니다.
mockData에formMode: ApplicationFormMode.INTERNAL,externalApplicationUrl: ''를 넣어 실제ApplicationFormData와 맞춰 둔 점 좋습니다.외부 지원서 플로우까지 스토리북/로컬 개발에서 손쉽게 확인하려면,
formMode: ApplicationFormMode.EXTERNAL- 유효한
externalApplicationUrl을 가진 별도 mock 케이스를 하나 더 추가해 두는 것도 나중에 디버깅할 때 도움이 될 것 같습니다.
Also applies to: 128-133
frontend/src/constants/INITIAL_FORM_DATA.ts (1)
7-7: 학기 구분 기준(currentMonth <= 6)이 의도한 비즈니스 룰인지 한 번만 확인해 주세요.
getMonth()가 011이므로 현재 로직은 “17월: FIRST / 812월: SECOND”로 동작합니다.6월, 3~8월 등)과 맞는지 짧게만 재확인해 주시면 좋겠습니다.
기존이< 6이었다면 7월을 어느 학기로 볼지 바뀌는 포인트라, 실제 서비스에서 원하는 학기 기준(예: 1frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx (1)
9-12: EXTERNAL 분기에서 모달 닫힘(setIsOpen(false))이 누락되어 보입니다.
formMode === ApplicationFormMode.EXTERNAL일 때window.open(...)후return하면서, 아래의setIsOpen(false)는 실행되지 않습니다.
- 단일 옵션(
list.length === 1) 경로에서는 모달이 안 열려 있어서 상관없지만,- 옵션 선택 모달에서 EXTERNAL 폼을 선택한 경우에는 새 탭이 뜨더라도 모달은 그대로 남아 있을 것 같습니다.
goWithForm안에서
- 함수 초반에
setIsOpen(false)를 호출하거나,finally블록에서 조건에 따라 닫아 주는 쪽으로 한 번 정리해 보시면 UX가 더 자연스러울 것 같습니다.Also applies to: 35-47
frontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.tsx (1)
327-351: 지원 플랫폼 검증을 고려하세요.힌트 텍스트에서 구글폼과 네이버폼만 지원한다고 명시하고 있으나, 실제 URL 검증 로직이 없습니다. 사용자 경험 개선을 위해 특정 플랫폼 URL 패턴을 검증하는 것을 고려해보세요.
예시:
const isSupportedPlatform = (url: string): boolean => { try { const urlObj = new URL(url); const hostname = urlObj.hostname.toLowerCase(); return ( hostname.includes('google.com') || hostname.includes('docs.google.com') || hostname.includes('naver.com') || hostname.includes('form.naver.com') ); } catch { return false; } };
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Jira integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (9)
frontend/src/apis/application/getApplication.ts(1 hunks)frontend/src/apis/application/updateApplication.ts(1 hunks)frontend/src/constants/INITIAL_FORM_DATA.ts(2 hunks)frontend/src/mocks/data/mockData.ts(2 hunks)frontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.styles.ts(3 hunks)frontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.tsx(6 hunks)frontend/src/pages/ApplicationFormPage/ApplicationFormPage.tsx(4 hunks)frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx(3 hunks)frontend/src/types/application.ts(2 hunks)
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-09-30T05:26:41.788Z
Learnt from: alsdddk
Repo: Moadong/moadong PR: 765
File: backend/src/main/java/moadong/club/service/ClubApplyService.java:431-435
Timestamp: 2025-09-30T05:26:41.788Z
Learning: In the Moadong codebase's club application feature (backend/src/main/java/moadong/club/), multiple ClubApplicationForm entities can have ACTIVE status for the same clubId, semesterYear, and semesterTerm simultaneously. There is no uniqueness constraint requiring only one ACTIVE form per semester.
Applied to files:
frontend/src/mocks/data/mockData.ts
📚 Learning: 2025-03-19T05:18:07.818Z
Learnt from: seongwon030
Repo: Moadong/moadong PR: 195
File: frontend/src/pages/AdminPage/AdminPage.tsx:7-7
Timestamp: 2025-03-19T05:18:07.818Z
Learning: AdminPage.tsx에서 현재 하드코딩된 클럽 ID('67d2e3b9b15c136c6acbf20b')는 로그인 기능 구현 후 동적으로 가져오는 방식으로 수정될 예정입니다.
Applied to files:
frontend/src/pages/ApplicationFormPage/ApplicationFormPage.tsxfrontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx
📚 Learning: 2025-09-21T02:23:27.796Z
Learnt from: seongwon030
Repo: Moadong/moadong PR: 744
File: frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx:47-48
Timestamp: 2025-09-21T02:23:27.796Z
Learning: ClubApplyButton 컴포넌트에서 ShareButton은 항상 렌더링되어야 하므로 정적 import를 사용하는 것이 적절함. 동적 import는 불필요함.
Applied to files:
frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx
🧬 Code graph analysis (6)
frontend/src/apis/application/getApplication.ts (1)
frontend/src/types/application.ts (1)
ApplicationFormData(49-58)
frontend/src/mocks/data/mockData.ts (1)
frontend/src/types/application.ts (3)
ApplicationFormMode(83-86)ApplicationFormMode(88-89)ApplicationForm(65-68)
frontend/src/pages/ApplicationFormPage/ApplicationFormPage.tsx (1)
frontend/src/hooks/useValidateAnswers.ts (1)
validateAnswers(3-14)
frontend/src/constants/INITIAL_FORM_DATA.ts (1)
frontend/src/types/application.ts (2)
ApplicationFormMode(83-86)ApplicationFormMode(88-89)
frontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.tsx (7)
frontend/src/context/AdminClubContext.tsx (1)
useAdminClubContext(40-47)frontend/src/hooks/queries/application/useGetApplication.ts (1)
useGetApplication(4-14)frontend/src/types/application.ts (3)
ApplicationFormData(49-58)ApplicationFormMode(83-86)ApplicationFormMode(88-89)frontend/src/apis/application/createApplication.ts (1)
createApplication(5-25)frontend/src/apis/application/updateApplication.ts (1)
updateApplication(5-31)frontend/src/constants/APPLICATION_FORM.ts (1)
APPLICATION_FORM(1-26)frontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.styles.ts (1)
QuestionDivider(44-49)
frontend/src/types/application.ts (1)
frontend/src/mocks/data/mockData.ts (1)
Question(21-28)
🔇 Additional comments (13)
frontend/src/apis/application/updateApplication.ts (1)
13-18: HTTP 메서드 PATCH 전환 시 백엔드 스펙만 한 번 확인 부탁드립니다.기존 PUT → PATCH 변경 자체는 부분 업데이트 의미에 더 잘 맞지만, 서버 쪽 엔드포인트가 실제로 PATCH를 허용하도록 변경/정의돼 있는지 확인하지 않으면 405/404가 날 수 있습니다. 백엔드 스펙과 맞는지만 한 번 더 체크해 주세요.
frontend/src/pages/ApplicationFormPage/ApplicationFormPage.tsx (1)
21-24:useParams제네릭 분리로 타입이 더 명시적이라 좋습니다.타입 정보가 분리돼서 읽기 쉬워졌고, 런타임 동작 변화도 없어서 그대로 가면 될 것 같습니다.
frontend/src/apis/application/getApplication.ts (1)
2-7:ApplicationFormData명시로 반환 타입이 분명해져서 좋습니다.
getApplication이 실제 서버 스키마와 동기화된 형태로Promise<ApplicationFormData>를 반환하도록 명시돼서 이후 사용처에서 타입 추론과 리팩터링 모두 수월해질 것 같습니다.frontend/src/constants/INITIAL_FORM_DATA.ts (1)
1-1: 초기 폼을 INTERNAL 모드로 두는 설정이 일관되어 좋습니다.
INITIAL_FORM_DATA에서 기본값을formMode: ApplicationFormMode.INTERNAL,externalApplicationUrl: ''로 두어,
새 폼 생성 시 기본이 내부 지원서가 되는 점이 현재 PR 방향(외부 링크는 선택적)과 잘 맞아 보입니다.Also applies to: 41-43
frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx (1)
48-53: 지원서 조회 실패 시 에러 메시지 정리 좋습니다.
getApplication실패 시에는 이제 단순히 재시도 안내 alert만 띄워서, 실패 상황에서 예외적인 리다이렉트 없이 일관된 UX를 주는 방향으로 정리된 것 같아 보입니다.frontend/src/types/application.ts (2)
49-58: 타입 정의가 올바르게 작성되었습니다.
questions필드를 선택적으로 변경하고formMode,externalApplicationUrl필드를 추가한 것은 내부/외부 지원서 모드를 지원하기에 적절합니다.
83-89: LGTM!
ApplicationFormMode이 const assertion 패턴을 올바르게 사용하여 타입 안전성을 보장합니다.frontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.tsx (4)
1-40: 초기 설정이 올바릅니다.
ApplicationFormMode타입과 필요한 state 추가가 적절하게 구성되었습니다.
60-89: LGTM!Mutation 정의와 쿼리 무효화 처리가 올바르게 구현되었습니다.
191-325: LGTM!
InternalApplicationComponent의 로직이 올바르게 구현되었으며,questions필드에 대한 방어적 가드 처리가 적절합니다.
139-189: LGTM!모드 전환 UI와 조건부 렌더링이 명확하게 구현되었습니다.
frontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.styles.ts (2)
1-23: LGTM!
css헬퍼 import와FormTitle마진 조정이 적절합니다.
122-163: LGTM!외부 지원서 폼을 위한 스타일 컴포넌트들이 올바르게 작성되었습니다.
frontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.styles.ts
Show resolved
Hide resolved
| useEffect(() => { | ||
| if (existingFormData) { | ||
| setFormData(existingFormData); | ||
|
|
||
| const questions = existingFormData.questions; | ||
| if (questions.length > 0) { | ||
| const maxId = Math.max(...questions.map((q: Question) => q.id)); | ||
| setNextId(maxId + 1); | ||
| } | ||
| } | ||
| if (!existingFormData) return; | ||
|
|
||
| const { | ||
| formMode = ApplicationFormMode.INTERNAL, | ||
| externalApplicationUrl = '', | ||
| questions = [], | ||
| } = existingFormData; | ||
|
|
||
| const currentQuestions = | ||
| questions.length > 0 ? questions : INITIAL_FORM_DATA.questions!; | ||
|
|
||
| setApplicationFormMode(formMode); | ||
| setExternalApplicationUrl(externalApplicationUrl); | ||
| setNextId(Math.max(...currentQuestions.map((q) => q.id)) + 1); | ||
| setFormData({ ...existingFormData, questions: currentQuestions }); | ||
| }, [existingFormData]); |
There was a problem hiding this comment.
빈 배열 처리 시 잠재적 버그가 있습니다.
Line 56에서 Math.max(...currentQuestions.map((q) => q.id))를 호출할 때, currentQuestions가 빈 배열이면 Math.max()는 -Infinity를 반환하여 nextId가 NaN이 됩니다.
다음과 같이 수정하여 빈 배열을 안전하게 처리하세요:
- setNextId(Math.max(...currentQuestions.map((q) => q.id)) + 1);
+ setNextId(
+ currentQuestions.length > 0
+ ? Math.max(...currentQuestions.map((q) => q.id)) + 1
+ : 1
+ );🤖 Prompt for AI Agents
In frontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.tsx
around lines 42 to 58, the call to
setNextId(Math.max(...currentQuestions.map((q) => q.id)) + 1) will produce NaN
when currentQuestions is an empty array because Math.max() returns -Infinity;
change this to compute a safe max id (e.g., if currentQuestions.length === 0 use
0 else Math.max(...ids)) and then call setNextId(maxId + 1) so nextId is valid
for empty question lists.
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check the INITIAL_FORM_DATA constant to verify it contains questions
cat frontend/src/constants/INITIAL_FORM_DATA.ts
Length of output: 1058
@lepitaaar 확인했습니다. 맞습니다!
INITIAL_FORM_DATA.questions가 항상 3개의 질문(이름 필드 포함)을 포함하고 있어서, Line 51의 로직에 따라 currentQuestions는 절대 빈 배열이 될 수 없네요. 제 우려는 기우였습니다. 감사합니다! 👍
frontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.tsx
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (1)
frontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.tsx (1)
35-40: 빈 질문 배열에서nextId계산 시 여전히 잘못된 값이 나올 수 있습니다.
currentQuestions가 빈 배열인 경우Math.max(...currentQuestions.map((q) => q.id))는-Infinity를 반환해nextId가NaN/-Infinity가 될 수 있습니다. 이후 새 질문 추가 시 잘못된 id가 사용될 수 있어 이전 리뷰 코멘트가 아직 반영되지 않았습니다.다음처럼
currentQuestions.length를 한 번 더 체크해서 안전하게 처리해 주세요.- setNextId(Math.max(...currentQuestions.map((q) => q.id)) + 1); + setNextId( + currentQuestions.length > 0 + ? Math.max(...currentQuestions.map((q) => q.id)) + 1 + : 1, + );Also applies to: 42-57
🧹 Nitpick comments (3)
frontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.tsx (3)
107-145: payload 구성 시active기본값과 외부 URL 검증을 조금 더 안전하게 다루면 좋겠습니다.
active필드 기본값
active: formData.active ?? ''로 직렬화 시 빈 문자열이 백엔드로 넘어갈 수 있습니다.- 타입 상 필수 필드라면 그냥 그대로 넘기고, 값이 빠지는 경우는 빌드 타임/런타임에서 잡는 편이 낫습니다.
예시 수정:
active: formData.active ?? '',
active: formData.active,(만약 도메인 상 기본값이 필요하다면, API 계약에 맞는 실제 상태 값으로 매핑해 주세요.)
외부 URL 검증 강화 (선택 사항이지만 권장)
현재는startsWith로 prefix만 검사하고 있어, 앞뒤 공백이나https://forms.gleevil.com같은 edge-case가 통과할 여지가 있습니다. 최소한trim()을 사용하고, 검증과 payload에 동일한 값을 쓰는 쪽이 안전합니다.예시:
const externalApplicationUrlAllowed = [
const externalApplicationUrlAllowed = [ 'https://forms.gle', 'https://docs.google.com/forms', 'https://form.naver.com', 'https://naver.me', ];
const isValidUrl = externalApplicationUrlAllowed.some((url) =>externalApplicationUrl.startsWith(url),);
const trimmedUrl = externalApplicationUrl.trim();const isValidUrl = externalApplicationUrlAllowed.some((url) =>trimmedUrl.startsWith(url),);@@
if (!isValidUrl) {
if (!trimmedUrl || !isValidUrl) { alert( '외부 지원서 링크는 Google Forms 또는 Naver Form 링크여야 합니다.', ); return; }
payload.externalApplicationUrl = externalApplicationUrl;
payload.externalApplicationUrl = trimmedUrl;
91-96: 헤더/모드 토글 및 제목 입력/저장 버튼 구조가 깔끔하며, 저장 중 비활성화까지 고려하면 더 좋겠습니다.
- 모드 토글 버튼에서
ApplicationFormModeenum을 그대로 사용해 상태를 관리하고,- 제목 입력은
formData.title과handleFormTitleChange로 단방향 데이터 플로우가 잘 맞춰져 있습니다.- INTERNAL / EXTERNAL 모드에 따라 서로 다른 컴포넌트를 분리 렌더링하는 구조도 이해하기 쉽습니다.
추가로, 이미
isCreating,isUpdating을 받고 있으므로 저장 중 중복 클릭을 막기 위해 버튼을 비활성화하면 UX 측면에서 개선될 것 같습니다.- <Button width={'150px'} animated onClick={handleSubmit}> + <Button + width='150px' + animated + onClick={handleSubmit} + disabled={isCreating || isUpdating} + > 저장하기 </Button>Also applies to: 157-205
223-304: 질문 수정 로직은 동작상 문제는 없어 보이지만, non-null assertion 제거 방향으로 리팩터링하면 더 안전합니다.현재
removeQuestion,updateQuestionField,handleTypeChange,handleRequiredChange는 모두:
- 바깥에서
if (!formData.questions) return;으로 가드하고,- 내부에서는
prev.questions!에 non-null assertion을 사용합니다.렌더 시점의
formData와setFormData안의prev가 항상 동일하다는 전제에 의존하게 되어, 장기적으로 유지보수 시 오류 여지가 있습니다.prev안에서 직접 가드하면 assertion 없이도 동일한 효과를 얻을 수 있습니다.예시 (removeQuestion 기준):
- const removeQuestion = (id: number) => { - if (!formData.questions) return; - setFormData((prev) => ({ - ...prev, - questions: prev.questions!.filter((q) => q.id !== id), - })); - }; + const removeQuestion = (id: number) => { + setFormData((prev) => { + if (!prev.questions) return prev; + return { + ...prev, + questions: prev.questions.filter((q) => q.id !== id), + }; + }); + };동일한 패턴을
updateQuestionField,handleTypeChange,handleRequiredChange에도 적용하면,questions가 선택 필드라는 타입 정의와 구현이 더 자연스럽게 맞아떨어질 것 같습니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Jira integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
frontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.tsx(6 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
frontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.tsx (7)
frontend/src/context/AdminClubContext.tsx (1)
useAdminClubContext(40-47)frontend/src/hooks/queries/application/useGetApplication.ts (1)
useGetApplication(4-14)frontend/src/types/application.ts (3)
ApplicationFormData(49-58)ApplicationFormMode(83-86)ApplicationFormMode(88-89)frontend/src/apis/application/createApplication.ts (1)
createApplication(5-25)frontend/src/apis/application/updateApplication.ts (1)
updateApplication(5-31)frontend/src/constants/APPLICATION_FORM.ts (1)
APPLICATION_FORM(1-26)frontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.styles.ts (1)
QuestionDivider(44-49)
🔇 Additional comments (3)
frontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.tsx (3)
25-30: 데이터 조회 및 저장/수정 플로우 전체적으로 일관성 있게 잘 구성되어 있습니다.
- 조회 훅에서
enabled조건으로 생성 모드에서의 불필요한 요청을 막고 있고,- 생성/수정 mutation 모두
allApplicationForms및 개별applicationForm쿼리 키를 적절히 무효화하고 있어 캐시 일관성도 좋아 보입니다.- 에러/로딩 상태 분기 렌더링도 명확합니다.
현재 구조 그대로 유지해도 무방해 보입니다.
Also applies to: 60-69, 71-89, 100-105
306-343: 내부 지원서 UI 렌더링 및QuestionBuilder연동이 자연스럽습니다.
CustomTextArea에 상수(APPLICATION_FORM.APPLICATION_DESCRIPTION)를 사용해 placeholder/최대 길이를 관리하는 방식이 명확하고,formData.questions?.map으로 optional 필드를 안전하게 순회하고 있습니다.QuestionBuilder에key={question.id}와id={index + 1}를 분리해 전달해 React key와 화면상 순번을 구분한 것도 좋습니다.- 첫 번째 질문만
readOnly로 고정하는 요구사항도index === 0조건으로 직관적으로 표현되어 있습니다.현재 구조는 그대로 유지해도 충분히 이해하기 쉽고 안정적으로 보입니다.
345-367: 외부 지원서 전용 컴포넌트 분리와 상태 연동이 적절합니다.
ExternalApplicationComponent로 별도 분리해externalApplicationUrl과 setter만 props로 받는 구조가 단순하고 재사용/테스트에도 유리합니다.- placeholder와 하단 힌트 텍스트로 현재 지원되는 제공자(구글폼/네이버폼)를 명확히 안내하고 있어, 상단
handleSubmit의 도메인 검증 로직과도 잘 맞습니다.현재 구현은 그대로 사용해도 무방해 보입니다.
#️⃣연관된 이슈
#846
📝작업 내용
<변경전>
<변경후>
또한 지원하기를 눌렀을때 외부지원서일 경우 새탭으로 이동하게 변경되었습니다.
기존 컴포넌트가 늘어남에 따라 외부지원서 컴포넌트와 외부지원서 컴포넌트로 분리를 진행헀습니다.
중점적으로 리뷰받고 싶은 부분(선택)
논의하고 싶은 부분(선택)
🫡 참고사항
Summary by CodeRabbit
릴리스 노트
새로운 기능
개선 사항
✏️ Tip: You can customize this high-level summary in your review settings.