[feature] 모집기간 달력 UX 개선: 시간 선택 추가, 날짜 로직 변경 및 시간대 오류 수정#664
[feature] 모집기간 달력 UX 개선: 시간 선택 추가, 날짜 로직 변경 및 시간대 오류 수정#664oesnuj merged 6 commits intodevelop-fefrom
Conversation
react-datepicker 라이브러리의 기본 CSS 명시도가 높아 커스텀 스타일이 적용되지 않는 문제를 해결합니다.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning
|
| Cohort / File(s) | Summary |
|---|---|
Recruit edit logicfrontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.tsx, frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/Calendar/Calendar.tsx |
시작/종료 DatePicker에 시간 선택 활성화(30분 간격), 입력 타이핑 차단, 한국어 포맷으로 표시. 시작/종료 변경 시 역전 발생하면 서로 보정. 불필요한 useMemo 제거. RecruitEditTab에서 초기 날짜를 현재 시각으로 폴백. |
Calendar styles overhaulfrontend/src/pages/AdminPage/tabs/RecruitEditTab/components/Calendar/Calendar.styles.ts |
컬러 토큰/공통 CSS 블록 도입, 2-패널(달력+시간) 레이아웃, 셀/호버/선택 상태 스타일 정비, 시간 리스트 전용 스타일 추가, 헤더/네비게이션/입력 필드 시각 업데이트. |
Recruitment period parsingfrontend/src/utils/recruitmentPeriodParser.ts |
파서가 'yyyy.MM.dd HH:mm'을 UTC 오프셋(+0000) 포함 형식으로 파싱하도록 변경해 시간대 인지 처리. 검증 흐름은 동일. |
Sequence Diagram(s)
sequenceDiagram
participant User as User
participant StartDP as Start DatePicker
participant EndDP as End DatePicker
participant Cal as Calendar Component
participant Parent as RecruitEditTab
User->>StartDP: 날짜/시간 선택
StartDP->>Cal: onChange(date)
Cal->>Parent: onChangeStart(date)
alt date > current End
Cal->>Parent: onChangeEnd(date) (보정)
end
User->>EndDP: 날짜/시간 선택
EndDP->>Cal: onChange(date)
Cal->>Parent: onChangeEnd(date)
alt date < current Start
Cal->>Parent: onChangeStart(date) (보정)
end
sequenceDiagram
participant Caller as Caller
participant Parser as recruitmentPeriodParser
Caller->>Parser: parseRecruitmentPeriod(startStr, endStr)
Parser->>Parser: parseRecruitmentDateString(s + " +0000")
Parser-->>Caller: {start: Date, end: Date} or Error
Estimated code review effort
🎯 3 (Moderate) | ⏱️ ~25 minutes
Assessment against linked issues
| Objective | Addressed | Explanation |
|---|---|---|
| 달력에 시간 선택 기능 추가 MOA-146 | ✅ | |
| 시작/종료 기간 역전 문제 해결 MOA-146 | ✅ |
Possibly related issues
- [feature] MOA-146 달력 시간 선택 기능 추가 및 기간 역전 문제 해결 #639 — 캘린더 시간 선택과 기간 역전 방지 요구사항과 동일한 변경사항을 포함.
Possibly related PRs
- RecruitEditTab에서 외부 지원서 URL을 업데이트하는 로직 추가 #610 — RecruitEditTab 수정과 동일 파일/영역(updatedData) 변경 연관.
- [feature] stringToDate 유틸리티 함수 단위 테스트 추가 #442 — 모집기간 파서 리팩터링/테스트 추가와 직접적으로 같은 모듈의 구현 변경 연관.
- [fix] 지원서 미등록 시 alert 반복 버그 수정 및 외부 링크/지원서 분기 처리 #605 — 동일 파서 변경으로 해당 컴포넌트를 소비하는 부분에 영향 가능.
Suggested labels
🐞 Bug
Suggested reviewers
- seongwon030
- lepitaaar
- Zepelown
Tip
🔌 Remote MCP (Model Context Protocol) integration is now available!
Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.
✨ Finishing Touches
- 📝 Generate Docstrings
🧪 Generate unit tests
- Create PR with unit tests
- Post copyable unit tests in a comment
- Commit unit tests in branch
feature/#639-calendar-datetime-upgrade-MOA-146
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.
🪧 Tips
Chat
There are 3 ways to chat with CodeRabbit:
- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
I pushed a fix in commit <commit_id>, please review it.Open a follow-up GitHub issue for this discussion.
- Files and specific lines of code (under the "Files changed" tab): Tag
@coderabbitaiin a new review comment at the desired location with your query. - PR comments: Tag
@coderabbitaiin a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
Support
Need help? Create a ticket on our support page for assistance with any issues or questions.
CodeRabbit Commands (Invoked using PR/Issue comments)
Type @coderabbitai help to get the list of available commands.
Other keywords and placeholders
- Add
@coderabbitai ignoreanywhere in the PR description to prevent this PR from being reviewed. - Add
@coderabbitai summaryto generate the high-level summary at a specific location in the PR description. - Add
@coderabbitaianywhere in the PR title to generate the title automatically.
Status, Documentation and Community
- Visit our Status Page to check the current availability of CodeRabbit.
- Visit our Documentation for detailed information on how to use CodeRabbit.
- Join our Discord Community to get help, request features, and share feedback.
- Follow us on X/Twitter for updates and announcements.
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (10)
frontend/src/utils/recruitmentPeriodParser.ts (1)
10-10: UTC 강제 파싱 로직 정상 확인 및 향후 TZ 대응 Optional Refactor 제안현재
parseRecruitmentDateString내부에서 입력 문자열에+0000을 강제 추가하는 방식은 의도한 대로 작동하며, 코드베이스 전반에서 TZ 정보(“Z” 또는 “+09:00” 등)가 포함된 문자열을 처리하는 부분은 아직 존재하지 않음이 확인되었습니다. 따라서 당장 수정은 불필요하지만, 백엔드가 추후 타임존 정보를 포함해 응답할 경우를 대비해 아래 Optional Refactor를 권장드립니다.• 호출 범위 확인
–parseRecruitmentDateString호출은frontend/src/utils/recruitmentPeriodParser.ts파일 내부(28–29행)로 한정되어 있습니다.
• TZ 포함 입력 사용 흔적 없음
– “Z” 또는 “[+-]HH(:?MM)?” 패턴을 사용하는 곳은 전혀 없습니다.제안 Optional Refactor 예시:
const NO_TZ_REGEX = /^\d{4}\.\d{2}\.\d{2} \d{2}:\d{2}$/; const WITH_TZ_REGEX = /^\d{4}\.\d{2}\.\d{2} \d{2}:\d{2}\s*(Z|[+-]\d{2}(?::?\d{2})?)$/i; export const parseRecruitmentDateString = (s: string): Date => { if (!NO_TZ_REGEX.test(s) && !WITH_TZ_REGEX.test(s)) { throw new Error( '유효하지 않은 날짜 형식입니다. 형식은 "YYYY.MM.DD HH:mm" 이어야 합니다.' ); } const normalized = WITH_TZ_REGEX.test(s) ? s : s + ' +0000'; const date = parse(normalized, 'yyyy.MM.dd HH:mm X', new Date()); if (!isValid(date)) { throw new Error( '유효하지 않은 날짜 형식입니다. 형식은 "YYYY.MM.DD HH:mm" 이어야 합니다.' ); } return date; };추가 아키텍처 제안
• API에서 ISO 8601(2025-08-17T07:00:00.000Z) 형식의start/end필드를 별도 제공하면, 방어 코드 대부분을 제거하고 TZ 관련 오류를 근본적으로 줄일 수 있습니다. 필요 시 마이그레이션 가이드 정리 지원 가능합니다.frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/Calendar/Calendar.styles.ts (4)
4-9: 색상 토큰 분리 잘 했습니다반복되는 색상 값을 토큰으로 추출해 가독성과 일관성이 좋아졌습니다. theme 연동이 가능하다면 추후 theme 변수로도 매핑해 재사용 범위를 넓힐 수 있겠습니다.
20-24: 매직 넘버 상수화 권장: 반경, 시간 패널 폭 등가이드에 따라 매직 넘버를 상수로 추출하면 의도가 더 명확해지고 추후 조정도 쉬워집니다. 예: border-radius(6), time panel width(120).
적용 예시:
import styled, { css } from 'styled-components'; const primary = 'rgba(255, 84, 20, 0.8)'; const primaryHover = 'rgba(255, 84, 20, 0.95)'; const white = '#fff'; const gray = 'rgba(0,0,0,0.5)'; const inputBg = 'rgba(0,0,0,0.05)'; +const RADIUS = 6; +const TIME_PANEL_WIDTH = 120; /* 재사용 블록 */ const selected = css` background-color: ${primary} !important; color: ${white} !important; `; const selectedHover = css` background-color: ${primaryHover}; color: ${white}; `; const cellBase = css` - border-radius: 6px; + border-radius: ${RADIUS}px; transition: background-color 0.08s ease, color 0.08s ease; `; /* 달력/시간 레이아웃 */ .react-datepicker__month-container { flex: 1 1 auto; } .react-datepicker__time-container { - flex: 0 0 120px; + flex: 0 0 ${TIME_PANEL_WIDTH}px; border-left: 1px solid rgba(0, 0, 0, 0.08); background: ${white}; }Also applies to: 46-54
147-151: 반복되는 hover 색상도 토큰화하면 더 깔끔합니다
rgba(255, 84, 20, 0.12)값이 여러 곳에서 반복됩니다. hoverBg 같은 토큰으로 추출을 추천합니다.예시:
const inputBg = 'rgba(0,0,0,0.05)'; +const hoverBg = 'rgba(255, 84, 20, 0.12)'; ... .react-datepicker__day:hover { - background: rgba(255, 84, 20, 0.12); + background: ${hoverBg}; }
122-140: 입력 포커스 스타일은 대비 과도 가능성 — 가독성 개선 제안input focus에 선택 상태 스타일을 그대로 적용하면 텍스트/배경 대비가 과해져 읽기 어려울 수 있습니다. focus에는 테두리/아웃라인 중심으로 강조하는 쪽이 무난합니다.
.react-datepicker__input-container input { ... &:focus { outline: none; - ${selected}; + box-shadow: 0 0 0 2px ${primary}; + background: ${white}; + color: inherit; } }frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.tsx (2)
41-48: externalApplicationUrl 빈 문자열 전송은 기존 값을 덮어쓸 위험 — undefined로 직렬화 생략 권장
''(빈 문자열)로 보내면 서버가 해당 필드를 빈 값으로 업데이트할 수 있습니다. 의도치 않은 데이터 손실을 막으려면 값이 없을 때는undefined로 두거나, 아예 필드를 제거하는 편이 안전합니다.적용 예시(간단안):
- externalApplicationUrl: clubDetail.externalApplicationUrl ?? '', + externalApplicationUrl: clubDetail.externalApplicationUrl ?? undefined,혹은 조건부 병합:
const updatedData = { id: clubDetail.id, recruitmentStart: recruitmentStart?.toISOString(), recruitmentEnd: recruitmentEnd?.toISOString(), recruitmentTarget, description, ...(clubDetail.externalApplicationUrl != null && { externalApplicationUrl: clubDetail.externalApplicationUrl, }), };API가
undefined를 생략으로 취급하는지 확인 부탁드립니다. 필요 시 타입도string | undefined로 조정해야 합니다.
41-45: toISOString(UTC) 전송 — 서버 기대 포맷 확인 권장프론트는 UTC ISO로 전송하고, 서버는 타임존 없는 문자열을 응답한다고 하셨습니다. 양방향 포맷이 불일치하면 변환/표시 상의 엣지 케이스가 남을 수 있습니다. 서버가 ISO 8601(UTC)로 저장/응답하도록 동기화하는지 확인 부탁드립니다.
frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/Calendar/Calendar.tsx (3)
22-29: 접근성 향상: 네비게이션 버튼에 aria-label 추가 제안키보드/스크린리더 사용자를 위해 명시적 레이블을 부여하세요.
<button onClick={decreaseMonth} disabled={prevMonthButtonDisabled} className='react-datepicker__navigation--custom react-datepicker__navigation--previous--custom' onMouseDown={(e) => e.preventDefault()} + aria-label='이전 달' > ... <button onClick={increaseMonth} disabled={nextMonthButtonDisabled} className='react-datepicker__navigation--custom react-datepicker__navigation--next--custom' onMouseDown={(e) => e.preventDefault()} + aria-label='다음 달' >Also applies to: 34-41
74-84: 시간 포맷/입력 방지 타입 명시로 일관성 및 타입 안정성 개선
- time 리스트 표현을 24시간제로 고정하려면
timeFormat='HH:mm'추가 권장(입력창 포맷과 일치).onChangeRaw의 파라미터 any 지양: 시그니처에 맞춰React.SyntheticEvent<HTMLInputElement>로 지정.- 매직 넘버(30) 상수화도 가이드에 부합합니다.
<DatePicker locale={ko} selected={recruitmentStart} onChange={handleStartChange} showTimeSelect - timeIntervals={30} + timeIntervals={30} + timeFormat='HH:mm' timeCaption='시간' dateFormat='yyyy년 MM월 dd일 (eee) HH:mm' shouldCloseOnSelect={false} popperPlacement='bottom-start' renderCustomHeader={(props) => <CustomHeader {...props} />} - onChangeRaw={(e: any) => e.preventDefault()} + onChangeRaw={(e: React.SyntheticEvent<HTMLInputElement>) => e.preventDefault()} /> ... <DatePicker locale={ko} selected={recruitmentEnd} onChange={handleEndChange} showTimeSelect - timeIntervals={30} + timeIntervals={30} + timeFormat='HH:mm' timeCaption='시간' dateFormat='yyyy년 MM월 dd일 (eee) HH:mm' shouldCloseOnSelect={false} popperPlacement='bottom-start' renderCustomHeader={(props) => <CustomHeader {...props} />} - onChangeRaw={(e: any) => e.preventDefault()} + onChangeRaw={(e: React.SyntheticEvent<HTMLInputElement>) => e.preventDefault()} />추가로, 가이드 준수를 위해 파일 상단에
const TIME_INTERVAL_MIN = 30;정의 후timeIntervals={TIME_INTERVAL_MIN}로 치환을 권장합니다.Also applies to: 88-98
85-85: 네이밍 오타로 보임: Tidle → Tilde 또는 Delimiter
~구분자 텍스트라면Tilde혹은Delimiter가 더 명확합니다. 스타일/사용처 동시 리네임을 제안합니다.변경 예시(스타일/사용부 동시):
-<Styled.Tidle>~</Styled.Tidle> +<Styled.Delimiter>~</Styled.Delimiter>styles.ts 내 export도 함께 변경 필요합니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (4)
frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.tsx(2 hunks)frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/Calendar/Calendar.styles.ts(3 hunks)frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/Calendar/Calendar.tsx(4 hunks)frontend/src/utils/recruitmentPeriodParser.ts(1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
frontend/**/*.{ts,tsx}
📄 CodeRabbit Inference Engine (frontend/.cursorrules)
frontend/**/*.{ts,tsx}: Replace magic numbers with named constants for clarity.
Replace complex or nested ternary operators with if/else statements or IIFEs for readability.
Assign complex boolean conditions to named variables.
Use consistent return types for similar functions and hooks.
Avoid hidden side effects; functions should only perform actions implied by their signature (Single Responsibility Principle).
Use unique, descriptive names for custom wrappers and functions to avoid ambiguity.
Define constants near related logic or ensure names link them clearly.
Files:
frontend/src/utils/recruitmentPeriodParser.tsfrontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.tsxfrontend/src/pages/AdminPage/tabs/RecruitEditTab/components/Calendar/Calendar.styles.tsfrontend/src/pages/AdminPage/tabs/RecruitEditTab/components/Calendar/Calendar.tsx
frontend/**/*.tsx
📄 CodeRabbit Inference Engine (frontend/.cursorrules)
frontend/**/*.tsx: Abstract complex logic/interactions into dedicated components or higher-order components (HOCs).
Separate significantly different conditional UI/logic into distinct components.
Colocate simple, localized logic or use inline definitions to reduce context switching.
Choose field-level or form-level cohesion based on form requirements.
Break down broad state management into smaller, focused hooks or contexts.
Use component composition instead of props drilling.
Files:
frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.tsxfrontend/src/pages/AdminPage/tabs/RecruitEditTab/components/Calendar/Calendar.tsx
🧠 Learnings (1)
📚 Learning: 2025-07-19T05:09:10.702Z
Learnt from: CR
PR: Moadong/moadong#0
File: frontend/.cursorrules:0-0
Timestamp: 2025-07-19T05:09:10.702Z
Learning: Applies to frontend/**/*.{ts,tsx} : Replace magic numbers with named constants for clarity.
Applied to files:
frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/Calendar/Calendar.styles.ts
🔇 Additional comments (6)
frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/Calendar/Calendar.styles.ts (2)
10-24: 재사용 CSS 블록(selected/hover/base) 도입 좋습니다선택/호버/공통 속성을 분리해 유지보수성이 좋아졌습니다.
199-218: 시간 리스트 항목의 선택 상태 구현 적합중첩 셀렉터(
&--selected)로.react-datepicker__time-list-item--selected를 정확히 커버합니다. hover/selected 간 전환도 일관적입니다.frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.tsx (1)
30-33: 초기값 now 폴백으로 null 방지 — 사용자 입력 보존 방식도 적절
prev ?? initial ?? now패턴으로 재랜더/재요청 시 사용자 입력을 덮어쓰지 않으면서 null을 회피한 점 좋습니다.frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/Calendar/Calendar.tsx (3)
26-27: onMouseDown preventDefault로 포커스/블러 문제 예방 적절헤더 네비게이션 클릭 시 input 포커스가 잦아들며 캘린더가 닫히는 이슈를 예방합니다. 👍
Also applies to: 37-38
53-55: 시작일 변경 시 종료일 자동 보정 로직 적합역전 상태를 최소한의 규칙으로 간결히 해소합니다. 퍼포먼스/의존성도 적절합니다.
63-65: 종료일 변경 시 시작일 자동 보정 로직 적합대칭적인 보정 로직으로 일관성이 좋습니다.
suhyun113
left a comment
There was a problem hiding this comment.
캘린더 시간 설정에서 파싱 이슈도 해결하고 더 나은 API 제안까지! 대단합니다ㅏㅏ
수고하셨어요~
#️⃣연관된 이슈
📝작업 내용
이번 PR에서는 관리자 페이지의 모집 기간 설정 캘린더에 대한 대대적인 UX 개선과 안정성 향상 작업을 진행했습니다.
📅 캘린더 UX/UI 개선
날짜 선택 로직 개선
minDate,maxDate)을 제거했습니다.시간 선택 기능 추가
showTimeSelect옵션을 활성화했습니다.날짜 표시 형식 변경
YYYY.MM.DD에서YYYY년 MM월 DD일 HH:mm과 같이 더 직관적인 한글 형식으로 변경했습니다.🐛 시간대(Timezone) 오류 수정
논의하고 싶은 부분(선택)
시간대 정보 누락: API가 시간대 정보가 없는 문자열(e.g., YYYY.MM.DD HH:mm)을 반환하여, 프론트엔드에서 UTC로 강제 해석하는 방어 코드가 들어가 있습니다.
데이터 구조: 시작일과 종료일이 ~로 연결된 단일 문자열로 전달되어, 불필요한 문자열 분리 및 파싱 작업이 필요합니다.
따라서 아래와 같이 각 필드를 분리하고, 값은 완전한 ISO 8601 형식으로 통일하는 것을 제안합니다.
AS-IS (현재)
{ "recruitmentPeriod": "2025.08.17 07:00 ~ 2025.08.20 10:00" }TO-BE (제안)
{ "recruitmentStart": "2025-08-17T07:00:00.000Z", "recruitmentEnd": "2025-08-20T10:00:00.000Z" }🫡 참고사항
기존에 시작일과 종료일 사이의 날짜 범위가 하이라이트되던 UI가 각 날짜만 개별적으로 선택/표시되도록 변경되었습니다.
Summary by CodeRabbit