Skip to content

Comments

[feature] 외부 지원서 링크를 추가할 수 있게한다#881

Merged
lepitaaar merged 14 commits intodevelop-fefrom
feature/#846-add-external-url-to-application-MOA-345
Nov 27, 2025
Merged

[feature] 외부 지원서 링크를 추가할 수 있게한다#881
lepitaaar merged 14 commits intodevelop-fefrom
feature/#846-add-external-url-to-application-MOA-345

Conversation

@lepitaaar
Copy link
Contributor

@lepitaaar lepitaaar commented Nov 22, 2025

#️⃣연관된 이슈

#846

📝작업 내용

스크린샷 2025-11-23 01 07 01 외부지원서 필드 추가 및 기존 저장하기 버튼 스타일 변경 image

<변경전>

스크린샷 2025-11-23 01 08 10

<변경후>

또한 지원하기를 눌렀을때 외부지원서일 경우 새탭으로 이동하게 변경되었습니다.

기존 컴포넌트가 늘어남에 따라 외부지원서 컴포넌트와 외부지원서 컴포넌트로 분리를 진행헀습니다.

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

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

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

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

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

🫡 참고사항

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 내부 및 외부 지원 양식 모드 추가 — 내부 양식과 외부 URL 기반 양식 전환 가능
    • 지원 페이지에서 외부 양식 링크를 새 탭으로 열기 지원
    • 외부 양식용 URL 입력 UI 및 유효성 검증 추가
  • 개선 사항

    • 지원 양식 데이터 구조 확장(모드·외부 URL 필드) 및 로딩/검증 강화
    • 신청 업데이트가 부분 변경 방식으로 처리되도록 개선

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

@lepitaaar lepitaaar self-assigned this Nov 22, 2025
@lepitaaar lepitaaar added ✨ Feature 기능 개발 💻 FE Frontend labels Nov 22, 2025
@vercel
Copy link

vercel bot commented Nov 22, 2025

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

Project Deployment Preview Comments Updated (UTC)
moadong Ready Ready Preview Comment Nov 22, 2025 4:38pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 22, 2025

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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.

📥 Commits

Reviewing files that changed from the base of the PR and between 6562be5 and f0c8444.

📒 Files selected for processing (1)
  • frontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.styles.ts (3 hunks)

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

외부 지원서 링크 지원을 추가합니다. ApplicationFormMode enum과 ApplicationFormData의 optional 필드(formMode, externalApplicationUrl, questions)를 도입하고, 에디터/폼/UI, 초기값, 목데이터, API 호출(getApplication 반환형, updateApplication 메서드) 및 ClubApplyButton 동작을 외부/내부 모드에 맞게 변경합니다.

Changes

Cohort / File(s) Summary
타입 및 상수 정의
frontend/src/types/application.ts
ApplicationFormMode 상수 및 타입 추가. ApplicationFormDataquestions?, formMode?, externalApplicationUrl? 필드 추가.
초기값 및 목 데이터
frontend/src/constants/INITIAL_FORM_DATA.ts, frontend/src/mocks/data/mockData.ts
ApplicationFormMode 임포트 추가. INITIAL_FORM_DATAmockDataformMode: ApplicationFormMode.INTERNALexternalApplicationUrl: '' 필드 추가, 일부 표현식 간소화.
API 모듈
frontend/src/apis/application/getApplication.ts, frontend/src/apis/application/updateApplication.ts
getApplicationApplicationFormData 명시적 import 및 반환형 Promise<ApplicationFormData>로 서명 정리(포맷 변경). updateApplication의 HTTP 메서드를 PUTPATCH로 변경.
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
Loading
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 사용)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • 주의 필요 항목:
    • ApplicationEditTab.tsx: 모드 전환·상태 초기화·질문 정규화 로직의 경계 조건(questions undefined) 검증
    • ClubApplyButton.tsx: 외부 URL 열기 흐름이 기존 모달/네비게이션과 충돌하지 않는지 확인
    • updateApplication의 PUT → PATCH 변경이 백엔드와 호환되는지 확인
    • 타입 변경으로 인한 코드베이스 내 null/undefined 처리 일관성 검사
    • 새 스타일 컴포넌트에 대한 타입/prop 인터페이스 검토

Possibly related issues

Possibly related PRs

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.

❤️ Share

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

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: 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도 고려해 볼 만합니다.

mockDataformMode: 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이었다면 7월을 어느 학기로 볼지 바뀌는 포인트라, 실제 서비스에서 원하는 학기 기준(예: 1
6월, 3~8월 등)과 맞는지 짧게만 재확인해 주시면 좋겠습니다.

frontend/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.

📥 Commits

Reviewing files that changed from the base of the PR and between f7c7411 and 09eacfb.

📒 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.tsx
  • frontend/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!

외부 지원서 폼을 위한 스타일 컴포넌트들이 올바르게 작성되었습니다.

Comment on lines 42 to 58
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]);
Copy link
Contributor

@coderabbitai coderabbitai bot Nov 22, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

빈 배열 처리 시 잠재적 버그가 있습니다.

Line 56에서 Math.max(...currentQuestions.map((q) => q.id))를 호출할 때, currentQuestions가 빈 배열이면 Math.max()-Infinity를 반환하여 nextIdNaN이 됩니다.

다음과 같이 수정하여 빈 배열을 안전하게 처리하세요:

-    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.

Copy link
Contributor Author

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

🏁 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는 절대 빈 배열이 될 수 없네요. 제 우려는 기우였습니다. 감사합니다! 👍

@lepitaaar lepitaaar changed the base branch from main to develop-fe November 22, 2025 16:17
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: 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를 반환해 nextIdNaN/-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 검증을 조금 더 안전하게 다루면 좋겠습니다.

  1. active 필드 기본값

    • active: formData.active ?? ''로 직렬화 시 빈 문자열이 백엔드로 넘어갈 수 있습니다.
    • 타입 상 필수 필드라면 그냥 그대로 넘기고, 값이 빠지는 경우는 빌드 타임/런타임에서 잡는 편이 낫습니다.

    예시 수정:

  •  active: formData.active ?? '',
    
  •  active: formData.active,
    
    
    (만약 도메인 상 기본값이 필요하다면, API 계약에 맞는 실제 상태 값으로 매핑해 주세요.)
    
    
  1. 외부 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: 헤더/모드 토글 및 제목 입력/저장 버튼 구조가 깔끔하며, 저장 중 비활성화까지 고려하면 더 좋겠습니다.

  • 모드 토글 버튼에서 ApplicationFormMode enum을 그대로 사용해 상태를 관리하고,
  • 제목 입력은 formData.titlehandleFormTitleChange로 단방향 데이터 플로우가 잘 맞춰져 있습니다.
  • 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을 사용합니다.

렌더 시점의 formDatasetFormData 안의 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.

📥 Commits

Reviewing files that changed from the base of the PR and between 09eacfb and 6562be5.

📒 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 필드를 안전하게 순회하고 있습니다.
  • QuestionBuilderkey={question.id}id={index + 1}를 분리해 전달해 React key와 화면상 순번을 구분한 것도 좋습니다.
  • 첫 번째 질문만 readOnly로 고정하는 요구사항도 index === 0 조건으로 직관적으로 표현되어 있습니다.

현재 구조는 그대로 유지해도 충분히 이해하기 쉽고 안정적으로 보입니다.


345-367: 외부 지원서 전용 컴포넌트 분리와 상태 연동이 적절합니다.

  • ExternalApplicationComponent로 별도 분리해 externalApplicationUrl과 setter만 props로 받는 구조가 단순하고 재사용/테스트에도 유리합니다.
  • placeholder와 하단 힌트 텍스트로 현재 지원되는 제공자(구글폼/네이버폼)를 명확히 안내하고 있어, 상단 handleSubmit의 도메인 검증 로직과도 잘 맞습니다.

현재 구현은 그대로 사용해도 무방해 보입니다.

Copy link
Member

@seongwon030 seongwon030 left a comment

Choose a reason for hiding this comment

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

리뷰 완료했습니다 고생하셨어요~

Copy link
Member

@seongwon030 seongwon030 left a comment

Choose a reason for hiding this comment

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

고생하셨슴다

@lepitaaar lepitaaar merged commit 43754a6 into develop-fe Nov 27, 2025
3 checks passed
@coderabbitai coderabbitai bot mentioned this pull request Dec 7, 2025
@lepitaaar lepitaaar deleted the feature/#846-add-external-url-to-application-MOA-345 branch January 4, 2026 07:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

💻 FE Frontend ✨ Feature 기능 개발

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants