Skip to content

[feature] 관리자 모집 기간 설정 시 분 단위 시간 선택을 지원한다#1293

Open
suhyun113 wants to merge 28 commits intodevelop-fefrom
feature/#1165-admin-recruitment-minute-time-MOA-624
Open

[feature] 관리자 모집 기간 설정 시 분 단위 시간 선택을 지원한다#1293
suhyun113 wants to merge 28 commits intodevelop-fefrom
feature/#1165-admin-recruitment-minute-time-MOA-624

Conversation

@suhyun113
Copy link
Collaborator

@suhyun113 suhyun113 commented Mar 3, 2026

#️⃣연관된 이슈

ex) #1165

📝작업 내용

관리자 페이지 동아리 모집 기간 설정이 시, 분 단위로 되도록 수정

변경 전 변경 후
image image

기존의 react-datepicker 기반 시간 선택 방식에서 발하던 제약 사항(30분 단위 설정, 디자인 일관성 부족)을 해결하기 위해, 커스텀 TimePickerPanel을 도입하고 전반적인 UI/UX를 개선했습니다.

주요 변경 사항

1. 시간/분 선택 단위로 수정

  • 기존 : 30분 단위로 고정되어 세밀한 시간 설정이 불가능했습니다.
  • 변경 : 시(0-23)와 분(0-59)을 각각 독립된 컬럼으로 분리하여 1분 단위로 자유롭게 설정할 수 있도록 개선했습니다.

2. 시간 선택 피커 스크롤 중앙 정렬

  • 기능: 패널이 열리거나 날짜/시간이 변경될 때, 현재 선택된 시(Hour)와 분(Minute) 아이템이 해당 스크롤 목록의 정중앙에 오도록 구현했습니다.

3. 달력 높이 연동 가변 레이아웃

  • 기능: 왼쪽 달력(DatePickerPanel)의 월별 행 수(4~6주)에 따라 변화하는 높이를 실시간으로 감지하도록 했습니다.
  • 변경: ResizeObserver를 통해 측정된 높이 값을 오른쪽 시간 피커에 전달하여, 전체 패널의 높이가 불균형해지지 않도록 가변적인 스크롤 영역을 제공했습니다.
  1. 날짜 선후 관계 자동 보정
    시작 날짜가 마감 날짜보다 늦어지거나, 마감 날짜가 시작 날짜보다 빨라질 경우 상대 날짜를 자동으로 동일하게 맞춰주는 로직을 추가하여 데이터 무결성을 보장했습니다.

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

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

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

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

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

🫡 참고사항

Summary by CodeRabbit

  • 새로운 기능

    • 통합 날짜·시간 범위 선택기 추가(월 네비게이션, 시/분 선택, 패널 높이 감지 및 외부 클릭 닫기).
    • 날짜/시간 패널(달력+시간) 분리 컴포넌트로 도입 및 모집일 표시 포맷 함수 추가(한국어).
  • 버그 수정 / 동작 개선

    • "상시모집" 토글 동작 개선: 토글 시 기간 백업/복원 로직 일관화 및 종료일 비활성화 처리 안정화.
  • 스타일

    • 날짜/시간 선택기 및 상시모집 버튼 UI/스타일 전면 개편.

@suhyun113 suhyun113 self-assigned this Mar 3, 2026
@suhyun113 suhyun113 added ✨ Feature 기능 개발 💻 FE Frontend labels Mar 3, 2026
@vercel
Copy link

vercel bot commented Mar 3, 2026

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

Project Deployment Actions Updated (UTC)
moadong Ready Ready Preview, Comment Mar 3, 2026 1:03pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 3, 2026

Warning

.coderabbit.yaml has a parsing error

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

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

Walkthrough

Calendar 기반 모집 기간 선택기를 제거하고, 새로운 DateTimeRangePicker(및 DatePickerPanel/TimePickerPanel/DateTimePanel)를 추가했으며, "상시모집" 상태 관련 prop 이름과 버튼 스타일을 변경했습니다.

Changes

Cohort / File(s) Summary
RecruitEditTab 스타일
frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.styles.ts
AlwaysRecruitButton 공개 prop을 { $active: boolean }에서 { $isAlwaysActive: boolean }로 변경하고, 기본(비활성) 색상/배경/테두리 스타일을 회색 계열로 변경. $isAlwaysActive일 때 활성 스타일로 전환하는 조건부 블록 추가.
RecruitEditTab 로직
frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.tsx
기존 Calendar 사용을 DateTimeRangePicker로 교체. isAlwaysRecruiting 상태 추가, 시작/종료 동기화 핸들러와 백업/복원 로직 재구성, 초기화 useEffect 및 제출 시 ISO 문자열 저장 정리.
기존 Calendar 제거
frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/Calendar/Calendar.tsx, frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/Calendar/Calendar.styles.ts
기존 Calendar 컴포넌트 및 관련 스타일 파일 전체 삭제(내부 커스텀 헤더, 시간 선택, react-datepicker 스타일 규칙 포함).
DateTimeRangePicker 및 스타일 추가
frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/DateTimeRangePicker/DateTimeRangePicker.tsx, .../DateTimeRangePicker.styles.ts
새 DateTimeRangePicker 컴포넌트와 스타일 추가. 두 입력(시작/종료) 표시, 패널 열림/닫힘, 외부 클릭 처리, disabledEnd 지원 등 UI 상호작용 구현.
DatePicker 패널 추가
frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/DateTimeRangePicker/DatePickerPanel.tsx, .../DatePickerPanel.styles.ts
inline react-datepicker 캘린더 렌더링 패널 추가. ResizeObserver로 높이 보고 기능, 월 이동/날짜 선택 핸들러 포함.
DateTime 패널 추가
frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/DateTimeRangePicker/DateTimePanel.tsx, .../DateTimePanel.styles.ts
DatePickerPanel과 TimePickerPanel을 조합해 날짜+시간 선택 UI를 제공하는 패널 컴포넌트 추가(헤더 내 월 네비게이션 포함).
TimePicker 패널 추가
frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/DateTimeRangePicker/TimePickerPanel.tsx, .../TimePickerPanel.styles.ts
시/분 스크롤 선택 UI 추가(0–23시, 0–59분), 초기 스크롤 정렬과 선택 강조 처리 포함.
유틸리티 추가
frontend/src/utils/recruitmentDateFormatter.ts
`formatRecruitmentDateTime(date: Date

Sequence Diagram(s)

sequenceDiagram
    participant Admin as RecruitEditTab
    participant Picker as DateTimeRangePicker
    participant Panel as DateTimePanel
    participant Calendar as DatePickerPanel
    participant Time as TimePickerPanel

    Admin->>Picker: 렌더(recruitmentStart, recruitmentEnd, disabledEnd)
    Picker->>Picker: 입력 클릭 → activePicker 설정
    Picker->>Panel: 패널 열기 (date, onChangeDate, viewMonth)
    Panel->>Calendar: 인라인 캘린더 렌더링 (selected, viewMonth)
    Panel->>Time: 시간/분 스크롤 렌더링 (selected)
    Calendar-->>Panel: 날짜 선택 이벤트(onChange)
    Time-->>Panel: 시간/분 선택 이벤트(onChange)
    Panel-->>Picker: 조합된 Date 반환(onChangeDate)
    Picker-->>Admin: recruitmentStart/recruitmentEnd 변경 콜백 호출
    Admin->>Admin: isAlwaysRecruiting 토글 → backup/restore 로직 실행
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • seongwon030
  • lepitaaar
  • oesnuj
  • Zepelown
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목은 '관리자 모집 기간 설정 시 분 단위 시간 선택을 지원한다'로 변경 사항의 핵심인 분 단위 시간 선택 기능을 명확하게 요약하고 있습니다.
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 (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/#1165-admin-recruitment-minute-time-MOA-624

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


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

❤️ Share

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

@github-actions
Copy link

github-actions bot commented Mar 3, 2026

🎨 UI 변경사항을 확인해주세요

변경된 스토리를 Chromatic에서 확인해주세요.

구분 링크
🔍 변경사항 리뷰 https://www.chromatic.com/build?appId=67904e61c16daa99a63b44a7&number=38
📖 Storybook https://67904e61c16daa99a63b44a7-enrrtdolsk.chromatic.com/

25개 스토리 변경 · 전체 58개 스토리 · 23개 컴포넌트

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: 4

🧹 Nitpick comments (5)
frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/DateTimeRangePicker/DateTimeRangePicker.styles.ts (1)

11-34: 접근성을 위한 포커스 스타일 및 비활성화 커서 추가 권장

키보드 네비게이션 사용자를 위한 포커스 스타일과 비활성화 상태의 커서 스타일이 누락되어 있습니다.

♻️ 제안된 개선 사항
   &:disabled {
     background-color: ${colors.gray[400]};
     color: ${colors.gray[500]};
+    cursor: not-allowed;
   }
+
+  &:focus-visible {
+    outline: 2px solid ${colors.primary[800]};
+    outline-offset: 2px;
+  }
 `;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/DateTimeRangePicker/DateTimeRangePicker.styles.ts`
around lines 11 - 34, The Input styled component is missing keyboard focus
styles and a disabled cursor; update the Input styled.button (component named
Input) to add a clear focus state (e.g., :focus-visible or :focus with an
accessible outline/box-shadow using theme colors) so keyboard users can see
focus, and add cursor: not-allowed to the &:disabled block so disabled buttons
convey non-interactivity; ensure the focus style does not conflict with the
existing $isActive styles.
frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/DateTimeRangePicker/TimePickerPanel.tsx (1)

62-65: 인라인 스타일이 styled-component와 중복됩니다.

ScrollListoverflow-y: auto가 이미 정의되어 있으므로 인라인 스타일의 overflowY: 'auto'는 중복입니다. position: 'relative'가 필요하다면 styled-component에 추가하는 것이 좋습니다.

♻️ 제안하는 수정

TimePickerPanel.styles.tsScrollListposition: relative를 추가하고:

 export const ScrollList = styled.div<{ $containerHeight: number }>`
   height: ${({ $containerHeight }) => $containerHeight}px;
   overflow-y: auto;
   margin: 20px 0;
+  position: relative;

TimePickerPanel.tsx에서 인라인 스타일을 제거:

         <Styled.ScrollList
           $containerHeight={height}
           ref={hourListRef}
-          style={{ overflowY: 'auto', position: 'relative' }}
         >
         <Styled.ScrollList
           $containerHeight={height}
           ref={minuteListRef}
-          style={{ overflowY: 'auto', position: 'relative' }}
         >

Also applies to: 81-84

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/DateTimeRangePicker/TimePickerPanel.tsx`
around lines 62 - 65, Styled.ScrollList is receiving an inline style that
duplicates its styled-component rules (overflow-y: auto) and also sets position:
'relative'; update the ScrollList styled-component in TimePickerPanel.styles.ts
to include position: relative (and keep its overflow-y definition), then remove
the inline style object from Styled.ScrollList in TimePickerPanel.tsx (the
instances using hourListRef and minuteListRef around the given lines and the
similar block at 81-84) so layout is driven solely by the styled-component.
frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/DateTimeRangePicker/DateTimePanel.styles.ts (1)

5-14: left: 0 속성이 중복 선언되어 있습니다.

Line 7과 Line 13에서 left: 0이 두 번 선언되어 있습니다. 하나를 제거해 주세요.

♻️ 제안하는 수정
 export const Panel = styled.div<{ $alignRight?: boolean }>`
   position: absolute;
   top: 55px;
   left: 0;
   background: ${colors.base.white};
   border-radius: 12px;
   box-shadow: 0 8px 20px rgba(0, 0, 0, 0.18);
   overflow: hidden;
   z-index: 10;
-  left: 0;
   right: auto;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/DateTimeRangePicker/DateTimePanel.styles.ts`
around lines 5 - 14, In DateTimePanel.styles.ts (the styled block used by
DateTimePanel), remove the duplicate CSS property declaration so only a single
"left: 0" remains; find the style rule in the DateTimePanel.styles (or the
styled component) and delete the redundant left: 0 line to avoid duplicate
declarations.
frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.styles.ts (1)

30-36: 조건부 스타일링 패턴이 다른 파일들과 일관되지 않습니다.

다른 스타일 파일들(예: DateTimePanel.styles.ts, TimePickerPanel.styles.ts)에서는 css 헬퍼를 사용하고 있습니다. 일관성을 위해 동일한 패턴을 사용하는 것을 권장합니다.

♻️ 제안하는 수정
+import styled, { css } from 'styled-components';
-import styled from 'styled-components';
   ${({ $isAlwaysActive }) =>
-     $isAlwaysActive &&
-  `
-  color: ${colors.base.white};
-  background-color: ${colors.primary[800]};
-  border: none;
-  `}
+    $isAlwaysActive &&
+    css`
+      color: ${colors.base.white};
+      background-color: ${colors.primary[800]};
+      border: none;
+    `}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.styles.ts`
around lines 30 - 36, Replace the inline conditional template string with the
styled-components css helper to match other files: import css from
styled-components (or named css) and change the block that checks the
$isAlwaysActive prop (the current ${({ $isAlwaysActive }) => $isAlwaysActive &&
`...` } expression) to return css`...` instead of a raw template string,
preserving the same properties (color, background-color, border) and the
$isAlwaysActive prop name so the pattern aligns with DateTimePanel.styles.ts and
TimePickerPanel.styles.ts.
frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/DateTimeRangePicker/DateTimePanel.tsx (1)

17-20: 부모 컴포넌트의 date 값 변경 시 viewMonth 동기화 고려

viewMonth는 컴포넌트 마운트 시에만 date로 초기화되므로, 피커가 열려있는 상태에서 부모의 date 값이 변경되면 viewMonth와 실제 선택된 날짜가 불일치할 수 있습니다. 현재 구조에서는 피커 타입 전환 시 컴포넌트가 다시 마운트되어 viewMonth가 재초기화되지만, 동일한 타입의 피커가 열려있는 상태에서 외부 업데이트가 발생할 경우 이 문제가 나타날 수 있습니다.

필요하다면 useEffectdate 변경을 감지하여 viewMonth를 동기화하는 것을 고려해 주세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/DateTimeRangePicker/DateTimePanel.tsx`
around lines 17 - 20, The viewMonth state (initialized with useState in
DateTimePanel) is only set once from the incoming date prop, so if the parent
updates date while the picker remains mounted the displayed month can get out of
sync; add a useEffect that watches the date prop and calls setViewMonth(date ||
new Date()) to synchronize viewMonth when date changes (keep the existing early
return if !date), referencing the viewMonth and setViewMonth state variables and
the date prop in DateTimePanel.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/DateTimeRangePicker/DatePickerPanel.styles.ts`:
- Around line 35-40: In the CSS selector block that hides datepicker UI
(.react-datepicker__triangle, .react-datepicker__navigation,
.react-datepicker__header, .react-datepicker__curront-month) there is a typo:
change the class selector .react-datepicker__curront-month to
.react-datepicker__current-month so the rule applies correctly to the current
month element.

In
`@frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/DateTimeRangePicker/DateTimeRangePicker.tsx`:
- Around line 57-60: When disabledEnd becomes true the end panel must be closed;
add a useEffect inside DateTimeRangePicker that watches disabledEnd and
activePicker and, if disabledEnd && activePicker === 'end', programmatically
close the end picker (call the existing togglePicker to clear the active picker
or call the component's setActivePicker/close handler so activePicker is no
longer 'end'). Ensure the same guard is applied for any render/visibility logic
that checks activePicker so the panel cannot remain open while disabledEnd is
true.

In `@frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.tsx`:
- Around line 63-68: The current branch replaces empty/unspecified recruitment
fields with new Date(), causing unintended saved dates; instead, call
recruitmentDateParser for clubDetail.recruitmentStart and
clubDetail.recruitmentEnd and preserve its null return for blank/"미정" values
(i.e., remove the fallback to new Date()), so parsedStart and parsedEnd become
whatever recruitmentDateParser returns (null or a Date); also update any
downstream usage of parsedStart/parsedEnd in this component (e.g., form initial
values or submit handlers) to handle nulls if necessary.
- Around line 89-112: The functional updater passed to setIsAlwaysRecruiting
currently performs side effects (mutating backupRangeRef.current and calling
setRecruitmentStart/setRecruitmentEnd) which must be moved out; change the
updater used in the event handler so it only returns the toggled boolean
(compute nextMode inside the updater or just toggle based on prior value) and
then, immediately after calling setIsAlwaysRecruiting, run the side-effect logic
in the handler or in a useEffect that watches isAlwaysRecruiting: when enabling,
set backupRangeRef.current = { start: recruitmentStart, end: recruitmentEnd };
when disabling, read backupRangeRef.current and call
setRecruitmentStart(baseDate) and setRecruitmentEnd(...) using
isFarFuture(backup.end) to decide the end value. Ensure all mutations and
setRecruitment* calls are removed from the function passed to
setIsAlwaysRecruiting.

---

Nitpick comments:
In
`@frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/DateTimeRangePicker/DateTimePanel.styles.ts`:
- Around line 5-14: In DateTimePanel.styles.ts (the styled block used by
DateTimePanel), remove the duplicate CSS property declaration so only a single
"left: 0" remains; find the style rule in the DateTimePanel.styles (or the
styled component) and delete the redundant left: 0 line to avoid duplicate
declarations.

In
`@frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/DateTimeRangePicker/DateTimePanel.tsx`:
- Around line 17-20: The viewMonth state (initialized with useState in
DateTimePanel) is only set once from the incoming date prop, so if the parent
updates date while the picker remains mounted the displayed month can get out of
sync; add a useEffect that watches the date prop and calls setViewMonth(date ||
new Date()) to synchronize viewMonth when date changes (keep the existing early
return if !date), referencing the viewMonth and setViewMonth state variables and
the date prop in DateTimePanel.

In
`@frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/DateTimeRangePicker/DateTimeRangePicker.styles.ts`:
- Around line 11-34: The Input styled component is missing keyboard focus styles
and a disabled cursor; update the Input styled.button (component named Input) to
add a clear focus state (e.g., :focus-visible or :focus with an accessible
outline/box-shadow using theme colors) so keyboard users can see focus, and add
cursor: not-allowed to the &:disabled block so disabled buttons convey
non-interactivity; ensure the focus style does not conflict with the existing
$isActive styles.

In
`@frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/DateTimeRangePicker/TimePickerPanel.tsx`:
- Around line 62-65: Styled.ScrollList is receiving an inline style that
duplicates its styled-component rules (overflow-y: auto) and also sets position:
'relative'; update the ScrollList styled-component in TimePickerPanel.styles.ts
to include position: relative (and keep its overflow-y definition), then remove
the inline style object from Styled.ScrollList in TimePickerPanel.tsx (the
instances using hourListRef and minuteListRef around the given lines and the
similar block at 81-84) so layout is driven solely by the styled-component.

In `@frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.styles.ts`:
- Around line 30-36: Replace the inline conditional template string with the
styled-components css helper to match other files: import css from
styled-components (or named css) and change the block that checks the
$isAlwaysActive prop (the current ${({ $isAlwaysActive }) => $isAlwaysActive &&
`...` } expression) to return css`...` instead of a raw template string,
preserving the same properties (color, background-color, border) and the
$isAlwaysActive prop name so the pattern aligns with DateTimePanel.styles.ts and
TimePickerPanel.styles.ts.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between eb54ce1 and 7af6d13.

📒 Files selected for processing (13)
  • frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.styles.ts
  • frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.tsx
  • frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/Calendar/Calendar.styles.ts
  • frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/Calendar/Calendar.tsx
  • frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/DateTimeRangePicker/DatePickerPanel.styles.ts
  • frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/DateTimeRangePicker/DatePickerPanel.tsx
  • frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/DateTimeRangePicker/DateTimePanel.styles.ts
  • frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/DateTimeRangePicker/DateTimePanel.tsx
  • frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/DateTimeRangePicker/DateTimeRangePicker.styles.ts
  • frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/DateTimeRangePicker/DateTimeRangePicker.tsx
  • frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/DateTimeRangePicker/TimePickerPanel.styles.ts
  • frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/DateTimeRangePicker/TimePickerPanel.tsx
  • frontend/src/utils/recruitmentDateFormatter.ts
💤 Files with no reviewable changes (2)
  • frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/Calendar/Calendar.tsx
  • frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/Calendar/Calendar.styles.ts

Comment on lines +63 to +68
const parsedStart = clubDetail.recruitmentStart
? recruitmentDateParser(clubDetail.recruitmentStart)
: null;
const end = clubDetail.recruitmentEnd
: new Date();
const parsedEnd = clubDetail.recruitmentEnd
? recruitmentDateParser(clubDetail.recruitmentEnd)
: null;
const isAlways = isFarFuture(end);

if (isAlways) {
setAlways(true);
backupRangeRef.current = { start, end };
setRecruitmentStart(start ?? now);
} else {
setRecruitmentStart(start ?? now);
setRecruitmentEnd(end ?? now);
}
: new Date();
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

빈 모집기간을 현재 시각으로 치환하면 저장 시 데이터가 오염될 수 있습니다.

recruitmentDateParser는 빈 문자열/미정null로 처리하도록 되어 있는데, 현재 분기에서는 빈 값이 new Date()로 들어가 의도치 않게 모집기간이 생성됩니다. 빈 값은 null을 유지하도록 파싱 경로를 통일해 주세요.

수정 예시
-    const parsedStart = clubDetail.recruitmentStart
-      ? recruitmentDateParser(clubDetail.recruitmentStart)
-      : new Date();
-    const parsedEnd = clubDetail.recruitmentEnd
-      ? recruitmentDateParser(clubDetail.recruitmentEnd)
-      : new Date();
+    const parsedStart = recruitmentDateParser(clubDetail.recruitmentStart ?? '');
+    const parsedEnd = recruitmentDateParser(clubDetail.recruitmentEnd ?? '');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.tsx` around
lines 63 - 68, The current branch replaces empty/unspecified recruitment fields
with new Date(), causing unintended saved dates; instead, call
recruitmentDateParser for clubDetail.recruitmentStart and
clubDetail.recruitmentEnd and preserve its null return for blank/"미정" values
(i.e., remove the fallback to new Date()), so parsedStart and parsedEnd become
whatever recruitmentDateParser returns (null or a Date); also update any
downstream usage of parsedStart/parsedEnd in this component (e.g., form initial
values or submit handlers) to handle nulls if necessary.

Comment on lines +89 to 112
setIsAlwaysRecruiting((prevMode) => {
const nextMode = !prevMode;
const now = new Date();
if (nextMode) {
// 상시모집 활성화 시 현재 날짜 백업
backupRangeRef.current = {
start: recruitmentStart,
end: recruitmentEnd,
};
} else {
// 상시모집 비활성화
const { start, end } = backupRangeRef.current;
const backupWasAlways = isFarFuture(end);
if (backupWasAlways) {
// 백업이 상시모집인 경우
const base = start ?? now;
setRecruitmentStart(base);
setRecruitmentEnd(base);
// 상시모집 비활성화 시 백업 데이터 복구
const backup = backupRangeRef.current;
const baseDate = backup.start || now;

setRecruitmentStart(baseDate);

if (isFarFuture(backup.end)) {
setRecruitmentEnd(baseDate);
} else {
// 백업이 상시모집이 아닌 경우
setRecruitmentStart(start ?? now);
setRecruitmentEnd(end ?? now);
setRecruitmentEnd(backup.end || now);
}
}
return !prev;
return nextMode;
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

React 19 useState functional updater Strict Mode re-invocation side effects

💡 Result:

In React 19, if you run your app under <StrictMode> in development, React may invoke your useState functional updater twice (e.g. setCount(c => /*...*/)), specifically because Strict Mode intentionally double-calls “functions that should be pure” to help you detect hidden side effects. This includes functions you pass to useState and to state “set” functions. [1]

What this means for setState(prev => next)

  • The updater function must be pure: compute next from prev only.
  • Do not perform side effects inside the updater (logging, analytics, mutations, network calls, writing to storage, etc.), because Strict Mode may run it twice and expose those side effects. [1]

What to do instead

  • Keep the updater as a pure calculation (no mutation of prev, no external writes). [1]
  • Move side effects to:
    • an event handler (often the best place), or
    • a useEffect that reacts to the committed state change (note: effects are also intentionally re-run an extra setup/cleanup cycle in dev Strict Mode). [1]

Source
[1] React docs: <StrictMode> (development-only double invocation and what gets double-called) https://react.dev/reference/react/StrictMode


setIsAlwaysRecruiting 함수형 업데이터 내부의 부수효과를 분리해야 합니다.

React 공식 문서에 따르면, Strict Mode의 개발 환경에서 useState의 함수형 업데이터는 의도적으로 두 번 호출됩니다. 업데이터는 순수 함수여야 하므로 이전 상태에서 다음 상태를 계산하는 로직만 포함해야 합니다. 현재 코드는 업데이터 내부에서 backupRangeRef.current 변경과 setRecruitmentStart, setRecruitmentEnd 호출이 함께 실행되는데, 이는 부수효과에 해당하므로 두 번 실행될 위험이 있습니다.

nextMode 계산을 먼저 한 후, 그 결과에 따라 ref 변경과 다른 상태 업데이트를 이벤트 핸들러에서 또는 useEffect에서 분리하여 처리하는 것이 안전합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.tsx` around
lines 89 - 112, The functional updater passed to setIsAlwaysRecruiting currently
performs side effects (mutating backupRangeRef.current and calling
setRecruitmentStart/setRecruitmentEnd) which must be moved out; change the
updater used in the event handler so it only returns the toggled boolean
(compute nextMode inside the updater or just toggle based on prior value) and
then, immediately after calling setIsAlwaysRecruiting, run the side-effect logic
in the handler or in a useEffect that watches isAlwaysRecruiting: when enabling,
set backupRangeRef.current = { start: recruitmentStart, end: recruitmentEnd };
when disabling, read backupRangeRef.current and call
setRecruitmentStart(baseDate) and setRecruitmentEnd(...) using
isFarFuture(backup.end) to decide the end value. Ensure all mutations and
setRecruitment* calls are removed from the function passed to
setIsAlwaysRecruiting.

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.

🧹 Nitpick comments (2)
frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/DateTimeRangePicker/DateTimePanel.styles.ts (2)

24-44: 색상은 토큰으로 통일하면 테마 유지보수가 더 쉬워집니다.

Header/NavButtoncolor: white;colors.base.white로 맞추면 파일 내 색상 사용 규칙이 일관됩니다.

제안 diff
 export const Header = styled.div`
   display: grid;
   grid-template-columns: 40px 1fr 40px 60px 60px;
   height: 44px;
   background: ${colors.primary[800]};
-  color: white;
+  color: ${colors.base.white};
   align-items: center;
   text-align: center;
 `;
 
 export const NavButton = styled.button`
   background: none;
   border: none;
-  color: white;
+  color: ${colors.base.white};
   font-size: 20px;
   cursor: pointer;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/DateTimeRangePicker/DateTimePanel.styles.ts`
around lines 24 - 44, Replace literal "white" color usage with the project's
color token for consistency: in the styled components Header and NavButton
change their color declarations from "color: white;" to use colors.base.white
(i.e. update the color property in Header and NavButton to colors.base.white) so
the file follows the tokenized color convention.

4-22: Panel의 중복 left 선언은 하나로 정리하는 게 좋습니다.

Line 7과 Line 13에 left: 0;가 중복되어 있어요. 동작에는 영향이 없지만 정렬 분기 가독성이 떨어집니다.

제안 diff
 export const Panel = styled.div<{ $alignRight?: boolean }>`
   position: absolute;
   top: 55px;
   left: 0;
   background: ${colors.base.white};
   border-radius: 12px;
   box-shadow: 0 8px 20px rgba(0, 0, 0, 0.18);
   overflow: hidden;
   z-index: 10;
-  left: 0;
   right: auto;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/DateTimeRangePicker/DateTimePanel.styles.ts`
around lines 4 - 22, The styled component Panel has duplicate left: 0
declarations; remove the redundant one so the default positioning sets left: 0
only once, and keep the existing alignment branch that sets right: 0 and left:
auto when the $alignRight prop is true (i.e., update the Panel styled.div to
have a single left: 0 and keep the ${({ $alignRight }) => ...} block unchanged
to flip to left: auto when $alignRight is truthy).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/DateTimeRangePicker/DateTimePanel.styles.ts`:
- Around line 24-44: Replace literal "white" color usage with the project's
color token for consistency: in the styled components Header and NavButton
change their color declarations from "color: white;" to use colors.base.white
(i.e. update the color property in Header and NavButton to colors.base.white) so
the file follows the tokenized color convention.
- Around line 4-22: The styled component Panel has duplicate left: 0
declarations; remove the redundant one so the default positioning sets left: 0
only once, and keep the existing alignment branch that sets right: 0 and left:
auto when the $alignRight prop is true (i.e., update the Panel styled.div to
have a single left: 0 and keep the ${({ $alignRight }) => ...} block unchanged
to flip to left: auto when $alignRight is truthy).

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 7af6d13 and 5cce4eb.

📒 Files selected for processing (2)
  • frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.styles.ts
  • frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/DateTimeRangePicker/DateTimePanel.styles.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.styles.ts

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.

♻️ Duplicate comments (1)
frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/DateTimeRangePicker/DateTimeRangePicker.tsx (1)

73-83: ⚠️ Potential issue | 🟡 Minor

disabledEnd 전환 시 패널 렌더 가드가 없어 한 프레임 노출될 수 있습니다.

현재는 useEffect로 닫기 때문에 disabledEnd=true로 바뀐 직후 한 번은 종료 패널이 렌더될 수 있습니다. 렌더 조건에서도 disabledEnd를 함께 확인해 즉시 차단해 주세요.

수정 예시
-      {activePicker && (
+      {(activePicker === 'start' || (activePicker === 'end' && !disabledEnd)) && (
         <DateTimePanel
           $alignRight={activePicker === 'end'}
           date={activePicker === 'start' ? recruitmentStart : recruitmentEnd}
           onChangeDate={
             activePicker === 'start'
               ? onChangeRecruitmentStart
               : onChangeRecruitmentEnd
           }
         />
       )}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/DateTimeRangePicker/DateTimeRangePicker.tsx`
around lines 73 - 83, The DateTimePanel can briefly render one frame when
disabledEnd flips true because the current guard only checks activePicker;
update the render condition around DateTimePanel to also verify disabledEnd
(i.e., only render when activePicker && !(activePicker === 'end' &&
disabledEnd)) so the end panel is blocked immediately; keep existing useEffect
close logic but add this immediate check referencing activePicker, disabledEnd,
DateTimePanel, recruitmentEnd, and onChangeRecruitmentEnd to prevent that
one-frame flash.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In
`@frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/DateTimeRangePicker/DateTimeRangePicker.tsx`:
- Around line 73-83: The DateTimePanel can briefly render one frame when
disabledEnd flips true because the current guard only checks activePicker;
update the render condition around DateTimePanel to also verify disabledEnd
(i.e., only render when activePicker && !(activePicker === 'end' &&
disabledEnd)) so the end panel is blocked immediately; keep existing useEffect
close logic but add this immediate check referencing activePicker, disabledEnd,
DateTimePanel, recruitmentEnd, and onChangeRecruitmentEnd to prevent that
one-frame flash.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 5cce4eb and 3718812.

📒 Files selected for processing (2)
  • frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/DateTimeRangePicker/DatePickerPanel.styles.ts
  • frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/DateTimeRangePicker/DateTimeRangePicker.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/DateTimeRangePicker/DatePickerPanel.styles.ts

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.

수고하셨습니다~! 궁금한 점 리뷰 달았어요

import { colors } from '@/styles/theme/colors';
import 'react-datepicker/dist/react-datepicker.css';

const commonCellLayout = css`
Copy link
Member

Choose a reason for hiding this comment

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

여기서 styled가 아닌 css를 쓴 이유가 있었나요?


const observer = new ResizeObserver(() => {
onHeightChange(monthElement.offsetHeight);
});
Copy link
Member

Choose a reason for hiding this comment

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

window resize랑 다르게 특정 요소 크기를 관찰하는 용도군요.

이전의 height와 다른 경우에만 state를 업뎃하면 매번 state를 최신화하는 것보다 낫지 않을까요?

interface DateTimePanelProps {
date: Date | null;
onChangeDate: (date: Date) => void;
$alignRight?: boolean;
Copy link
Member

Choose a reason for hiding this comment

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

styled componenets의 특성을 잘 파악하시고 쓰셨군요 👍

Comment on lines +28 to +30
const formattedYearMonth = `${viewMonth.getFullYear()}.${String(
viewMonth.getMonth() + 1,
).padStart(2, '0')}`;
Copy link
Member

Choose a reason for hiding this comment

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

여기선 useMemo로 연산비용을 최적화할 수 있어요. 물론 자주 같은 값을 사용하지 않을 것 같아서 불필요할 수도 있지만 생각나서 적어봅니다. useMemo

Comment on lines +6 to +7
return format(date, 'yyyy년 MM월 dd일 (eee) HH:mm', { locale: ko });
};
Copy link
Member

Choose a reason for hiding this comment

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

라이브러리 유틸을 사용하는 경우에는 테스트코드가 필요할까요?

Comment on lines +47 to +50
const updateHour = (hour: number) => {
const nextDate = new Date(selectedDate);
nextDate.setHours(hour);
onChangeDate(nextDate);
Copy link
Member

Choose a reason for hiding this comment

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

Date객체가 mutable이라 객체를 직접 수정하는 방식보단 복사본 만들어서 업뎃하는게 어떨까요?

Comment on lines +38 to +42
useLayoutEffect(() => {
const scrollTimer = setTimeout(() => {
alignScrollToCenter(hourListRef.current, selectedDate.getHours());
alignScrollToCenter(minuteListRef.current, selectedDate.getMinutes());
}, 0);
Copy link
Member

Choose a reason for hiding this comment

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

useLayoutEffect로 깜빡임 방지한 것 좋습니다.
혹시 setTimeout에 0을 걸어둔 게 어떤 의도인가요?

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