Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .github/workflows/frontend-dev-deploy.yml
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
2 changes: 1 addition & 1 deletion .github/workflows/frontend-prod-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,4 @@ jobs:
git pull origin main
yarn install
yarn build
pm2 restart ecosystem.config.js --update-env
pm2 restart ecosystem.config.js --env production --update-env
7 changes: 7 additions & 0 deletions client/ecosystem.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ module.exports = {
env: {
NODE_ENV: 'production',
},

env_development: {
NODE_ENV: 'development',
},
env_production: {
NODE_ENV: 'production',
},
},
],
};
30 changes: 29 additions & 1 deletion client/next.config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {withSentryConfig} from '@sentry/nextjs';
import type {NextConfig} from 'next';
import {URLS} from '@constants/urls';

Expand Down Expand Up @@ -33,4 +34,31 @@ const nextConfig: NextConfig = {
],
};

export default nextConfig;
export default withSentryConfig(nextConfig, {
// For all available options, see:
// https://www.npmjs.com/package/@sentry/webpack-plugin#options

org: 'crew-wiki',

project: 'crew-wiki',

// Only print logs for uploading source maps in CI
silent: !process.env.CI,

// For all available options, see:
// https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/

// Upload a larger set of source maps for prettier stack traces (increases build time)
widenClientFileUpload: true,

// Uncomment to route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers.
// This can increase your server load as well as your hosting bill.
// Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client-
// side errors will fail.
// tunnelRoute: "/monitoring",

// Automatically tree-shake Sentry logger statements to reduce bundle size
disableLogger: true,
sourcemaps: {deleteSourcemapsAfterUpload: true},
authToken: process.env.SENTRY_AUTH_TOKEN,
});
1 change: 1 addition & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
},
"dependencies": {
"@amplitude/analytics-browser": "^2.11.11",
"@sentry/nextjs": "10",
"@toast-ui/react-editor": "^3.2.3",
"dayjs": "^1.11.13",
"framer-motion": "^11.15.0",
Expand Down
18 changes: 18 additions & 0 deletions client/sentry.server.config.ts
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,
});
30 changes: 30 additions & 0 deletions client/src/app/global-error.tsx
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>
);
}
169 changes: 169 additions & 0 deletions client/src/components/common/CustomCalendar/CustomCalendar.tsx
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 = {
Copy link
Member

Choose a reason for hiding this comment

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

저희 props 타입을 선언할 때는 interface를 사용하기로 컨벤션을 정했던 것 같아요!

value: Date | null;
className?: string;
placeholder?: string;
invalid?: boolean;
Copy link
Contributor

Choose a reason for hiding this comment

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

invalid는 어떤 역할을 하나요?
사실 prop을 true로 넣고 실행해봤는데 아무 변화가 없어서요ㅜㅜ

Copy link
Contributor Author

Choose a reason for hiding this comment

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

invalid는 disabled와 같은 역할이에요! invalid 상태가 되면 input필드 테두리가 error 색으로 변하는데, 이 색이 포커스 상태가 되었을 때와 유사해서 잘 안보일 수도 있을 것 같네욤..! (input 공통 컴포넌트와 동일하게 만들었어요)

에러 상태 시 포커스 상태 시
image image

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) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

handleDateChange를 calendarDays 안에서 선언해서 사용해도 좋을 것 같아요.
calendarDays에서만 사용되는 메서드이고 아래 useMemo의 의존성 배열 경고로 이런 메시지가 나오는데

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);
Copy link
Contributor

Choose a reason for hiding this comment

The 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);
Copy link
Contributor

Choose a reason for hiding this comment

The 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[] = [];
Copy link
Member

Choose a reason for hiding this comment

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

여기에 JSX.Element[] 타입을 추가한 이유는 무엇인가요??


for (let i = 0; i < firstDayOfMonth; i++) {
days.push(<div key={`prev-${i}`} className="h-10 w-10" />);
Copy link
Contributor

Choose a reason for hiding this comment

The 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'
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
? 'bg-primary text-white font-bold bg-primary-primary'
? 'text-white font-bold bg-primary-primary'

클래스가 중복으로 들어가 있어서 확인해보니 bg-primary를 삭제해도 제대로 보이네요!

: 'text-grayscale-700 active:bg-primary-container md:hover:bg-primary-container';
const finalClasses = `${baseClasses} ${isSelected ? selectedClasses : `${todayClasses} ${selectedClasses}`}`;
Copy link
Contributor

Choose a reason for hiding this comment

The 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
Copy link
Contributor

Choose a reason for hiding this comment

The 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="달력 열기"
Copy link
Member

Choose a reason for hiding this comment

The 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>
Copy link
Contributor

Choose a reason for hiding this comment

The 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;
30 changes: 30 additions & 0 deletions client/src/instrumentation-client.ts
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;
9 changes: 9 additions & 0 deletions client/src/instrumentation.ts
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;
6 changes: 6 additions & 0 deletions client/src/utils/date.ts
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');
Copy link
Contributor

Choose a reason for hiding this comment

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

01, 02 디테일 넘 좋습니다~

Copy link
Member

Choose a reason for hiding this comment

The 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}`;
};
Loading