-
Couldn't load subscription status.
- Fork 1
커스텀 캘린더 구현 #144
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
base: develop
Are you sure you want to change the base?
커스텀 캘린더 구현 #144
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| name: frontend-dev-deploy | ||
|
|
||
| on: | ||
| workflow_dispatch: # 직접 실행 시에만 동작 | ||
|
|
||
| jobs: | ||
| deploy: | ||
| runs-on: ubuntu-latest | ||
|
|
||
| steps: | ||
| - name: Deploy to EC2 | ||
| uses: appleboy/ssh-action@master | ||
| with: | ||
| host: ${{ secrets.SSH_DEV_HOST }} | ||
| username: ${{ secrets.SSH_DEV_USERNAME }} | ||
| key: ${{ secrets.SSH_DEV_KEY }} | ||
| port: ${{ secrets.SSH_DEV_PORT }} | ||
| script: | | ||
| cd ${{ secrets.EC2_DEV_DEPLOY_DIR }} | ||
| source ~/.bashrc | ||
| source ~/.nvm/nvm.sh | ||
| nvm use 22.16.0 | ||
| git pull origin develop | ||
| yarn install | ||
| yarn build | ||
| pm2 restart ecosystem.config.js --env development --update-env |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| // This file configures the initialization of Sentry on the server. | ||
| // The config you add here will be used whenever the server handles a request. | ||
| // https://docs.sentry.io/platforms/javascript/guides/nextjs/ | ||
|
|
||
| import * as Sentry from '@sentry/nextjs'; | ||
|
|
||
| Sentry.init({ | ||
| dsn: process.env.NEXT_PUBLIC_SENTRY_DSN || '', | ||
|
|
||
| // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control. | ||
| tracesSampleRate: 1, | ||
|
|
||
| // Enable logs to be sent to Sentry | ||
| enableLogs: true, | ||
|
|
||
| // Setting this option to true will print useful information to the console while you're setting up Sentry. | ||
| debug: false, | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| 'use client'; | ||
|
|
||
| import Button from '@components/common/Button'; | ||
| import {URLS} from '@constants/urls'; | ||
| import * as Sentry from '@sentry/nextjs'; | ||
| import {useRouter} from 'next/navigation'; | ||
| import {useEffect} from 'react'; | ||
|
|
||
| export default function GlobalError({error}: {error: Error & {digest?: string}}) { | ||
| const router = useRouter(); | ||
|
|
||
| useEffect(() => { | ||
| Sentry.captureException(error); | ||
| }, [error]); | ||
|
|
||
| const goToMain = () => { | ||
| router.push(`${URLS.wiki}/${URLS.daemoon}`); | ||
| }; | ||
|
|
||
| return ( | ||
| <html> | ||
| <body> | ||
| <h1 className="font-bm text-2xl text-grayscale-800">{error.message}</h1> | ||
| <Button style="primary" size="xs" onClick={goToMain}> | ||
| 메인으로 | ||
| </Button> | ||
| </body> | ||
| </html> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,169 @@ | ||||||
| 'use client'; | ||||||
|
|
||||||
| import {formatDate} from '@utils/date'; | ||||||
| import Image from 'next/image'; | ||||||
| import {JSX, useEffect, useMemo, useRef, useState} from 'react'; | ||||||
| import {twMerge} from 'tailwind-merge'; | ||||||
|
|
||||||
| type CustomCalendarProps = { | ||||||
| value: Date | null; | ||||||
| className?: string; | ||||||
| placeholder?: string; | ||||||
| invalid?: boolean; | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. invalid는 어떤 역할을 하나요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||
| onChange: (date: Date | null) => void; | ||||||
| }; | ||||||
|
|
||||||
| const CustomCalendar = ({value, className, placeholder, invalid, onChange}: CustomCalendarProps) => { | ||||||
| const [isOpen, setIsOpen] = useState(false); | ||||||
| const [displayDate, setDisplayDate] = useState(value || new Date()); | ||||||
| const wrapperRef = useRef<HTMLDivElement>(null); | ||||||
|
|
||||||
| useEffect(() => { | ||||||
| const handleClickOutside = (event: MouseEvent) => { | ||||||
| if (wrapperRef.current && !wrapperRef.current.contains(event.target as Node)) { | ||||||
| setIsOpen(false); | ||||||
| } | ||||||
| }; | ||||||
| document.addEventListener('mousedown', handleClickOutside); | ||||||
| return () => { | ||||||
| document.removeEventListener('mousedown', handleClickOutside); | ||||||
| }; | ||||||
| }, []); | ||||||
|
|
||||||
| const handleDateChange = (day: number) => { | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. handleDateChange를 calendarDays 안에서 선언해서 사용해도 좋을 것 같아요. React Hook useMemo has a missing dependency: 'handleDateChange'. Either include it or remove the dependency array.eslintreact-hooks/exhaustive-deps calendarDays 안에 넣으면 해결될 것 같아요 |
||||||
| const newDate = new Date(displayDate.getFullYear(), displayDate.getMonth(), day); | ||||||
|
|
||||||
| if (value && formatDate(newDate) === formatDate(value)) { | ||||||
| onChange(null); | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이 부분 궁금해요! |
||||||
| } else { | ||||||
| onChange(newDate); | ||||||
| } | ||||||
|
|
||||||
| setIsOpen(false); | ||||||
| }; | ||||||
|
|
||||||
| const changeMonth = (offset: number) => { | ||||||
| setDisplayDate(prev => { | ||||||
| const newDate = new Date(prev.getFullYear(), prev.getMonth() + offset, 1); | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. offset으로 받아서 전 달의 -1, +1 해주어서 계산하는 방식 좋은 것 같아요! |
||||||
| return newDate; | ||||||
| }); | ||||||
| }; | ||||||
|
|
||||||
| const toggleCalendar = () => { | ||||||
| setIsOpen(prev => !prev); | ||||||
| }; | ||||||
|
|
||||||
| const calendarDays = useMemo(() => { | ||||||
| const year = displayDate.getFullYear(); | ||||||
| const month = displayDate.getMonth(); | ||||||
| const firstDayOfMonth = new Date(year, month, 1).getDay(); | ||||||
| const lastDateOfMonth = new Date(year, month + 1, 0).getDate(); | ||||||
| const days: JSX.Element[] = []; | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 여기에 |
||||||
|
|
||||||
| for (let i = 0; i < firstDayOfMonth; i++) { | ||||||
| days.push(<div key={`prev-${i}`} className="h-10 w-10" />); | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 전 달 영역이나 다음 달 영역을 클릭했을 때 달이 이동되어도 좋을 것 같아요. |
||||||
| } | ||||||
|
|
||||||
| for (let day = 1; day <= lastDateOfMonth; day++) { | ||||||
| const date = new Date(year, month, day); | ||||||
| const isToday = formatDate(date) === formatDate(new Date()); | ||||||
| const isSelected = value && value.getFullYear() === year && value.getMonth() === month && value.getDate() === day; | ||||||
|
|
||||||
| const baseClasses = 'flex items-center justify-center h-10 w-10 text-sm rounded-full cursor-pointer select-none'; | ||||||
| const todayClasses = isToday ? 'font-bold bg-grayscale-50' : ''; | ||||||
| const selectedClasses = isSelected | ||||||
| ? 'bg-primary text-white font-bold bg-primary-primary' | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
클래스가 중복으로 들어가 있어서 확인해보니 |
||||||
| : 'text-grayscale-700 active:bg-primary-container md:hover:bg-primary-container'; | ||||||
| const finalClasses = `${baseClasses} ${isSelected ? selectedClasses : `${todayClasses} ${selectedClasses}`}`; | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. final이라서 위의 모든 경우에 해당이 안되는 경우인가 생각했었는데 보니깐 base부터 selected까지 모두 통합한 변수였군요 |
||||||
|
|
||||||
| days.push( | ||||||
| <div key={day} onClick={() => handleDateChange(day)} className={finalClasses}> | ||||||
| {day} | ||||||
| </div>, | ||||||
| ); | ||||||
| } | ||||||
|
|
||||||
| return days; | ||||||
| }, [displayDate, value]); | ||||||
|
|
||||||
| return ( | ||||||
| <div ref={wrapperRef} className="relative w-full"> | ||||||
| <div className="relative"> | ||||||
| <input | ||||||
| type="text" | ||||||
| value={value ? formatDate(value) : ''} | ||||||
| placeholder={placeholder} | ||||||
| readOnly | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 키보드로 날짜 입력을 막고 클릭으로만 날짜를 선택하기 위해 readOnly를 설정하신거죠? 좋습니다 |
||||||
| onClick={toggleCalendar} | ||||||
| className={twMerge( | ||||||
| 'w-full font-pretendard text-base font-normal text-grayscale-800 outline-none placeholder:text-grayscale-lightText focus:border-secondary-400 disabled:border-grayscale-200 disabled:text-grayscale-400', | ||||||
| className, | ||||||
| invalid ? 'border-error-error hover:border-error-error focus:border-error-error' : '', | ||||||
| )} | ||||||
| /> | ||||||
| <button | ||||||
| onClick={toggleCalendar} | ||||||
| className="absolute inset-y-0 right-0 flex items-center pr-3" | ||||||
| aria-label="달력 열기" | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 접근성까지!!🚀🔥🔥 요새 미션으로 배운 내용이라서 반갑네요ㅋㅋㅋㅋ |
||||||
| > | ||||||
| <Image | ||||||
| src={`${process.env.NEXT_PUBLIC_CDN_DOMAIN}/images/calendar-icon.svg`} | ||||||
| width={24} | ||||||
| height={24} | ||||||
| alt="calendar icon" | ||||||
| className="h-6 w-6" | ||||||
| /> | ||||||
| </button> | ||||||
| </div> | ||||||
|
|
||||||
| {isOpen && ( | ||||||
| <div className="absolute top-full z-10 mt-2 w-full rounded-xl bg-white p-4 shadow-lg max-[768px]:p-6"> | ||||||
| <div className="mb-4 flex items-center justify-between"> | ||||||
| <button | ||||||
| onClick={() => changeMonth(-1)} | ||||||
| className="rounded-full p-2 transition-colors active:bg-primary-container md:hover:bg-primary-container" | ||||||
| aria-label="이전 달" | ||||||
| > | ||||||
| <Image | ||||||
| src={`${process.env.NEXT_PUBLIC_CDN_DOMAIN}/images/chevron-left-icon.svg`} | ||||||
| width={24} | ||||||
| height={24} | ||||||
| alt="chevron left icon" | ||||||
| className="h-6 w-6" | ||||||
| /> | ||||||
| </button> | ||||||
| <h2 className="text-center text-lg font-bold text-grayscale-800 max-[768px]:text-sm"> | ||||||
| {`${displayDate.getFullYear()}년 ${displayDate.getMonth() + 1}월`} | ||||||
| </h2> | ||||||
| <button | ||||||
| onClick={() => changeMonth(1)} | ||||||
| className="rounded-full p-2 transition-colors active:bg-primary-container md:hover:bg-primary-container" | ||||||
| aria-label="다음 달" | ||||||
| > | ||||||
| <Image | ||||||
| src={`${process.env.NEXT_PUBLIC_CDN_DOMAIN}/images/chevron-right-icon.svg`} | ||||||
| width={24} | ||||||
| height={24} | ||||||
| alt="chevron right icon" | ||||||
| className="h-6 w-6" | ||||||
| /> | ||||||
| </button> | ||||||
| </div> | ||||||
|
|
||||||
| <div> | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이 div 태그는 어떤 역할을 하나요? 아무 css 속성이 적용되어있지 않아서 질문드립니다. |
||||||
| <div className="mb-2 grid grid-cols-7 gap-1 text-center font-pretendard text-sm text-grayscale-500"> | ||||||
| {['일', '월', '화', '수', '목', '금', '토'].map((day, i) => ( | ||||||
| <div key={day} className={`${i === 0 ? 'text-error-error' : ''} ${i === 6 ? 'text-primary-700' : ''}`}> | ||||||
| {day} | ||||||
| </div> | ||||||
| ))} | ||||||
| </div> | ||||||
| <div className="grid grid-cols-7 place-items-center gap-1">{calendarDays}</div> | ||||||
| </div> | ||||||
| </div> | ||||||
| )} | ||||||
| </div> | ||||||
| ); | ||||||
| }; | ||||||
|
|
||||||
| export default CustomCalendar; | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| // This file configures the initialization of Sentry on the client. | ||
| // The added config here will be used whenever a users loads a page in their browser. | ||
| // https://docs.sentry.io/platforms/javascript/guides/nextjs/ | ||
|
|
||
| import * as Sentry from '@sentry/nextjs'; | ||
|
|
||
| Sentry.init({ | ||
| dsn: process.env.NEXT_PUBLIC_SENTRY_DSN || '', | ||
|
|
||
| // Add optional integrations for additional features | ||
| integrations: [Sentry.replayIntegration()], | ||
|
|
||
| // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control. | ||
| tracesSampleRate: 1, | ||
| // Enable logs to be sent to Sentry | ||
| enableLogs: true, | ||
|
|
||
| // Define how likely Replay events are sampled. | ||
| // This sets the sample rate to be 10%. You may want this to be 100% while | ||
| // in development and sample at a lower rate in production | ||
| replaysSessionSampleRate: 0.1, | ||
|
|
||
| // Define how likely Replay events are sampled when an error occurs. | ||
| replaysOnErrorSampleRate: 1.0, | ||
|
|
||
| // Setting this option to true will print useful information to the console while you're setting up Sentry. | ||
| debug: false, | ||
| }); | ||
|
|
||
| export const onRouterTransitionStart = Sentry.captureRouterTransitionStart; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| import * as Sentry from '@sentry/nextjs'; | ||
|
|
||
| export async function register() { | ||
| if (process.env.NEXT_RUNTIME === 'nodejs') { | ||
| await import('../sentry.server.config'); | ||
| } | ||
| } | ||
|
|
||
| export const onRequestError = Sentry.captureRequestError; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| export const formatDate = (date: Date): string => { | ||
| const year = date.getFullYear(); | ||
| const month = String(date.getMonth() + 1).padStart(2, '0'); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 01, 02 디테일 넘 좋습니다~ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 저는 month가 항상 0부터 시작해서 헷갈리는데 프룬은 구현하면서 안 헷갈리셨을지 궁금해요ㅋㅋㅋㅋ |
||
| const day = String(date.getDate()).padStart(2, '0'); | ||
| return `${year}.${month}.${day}`; | ||
| }; | ||


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.
저희 props 타입을 선언할 때는
interface를 사용하기로 컨벤션을 정했던 것 같아요!