-
Notifications
You must be signed in to change notification settings - Fork 0
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
[feat] 카카오 , 구글 로그인 구현 완료 #78
Conversation
📝 WalkthroughWalkthrough이번 PR에서는 불필요한 파일과 함수들을 삭제하고, 소셜 로그인 기능의 구성을 개선하는 변경 사항이 포함되었습니다. 구체적으로, 메인 페이지와 Kakao 로그인 기능을 담당하던 파일이 제거되었으며, 로그인 제공자 설정이 수정되었습니다. 또한, 인증 설정을 위한 새로운 구성 파일과 환경 변수 유효성 검증 함수를 제공하는 유틸리티 파일이 추가되었고, 로그인 버튼 컴포넌트는 버튼 이벤트에서 링크 기반 네비게이션으로 변경되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant U as 사용자
participant LB as LoginButton 컴포넌트
participant AC as AuthConfig 모듈
participant SP as 소셜 인증 제공자
U->>LB: 소셜 로그인 링크 클릭
LB->>AC: getAuthUrl[provider]() 호출
AC-->>LB: 인증 URL 반환
LB->>U: Link를 통한 사용자 리디렉션
U->>SP: 소셜 로그인 프로세스 시작
Suggested reviewers
Tip ⚡🧪 Multi-step agentic review comment chat (experimental)
📜 Recent review detailsConfiguration used: .coderabbit.yml 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
Documentation and Community
|
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: 0
🧹 Nitpick comments (1)
src/features/auth/login/model/providers/google.ts (1)
1-12
: 구글 로그인 기능 구현이 완료되었습니다.구글 로그인 기능이 잘 구현되었습니다. 환경 변수 검증 및 오류 처리도 잘 되어 있습니다. 다만 몇 가지 개선 사항을 제안합니다:
NEXT_PUBLIC_GOOGLE_REDIRECT_URI_LOCAL
이라는 이름은 로컬 개발 환경만을 위한 것으로 보입니다. 프로덕션 환경을 위한 리다이렉트 URI도 필요할 것 같습니다.- 콘솔 오류 메시지만으로는 사용자가 문제를 인지하기 어렵습니다. 사용자에게 표시할 오류 처리 방법을 추가하는 것이 좋겠습니다.
export const googleLogin = () => { const clientId = process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID; - const redirectUri = process.env.NEXT_PUBLIC_GOOGLE_REDIRECT_URI_LOCAL; + // 환경에 따라 적절한 리다이렉트 URI 사용 + const redirectUri = process.env.NODE_ENV === 'production' + ? process.env.NEXT_PUBLIC_GOOGLE_REDIRECT_URI + : process.env.NEXT_PUBLIC_GOOGLE_REDIRECT_URI_LOCAL; if (!clientId || !redirectUri) { console.error('구글 로그인 설정이 잘못되었습니다.'); + // 사용자에게 오류 표시를 위한 처리 추가 + // 예: 오류 상태 설정 또는 토스트 메시지 표시 return; } const googleAuthUrl = `https://accounts.google.com/o/oauth2/v2/auth?client_id=${clientId}&redirect_uri=${redirectUri}&response_type=code&scope=email profile`; window.location.href = googleAuthUrl; };
📜 Review details
Configuration used: .coderabbit.yml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
src/app/main/page.tsx
(0 hunks)src/features/auth/login/config/loginProviderConfig.ts
(2 hunks)src/features/auth/login/model/providers/google.ts
(1 hunks)
💤 Files with no reviewable changes (1)
- src/app/main/page.tsx
🔇 Additional comments (2)
src/features/auth/login/config/loginProviderConfig.ts (2)
1-1
: 구글 로그인 import 구문이 추가되었습니다.구글 로그인 함수를 가져오는 import 구문이 잘 추가되었습니다.
27-27
: 구글 로그인 함수가 올바르게 설정되었습니다.
loginFn
이 카카오 로그인에서 구글 로그인으로 정상적으로 변경되었습니다. 주석에 명시된 대로 변경이 이루어졌습니다.참고: 35번 라인의 Apple 로그인 함수도 향후
appleLogin
으로 변경될 예정인 것으로 보입니다. 이 부분도 구현이 완료되면 업데이트가 필요합니다.
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.
넵넵 확인했습니다. 👍👍👍
로컬에서 로그인 확인 가능하던가요??
네 구글, 카카오 모두 토큰 정상적으로 저장 되고있습니다 😊 |
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.
수고 많으셨어요!! 👍
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: 2
🧹 Nitpick comments (5)
src/features/auth/login/ui/LoginButton.tsx (2)
42-42
: console.log 제거 필요디버깅 목적의 console.log는 배포 전에 제거하는 것이 좋습니다. 프로덕션 환경에서는 로그가 남지 않도록 해주세요.
- console.log('authUrl : ' + authUrl);
45-47
: 버튼에서 링크로 적절하게 변경버튼 요소에서 앵커(
<a>
) 요소로 변경하여 직접 인증 URL로 이동하도록 구현한 것은 적절합니다. 하지만 웹 접근성을 위해 다음과 같은 속성을 추가하는 것이 좋습니다:<a href={authUrl} + rel="noopener noreferrer" + target="_blank" className={`flex w-full items-center justify-center rounded-md ${config.bgColor} px-4 py-[15px] ${config.textColor}`} >src/features/auth/login/config/authConfig.ts (3)
34-48
: 리다이렉트 URL 로직 개선 가능현재
getRedirectUrl
함수는 switch 문을 사용하여 각 제공자별로 리다이렉트 URL을 반환합니다. 코드를 더 간결하게 만들기 위해 직접 해당 속성에 접근하는 방식으로 리팩토링할 수 있습니다.export const getRedirectUrl = (provider: SocialProvider): string => { - switch (provider) { - case 'KAKAO': - return providerEnvConfig.KAKAO.redirectUrl; - case 'GOOGLE': - return providerEnvConfig.GOOGLE.redirectUrl; - case 'APPLE': - return providerEnvConfig.APPLE.redirectUrl; - default: - return ''; - } + return providerEnvConfig[provider]?.redirectUrl || ''; };
50-77
: 인증 URL 생성 함수 중복 코드 개선각 제공자별 인증 URL 생성 코드에 중복이 있습니다. 공통 로직을 추출하여 코드 중복을 줄이고 유지보수성을 높일 수 있습니다.
export const getAuthUrl = { + _createUrl: (baseUrl: string, params: Record<string, string>) => { + const queryParams = Object.entries(params) + .map(([key, value]) => + key === 'scope' && value ? + `${key}=${encodeURIComponent(value)}` : + `${key}=${value}` + ) + .join('&'); + return `${baseUrl}?${queryParams}`; + }, KAKAO: () => { const { restApiKey } = providerEnvConfig.KAKAO; const redirectUrl = getRedirectUrl('KAKAO'); const scope = providerEnvConfig.KAKAO.scope; - return `${AUTH_BASE_URLS.KAKAO}?client_id=${restApiKey}&redirect_uri=${redirectUrl}&response_type=code&scope=${scope}`; + return getAuthUrl._createUrl(AUTH_BASE_URLS.KAKAO, { + client_id: restApiKey, + redirect_uri: redirectUrl, + response_type: 'code', + scope + }); }, GOOGLE: () => { const { clientId } = providerEnvConfig.GOOGLE; const redirectUrl = getRedirectUrl('GOOGLE'); const scope = providerEnvConfig.GOOGLE.scope; - return `${AUTH_BASE_URLS.GOOGLE}?client_id=${clientId}&redirect_uri=${redirectUrl}&response_type=code&scope=${encodeURIComponent(scope)}`; + return getAuthUrl._createUrl(AUTH_BASE_URLS.GOOGLE, { + client_id: clientId, + redirect_uri: redirectUrl, + response_type: 'code', + scope + }); }, // APPLE도 동일하게 수정 };
1-77
: 전체적인 구조와 보안 고려사항전반적인 파일 구조와 인증 URL 생성 로직은 잘 구현되었습니다. 그러나 다음 사항을 고려해 보세요:
현재 코드에서는
scope
값 외에는 URL 파라미터에 대한 인코딩이 되지 않고 있습니다. 모든 URL 파라미터에 대해encodeURIComponent
를 적용하는 것이 안전합니다.각 제공자의 상태(state) 파라미터를 추가하여 CSRF 공격을 방지하는 것이 좋습니다. 특히 OAuth 인증에서는 이 값을 사용하여 요청과 응답의 일치 여부를 확인합니다.
각 소셜 로그인 별로 오류 처리 로직을 추가하는 것을 고려해보세요.
📜 Review details
Configuration used: .coderabbit.yml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
src/features/auth/login/config/authConfig.ts
(1 hunks)src/features/auth/login/config/loginProviderConfig.ts
(1 hunks)src/features/auth/login/model/providers/kakao.ts
(0 hunks)src/features/auth/login/ui/LoginButton.tsx
(2 hunks)
💤 Files with no reviewable changes (1)
- src/features/auth/login/model/providers/kakao.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- src/features/auth/login/config/loginProviderConfig.ts
🔇 Additional comments (3)
src/features/auth/login/ui/LoginButton.tsx (2)
8-8
: 적절한 import 추가
getAuthUrl
함수를 import 하여 소셜 로그인 URL을 가져오는 패턴으로 변경한 것은 좋은 접근 방식입니다.
40-41
: 소셜 로그인 URL 생성 로직 구현소셜 로그인 제공자에 따라 동적으로 인증 URL을 생성하는 방식으로 변경한 것은 좋습니다. 이렇게 함으로써 각 제공자별 로그인 로직이 분리되어 유지보수성이 향상됩니다.
src/features/auth/login/config/authConfig.ts (1)
1-11
: 인증 URL 상수 정의 적절각 소셜 로그인 제공자별 인증 기본 URL을 상수로 정의한 것은 좋은 접근 방식입니다. 코드의 가독성과 유지보수성이 향상됩니다.
/** | ||
* 각 소셜 로그인 제공자별 환경 변수 설정 | ||
*/ | ||
export const providerEnvConfig = { | ||
KAKAO: { | ||
restApiKey: process.env.NEXT_PUBLIC_KAKAO_REST_API_KEY || '', | ||
redirectUrl: process.env.NEXT_PUBLIC_KAKAO_REDIRECT_URI_LOCAL || '', | ||
scope: '', | ||
}, | ||
GOOGLE: { | ||
clientId: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID || '', | ||
redirectUrl: process.env.NEXT_PUBLIC_GOOGLE_REDIRECT_URI_LOCAL || '', | ||
scope: 'email profile', | ||
}, | ||
APPLE: { | ||
clientId: process.env.NEXT_PUBLIC_APPLE_CLIENT_ID || '', | ||
redirectUrl: process.env.NEXT_PUBLIC_APPLE_REDIRECT_URI_LOCAL || '', | ||
scope: 'email profile', | ||
}, | ||
}; |
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.
🛠️ Refactor suggestion
환경 변수 설정 및 예외 처리
환경 변수를 통해 설정 값들을 관리하는 것은 좋습니다. 하지만 환경 변수가 없을 경우 빈 문자열(''
)로 설정하고 있어, 이로 인해 런타임 에러가 발생할 수 있습니다. 환경 변수가 없을 때의 예외 처리를 추가하고, 빈 값이면 경고를 표시하는 로직을 고려해보세요.
export const providerEnvConfig = {
KAKAO: {
restApiKey: process.env.NEXT_PUBLIC_KAKAO_REST_API_KEY || '',
redirectUrl: process.env.NEXT_PUBLIC_KAKAO_REDIRECT_URI_LOCAL || '',
scope: '',
},
// ... 다른 제공자들
};
+ // 환경 변수 유효성 검사
+ const validateEnvConfig = () => {
+ if (!providerEnvConfig.KAKAO.restApiKey) {
+ console.warn('카카오 REST API Key가 설정되지 않았습니다.');
+ }
+ if (!providerEnvConfig.KAKAO.redirectUrl) {
+ console.warn('카카오 리다이렉트 URL이 설정되지 않았습니다.');
+ }
+ // 구글, 애플도 유사하게 체크
+ };
+
+ // 앱 초기화 시 유효성 검사 실행
+ validateEnvConfig();
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
/** | |
* 각 소셜 로그인 제공자별 환경 변수 설정 | |
*/ | |
export const providerEnvConfig = { | |
KAKAO: { | |
restApiKey: process.env.NEXT_PUBLIC_KAKAO_REST_API_KEY || '', | |
redirectUrl: process.env.NEXT_PUBLIC_KAKAO_REDIRECT_URI_LOCAL || '', | |
scope: '', | |
}, | |
GOOGLE: { | |
clientId: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID || '', | |
redirectUrl: process.env.NEXT_PUBLIC_GOOGLE_REDIRECT_URI_LOCAL || '', | |
scope: 'email profile', | |
}, | |
APPLE: { | |
clientId: process.env.NEXT_PUBLIC_APPLE_CLIENT_ID || '', | |
redirectUrl: process.env.NEXT_PUBLIC_APPLE_REDIRECT_URI_LOCAL || '', | |
scope: 'email profile', | |
}, | |
}; | |
/** | |
* 각 소셜 로그인 제공자별 환경 변수 설정 | |
*/ | |
export const providerEnvConfig = { | |
KAKAO: { | |
restApiKey: process.env.NEXT_PUBLIC_KAKAO_REST_API_KEY || '', | |
redirectUrl: process.env.NEXT_PUBLIC_KAKAO_REDIRECT_URI_LOCAL || '', | |
scope: '', | |
}, | |
GOOGLE: { | |
clientId: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID || '', | |
redirectUrl: process.env.NEXT_PUBLIC_GOOGLE_REDIRECT_URI_LOCAL || '', | |
scope: 'email profile', | |
}, | |
APPLE: { | |
clientId: process.env.NEXT_PUBLIC_APPLE_CLIENT_ID || '', | |
redirectUrl: process.env.NEXT_PUBLIC_APPLE_REDIRECT_URI_LOCAL || '', | |
scope: 'email profile', | |
}, | |
}; | |
// 환경 변수 유효성 검사 | |
const validateEnvConfig = () => { | |
if (!providerEnvConfig.KAKAO.restApiKey) { | |
console.warn('카카오 REST API Key가 설정되지 않았습니다.'); | |
} | |
if (!providerEnvConfig.KAKAO.redirectUrl) { | |
console.warn('카카오 리다이렉트 URL이 설정되지 않았습니다.'); | |
} | |
// 구글, 애플도 유사하게 체크 | |
}; | |
// 앱 초기화 시 유효성 검사 실행 | |
validateEnvConfig(); |
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: 0
🧹 Nitpick comments (1)
src/features/auth/login/ui/LoginButton.tsx (1)
43-43
: 프로덕션 코드에서 콘솔 로그 제거 필요.디버깅 목적의 콘솔 로그는 개발 과정에서는 유용하지만, 프로덕션 배포 전에 제거하는 것이 좋습니다.
- console.log('authUrl : ' + authUrl);
📜 Review details
Configuration used: .coderabbit.yml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
src/features/auth/login/ui/LoginButton.tsx
(2 hunks)
🔇 Additional comments (4)
src/features/auth/login/ui/LoginButton.tsx (4)
4-4
: Link 컴포넌트 도입으로 네비게이션 기능 개선.Next.js의 Link 컴포넌트를 사용하여 사용자를 인증 URL로 리다이렉션하는 좋은 접근 방식입니다. 이는 OAuth 인증 흐름에 적합합니다.
9-9
: 인증 URL 설정을 별도 파일로 분리한 점이 좋습니다.인증 URL 로직을 별도의 설정 파일로 분리하는 것은 관심사의 분리(separation of concerns) 원칙을 잘 반영한 설계입니다.
41-42
: 동적 인증 URL 생성 로직 구현.제공자(provider)에 따라 적절한 인증 URL을 동적으로 가져오는 방식으로 잘 구현되었습니다.
46-48
: 버튼에서 Link 컴포넌트로의 전환.기존 버튼 대신 Link 컴포넌트를 사용하여 소셜 로그인 흐름을 구현한 것은 좋은 개선입니다. 이러한 접근 방식은 OAuth 인증 흐름에 더 적합하며, 사용자 경험을 향상시킵니다.
Also applies to: 52-52
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
🧹 Nitpick comments (1)
src/features/auth/login/ui/LoginButton.tsx (1)
44-46
: 버튼에서 Link 컴포넌트로 마이그레이션.기존 버튼에서
next/link
의 Link 컴포넌트로 변경한 것은 적절한 선택입니다. 이는 다음과 같은 이점을 제공합니다:
- 클라이언트 측 라우팅 최적화
- 사전 페이지 로딩 지원
- 접근성 향상
다만, Link 컴포넌트에 추가할 수 있는 다음 속성들을 고려해볼 수 있습니다:
<Link href={authUrl} + prefetch={false} + target="_blank" + rel="noopener noreferrer" className={`flex w-full items-center justify-center rounded-md ${config.bgColor} px-4 py-[15px] ${config.textColor}`} >
prefetch={false}
: 소셜 로그인 페이지는 미리 로드할 필요가 없습니다.target="_blank"
: 로그인 페이지를 새 탭에서 열어 기존 상태를 유지할 수 있습니다.rel="noopener noreferrer"
: 새 탭 열 때의 보안 취약점 방지를 위한 속성입니다.
📜 Review details
Configuration used: .coderabbit.yml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
src/features/auth/login/config/authConfig.ts
(1 hunks)src/features/auth/login/ui/LoginButton.tsx
(2 hunks)src/features/auth/login/utils/envVaild.ts
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- src/features/auth/login/config/authConfig.ts
🔇 Additional comments (5)
src/features/auth/login/ui/LoginButton.tsx (4)
4-4
:next/link
컴포넌트 활용으로 네비게이션 개선.Link 컴포넌트를 가져와서 소셜 로그인 버튼을 클릭하여 인증 URL로 이동하는 방식으로 변경한 점이 좋습니다. 이는 내비게이션에 최적화된 Next.js의 권장 방식입니다.
9-9
: 인증 URL 구성 분리로 코드 모듈화 개선.인증 URL을 가져오는 로직을 별도의
authConfig
파일로 분리하여 코드 모듈화와 관심사 분리를 잘 구현했습니다. 이렇게 함으로써 소셜 로그인 설정이 변경되더라도 컴포넌트 수정 없이 설정 파일만 수정하면 되므로 유지보수성이 향상됩니다.
41-41
: 제공자별 인증 URL 동적 설정 구현.
getAuthUrl[provider]()
방식으로 동적으로 소셜 로그인 제공자에 맞는 인증 URL을 가져오는 접근 방식이 효율적입니다. 이는 코드 중복을 줄이고 새로운 로그인 제공자 추가 시 확장성을 제공합니다.
50-50
: Link 컴포넌트 구조 변경 완료.컴포넌트 구조를 일관되게 유지하며 닫는 태그까지 적절하게 변경하였습니다.
src/features/auth/login/utils/envVaild.ts (1)
1-6
: 환경 변수 유효성 검사 함수가 잘 구현되었습니다.환경 변수가 정의되어 있는지 확인하고 적절한 오류 메시지를 표시하는 함수입니다. 코드가 간결하고 목적에 맞게 잘 작성되었습니다.
export function optional(value: string | undefined, defaultValue: string = '') { | ||
if (value === undefined) { | ||
throw new Error('scope가 설정되지 않았습니다'); | ||
} | ||
return value || defaultValue; | ||
} |
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.
🛠️ Refactor suggestion
optional
함수의 이름과 동작이 일치하지 않습니다.
함수 이름이 optional
이지만 value
가 undefined
인 경우 오류를 발생시키고 있습니다. 또한 오류 메시지가 "scope가 설정되지 않았습니다"로 특정 사용 사례에 맞춰져 있어 범용적인 유틸리티 함수로 사용하기 어렵습니다.
아래와 같이 수정하는 것을 고려해보세요:
-export function optional(value: string | undefined, defaultValue: string = '') {
- if (value === undefined) {
- throw new Error('scope가 설정되지 않았습니다');
- }
- return value || defaultValue;
+export function optional(value: string | undefined, defaultValue: string = '') {
+ return value || defaultValue;
+}
+
+export function validateScope(scope: string | undefined, defaultValue: string = '') {
+ if (scope === undefined) {
+ throw new Error('scope가 설정되지 않았습니다');
+ }
+ return scope || defaultValue;
}
또는 범용적인 함수로 사용하려면:
-export function optional(value: string | undefined, defaultValue: string = '') {
- if (value === undefined) {
- throw new Error('scope가 설정되지 않았습니다');
- }
- return value || defaultValue;
+export function optional(value: string | undefined, defaultValue: string = '') {
+ return value || defaultValue;
+}
+
+export function requiredWithIdentifier(key: string, value: string | undefined, defaultValue: string = '') {
+ if (value === undefined) {
+ throw new Error(`${key}가 설정되지 않았습니다`);
+ }
+ return value || defaultValue;
}
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
export function optional(value: string | undefined, defaultValue: string = '') { | |
if (value === undefined) { | |
throw new Error('scope가 설정되지 않았습니다'); | |
} | |
return value || defaultValue; | |
} | |
export function optional(value: string | undefined, defaultValue: string = '') { | |
return value || defaultValue; | |
} | |
export function validateScope(scope: string | undefined, defaultValue: string = '') { | |
if (scope === undefined) { | |
throw new Error('scope가 설정되지 않았습니다'); | |
} | |
return scope || defaultValue; | |
} |
export function optional(value: string | undefined, defaultValue: string = '') { | |
if (value === undefined) { | |
throw new Error('scope가 설정되지 않았습니다'); | |
} | |
return value || defaultValue; | |
} | |
export function optional(value: string | undefined, defaultValue: string = '') { | |
return value || defaultValue; | |
} | |
export function requiredWithIdentifier(key: string, value: string | undefined, defaultValue: string = '') { | |
if (value === undefined) { | |
throw new Error(`${key}가 설정되지 않았습니다`); | |
} | |
return value || defaultValue; | |
} |
📌 개요
📋 변경사항
기능
화면
전달사항
✅ 체크사항
Summary by CodeRabbit
신규 기능
리팩토링