-
Notifications
You must be signed in to change notification settings - Fork 3
[release] FE v1.1.18 #1070
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[release] FE v1.1.18 #1070
Conversation
- Award 타입에 year(number), semester(SemesterTermType) 분리
- SemesterTerm 상수 추가 ('FIRST' | 'SECOND')
- AwardEditor 새 타입에 맞게 로직 수정
- ClubIntroContent 로컬 타입 제거, @/types/club에서 import로 통일
- Award 타입에 year(number), semester(SemesterTermType) 분리
- SemesterTerm 상수 추가 ('FIRST' | 'SECOND')
- AwardEditor 새 타입에 맞게 로직 수정
- ClubIntroContent 로컬 타입 제거, @/types/club 통일
- openFaqIndices → openFaqIndexes 리네임
- getAwardKey 함수에 index 파라미터 추가 - 같은 년도-학기에 여러 수상이 있어도 고유한 key 생성 - AwardEditor와 ClubIntroContent 모두 수정 - 백엔드 중복 방지 로직 부재에 대응
…pe-MOA-523 [refactor] year, semester 타입 분리에 따른 리팩토링
- formatSemesterLabel: Award의 year와 semester를 포맷팅하여 문자열 반환 - getAwardKey: Award 객체와 인덱스로 고유 키 생성 - ClubIntroContent에서 사용하던 함수를 재사용 가능하도록 분리
- formatSemesterLabel 테스트 8개 (정상 케이스, null/undefined 처리) - getAwardKey 테스트 5개 (고유성 검증, 엣지 케이스) - Given 데이터 공통화로 테스트 가독성 향상 - 모든 테스트 통과 (13/13)
- FAQ 토글 상태를 Array에서 Set으로 변경 (O(n) → O(1) 조회) - handleToggleFaq를 useCallback으로 메모이제이션 - validAwards를 useMemo로 계산하여 불필요한 재계산 방지 - 유효하지 않은 award(year/semester 누락) 렌더링 방지 - formatSemesterLabel, getAwardKey 함수를 utils로 분리 및 import
- trackEvent 호출을 setOpenFaqIndexes updater 함수 외부로 이동 - React 공식 문서의 updater 순수성 요구사항 준수 - StrictMode에서 trackEvent 중복 호출 방지 - openFaqIndexes를 useCallback 의존성 배열에 추가
…rmance-and-award-utils-MOA-535 [refactor] 동아리 소개 컴포넌트 리팩토링 및 유틸 함수 단위 테스트 추가
…tent-MOA-296 [feat] 지원자 상태 변경 SSE 실시간 동기화 구현
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning
|
| 그룹 / 파일 | 변경 요약 |
|---|---|
의존성 및 SSE 기반 인프라 frontend/package.json, frontend/src/apis/clubSSE.ts, frontend/src/types/applicants.ts |
eventsource 의존성 추가; EventSource 기반 인증 SSE 연결 설정 및 applicant-status-changed 이벤트 리스너 구현; ApplicantStatusEvent, ApplicantSSECallbacks 타입 정의 |
AdminClubContext SSE 통합 frontend/src/context/AdminClubContext.tsx |
applicationFormId 상태 및 setter 추가; SSE 생성 및 이벤트 핸들링 구현(handleApplicantStatusChange); 지수 백오프 재시도 로직 및 정리 작업 포함 |
ApplicantsTab SSE 생명주기 frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx, frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantDetailPage/ApplicantDetailPage.tsx |
setApplicationFormId를 context에서 추출하여 SSE 설정; useEffect로 applicationFormId 관리 및 초기 데이터 로드; ApplicantDetailPage 의존성 배열 확장(status, memo 추가) |
어워드 타입 및 유틸리티 리팩토링 frontend/src/types/club.ts, frontend/src/utils/awardHelpers.ts, frontend/src/utils/awardHelpers.test.ts |
SemesterTerm 열거형 및 SemesterTermType 타입 정의; Award 인터페이스에 year 필드 추가 및 semester를 문자열에서 열거형으로 변경; formatSemesterLabel, getAwardKey 유틸리티 함수 추가 및 테스트 작성 |
AwardEditor 열거형 기반 리팩토링 frontend/src/pages/AdminPage/tabs/ClubIntroEditTab/components/AwardEditor/AwardEditor.tsx |
SemesterTerm 상수 사용으로 마이그레이션; 복합 키(year-semester-index) 기반 상태 관리 및 중복 검사 개선; formatSemesterLabel, getAwardKey 유틸리티 활용; 포커싱 로직 재구현 |
ClubIntroContent 타입 통합 및 성능 최적화 frontend/src/pages/ClubDetailPage/components/ClubIntroContent/ClubIntroContent.tsx |
로컬 타입 선언 제거 및 @/types/club에서 Award, FAQ, IdealCandidate 임포트; useMemo로 어워드 필터링, useCallback으로 FAQ 토글 핸들러 최적화; FAQ 열림 상태를 배열에서 Set으로 변경 |
Sequence Diagram
sequenceDiagram
participant User as 사용자 (관리자)
participant ApplicantsTab as ApplicantsTab<br/>(React Component)
participant AdminClubContext as AdminClubContext<br/>(상태 관리)
participant SSE as SSE 연결<br/>(EventSource)
participant Backend as 백엔드<br/>(/api/club/.../sse)
User->>ApplicantsTab: 지원자 탭 진입
ApplicantsTab->>AdminClubContext: setApplicationFormId(formId) 호출
AdminClubContext->>SSE: createApplicantSSE(formId) 실행
SSE->>Backend: GET /api/club/applicant/{formId}/sse<br/>(Bearer 토큰 포함)
Backend-->>SSE: EventSource 연결 수립
loop 지원자 상태 변경 감지
Backend-->>SSE: applicant-status-changed 이벤트<br/>(ApplicantStatusEvent)
SSE->>AdminClubContext: handleApplicantStatusChange(event)<br/>호출 (onStatusChange)
AdminClubContext->>AdminClubContext: applicantsData 업데이트<br/>(status/memo 반영)
AdminClubContext-->>ApplicantsTab: 상태 변경 알림
ApplicantsTab-->>User: UI 업데이트 표시
end
User->>ApplicantsTab: 지원자 탭 나감
ApplicantsTab->>AdminClubContext: setApplicationFormId(null) 호출
AdminClubContext->>SSE: EventSource 정리<br/>(close())
Estimated code review effort
🎯 4 (Complex) | ⏱️ ~50 minutes
Possibly related PRs
- PR
#1062: SSE 기반 지원자 상태 동기화 구현으로 동일한 createApplicantSSE, 타입, AdminClubContext/ApplicantsTab 통합 변경사항 포함 - PR
#1068: 동일한 awardHelpers 유틸리티(formatSemesterLabel, getAwardKey) 추가 및 ClubIntroContent에서 사용하는 변경사항 포함 - PR
#787: AdminClubContext에서 applicationFormId 및 setApplicationFormId 추가하여 Admin 페이지로 전파하는 동일한 변경사항 포함
Suggested labels
📬 API, ✅ Test, 🔨 Refactor
Suggested reviewers
- lepitaaar
- oesnuj
- suhyun113
🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 inconclusive)
| Check name | Status | Explanation | Resolution |
|---|---|---|---|
| Title check | ❓ Inconclusive | PR 제목 '[release] FE v1.1.18'은 릴리스 버전 번호만 표시할 뿐, 실제 변경 사항의 주요 내용을 구체적으로 설명하지 않습니다. | 제목을 '[release] FE v1.1.18: Award 타입 리팩토링 및 SSE 기능 추가' 등으로 변경하여 주요 변경 사항을 명확히 나타내세요. |
✅ Passed checks (2 passed)
| Check name | Status | Explanation |
|---|---|---|
| Description Check | ✅ Passed | Check skipped - CodeRabbit’s high-level summary is enabled. |
| Docstring Coverage | ✅ Passed | No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. |
✏️ Tip: You can configure your own custom pre-merge checks in the settings.
✨ Finishing touches
- 📝 Generate docstrings
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.
Comment @coderabbitai help to get the list of available commands and usage tips.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@frontend/package.json`:
- Line 32: Remove the browser polyfill import and dependency: in
frontend/src/apis/club.ts delete the import statement "import { EventSource }
from 'eventsource'" and switch to the native global EventSource in your
event-handling code; also remove "eventsource" from dependencies in
frontend/package.json (move it to devDependencies if you need it only for
tests/Node), or implement a conditional/Node-only require when running in
test/CI to load the polyfill (keep symbol references: EventSource import in
club.ts and the "eventsource" package entry in package.json).
🧹 Nitpick comments (7)
frontend/src/utils/awardHelpers.ts (1)
3-9: formatSemesterLabel 시그니처를 nullable로 명시하면 더 안전합니다현재 구현은 null/undefined도 처리하지만 타입은
Award로 고정되어 있어 테스트에서 강제 캐스팅이 필요합니다. 시그니처를 넓히면 의도가 명확해지고 사용성이 좋아집니다.♻️ 제안 변경
-export const formatSemesterLabel = (award: Award): string | null => { - if (award?.year && award?.semester) { +export const formatSemesterLabel = ( + award: Award | null | undefined, +): string | null => { + if (award?.year != null && award?.semester) { const semesterLabel = award.semester === SemesterTerm.FIRST ? '1학기' : '2학기'; return `${award.year} ${semesterLabel}`; } return null; };frontend/src/apis/club.ts (2)
88-134: 불필요한 내부 함수 래퍼를 제거하세요.
connect함수가 정의되어 있지만 한 번만 호출되고,eventSource변수도connect()결과를 직접 반환하므로 불필요합니다. 코드를 단순화할 수 있습니다.♻️ 제안하는 리팩토링
export const createApplicantSSE = ( applicationFormId: string, eventHandlers: ApplicantSSECallbacks, ): EventSource | null => { - let eventSource: EventSource | null = null; - - const connect = (): EventSource | null => { - const accessToken = localStorage.getItem('accessToken'); - if (!accessToken) return null; + const accessToken = localStorage.getItem('accessToken'); + if (!accessToken) return null; - const source = new EventSource( - `${API_BASE_URL}/api/club/applicant/${applicationFormId}/sse`, - { - fetch: (input, init) => - fetch(input, { - ...init, - headers: { - ...init?.headers, - Authorization: `Bearer ${accessToken}`, - }, - credentials: 'include', - }), - }, - ); + const source = new EventSource( + `${API_BASE_URL}/api/club/applicant/${applicationFormId}/sse`, + { + fetch: (input, init) => + fetch(input, { + ...init, + headers: { + ...init?.headers, + Authorization: `Bearer ${accessToken}`, + }, + credentials: 'include', + }), + }, + ); - source.addEventListener('applicant-status-changed', (e) => { - try { - const eventData: ApplicantStatusEvent = JSON.parse(e.data); - eventHandlers.onStatusChange(eventData); - } catch (parseError) { - console.error('SSE PARSING ERROR:', parseError); - } - }); + source.addEventListener('applicant-status-changed', (e) => { + try { + const eventData: ApplicantStatusEvent = JSON.parse(e.data); + eventHandlers.onStatusChange(eventData); + } catch (parseError) { + console.error('SSE PARSING ERROR:', parseError); + } + }); - source.onerror = (error) => { - source.close(); - eventHandlers.onError( - new Error(error?.message || 'SSE connection error'), - ); - }; + source.onerror = (error) => { + source.close(); + eventHandlers.onError( + new Error(error?.message || 'SSE connection error'), + ); + }; - return source; - }; - - eventSource = connect(); - return eventSource; + return source; };
122-127: EventSource 에러 이벤트 타입을 확인하세요.
EventSource의onerror이벤트는Event타입으로,message속성이 없습니다.error?.message는 항상undefined가 되어 폴백 메시지가 사용됩니다. 의도된 동작이라면 괜찮지만, 명확성을 위해 단순화하는 것이 좋습니다.♻️ 제안하는 수정
source.onerror = (error) => { source.close(); - eventHandlers.onError( - new Error(error?.message || 'SSE connection error'), - ); + eventHandlers.onError(new Error('SSE connection error')); };frontend/src/context/AdminClubContext.tsx (1)
85-90: 재연결 시 지수 백오프(exponential backoff) 적용을 고려하세요.현재 고정된 2초 지연으로 재연결을 시도합니다. 서버 장애 시 모든 클라이언트가 동시에 재연결을 시도하는 "thundering herd" 문제를 방지하기 위해 지수 백오프 패턴을 고려해 보세요.
♻️ 지수 백오프 예시
const reconnectAttemptsRef = useRef(0); const MAX_RECONNECT_DELAY = 30000; // onError 내부에서: const delay = Math.min(1000 * Math.pow(2, reconnectAttemptsRef.current), MAX_RECONNECT_DELAY); reconnectAttemptsRef.current += 1; reconnectTimeoutRef.current = window.setTimeout(() => { reconnectTimeoutRef.current = null; sseConnect(); }, delay); // 연결 성공 시 reconnectAttemptsRef.current = 0; 으로 리셋frontend/src/pages/ClubDetailPage/components/ClubIntroContent/ClubIntroContent.tsx (1)
32-51:openFaqIndexes를 의존성에서 제거하세요.
openFaqIndexes가useCallback의존성 배열에 포함되어 있어, FAQ를 토글할 때마다 콜백이 재생성됩니다.isOpening계산을setOpenFaqIndexes내부로 이동하면 이 의존성을 제거할 수 있습니다.♻️ 제안하는 리팩토링
const handleToggleFaq = useCallback( (index: number) => { - const isOpening = !openFaqIndexes.has(index); + let isOpening = false; setOpenFaqIndexes((prev) => { + isOpening = !prev.has(index); const newSet = new Set(prev); if (isOpening) newSet.add(index); else newSet.delete(index); return newSet; }); if (faqs?.[index]) { trackEvent(USER_EVENT.FAQ_TOGGLE_CLICKED, { question: faqs[index].question, action: isOpening ? 'open' : 'close', }); } }, - [faqs, trackEvent, openFaqIndexes], + [faqs, trackEvent], );frontend/src/pages/AdminPage/tabs/ClubIntroEditTab/components/AwardEditor/AwardEditor.tsx (2)
20-24: 공유 유틸리티 함수를 재사용하세요.
formatSemesterLabel과getAwardKey함수가@/utils/awardHelpers.ts에 이미 정의되어 있습니다. 코드 중복을 피하고 일관성을 유지하기 위해 공유 유틸리티를 import하여 사용하는 것을 권장합니다.♻️ 제안하는 변경
import { CustomDropDown } from '@/components/common/CustomDropDown/CustomDropDown'; import { Award, SemesterTerm, SemesterTermType } from '@/types/club'; +import { formatSemesterLabel, getAwardKey } from '@/utils/awardHelpers'; import * as Styled from './AwardEditor.styles'; // ... const getSemesterSortValue = (award: Award): number => { const semesterValue = award.semester === SemesterTerm.FIRST ? 1 : 2; return award.year * 10 + semesterValue; }; -const formatSemesterLabel = (award: Award): string => { - const semesterLabel = - award.semester === SemesterTerm.FIRST ? '1학기' : '2학기'; - return `${award.year} ${semesterLabel}`; -}; // ... 내부의 getAwardKey 함수도 제거
220-227: 참조 비교에 의존하는 로직이 취약합니다.
originalAward.achievements === award.achievements참조 비교는 현재 동작하지만,sortedAwards가 얕은 복사를 사용하기 때문입니다. 향후 깊은 복사나 데이터 변환이 적용되면 이 비교가 실패할 수 있습니다. 정렬 시 원본 인덱스를 함께 추적하는 것이 더 안전합니다.♻️ 더 안전한 접근 방식
-const sortedAwards = [...awards].sort( - (awardA, awardB) => - getSemesterSortValue(awardB) - getSemesterSortValue(awardA), -); +const sortedAwards = awards + .map((award, index) => ({ award, originalIndex: index })) + .sort( + (a, b) => getSemesterSortValue(b.award) - getSemesterSortValue(a.award), + ); // 렌더링 시: -{sortedAwards.map((award, sortedIndex) => { - const originalIndex = awards.findIndex( - (originalAward, idx) => - originalAward.year === award.year && - originalAward.semester === award.semester && - originalAward.achievements === award.achievements, - ); +{sortedAwards.map(({ award, originalIndex }) => { const awardKey = getAwardKey(award, originalIndex); // ...
[fix] createApplicationSSE import 분리
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@frontend/src/apis/clubSSE.ts`:
- Around line 14-17: The connect function currently returns null when no
accessToken is found, which prevents the consumer's onError-based reconnection
logic from running; update connect (in frontend/src/apis/clubSSE.ts) to treat
missing token as an error by invoking the provided onError callback (the same
handler used for other connection failures) with a descriptive Error (e.g.,
"missing access token") and then return null, ensuring AdminClubContext's
reconnection logic (2s retry) is triggered; make sure to reference the existing
onError parameter/closure used when creating the EventSource so you call that
exact function.
lepitaaar
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
#️⃣연관된 이슈
📝작업 내용
중점적으로 리뷰받고 싶은 부분(선택)
논의하고 싶은 부분(선택)
🫡 참고사항
Summary by CodeRabbit
새로운 기능
개선사항
✏️ Tip: You can customize this high-level summary in your review settings.