-
Notifications
You must be signed in to change notification settings - Fork 0
문의하기 ui api 연결 (#issue 256) #259
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
Conversation
|
""" Walkthrough이번 변경사항에서는 고객센터의 "문의하기" 기능이 새롭게 추가되었습니다. 문의하기 페이지를 위한 UI 컴포넌트, 스타일, 상태 및 파일 업로드 처리가 구현되었으며, 문의 데이터를 서버로 전송하는 API와 React Query 기반의 커스텀 훅이 도입되었습니다. 라우트 설정을 통해 문의하기 페이지가 접근 가능하도록 추가되었고, 관련 상수 및 타입 정의도 포함되었습니다. 일부 기존 상수의 네이밍 일관성 개선도 이루어졌습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant InquiryPage
participant usePostInquiry
participant postInquiry (API)
participant Server
User->>InquiryPage: 문의 폼 입력 및 파일 첨부
User->>InquiryPage: 문의 제출 버튼 클릭
InquiryPage->>usePostInquiry: mutate(FormData)
usePostInquiry->>postInquiry (API): postInquiry(FormData)
postInquiry (API)->>Server: POST /inquiry (multipart/form-data)
Server-->>postInquiry (API): 응답 반환
postInquiry (API)-->>usePostInquiry: 성공/실패 결과
usePostInquiry-->>InquiryPage: 쿼리 무효화 및 결과 전달
InquiryPage-->>User: 폼 초기화 및 결과 안내
Assessment against linked issues
Poem
✨ Finishing Touches
🪧 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
CodeRabbit Configuration File (
|
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: 4
🧹 Nitpick comments (4)
src/components/mypage/activityLog/ActivityLog.tsx (1)
6-8: 불필요한 Fragment 제거 제안
단일 자식만 반환하므로 Fragment(<>…</>)를 제거하면 코드가 더 간결해집니다.- return ( - <> - <ContentTab $justifyContent='space-around' filter={ACTIVITY_FILTER} /> - </> - ); + return <ContentTab $justifyContent='space-around' filter={ACTIVITY_FILTER} />;src/api/inquiry.api.ts (1)
10-10: 프로덕션 코드에서 console.log 사용 지양프로덕션 환경에서는 console.log 대신 적절한 로깅 메커니즘을 사용하는 것이 좋습니다. 개발 중 디버깅 용도로만 사용하고 나중에 제거하거나 더 나은 로깅 시스템으로 대체하세요.
src/hooks/usePostInquiry.ts (1)
11-11: 특정 쿼리만 무효화하는 것이 더 효율적입니다
queryClient.invalidateQueries()는 모든 쿼리를 무효화합니다. 이는 불필요한 리페칭을 발생시킬 수 있습니다. 대신 관련 쿼리만 무효화하는 것이 좋습니다.onSuccess: () => { - queryClient.invalidateQueries(); + queryClient.invalidateQueries({ queryKey: ['inquiries'] }); },이렇게 하면 '문의하기' 관련 쿼리만 무효화되어 성능이 향상됩니다. 사용 중인 쿼리 키에 따라 적절히 조정해주세요.
src/components/common/customerService/inquiry/Inquiry.styled.ts (1)
1-1: 사용하지 않는 import가 있습니다.
ChevronDownIcon을 가져왔지만 이 파일 내에서 사용하지 않고 있습니다. 실제로는Inquiry.tsx파일에서 사용되는 것으로 보입니다.다음과 같이 수정하세요:
- import { ChevronDownIcon } from '@heroicons/react/24/outline'; import styled, { css } from 'styled-components';🧰 Tools
🪛 ESLint
[error] 1-1: 'ChevronDownIcon' is defined but never used.
(@typescript-eslint/no-unused-vars)
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (15)
src/api/inquiry.api.ts(1 hunks)src/components/common/customerService/inquiry/Inquiry.styled.ts(1 hunks)src/components/common/customerService/inquiry/Inquiry.tsx(1 hunks)src/components/common/header/Header.tsx(1 hunks)src/components/home/searchFiltering/filteringContents/FilteringContents.styled.ts(2 hunks)src/components/home/searchFiltering/filteringContents/FilteringContents.tsx(1 hunks)src/components/home/searchFiltering/filteringContents/filtering/Filtering.styled.ts(2 hunks)src/components/home/searchFiltering/filteringContents/filtering/Filtering.tsx(1 hunks)src/components/mypage/activityLog/ActivityLog.tsx(1 hunks)src/components/mypage/notifications/Notifications.tsx(1 hunks)src/constants/customerService.ts(1 hunks)src/constants/myPageFilter.ts(2 hunks)src/hooks/usePostInquiry.ts(1 hunks)src/models/inquiry.ts(1 hunks)src/routes/AppRoutes.tsx(2 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (6)
src/components/mypage/activityLog/ActivityLog.tsx (2)
src/components/mypage/ContentTab.tsx (1)
ContentTab(19-79)src/constants/myPageFilter.ts (1)
ACTIVITY_FILTER(25-28)
src/components/common/header/Header.tsx (1)
src/constants/routes.ts (1)
ROUTES(1-28)
src/components/mypage/notifications/Notifications.tsx (2)
src/components/mypage/ContentTab.tsx (1)
ContentTab(19-79)src/constants/myPageFilter.ts (1)
NOTIFICATION_FILTER(3-23)
src/hooks/usePostInquiry.ts (2)
src/models/createProject.ts (1)
FormData(6-19)src/api/inquiry.api.ts (1)
postInquiry(3-14)
src/api/inquiry.api.ts (1)
src/api/http.api.ts (1)
httpClient(78-78)
src/routes/AppRoutes.tsx (3)
src/components/common/customerService/inquiry/Inquiry.tsx (1)
Inquiry(13-128)src/constants/routes.ts (1)
ROUTES(1-28)src/components/common/error/QueryErrorBoundary.tsx (1)
QueryErrorBoundary(6-20)
🪛 ESLint
src/hooks/usePostInquiry.ts
[error] 8-8: 'mutate' is assigned a value but never used.
(@typescript-eslint/no-unused-vars)
src/components/common/customerService/inquiry/Inquiry.styled.ts
[error] 1-1: 'ChevronDownIcon' is defined but never used.
(@typescript-eslint/no-unused-vars)
⏰ Context from checks skipped due to timeout of 90000ms (1)
- GitHub Check: Run Chromatic
🔇 Additional comments (32)
src/components/home/searchFiltering/filteringContents/FilteringContents.styled.ts (2)
1-1:css헬퍼가 적절하게 추가되었습니다.styled-components에서
css헬퍼를 추가하는 것은 조건부 스타일링을 위한 좋은 접근 방식입니다. 이를 통해 $isOpen 상태에 따라 스타일을 동적으로 적용할 수 있습니다.
38-56: SVG 회전 애니메이션이 잘 구현되었습니다.$isOpen 상태에 따라 SVG 아이콘을 회전시키는 애니메이션이 적절하게 구현되었습니다. 트랜지션 속성을 사용하여 부드러운 애니메이션 효과를 제공하고 있으며, css 헬퍼를 통해 조건부 스타일링을 깔끔하게 적용했습니다.
다른 필터링 컴포넌트들과의 일관성을 유지하는 접근 방식이 좋습니다.
src/components/home/searchFiltering/filteringContents/FilteringContents.tsx (1)
43-46: $isOpen 상태 프롭이 올바르게 전달되었습니다.SkillTagButton 컴포넌트에 $isOpen 프롭을 추가하여 skillTagButtonToggle 상태를 전달한 것은 좋은 방식입니다. 이를 통해 styled-components에서 조건부 스타일링이 가능해집니다.
버튼의 토글 상태와 시각적 표현이 일치하도록 만든 설계가 적절합니다.
src/components/home/searchFiltering/filteringContents/filtering/Filtering.tsx (1)
76-79: DefaultValueButton에 $isOpen 상태 프롭이 올바르게 추가되었습니다.DefaultValueButton 컴포넌트에 dropDownToggle 상태를 $isOpen 프롭으로 전달한 것은 좋은 접근 방식입니다. 이를 통해 드롭다운의 열림/닫힘 상태에 따라 SVG 아이콘이 회전하게 됩니다.
이 구현은 FilteringContents.tsx의 SkillTagButton과 일관된 패턴을 따르고 있어 UI 경험이 통일되었습니다.
src/components/home/searchFiltering/filteringContents/filtering/Filtering.styled.ts (2)
1-1:css헬퍼가 적절하게 추가되었습니다.styled-components에서
css헬퍼를 가져오는 것은 조건부 스타일링을 위한 좋은 방법입니다. 이를 통해 $isOpen 상태에 따라 스타일을 동적으로 적용할 수 있습니다.
19-37: SVG 회전 애니메이션이 잘 구현되었습니다.$isOpen 상태에 따라 SVG 아이콘을 회전시키는 애니메이션이 잘 구현되었습니다. 300ms의 트랜지션 시간과 ease-in-out 효과를 사용하여 부드러운 애니메이션 효과를 제공하고 있으며, css 헬퍼를 통해 조건부 스타일링을 깔끔하게 적용했습니다.
이 구현은 FilteringContents.styled.ts의 SkillTagButton과 일관된 접근 방식을 사용하여 UI 경험의 일관성을 유지하고 있습니다.
src/constants/myPageFilter.ts (2)
3-23: 상수 네이밍 컨벤션 일치 확인
NOTIFICATION_FILTER상수를 대문자 스네이크 케이스로 변경한 것은 프로젝트 전반의 상수 네이밍 규칙과 잘 부합합니다.
25-28:Details
✅ Verification successful
이전 필터명 잔여 사용처 검증 요청
notificationFilter,activityFilter의 옛 이름이 코드 어딘가에 남아있지 않은지 반드시 확인해주세요.
아래 스크립트를 실행해 잔여 사용처를 검색할 수 있습니다.
🏁 Script executed:
#!/bin/bash # 잔여 필터명 검색 rg -n "notificationFilter" . rg -n "activityFilter" .Length of output: 53
필터명 잔여 사용처 없음 확인됨
notificationFilter와activityFilter에 대한 전수 검색 결과, 코드베이스 내에 잔여 사용처가 없습니다.src/components/mypage/activityLog/ActivityLog.tsx (2)
1-2: 올바른 상수 임포트 확인
ACTIVITY_FILTER를 경로에서 정확히 가져오고 있으므로, 상수명이 변경된 부분이 올바르게 반영되었습니다.
7-7: 필터 전달 로직 적절
ContentTab컴포넌트에filter={ACTIVITY_FILTER}를 전달한 부분이 정상 작동할 것으로 보입니다.src/components/mypage/notifications/Notifications.tsx (2)
1-1: 상수 임포트 일관성 확인
NOTIFICATION_FILTER를 올바른 경로에서 가져오고 있어 네이밍 변경이 정상 반영되었습니다.
6-6: 필터 전달 로직 적절
ContentTab에filter={NOTIFICATION_FILTER}를 전달한 부분도 문제없이 동작할 것으로 보입니다.src/models/inquiry.ts (1)
1-5: 인터페이스가 잘 정의되어 있습니다!인터페이스가 간결하고 문의 폼에 필요한 필드를 명확하게 정의하고 있습니다. 타입 안전성을 제공하여 앱 전체에서 일관된 데이터 구조를 보장합니다.
src/components/common/header/Header.tsx (1)
77-77: 경로 변경이 적절합니다
ROUTES.inquiry경로로 변경한 것은 새로 추가된 문의하기 페이지와 일치시키기 위한 적절한 변경입니다.src/routes/AppRoutes.tsx (2)
26-28: 문의하기 컴포넌트의 지연 로딩 구현이 잘 되었습니다.다른 컴포넌트들과 마찬가지로 Inquiry 컴포넌트를 lazy 로딩으로 구현하여 애플리케이션의 초기 로딩 시간을 최적화했습니다.
138-149: 문의하기 라우트 설정이 적절하게 구현되었습니다.문의하기 페이지가 다음과 같이 적절하게 구성되었습니다:
- 인증된 사용자만 접근할 수 있도록
ProtectRoute로 보호됩니다- 쿼리 오류를 적절히 처리하기 위한
QueryErrorBoundary적용- 레이아웃 컴포넌트 내에 정상적으로 배치
이는 프로젝트의 다른 보호된 라우트와 일관된 패턴을 따르고 있습니다.
src/constants/customerService.ts (3)
1-6: 문의 카테고리 상수가 적절하게 정의되었습니다.문의 카테고리를 객체 배열로 정의하여 재사용성과 유지보수성을 높였습니다. TypeScript의
as const어서션을 사용하여 타입 안전성을 확보한 점이 좋습니다.
8-9: 빈 이미지 상수 정의가 적절합니다.파일 선택이 없을 때 표시할 빈 이미지를 base64 인코딩된 1x1 투명 GIF로 정의한 것은 좋은 접근 방식입니다.
11-14: UI 메시지 상수가 잘 정의되었습니다.카테고리 기본값과 파일 선택 기본 메시지를 객체로 분리하여 관리하는 것은 코드의 가독성과 유지보수성을 높이는 좋은 방법입니다.
src/components/common/customerService/inquiry/Inquiry.tsx (6)
1-12: 필요한 의존성들을 적절히 임포트했습니다.필요한 컴포넌트, 함수, 타입 및 상수들을 적절하게 임포트하고 있습니다.
13-24: 상태 관리가 잘 구현되었습니다.폼의 다양한 상태(카테고리, 제목, 내용, 파일)를 React의
useState를 사용하여 적절하게 관리하고 있습니다.
50-59: 이벤트 핸들러 함수들이 잘 구현되었습니다.카테고리 선택과 파일 변경을 처리하는 이벤트 핸들러가 명확하고 효과적으로 구현되었습니다.
61-105: 폼 상단 UI 구현이 적절합니다.헤더, 카테고리 선택 드롭다운, 제목 입력 필드가 깔끔하게 구성되었습니다.
106-120: 콘텐츠 및 파일 업로드 UI가 잘 구현되었습니다.텍스트 영역과 파일 업로드 기능이 명확하게 구현되었습니다.
78-83: 히든 인풋 필드 사용이 적절합니다.카테고리 선택 값을 히든 인풋 필드로 관리하는 방식이 효과적입니다.
src/components/common/customerService/inquiry/Inquiry.styled.ts (7)
4-18: 컨테이너와 헤더 스타일이 적절하게 구현되었습니다.컨테이너와 헤더의 레이아웃이 플렉스 방식으로 잘 구현되었습니다.
19-28: 폼 레이아웃이 잘 구현되었습니다.문의 폼의 레이아웃이 적절한 너비와 중앙 정렬로 잘 구현되었습니다.
29-38: 내비게이션 및 카테고리 래퍼 스타일이 잘 구현되었습니다.드롭다운 메뉴를 위한 상대적 위치 지정이 적절합니다.
39-91: 카테고리 선택 UI가 효과적으로 구현되었습니다.카테고리 선택 버튼과 드롭다운 메뉴의 스타일링이 잘 구현되었습니다. 특히 화살표 아이콘 회전 애니메이션이 좋은 사용자 경험을 제공합니다.
93-114: 제목 입력과 콘텐츠 영역 스타일이 적절합니다.입력 필드와 텍스트 영역의 스타일링이 일관성 있게 구현되었습니다.
116-165: 파일 업로드 UI가 사용자 친화적으로 구현되었습니다.파일 선택 레이블, 파일명 표시 영역, 숨겨진 파일 입력, 이미지 미리보기가 효과적으로
구현되었습니다.
167-191: 제출 버튼 스타일이 잘 구현되었습니다.제출 버튼의 위치와 스타일링, 그리고 호버 효과가 사용자 친화적으로 구현되었습니다.
| export const postInquiry = async (formData: FormData) => { | ||
| try { | ||
| const response = await httpClient.post('/inquiry', formData, { | ||
| headers: { | ||
| 'Content-Type': 'multipart/form-data', | ||
| }, | ||
| }); | ||
| console.log(response); | ||
| } catch (e) { | ||
| console.log('문의하기 에러', e); | ||
| } | ||
| }; |
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
에러 처리 및 반환 값 개선이 필요합니다
현재 API 함수는 console.log를 사용하여 응답과 오류를 기록하고 있으며, 반환 값이 없습니다. 이는 호출하는 컴포넌트에서 성공/실패 여부를 확인하기 어렵게 만듭니다.
다음과 같이 개선하는 것을 추천합니다:
-export const postInquiry = async (formData: FormData) => {
+export const postInquiry = async (formData: FormData): Promise<any> => {
try {
const response = await httpClient.post('/inquiry', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
- console.log(response);
+ return response.data;
} catch (e) {
- console.log('문의하기 에러', e);
+ console.error('문의하기 에러', e);
+ throw e;
}
};이렇게 수정하면:
- 함수가 응답 데이터를 반환하게 되어 호출하는 측에서 사용할 수 있습니다.
- 에러가 발생하면 더 명확하게 로깅하고 상위로 전파하여 호출하는 측에서 처리할 수 있게 됩니다.
- Promise의 반환 타입을 명시하여 타입 안전성이 향상됩니다.
📝 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 postInquiry = async (formData: FormData) => { | |
| try { | |
| const response = await httpClient.post('/inquiry', formData, { | |
| headers: { | |
| 'Content-Type': 'multipart/form-data', | |
| }, | |
| }); | |
| console.log(response); | |
| } catch (e) { | |
| console.log('문의하기 에러', e); | |
| } | |
| }; | |
| export const postInquiry = async (formData: FormData): Promise<any> => { | |
| try { | |
| const response = await httpClient.post('/inquiry', formData, { | |
| headers: { | |
| 'Content-Type': 'multipart/form-data', | |
| }, | |
| }); | |
| return response.data; | |
| } catch (e) { | |
| console.error('문의하기 에러', e); | |
| throw e; | |
| } | |
| }; |
| const handleSubmitInquiry = (e: React.FormEvent<HTMLFormElement>) => { | ||
| e.preventDefault(); | ||
|
|
||
| const formData = new FormData(e.currentTarget as HTMLFormElement); | ||
|
|
||
| const formDataObj: InquiryFormData = { | ||
| category: formData.get('category') as string, | ||
| title: formData.get('title') as string, | ||
| content: formData.get('content') as string, | ||
| }; | ||
|
|
||
| const data = new Blob([JSON.stringify(formDataObj)], { | ||
| type: 'application/json', | ||
| }); | ||
|
|
||
| formData.append('inquiryDto', data); | ||
|
|
||
| postInquiry(formData); | ||
| setCategoryValue(INQUIRY_MESSAGE.categoryDefault); | ||
| setFileValue(INQUIRY_MESSAGE.fileDefault); | ||
| setFileImage(EMPTY_IMAGE); | ||
| setTitle(''); | ||
| setContent(''); | ||
| }; |
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
폼 제출 로직에 에러 처리가 필요합니다.
문의 제출 처리 로직은 잘 구현되었지만, API 호출 결과에 대한 에러 처리와 로딩 상태 관리가 없습니다. 사용자에게 제출 성공/실패 피드백을 제공하는 것이 좋습니다.
다음과 같이 수정하는 것을 제안합니다:
- import { postInquiry } from '../../../../api/inquiry.api';
+ import { usePostInquiry } from '../../../../hooks/usePostInquiry';
export default function Inquiry() {
// 기존 상태 변수들...
+ const { mutate: postInquiry, isPending } = usePostInquiry();
+ const [submitStatus, setSubmitStatus] = useState<'idle' | 'success' | 'error'>('idle');
const handleSubmitInquiry = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
// 기존 코드...
- postInquiry(formData);
+ postInquiry(formData, {
+ onSuccess: () => {
+ setSubmitStatus('success');
+ // 기존 리셋 로직...
+
+ // 일정 시간 후 상태 초기화
+ setTimeout(() => setSubmitStatus('idle'), 3000);
+ },
+ onError: () => {
+ setSubmitStatus('error');
+ }
+ });
};
// 컴포넌트 렌더링 부분에 성공/에러 메시지 표시 추가📝 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.
| const handleSubmitInquiry = (e: React.FormEvent<HTMLFormElement>) => { | |
| e.preventDefault(); | |
| const formData = new FormData(e.currentTarget as HTMLFormElement); | |
| const formDataObj: InquiryFormData = { | |
| category: formData.get('category') as string, | |
| title: formData.get('title') as string, | |
| content: formData.get('content') as string, | |
| }; | |
| const data = new Blob([JSON.stringify(formDataObj)], { | |
| type: 'application/json', | |
| }); | |
| formData.append('inquiryDto', data); | |
| postInquiry(formData); | |
| setCategoryValue(INQUIRY_MESSAGE.categoryDefault); | |
| setFileValue(INQUIRY_MESSAGE.fileDefault); | |
| setFileImage(EMPTY_IMAGE); | |
| setTitle(''); | |
| setContent(''); | |
| }; | |
| // 기존 import | |
| -import { postInquiry } from '../../../../api/inquiry.api'; | |
| +import { usePostInquiry } from '../../../../hooks/usePostInquiry'; | |
| export default function Inquiry() { | |
| // 기존 상태 변수들... | |
| const { mutate: postInquiry, isPending } = usePostInquiry(); | |
| const [submitStatus, setSubmitStatus] = useState<'idle' | 'success' | 'error'>('idle'); | |
| const handleSubmitInquiry = (e: React.FormEvent<HTMLFormElement>) => { | |
| e.preventDefault(); | |
| const formData = new FormData(e.currentTarget as HTMLFormElement); | |
| const formDataObj: InquiryFormData = { | |
| category: formData.get('category') as string, | |
| title: formData.get('title') as string, | |
| content: formData.get('content') as string, | |
| }; | |
| const data = new Blob([JSON.stringify(formDataObj)], { | |
| type: 'application/json', | |
| }); | |
| formData.append('inquiryDto', data); | |
| postInquiry(formData, { | |
| onSuccess: () => { | |
| setSubmitStatus('success'); | |
| // 기존 리셋 로직 | |
| setCategoryValue(INQUIRY_MESSAGE.categoryDefault); | |
| setFileValue(INQUIRY_MESSAGE.fileDefault); | |
| setFileImage(EMPTY_IMAGE); | |
| setTitle(''); | |
| setContent(''); | |
| // 일정 시간 후 상태 초기화 | |
| setTimeout(() => setSubmitStatus('idle'), 3000); | |
| }, | |
| onError: () => { | |
| setSubmitStatus('error'); | |
| } | |
| }); | |
| }; | |
| // 컴포넌트 렌더링 부분에 성공/에러 메시지 표시 추가 | |
| } |
| </S.InquiryForm> | ||
| </S.Container> | ||
| ); | ||
| } |
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.
customerService폴더가 common에 들어가 있는데, 잘 못 넣으신 것 같아요
확인 한번 해주세요~
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.
문의하기가 헤더 유저 토스트부분이랑 마이페이지 내 문의글, 그리고 FAQ 공지사항 각각 들어가서 common으로 했습니다.
FAQ, 공지사항도 그렇게 생각했는데 다르게 생각해야하나요??
| const [fileValue, setFileValue] = useState<string>( | ||
| INQUIRY_MESSAGE.fileDefault | ||
| ); | ||
| const [fileImage, setFileImage] = useState<string>(EMPTY_IMAGE); |
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.
폼 구조가 늘어남에 따라, 상태도 많아질 수도 있을 것 같다고 생각이 들게됩니다!
그래서 아래와 같이 객체형식으로 생각해봤는데 어떠실까요?!
const [form, setForm] = useState<string>({ title: '', content: '', fileValue: INQUIRY_MESSAGE.fileDefault, fileImage: EMPTY_IMAGE, });
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.
ㅋㅋ귀찮아서 그냥 다 따로 뺐는데..수정할게여 ㅋㅋ
| setFileImage(EMPTY_IMAGE); | ||
| setTitle(''); | ||
| setContent(''); | ||
| }; |
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.
이 부분도 const handleFormChange = (key: keyof typeof form, value: string) => { setForm(prev => ({ ...prev, [key]: value })); };
상태를 객체로 관리하면 이런식으로도 표현해볼 수 있을 것 같습니다!
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 (2)
src/hooks/usePostInquiry.ts (2)
11-11: 특정 쿼리만 무효화하도록 쿼리 키를 지정하세요.현재
queryClient.invalidateQueries()를 호출하면 캐시의 모든 쿼리가 무효화되어 불필요한 리패치가 발생할 수 있습니다. 문의 관련 쿼리만 무효화하도록 쿼리 키를 지정하는 것이 더 효율적입니다.queryClient.invalidateQueries({ queryKey: ['inquiries'] });이렇게 수정하면 문의 관련 쿼리만 무효화되어 애플리케이션의 성능이 향상됩니다.
8-13: onError 핸들러 추가를 고려하세요.현재 mutation에는
onSuccess핸들러만 있고onError핸들러가 없습니다. 에러 처리를 위해onError핸들러를 추가하는 것이 좋습니다.const mutate = useMutation<void, AxiosError, FormData>({ mutationFn: (formData) => postInquiry(formData), onSuccess: () => { queryClient.invalidateQueries(); }, onError: (error) => { console.error('문의 제출 중 오류 발생:', error); // 여기에 에러 처리 로직 추가 (예: 토스트 메시지 표시) }, });이렇게 추가하면 사용자에게 문의 제출 실패에 대한 피드백을 제공할 수 있습니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
src/components/common/customerService/inquiry/Inquiry.tsx(1 hunks)src/hooks/usePostInquiry.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- src/components/common/customerService/inquiry/Inquiry.tsx
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/hooks/usePostInquiry.ts (2)
src/models/createProject.ts (1)
FormData(6-19)src/api/inquiry.api.ts (1)
postInquiry(3-14)
⏰ Context from checks skipped due to timeout of 90000ms (1)
- GitHub Check: accessibility-test
🔇 Additional comments (1)
src/hooks/usePostInquiry.ts (1)
1-16: 커스텀 훅이 올바르게 구현되었습니다.
usePostInquiry커스텀 훅이mutate객체를 반환하도록 올바르게 구현되었습니다. 이제 컴포넌트에서 다음과 같이 사용할 수 있습니다:const { mutate, isLoading, isError, error } = usePostInquiry(); // 사용 예시 const handleSubmit = (formData: FormData) => { mutate(formData); };
| const mutate = useMutation<void, AxiosError, FormData>({ | ||
| mutationFn: (formData) => postInquiry(formData), | ||
| onSuccess: () => { | ||
| queryClient.invalidateQueries(); | ||
| }, | ||
| }); |
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
useMutation이 API 함수의 반환 값과 일치하도록 업데이트가 필요합니다.
현재 postInquiry API 함수는 성공 시 응답을 반환하지 않고 내부에서만 콘솔에 기록하고 있습니다. 또한 에러를 내부에서 캐치하여 상위로 전파하지 않아 useMutation이 에러를 적절히 처리할 수 없습니다.
API 함수를 다음과 같이 수정하는 것을 권장합니다:
export const postInquiry = async (formData: FormData) => {
const response = await httpClient.post('/inquiry', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
return response.data;
};이렇게 수정하면 에러를 적절히 전파하고 결과 데이터를 반환하여 훅에서 더 효과적으로 처리할 수 있습니다.
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
♻️ Duplicate comments (1)
src/components/common/customerService/inquiry/Inquiry.tsx (1)
59-68: 🛠️ Refactor suggestion사용자에게 폼 제출 피드백을 제공하세요.
현재 제출 성공이나 실패에 대한 사용자 피드백이 없습니다. 사용자 경험 향상을 위해 성공/실패 메시지나 로딩 상태를 추가하는 것이 좋습니다.
+ const [submitStatus, setSubmitStatus] = useState<'idle' | 'success' | 'error'>('idle'); // ... if (isValid) { - postInquiry(formData); + postInquiry(formData, { + onSuccess: () => { + setSubmitStatus('success'); setForm({ category: INQUIRY_MESSAGE.categoryDefault, title: '', content: '', fileValue: INQUIRY_MESSAGE.fileDefault, fileImage: null, }); + // 일정 시간 후 상태 초기화 + setTimeout(() => setSubmitStatus('idle'), 3000); + }, + onError: () => { + setSubmitStatus('error'); + } + }); } // 컴포넌트 렌더링 부분에 성공/에러 메시지 표시 추가
🧹 Nitpick comments (4)
src/components/common/customerService/inquiry/Inquiry.tsx (4)
3-3: 사용되지 않는 import를 제거하세요.
EMPTY_IMAGEimport가 선언되었지만 코드 내에서 사용되지 않습니다. 미사용 변수는 코드 가독성을 저하시키고 혼란을 줄 수 있습니다.import { - EMPTY_IMAGE, INQUIRY_CATEGORY, INQUIRY_MESSAGE, } from '../../../../constants/customerService';🧰 Tools
🪛 ESLint
[error] 3-3: 'EMPTY_IMAGE' is defined but never used.
(@typescript-eslint/no-unused-vars)
47-57: 폼 유효성 검사 로직을 개선하세요.현재
formData.entries()를 순회하며 검사하는 방식은 복잡합니다. 이미 상태 값이 있으므로 직접 비교하는 것이 더 명확하고 효율적입니다.- let isValid = true; - - Array.from(formData.entries()).forEach(([key, value]) => { - if (key === 'category' && value === INQUIRY_MESSAGE.categoryDefault) - return (isValid = false); - if (key === 'title' && value === '') return (isValid = false); - if (key === 'content' && value === '') return (isValid = false); - }); - - if (isValid) { + if ( + form.category !== INQUIRY_MESSAGE.categoryDefault && + form.title.trim() !== '' && + form.content.trim() !== '' + ) {
95-118: 접근성을 고려한 드롭다운 구현이 필요합니다.현재 카테고리 드롭다운은 접근성 측면에서 개선이 필요합니다. 키보드 탐색 지원과 적절한 ARIA 속성을 추가하여 스크린 리더 사용자의 접근성을 향상시키세요.
<S.CategorySelect onClick={() => setIsOpen((prev) => !prev)} $isOpen={isOpen} + aria-haspopup="listbox" + aria-expanded={isOpen} + role="combobox" + tabIndex={0} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + setIsOpen((prev) => !prev); + } + }} > {form.category} <ChevronDownIcon /> <S.CategoryValueInput type='hidden' name='category' value={form.category} /> </S.CategorySelect> {isOpen && ( <S.CategoryButtonWrapper + role="listbox" + aria-label="카테고리 선택" > {INQUIRY_CATEGORY.map((category) => ( <Fragment key={category.title}> <S.CategoryButton onClick={() => handleClickCategoryValue(category.title)} + role="option" + aria-selected={form.category === category.title} + tabIndex={0} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + handleClickCategoryValue(category.title); + } + }} > {category.title} </S.CategoryButton> </Fragment> ))} </S.CategoryButtonWrapper> )}
149-149: 불필요한 조건부 코드를 간소화하세요.
{form.fileImage && <S.FileImg src={form.fileImage || ''} />}에서form.fileImage가 이미 조건에서 확인되었으므로||연산자는 불필요합니다.- {form.fileImage && <S.FileImg src={form.fileImage || ''} />} + {form.fileImage && <S.FileImg src={form.fileImage} />}
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
src/components/common/customerService/inquiry/Inquiry.tsx(1 hunks)
🧰 Additional context used
🧠 Learnings (1)
src/components/common/customerService/inquiry/Inquiry.tsx (1)
Learnt from: YouD0313
PR: devpalsPlus/frontend#259
File: src/components/common/customerService/inquiry/Inquiry.tsx:80-83
Timestamp: 2025-04-29T12:55:53.123Z
Learning: React에서 controlled 컴포넌트(value prop이 있는 input)를 사용할 때는 onChange 핸들러도 함께 제공해야 합니다. 그렇지 않으면 입력 필드가 읽기 전용이 되어 사용자가 값을 입력할 수 없습니다.
🧬 Code Graph Analysis (1)
src/components/common/customerService/inquiry/Inquiry.tsx (4)
src/api/inquiry.api.ts (1)
postInquiry(3-14)src/hooks/usePostInquiry.ts (1)
usePostInquiry(5-16)src/constants/customerService.ts (2)
INQUIRY_MESSAGE(11-14)INQUIRY_CATEGORY(1-6)src/models/inquiry.ts (1)
InquiryFormData(1-5)
🪛 ESLint
src/components/common/customerService/inquiry/Inquiry.tsx
[error] 3-3: 'EMPTY_IMAGE' is defined but never used.
(@typescript-eslint/no-unused-vars)
| <S.InquiryFile | ||
| name='images' | ||
| type='file' | ||
| accept='.jpg, .jpeg, .png' | ||
| id='upload' | ||
| onChange={(e) => handleChangeFile(e)} | ||
| /> |
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
파일 업로드 크기 제한을 추가하세요.
현재 이미지 파일 형식은 제한되어 있지만, 파일 크기에 대한 제한이 없습니다. 대용량 이미지 파일은 성능 문제를 일으킬 수 있으므로 크기 제한을 추가하는 것이 좋습니다.
const handleChangeFile = (e: React.ChangeEvent<HTMLInputElement>) => {
const fileValue = e.target.value;
const image = e.target.files?.[0];
+
+ // 파일 크기 제한 (예: 5MB)
+ const MAX_FILE_SIZE = 5 * 1024 * 1024;
+ if (image && image.size > MAX_FILE_SIZE) {
+ alert('파일 크기는 5MB 이하여야 합니다.');
+ e.target.value = '';
+ return;
+ }
+
const fileImage = image ? URL.createObjectURL(image) : null;
setForm((prev) => ({ ...prev, fileValue, fileImage }));
};📝 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.
| <S.InquiryFile | |
| name='images' | |
| type='file' | |
| accept='.jpg, .jpeg, .png' | |
| id='upload' | |
| onChange={(e) => handleChangeFile(e)} | |
| /> | |
| const handleChangeFile = (e: React.ChangeEvent<HTMLInputElement>) => { | |
| const fileValue = e.target.value; | |
| const image = e.target.files?.[0]; | |
| // 파일 크기 제한 (예: 5MB) | |
| const MAX_FILE_SIZE = 5 * 1024 * 1024; | |
| if (image && image.size > MAX_FILE_SIZE) { | |
| alert('파일 크기는 5MB 이하여야 합니다.'); | |
| e.target.value = ''; | |
| return; | |
| } | |
| const fileImage = image ? URL.createObjectURL(image) : null; | |
| setForm((prev) => ({ ...prev, fileValue, fileImage })); | |
| }; |
| const handleChangeFile = (e: React.ChangeEvent<HTMLInputElement>) => { | ||
| const fileValue = e.target.value; | ||
| const image = e.target.files?.[0]; | ||
| const fileImage = image ? URL.createObjectURL(image) : null; | ||
| setForm((prev) => ({ ...prev, fileValue, fileImage })); | ||
| }; |
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
메모리 누수 방지를 위해 객체 URL을 관리하세요.
URL.createObjectURL로 생성된 URL은 사용이 끝나면 명시적으로 해제해야 메모리 누수를 방지할 수 있습니다.
import React, { useState, useEffect } from 'react';
// ...
const handleChangeFile = (e: React.ChangeEvent<HTMLInputElement>) => {
const fileValue = e.target.value;
const image = e.target.files?.[0];
const fileImage = image ? URL.createObjectURL(image) : null;
setForm((prev) => ({ ...prev, fileValue, fileImage }));
};
+ // 컴포넌트 언마운트 시 객체 URL 해제
+ useEffect(() => {
+ return () => {
+ if (form.fileImage) {
+ URL.revokeObjectURL(form.fileImage);
+ }
+ };
+ }, [form.fileImage]);📝 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.
| const handleChangeFile = (e: React.ChangeEvent<HTMLInputElement>) => { | |
| const fileValue = e.target.value; | |
| const image = e.target.files?.[0]; | |
| const fileImage = image ? URL.createObjectURL(image) : null; | |
| setForm((prev) => ({ ...prev, fileValue, fileImage })); | |
| }; | |
| // --- at the top of the file --- | |
| -import React, { useState } from 'react'; | |
| +import React, { useState, useEffect } from 'react'; | |
| // ... | |
| export default function Inquiry() { | |
| // existing state, including something like: | |
| // const [form, setForm] = useState({ ..., fileImage: null }); | |
| const handleChangeFile = (e: React.ChangeEvent<HTMLInputElement>) => { | |
| const fileValue = e.target.value; | |
| const image = e.target.files?.[0]; | |
| const fileImage = image ? URL.createObjectURL(image) : null; | |
| setForm((prev) => ({ ...prev, fileValue, fileImage })); | |
| }; | |
| // 컴포넌트 언마운트 시 및 fileImage 변경 시 이전 객체 URL 해제 | |
| useEffect(() => { | |
| return () => { | |
| if (form.fileImage) { | |
| URL.revokeObjectURL(form.fileImage); | |
| } | |
| }; | |
| }, [form.fileImage]); | |
| // ...rest of your component | |
| } |
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: 5
🧹 Nitpick comments (2)
src/pages/customerService/inquiry/Inquiry.tsx (2)
31-47: 폼 데이터 처리 로직 개선이 필요합니다.FormData 생성 및 JSON 변환 과정이 복잡하게 구현되어 있습니다. 서버 API와의 통합을 위해 이런 방식이 필요할 수 있으나, 코드를 더 간결하게 만들 수 있습니다.
const handleSubmitInquiry = (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); const formData = new FormData(e.currentTarget as HTMLFormElement); - const formDataObj: InquiryFormData = { - category: formData.get('category') as string, - title: formData.get('title') as string, - content: formData.get('content') as string, - }; - - const data = new Blob([JSON.stringify(formDataObj)], { - type: 'application/json', - }); - - formData.append('inquiryDto', data); + // InquiryFormData 객체 생성 + const inquiryData: InquiryFormData = { + category: formData.get('category') as string, + title: formData.get('title') as string, + content: formData.get('content') as string, + }; + + // JSON 문자열로 변환하여 FormData에 추가 + formData.append('inquiryDto', new Blob([JSON.stringify(inquiryData)], { + type: 'application/json', + }));
93-117: 카테고리 드롭다운의 접근성 개선이 필요합니다.현재 카테고리 드롭다운은 키보드 탐색이 불가능하여 접근성 문제가 있습니다. 또한
aria-*속성이 없어 스크린 리더 사용자가 상태를 인식하기 어렵습니다.<S.CategoryWrapper> <S.CategorySelect onClick={() => setIsOpen((prev) => !prev)} $isOpen={isOpen} + role="combobox" + aria-expanded={isOpen} + aria-haspopup="listbox" + aria-labelledby="category-label" + tabIndex={0} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + setIsOpen((prev) => !prev); + } + }} > + <span id="category-label" className="sr-only">카테고리 선택</span> {form.category} <ChevronDownIcon /> <S.CategoryValueInput type='hidden' name='category' value={form.category} /> </S.CategorySelect> {isOpen && ( <S.CategoryButtonWrapper + role="listbox" + aria-labelledby="category-label" > {INQUIRY_CATEGORY.map((category) => ( <Fragment key={category.title}> <S.CategoryButton onClick={() => handleClickCategoryValue(category.title)} + role="option" + aria-selected={form.category === category.title} + tabIndex={0} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + handleClickCategoryValue(category.title); + } + }} > {category.title} </S.CategoryButton> </Fragment> ))} </S.CategoryButtonWrapper> )} </S.CategoryWrapper>
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
src/pages/customerService/inquiry/Inquiry.styled.ts(1 hunks)src/pages/customerService/inquiry/Inquiry.tsx(1 hunks)src/routes/AppRoutes.tsx(2 hunks)
✅ Files skipped from review due to trivial changes (1)
- src/pages/customerService/inquiry/Inquiry.styled.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- src/routes/AppRoutes.tsx
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/pages/customerService/inquiry/Inquiry.tsx (5)
src/api/inquiry.api.ts (1)
postInquiry(3-14)src/hooks/usePostInquiry.ts (1)
usePostInquiry(5-16)src/constants/customerService.ts (2)
INQUIRY_MESSAGE(11-14)INQUIRY_CATEGORY(1-6)src/models/createProject.ts (1)
FormData(6-19)src/models/inquiry.ts (1)
InquiryFormData(1-5)
🔇 Additional comments (3)
src/pages/customerService/inquiry/Inquiry.tsx (3)
1-10: 필요한 임포트가 올바르게 구성되어 있습니다.임포트 구성이 적절하게 되어 있고, 필요한 컴포넌트와 타입이 잘 가져와져 있습니다.
12-18: FormStateType 인터페이스 설계가 적절합니다.폼 상태를 관리하기 위한 타입 정의가 명확하게 되어 있습니다. 특히 파일 업로드 관련 필드가 잘 분리되어 있습니다.
20-29: 초기 상태 설정이 잘 되어 있습니다.useState를 활용한 상태 관리가 적절하게 구현되어 있습니다. 초기값으로 상수를 활용한 것도 좋은 방식입니다.
| // 모달처리하기 | ||
| let isValid = true; | ||
|
|
||
| Array.from(formData.entries()).forEach(([key, value]) => { | ||
| if (key === 'category' && value === INQUIRY_MESSAGE.categoryDefault) | ||
| return (isValid = false); | ||
| if (key === 'title' && value === '') return (isValid = false); | ||
| if (key === 'content' && value === '') return (isValid = false); | ||
| }); |
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
폼 유효성 검증 피드백이 없습니다.
48번 라인의 주석("모달처리하기")처럼 유효성 검증 실패 시 사용자에게 피드백을 제공하는 기능이 구현되어 있지 않습니다. 사용자가 왜 제출이 안 되는지 알 수 없습니다.
// 모달처리하기
let isValid = true;
+let errorMessage = '';
Array.from(formData.entries()).forEach(([key, value]) => {
- if (key === 'category' && value === INQUIRY_MESSAGE.categoryDefault)
- return (isValid = false);
- if (key === 'title' && value === '') return (isValid = false);
- if (key === 'content' && value === '') return (isValid = false);
+ if (key === 'category' && value === INQUIRY_MESSAGE.categoryDefault) {
+ isValid = false;
+ errorMessage = '카테고리를 선택해주세요.';
+ return;
+ }
+ if (key === 'title' && value === '') {
+ isValid = false;
+ errorMessage = '제목을 입력해주세요.';
+ return;
+ }
+ if (key === 'content' && value === '') {
+ isValid = false;
+ errorMessage = '내용을 입력해주세요.';
+ return;
+ }
});
if (isValid) {
postInquiry(formData);
setForm({
category: INQUIRY_MESSAGE.categoryDefault,
title: '',
content: '',
fileValue: INQUIRY_MESSAGE.fileDefault,
fileImage: null,
});
+} else {
+ // 에러 메시지 표시 (모달 또는 인라인 알림)
+ alert(errorMessage); // 임시 해결책, 실제로는 모달 또는 토스트 메시지로 대체
}📝 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.
| // 모달처리하기 | |
| let isValid = true; | |
| Array.from(formData.entries()).forEach(([key, value]) => { | |
| if (key === 'category' && value === INQUIRY_MESSAGE.categoryDefault) | |
| return (isValid = false); | |
| if (key === 'title' && value === '') return (isValid = false); | |
| if (key === 'content' && value === '') return (isValid = false); | |
| }); | |
| // 모달처리하기 | |
| let isValid = true; | |
| let errorMessage = ''; | |
| Array.from(formData.entries()).forEach(([key, value]) => { | |
| if (key === 'category' && value === INQUIRY_MESSAGE.categoryDefault) { | |
| isValid = false; | |
| errorMessage = '카테고리를 선택해주세요.'; | |
| return; | |
| } | |
| if (key === 'title' && value === '') { | |
| isValid = false; | |
| errorMessage = '제목을 입력해주세요.'; | |
| return; | |
| } | |
| if (key === 'content' && value === '') { | |
| isValid = false; | |
| errorMessage = '내용을 입력해주세요.'; | |
| return; | |
| } | |
| }); | |
| if (isValid) { | |
| postInquiry(formData); | |
| setForm({ | |
| category: INQUIRY_MESSAGE.categoryDefault, | |
| title: '', | |
| content: '', | |
| fileValue: INQUIRY_MESSAGE.fileDefault, | |
| fileImage: null, | |
| }); | |
| } else { | |
| // 에러 메시지 표시 (모달 또는 인라인 알림) | |
| alert(errorMessage); // 임시 해결책, 실제로는 모달 또는 토스트 메시지로 대체 | |
| } |
| <S.InquiryForm | ||
| onSubmit={handleSubmitInquiry} | ||
| method='post' | ||
| encType='multipart/form-data' | ||
| > |
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
폼 제출 시 로딩 상태 처리가 필요합니다.
API 호출 중 로딩 상태를 표시하는 기능이 없어, 사용자는 제출 버튼을 여러 번 클릭할 수 있습니다. 이로 인해 중복 제출 위험이 있습니다.
export default function Inquiry() {
- const { mutate: postInquiry } = usePostInquiry();
+ const { mutate: postInquiry, isPending } = usePostInquiry();
// ... 기존 코드 ...
return (
<S.Container>
<S.Header>
<S.HeaderTitle>DevPals 문의하기</S.HeaderTitle>
</S.Header>
<S.InquiryForm
onSubmit={handleSubmitInquiry}
method='post'
encType='multipart/form-data'
+ disabled={isPending}
>
{/* ... 기존 폼 내용 ... */}
<S.SendButtonWrapper>
- <S.SendButton type='submit'>제출</S.SendButton>
+ <S.SendButton type='submit' disabled={isPending}>
+ {isPending ? '제출 중...' : '제출'}
+ </S.SendButton>
</S.SendButtonWrapper>
</S.InquiryForm>
</S.Container>
);
}| <S.InquiryFileWrapper> | ||
| <S.InquiryFileLabel htmlFor='upload'>파일찾기</S.InquiryFileLabel> | ||
| <S.InquiryShowFile>{form.fileValue}</S.InquiryShowFile> | ||
| <S.InquiryFile | ||
| name='images' | ||
| type='file' | ||
| accept='.jpg, .jpeg, .png' | ||
| id='upload' | ||
| onChange={(e) => handleChangeFile(e)} | ||
| /> | ||
| {form.fileImage && <S.FileImg src={form.fileImage || ''} />} | ||
| </S.InquiryFileWrapper> |
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
파일 업로드 기능에 파일 크기 검증이 누락되었습니다.
현재 파일 업로드는 확장자만 제한하고 있으며, 파일 크기 검증이 없습니다. 대용량 파일 업로드 시 서버에 부담을 줄 수 있습니다.
const handleChangeFile = (e: React.ChangeEvent<HTMLInputElement>) => {
const fileValue = e.target.value;
const image = e.target.files?.[0];
+
+ // 파일 크기 검증 (예: 5MB 제한)
+ if (image && image.size > 5 * 1024 * 1024) {
+ alert('파일 크기는 5MB 이하만 가능합니다.');
+ e.target.value = '';
+ setForm((prev) => ({
+ ...prev,
+ fileValue: INQUIRY_MESSAGE.fileDefault,
+ fileImage: null
+ }));
+ return;
+ }
+
const fileImage = image ? URL.createObjectURL(image) : null;
setForm((prev) => ({ ...prev, fileValue, fileImage }));
};📝 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.
| <S.InquiryFileWrapper> | |
| <S.InquiryFileLabel htmlFor='upload'>파일찾기</S.InquiryFileLabel> | |
| <S.InquiryShowFile>{form.fileValue}</S.InquiryShowFile> | |
| <S.InquiryFile | |
| name='images' | |
| type='file' | |
| accept='.jpg, .jpeg, .png' | |
| id='upload' | |
| onChange={(e) => handleChangeFile(e)} | |
| /> | |
| {form.fileImage && <S.FileImg src={form.fileImage || ''} />} | |
| </S.InquiryFileWrapper> | |
| const handleChangeFile = (e: React.ChangeEvent<HTMLInputElement>) => { | |
| const fileValue = e.target.value; | |
| const image = e.target.files?.[0]; | |
| // 파일 크기 검증 (예: 5MB 제한) | |
| if (image && image.size > 5 * 1024 * 1024) { | |
| alert('파일 크기는 5MB 이하만 가능합니다.'); | |
| e.target.value = ''; | |
| setForm((prev) => ({ | |
| ...prev, | |
| fileValue: INQUIRY_MESSAGE.fileDefault, | |
| fileImage: null, | |
| })); | |
| return; | |
| } | |
| const fileImage = image ? URL.createObjectURL(image) : null; | |
| setForm((prev) => ({ ...prev, fileValue, fileImage })); | |
| }; |
| const handleChangeFile = (e: React.ChangeEvent<HTMLInputElement>) => { | ||
| const fileValue = e.target.value; | ||
| const image = e.target.files?.[0]; | ||
| const fileImage = image ? URL.createObjectURL(image) : null; | ||
| setForm((prev) => ({ ...prev, fileValue, fileImage })); | ||
| }; |
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
이미지 URL 메모리 누수 가능성이 있습니다.
URL.createObjectURL로 생성된 객체 URL을 해제하지 않아 메모리 누수가 발생할 수 있습니다. 컴포넌트 언마운트 시 또는 새 파일 선택 시 이전 URL을 해제해야 합니다.
import React, { useState, useEffect } from 'react';
// 컴포넌트 내부
+ const [prevObjectUrl, setPrevObjectUrl] = useState<string | null>(null);
const handleChangeFile = (e: React.ChangeEvent<HTMLInputElement>) => {
const fileValue = e.target.value;
const image = e.target.files?.[0];
+
+ // 이전 객체 URL 해제
+ if (prevObjectUrl) {
+ URL.revokeObjectURL(prevObjectUrl);
+ }
+
const fileImage = image ? URL.createObjectURL(image) : null;
+ setPrevObjectUrl(fileImage);
setForm((prev) => ({ ...prev, fileValue, fileImage }));
};
+ // 컴포넌트 언마운트 시 객체 URL 해제
+ useEffect(() => {
+ return () => {
+ if (prevObjectUrl) {
+ URL.revokeObjectURL(prevObjectUrl);
+ }
+ };
+ }, [prevObjectUrl]);📝 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.
| const handleChangeFile = (e: React.ChangeEvent<HTMLInputElement>) => { | |
| const fileValue = e.target.value; | |
| const image = e.target.files?.[0]; | |
| const fileImage = image ? URL.createObjectURL(image) : null; | |
| setForm((prev) => ({ ...prev, fileValue, fileImage })); | |
| }; | |
| import React, { useState, useEffect } from 'react'; | |
| function Inquiry(/* props */) { | |
| // 이전에 생성했던 object URL을 저장 | |
| const [prevObjectUrl, setPrevObjectUrl] = useState<string | null>(null); | |
| const handleChangeFile = (e: React.ChangeEvent<HTMLInputElement>) => { | |
| const fileValue = e.target.value; | |
| const image = e.target.files?.[0]; | |
| // 새 파일 선택 전, 이전에 생성한 object URL 해제 | |
| if (prevObjectUrl) { | |
| URL.revokeObjectURL(prevObjectUrl); | |
| } | |
| const fileImage = image ? URL.createObjectURL(image) : null; | |
| setPrevObjectUrl(fileImage); | |
| setForm((prev) => ({ ...prev, fileValue, fileImage })); | |
| }; | |
| // 컴포넌트 언마운트 시, 마지막으로 생성한 object URL 해제 | |
| useEffect(() => { | |
| return () => { | |
| if (prevObjectUrl) { | |
| URL.revokeObjectURL(prevObjectUrl); | |
| } | |
| }; | |
| }, [prevObjectUrl]); | |
| return ( | |
| // … JSX … | |
| ); | |
| } | |
| export default Inquiry; |
| if (isValid) { | ||
| postInquiry(formData); | ||
| setForm({ | ||
| category: INQUIRY_MESSAGE.categoryDefault, | ||
| title: '', | ||
| content: '', | ||
| fileValue: INQUIRY_MESSAGE.fileDefault, | ||
| fileImage: null, | ||
| }); | ||
| } | ||
| }; |
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
폼 제출 후 성공 피드백이 없습니다.
폼이 성공적으로 제출된 후 사용자에게 피드백을 제공하는 로직이 없습니다. 폼이 초기화되지만 사용자는 제출 성공 여부를 알 수 없습니다.
if (isValid) {
- postInquiry(formData);
+ postInquiry(formData, {
+ onSuccess: () => {
+ // 성공 메시지 표시
+ alert('문의가 성공적으로 제출되었습니다.'); // 실제로는 모달 또는 토스트 메시지로 대체
+
+ // 폼 초기화
+ setForm({
+ category: INQUIRY_MESSAGE.categoryDefault,
+ title: '',
+ content: '',
+ fileValue: INQUIRY_MESSAGE.fileDefault,
+ fileImage: null,
+ });
+ },
+ onError: (error) => {
+ // 에러 메시지 표시
+ alert('문의 제출 중 오류가 발생했습니다. 다시 시도해주세요.');
+ }
+ });
- setForm({
- category: INQUIRY_MESSAGE.categoryDefault,
- title: '',
- content: '',
- fileValue: INQUIRY_MESSAGE.fileDefault,
- fileImage: null,
- });
}📝 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.
| if (isValid) { | |
| postInquiry(formData); | |
| setForm({ | |
| category: INQUIRY_MESSAGE.categoryDefault, | |
| title: '', | |
| content: '', | |
| fileValue: INQUIRY_MESSAGE.fileDefault, | |
| fileImage: null, | |
| }); | |
| } | |
| }; | |
| if (isValid) { | |
| postInquiry(formData, { | |
| onSuccess: () => { | |
| // 성공 메시지 표시 | |
| alert('문의가 성공적으로 제출되었습니다.'); // 실제로는 모달 또는 토스트 메시지로 대체 | |
| // 폼 초기화 | |
| setForm({ | |
| category: INQUIRY_MESSAGE.categoryDefault, | |
| title: '', | |
| content: '', | |
| fileValue: INQUIRY_MESSAGE.fileDefault, | |
| fileImage: null, | |
| }); | |
| }, | |
| onError: (error) => { | |
| // 에러 메시지 표시 | |
| alert('문의 제출 중 오류가 발생했습니다. 다시 시도해주세요.'); | |
| } | |
| }); | |
| } | |
| }; |
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
♻️ Duplicate comments (5)
src/pages/customerService/inquiry/Inquiry.tsx (5)
20-21: React Query의 상태 관리 기능이 충분히 활용되지 않았습니다.
usePostInquiry훅에서 반환하는isPending,isError,error상태를 사용하여 로딩 상태와 오류 처리를 구현할 수 있습니다. 현재는 단순히mutate함수만 사용하고 있어 사용자 경험이 제한적입니다.
31-68: 폼 제출 로직에서 사용자 피드백과 오류 처리가 미흡합니다.폼 제출 시 유효성 검증 실패, 성공, 오류 발생 시 사용자에게 적절한 피드백이 제공되지 않습니다. 또한 제출 중 로딩 상태를 표시하지 않아 사용자가 중복 제출할 가능성이 있습니다.
103-107: 폼 제출 중 로딩 상태 표시가 필요합니다.폼 제출 중에는 중복 제출을 방지하기 위해 폼 요소를 비활성화하고 로딩 상태를 표시해야 합니다.
168-170: 제출 버튼에 로딩 상태 및 피드백 개선이 필요합니다.현재 제출 버튼은 API 호출 중에도 계속 클릭 가능한 상태로 유지되어 중복 제출이 가능합니다. 또한 제출 성공/실패에 대한 피드백이 없습니다.
74-88: 🛠️ Refactor suggestion파일 업로드 후 초기화 로직이 누락되었습니다.
파일 크기 제한 검증은 구현되었지만, 제한을 초과한 파일 선택 시
e.target.value = ''로 입력 필드만 초기화하고 폼 상태는 업데이트하지 않습니다. 이로 인해 UI와 상태 간의 불일치가 발생할 수 있습니다.다음과 같이 폼 상태도 함께 초기화해야 합니다:
if (image && image.size > MAX_FILE_SIZE) { alert('파일 크기는 5MB 이하만 가능합니다.'); e.target.value = ''; + setForm(prev => ({ + ...prev, + fileValue: INQUIRY_MESSAGE.fileDefault, + fileImage: null + })); return; }
🧹 Nitpick comments (2)
src/pages/customerService/inquiry/Inquiry.tsx (2)
154-166: 파일 업로드 UI 접근성 개선이 필요합니다.파일 업로드 UI가 시각적으로는 구현되었지만, 접근성 측면에서 개선이 필요합니다. 파일 형식 제한(.jpg, .jpeg, .png)에 대한 안내가 사용자에게 표시되지 않으며, 스크린 리더 사용자를 위한 적절한 레이블과 설명이 부족합니다.
접근성을 향상시키기 위해 다음과 같은 개선을 권장합니다:
<S.InquiryFileWrapper> <S.InquiryFileLabel htmlFor='upload'>파일찾기</S.InquiryFileLabel> - <S.InquiryShowFile>{form.fileValue}</S.InquiryShowFile> + <S.InquiryShowFile aria-live="polite">{form.fileValue}</S.InquiryShowFile> + <S.FileFormatInfo>지원 형식: JPG, JPEG, PNG (최대 5MB)</S.FileFormatInfo> <S.InquiryFile name='images' type='file' accept='.jpg, .jpeg, .png' id='upload' + aria-label="이미지 파일 업로드 (JPG, JPEG, PNG 형식, 최대 5MB)" onChange={(e) => handleChangeFile(e)} /> {form.fileImage && <S.FileImg src={form.fileImage || ''} alt="업로드된 이미지 미리보기" />} </S.InquiryFileWrapper>관련 스타일 컴포넌트도 추가해야 합니다:
// Inquiry.styled.ts에 추가 export const FileFormatInfo = styled.p` font-size: 12px; color: #666; margin-top: 4px; `;
1-175: 컴포넌트의 관심사 분리가 필요합니다.현재
Inquiry컴포넌트는 폼 상태 관리, 유효성 검증, 파일 처리, API 통신 등 여러 책임을 가지고 있어 유지보수가 어렵습니다. 관심사 분리를 통해 코드를 더 모듈화하는 것이 좋습니다.다음과 같이 컴포넌트를 분리하는 것을 권장합니다:
InquiryCategorySelect- 카테고리 선택 드롭다운 로직InquiryFileUpload- 파일 업로드 및 미리보기 로직useInquiryForm- 폼 상태 관리와 유효성 검증을 위한 커스텀 훅예를 들어 파일 업로드 컴포넌트는 다음과 같이 분리할 수 있습니다:
// InquiryFileUpload.tsx interface InquiryFileUploadProps { fileValue: string; fileImage: string | null; onChange: (fileValue: string, fileImage: string | null, file?: File) => void; } function InquiryFileUpload({ fileValue, fileImage, onChange }: InquiryFileUploadProps) { const handleChangeFile = (e: React.ChangeEvent<HTMLInputElement>) => { const newFileValue = e.target.value; const image = e.target.files?.[0]; // 파일 크기 제한 (예: 5MB) const MAX_FILE_SIZE = 5 * 1024 * 1024; if (image && image.size > MAX_FILE_SIZE) { alert('파일 크기는 5MB 이하만 가능합니다.'); e.target.value = ''; onChange(INQUIRY_MESSAGE.fileDefault, null); return; } const newFileImage = image ? URL.createObjectURL(image) : null; onChange(newFileValue, newFileImage, image); }; useEffect(() => { return () => { if (fileImage) { URL.revokeObjectURL(fileImage); } }; }, [fileImage]); return ( <S.InquiryFileWrapper> <S.InquiryFileLabel htmlFor='upload'>파일찾기</S.InquiryFileLabel> <S.InquiryShowFile aria-live="polite">{fileValue}</S.InquiryShowFile> <S.FileFormatInfo>지원 형식: JPG, JPEG, PNG (최대 5MB)</S.FileFormatInfo> <S.InquiryFile name='images' type='file' accept='.jpg, .jpeg, .png' id='upload' aria-label="이미지 파일 업로드 (JPG, JPEG, PNG 형식, 최대 5MB)" onChange={handleChangeFile} /> {fileImage && <S.FileImg src={fileImage} alt="업로드된 이미지 미리보기" />} </S.InquiryFileWrapper> ); }이렇게 분리하면 각 컴포넌트는 단일 책임을 가지게 되어 코드의 가독성과 유지보수성이 향상됩니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
src/components/mypage/ContentTab.tsx(1 hunks)src/components/userPage/joinedProject/UserJoinProject.tsx(2 hunks)src/models/userProject.ts(1 hunks)src/pages/customerService/MoveInquiredLink.tsx(1 hunks)src/pages/customerService/inquiry/Inquiry.tsx(1 hunks)
✅ Files skipped from review due to trivial changes (3)
- src/components/mypage/ContentTab.tsx
- src/pages/customerService/MoveInquiredLink.tsx
- src/components/userPage/joinedProject/UserJoinProject.tsx
⏰ Context from checks skipped due to timeout of 90000ms (1)
- GitHub Check: accessibility-test
🔇 Additional comments (2)
src/models/userProject.ts (1)
28-31: 타입 일관성 개선에 대한 변경 확인
SelectUserProject인터페이스에서ownProjects속성의 타입이AppliedProject[]에서JoinedProject[]로 변경되었습니다. 이 변경으로ownProjects가acceptedProjects와 동일한 데이터 구조를 갖게 되어 일관성이 향상되었습니다.이 변경으로 인해 아래 사항들을 고려해야 합니다:
ownProjects를 제공하는 API가JoinedProject구조에 맞는 데이터를 반환하는지 확인ownProjects를 사용하는 컴포넌트들이 새로운 타입 구조를 올바르게 처리하는지 확인이 변경은 사용자 프로젝트 데이터를 더 일관되게 처리할 수 있게 해줍니다.
src/pages/customerService/inquiry/Inquiry.tsx (1)
90-96: 파일 이미지 URL 메모리 관리가 개선되었습니다.
useEffect를 사용하여 컴포넌트 언마운트 시URL.revokeObjectURL을 호출하는 로직이 잘 구현되었습니다. 이는 이전 리뷰에서 지적된 메모리 누수 문제를 해결합니다.
구현내용
문의하기 ui api 연결
연관이슈
close #256
Summary by CodeRabbit
신규 기능
UI/UX 개선
버그 수정 및 기타