-
Notifications
You must be signed in to change notification settings - Fork 0
관리자 배너 페이지 구현( Feat/#347 ) #354
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관리자 배너 페이지가 새롭게 구현되었습니다. 배너 목록 조회, 생성, 수정, 삭제를 위한 API와 React 컴포넌트, 상태 관리 훅, 스타일 컴포넌트, Mock 서버 핸들러, 타입 정의 등이 추가되었습니다. 전체적으로 배너 관리 기능의 UI와 데이터 흐름이 완성되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant Admin as Admin User
participant UI as BannerList Component
participant API as banner.api.ts
participant Server as Backend/Mock
Admin->>UI: 페이지 접속
UI->>API: getBannerList()
API->>Server: GET /banner
Server-->>API: 배너 리스트 반환
API-->>UI: 배너 데이터 반환
UI-->>Admin: 배너 테이블 렌더링
Admin->>UI: 신규 배너 생성 입력
UI->>API: postBanner(formData)
API->>Server: POST /banner
Server-->>API: 생성된 배너 반환
API-->>UI: 결과 반환 및 쿼리 갱신
Admin->>UI: 배너 수정/삭제 클릭
UI->>API: patchBanner 또는 deleteBanner
API->>Server: PATCH/DELETE /banner/:id
Server-->>API: 결과 반환
API-->>UI: 결과 반환 및 쿼리 갱신
Assessment against linked issues
Suggested labels
Suggested reviewers
Poem
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
npm error Exit handler never called! 📜 Recent review detailsConfiguration used: CodeRabbit UI 📒 Files selected for processing (10)
✅ Files skipped from review due to trivial changes (1)
🚧 Files skipped from review as they are similar to previous changes (9)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
✨ Finishing Touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. 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: 20
🔭 Outside diff range comments (1)
src/components/admin/banner/tableHeader/TableHeader.styled.ts (1)
3-42: 코드 중복 문제 해결 필요
BannerList.styled.ts에서 이미 동일한 테이블 스타일들이 정의되어 있습니다. 중복된 스타일 정의로 인한 일관성 문제와 유지보수성 저하가 우려됩니다.다음 중 하나의 방식으로 중복을 해결하세요:
- 공통 스타일 파일로 분리하여 재사용
- 한 파일에서만 정의하고 다른 파일에서 import하여 사용
-export const TableHeader = styled.thead` - background-color: ${({ theme }) => theme.buttonScheme.primary.bg}; -`; +// BannerList.styled.ts에서 import하여 사용하거나 공통 파일로 분리
🧹 Nitpick comments (16)
src/pages/admin/adminBanner/AdminBanner.styled.ts (1)
3-5: 고정 너비 설정 검토 필요Container의 고정 너비 1300px 설정이 다양한 화면 크기에 적절한지 검토해 주세요. 특히 작은 화면에서의 가독성과 반응형 디자인을 고려하여
max-width: 1300px와 같은 유연한 접근 방식을 검토해 보시기 바랍니다.다음과 같은 대안을 고려해보세요:
export const Container = styled.div` - width: 1300px; + width: 100%; + max-width: 1300px; + margin: 0 auto; `;src/components/admin/banner/newBannerRow/NewBannerRow.styled.ts (2)
3-10: 스타일드 컴포넌트 중복을 개선해보세요.
TableRow컴포넌트가BannerRow.styled.ts와 매우 유사합니다. 공통 스타일드 컴포넌트를 별도 파일로 분리하여 재사용성을 높이는 것을 고려해보세요.공통 스타일드 컴포넌트를 위한 별도 파일을 생성하는 것을 제안합니다:
// src/components/admin/banner/common/CommonTable.styled.ts export const TableRow = styled.tr<{ variant?: 'default' | 'new' }>` border-bottom: 1px solid #e9ecef; background-color: ${({ variant }) => variant === 'new' ? '#f8f9fa' : 'transparent'}; &:hover { background-color: #e9ecef; } `;
20-27: 이미지 셀 스타일링에 중복이 있습니다.
BannerRow.styled.ts와BannerList.styled.ts에서 동일한 스타일의ImageCell이 정의되어 있습니다. 중복을 제거하고 공통 컴포넌트로 추출하는 것을 권장합니다.src/components/admin/banner/dateRange/DateRange.tsx (2)
23-30: 날짜 입력 필드의 placeholder 속성을 검토해보세요.HTML
date타입 입력 필드에서는placeholder속성이 효과적으로 작동하지 않을 수 있습니다. 브라우저마다 다르게 동작할 수 있으므로, 대신aria-label이나 주변 레이블을 사용하는 것을 권장합니다.<S.DateInput type='date' aria-label={placeholder?.start || '시작 날짜'} value={startDate} onChange={(e: ChangeEvent<HTMLInputElement>) => onStartDateChange(e.target.value) } />
15-41: 날짜 범위 유효성 검사 추가를 고려해보세요.시작 날짜가 종료 날짜보다 늦은 경우에 대한 유효성 검사나 사용자 피드백을 추가하는 것을 권장합니다.
컴포넌트 내부에서 유효성 검사를 추가하거나, 상위 컴포넌트에서 처리할 수 있도록 validation 콜백을 props로 추가할 수 있습니다:
interface DateRangeProps { // ... existing props onValidationChange?: (isValid: boolean) => void; }src/hooks/admin/useBannerMutations.ts (1)
11-11: State 타입명을 더 구체적으로 개선해보세요.
State타입은 너무 일반적입니다.MutationState또는OperationResult같은 더 구체적인 이름을 사용하는 것을 권장합니다.export type MutationState = 'success' | 'fail';src/components/admin/banner/tableHeader/TableHeader.styled.ts (1)
23-41: 하드코딩된 너비 값으로 인한 유지보수성 저하각 열의 너비가 하드코딩되어 있어 향후 테이블 구조 변경 시 유지보수가 어려울 수 있습니다.
상수 또는 테마 객체로 분리하여 관리하는 것을 고려해보세요:
+const COLUMN_WIDTHS = { + NO: '50px', + IMAGE: '280px', + STATUS: '100px', + VISIBILITY: '160px', + PERIOD: '240px', + MANAGEMENT: '140px', +} as const; export const TableHeaderCell = styled.th` padding: 16px; text-align: center; font-weight: 600; font-size: 14px; color: #ffffff; border-bottom: 2px solid #dee2e6; &:nth-child(1) { - width: 50px; + width: ${COLUMN_WIDTHS.NO}; } // ... 나머지 열에도 적용 `;src/hooks/admin/useImageManagement.ts (1)
22-41: 동적 입력 요소 생성 패턴 개선 필요동적으로 입력 요소를 생성하고 DOM에 추가/제거하는 방식은 작동하지만, 더 나은 패턴을 고려해보세요. 특히 사용자가 파일 선택을 취소한 경우에도 안전하게 정리됩니다.
ref를 활용한 더 안전한 패턴으로 개선할 수 있습니다:
const handleImageClick = ( bannerId?: number, onImageChange?: (imageUrl: string, bannerId?: number) => void ) => { - const input = document.createElement('input'); - input.type = 'file'; - input.accept = 'image/*'; - input.style.display = 'none'; - - input.onchange = (e) => { - const file = (e.target as HTMLInputElement).files?.[0]; - if (file) { - handleImageUpload(file, bannerId, onImageChange); - } - }; - - document.body.appendChild(input); - input.click(); - document.body.removeChild(input); + if (fileInputRef.current) { + fileInputRef.current.onchange = (e) => { + const file = (e.target as HTMLInputElement).files?.[0]; + if (file) { + handleImageUpload(file, bannerId, onImageChange); + } + }; + fileInputRef.current.click(); + } };src/components/admin/banner/BannerList.tsx (1)
45-47: 로딩 상태 UI 개선 고려현재 로딩 상태가 매우 기본적입니다. 관리자 페이지의 사용자 경험을 위해 더 나은 로딩 UI를 고려해보세요.
스켈레톤 UI나 스피너 컴포넌트로 개선할 수 있습니다:
if (isLoading) { - return <div>Loading...</div>; + return <LoadingSpinner />; + // 또는 <SkeletonTable /> }src/api/admin/banner.api.ts (2)
7-7: 프로덕션 환경에서는 디버깅 로그를 제거하세요.API 응답을 콘솔에 출력하는 것은 민감한 정보 노출 위험이 있습니다.
- console.log(response.data);
17-28: FormData 로깅 시 보안 위험을 고려하세요.FormData 내용을 모두 로깅하면 민감한 정보가 노출될 수 있습니다. 프로덕션 환경에서는 이 로깅을 제거하거나 제한하는 것이 좋습니다.
src/components/admin/banner/newBannerRow/NewBannerRow.tsx (1)
37-51: 파일 입력 처리 방식을 개선하세요.동적으로 DOM 요소를 생성하고 제거하는 방식보다는 ref를 사용하는 것이 더 안전합니다.
+import { useRef } from 'react'; const NewBannerRow = ({ newBanner, canCreateBanner, onInputChange, onCreate, }: NewBannerRowProps) => { + const fileInputRef = useRef<HTMLInputElement>(null); - const handleImageClick = () => { - const input = document.createElement('input'); - input.type = 'file'; - input.accept = 'image/*'; - input.style.display = 'none'; - - input.onchange = (event) => - handleFileInputChange( - event as unknown as React.ChangeEvent<HTMLInputElement> - ); - - document.body.appendChild(input); - input.click(); - document.body.removeChild(input); - }; + const handleImageClick = () => { + fileInputRef.current?.click(); + }; return ( <S.TableRow> + <input + ref={fileInputRef} + type="file" + accept="image/*" + style={{ display: 'none' }} + onChange={handleFileInputChange} + /> {/* ... rest of component */} </S.TableRow> ); };src/mock/adminMock.ts (1)
121-122: ID 생성 방식의 충돌 가능성을 고려하세요.
Date.now()를 ID로 사용하면 빠른 연속 요청 시 중복 ID가 생성될 수 있습니다.- id: Date.now(), + id: Date.now() + Math.random(),src/components/admin/banner/bannerRow/BannerRow.tsx (2)
36-52: 파일 입력을 위한 더 나은 React 패턴 사용 고려현재 구현은 동작하지만, DOM을 직접 조작하는 방식은 React의 선언적 패러다임과 맞지 않습니다. 컴포넌트가 언마운트될 때 정리되지 않을 수 있는 문제도 있습니다.
다음과 같은 ref 기반 접근 방식을 고려해보세요:
+import { useState, useRef } from 'react'; const BannerRow = ({...}) => { const [hoveredImageId, setHoveredImageId] = useState<number | null>(null); + const fileInputRef = useRef<HTMLInputElement>(null); const handleImageClick = () => { - const input = document.createElement('input'); - input.type = 'file'; - input.accept = 'image/*'; - input.style.display = 'none'; - - input.onchange = async (event) => { - const file = (event.target as HTMLInputElement).files?.[0]; - if (file) { - await onImageChange(banner.id, file); - } - }; - - document.body.appendChild(input); - input.click(); - document.body.removeChild(input); + fileInputRef.current?.click(); }; + const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => { + const file = event.target.files?.[0]; + if (file) { + await onImageChange(banner.id, file); + event.target.value = ''; // Reset input + } + }; return ( <S.TableRow key={banner.id}> + <input + ref={fileInputRef} + type="file" + accept="image/*" + style={{ display: 'none' }} + onChange={handleFileChange} + />
72-72: 불필요한 key prop 제거
BannerRow컴포넌트가 리스트 아이템으로 사용될 때 부모 컴포넌트에서 key를 할당해야 합니다. 여기서는 불필요합니다.- <S.TableRow key={banner.id}> + <S.TableRow>src/hooks/admin/useBannerManagement.ts (1)
177-177: 타입 단언(Type Assertion) 대신 타입 가드 사용 권장
as BannerFormData타입 단언은 타입 안전성을 해칠 수 있습니다.const handleInputChange = ( field: keyof BannerFormData, value: string | boolean | File ) => { - setNewBanner((prev) => ({ ...prev, [field]: value } as BannerFormData)); + setNewBanner((prev) => { + const updated = { ...prev }; + if (field === 'imageUrl' && value instanceof File) { + updated.imageUrl = value; + } else if (field === 'visible' && typeof value === 'boolean') { + updated.visible = value; + } else if (field === 'always' && typeof value === 'boolean') { + updated.always = value; + } else if ((field === 'startDate' || field === 'endDate') && typeof value === 'string') { + updated[field] = value; + } + return updated; + }); };
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (28)
src/api/admin/banner.api.ts(1 hunks)src/components/admin/banner/BannerList.styled.ts(1 hunks)src/components/admin/banner/BannerList.tsx(1 hunks)src/components/admin/banner/bannerRow/BannerRow.styled.ts(1 hunks)src/components/admin/banner/bannerRow/BannerRow.tsx(1 hunks)src/components/admin/banner/dateRange/DateRange.styled.ts(1 hunks)src/components/admin/banner/dateRange/DateRange.tsx(1 hunks)src/components/admin/banner/imageUploadArea/ImageUploadArea.styled.ts(1 hunks)src/components/admin/banner/imageUploadArea/ImageUploadArea.tsx(1 hunks)src/components/admin/banner/newBannerRow/NewBannerRow.styled.ts(1 hunks)src/components/admin/banner/newBannerRow/NewBannerRow.tsx(1 hunks)src/components/admin/banner/radioGroup/RadioGroup.styled.ts(1 hunks)src/components/admin/banner/radioGroup/RadioGroup.tsx(1 hunks)src/components/admin/banner/tableHeader/TableHeader.styled.ts(1 hunks)src/components/admin/banner/tableHeader/TableHeader.tsx(1 hunks)src/components/admin/banner/toggleSwitch/ToggleSwitch.styled.ts(1 hunks)src/components/admin/banner/toggleSwitch/ToggleSwitch.tsx(1 hunks)src/hooks/admin/useBannerManagement.ts(1 hunks)src/hooks/admin/useBannerMutations.ts(1 hunks)src/hooks/admin/useGetAllBannerList.ts(1 hunks)src/hooks/admin/useImageManagement.ts(1 hunks)src/hooks/queries/keys.ts(2 hunks)src/main.tsx(1 hunks)src/mock/adminMock.ts(2 hunks)src/mock/browser.ts(2 hunks)src/models/admin/banner.ts(1 hunks)src/pages/admin/adminBanner/AdminBanner.styled.ts(1 hunks)src/pages/admin/adminBanner/AdminBanner.tsx(1 hunks)
🧰 Additional context used
🧠 Learnings (6)
📓 Common learnings
Learnt from: YouD0313
PR: devpalsPlus/frontend#334
File: src/components/common/admin/title/AdminTitle.styled.ts:4-10
Timestamp: 2025-06-05T15:59:56.230Z
Learning: YouD0313 prefers using position: fixed for admin headers due to layout requirements, even if it requires viewport-relative width calculations like calc(100vw - 20rem).
src/hooks/admin/useGetAllBannerList.ts (2)
Learnt from: layout-SY
PR: devpalsPlus/frontend#246
File: src/hooks/useAlarmList.ts:1-21
Timestamp: 2025-04-23T13:39:08.960Z
Learning: React Query의 useQuery 훅에서 queryFn으로 Promise를 반환하는 함수를 사용할 때는 불필요한 async/await을 사용하지 않는 것이 좋습니다. 예를 들어 `queryFn: async () => await getAlarmList()` 대신에 `queryFn: () => getAlarmList()`와 같이 직접 Promise를 반환하는 형태로 작성하는 것이 더 효율적입니다.
Learnt from: layout-SY
PR: devpalsPlus/frontend#246
File: src/hooks/useAlarmList.ts:1-21
Timestamp: 2025-04-23T13:39:08.960Z
Learning: React Query의 useQuery 훅에서 queryFn으로 Promise를 반환하는 함수를 사용할 때는 불필요한 async/await을 사용하지 않는 것이 좋습니다. 예를 들어 `queryFn: async () => await getAlarmList()` 대신에 `queryFn: () => getAlarmList()`와 같이 직접 Promise를 반환하는 형태로 작성하는 것이 더 효율적입니다.
src/components/admin/banner/radioGroup/RadioGroup.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.136Z
Learning: React에서 controlled 컴포넌트(value prop이 있는 input)를 사용할 때는 onChange 핸들러도 함께 제공해야 합니다. 그렇지 않으면 입력 필드가 읽기 전용이 되어 사용자가 값을 입력할 수 없습니다.
src/components/admin/banner/BannerList.tsx (2)
Learnt from: YouD0313
PR: devpalsPlus/frontend#278
File: src/pages/login/LoginSuccess.tsx:13-24
Timestamp: 2025-05-11T09:39:02.041Z
Learning: 사용자 알림은 브라우저의 기본 alert() 대신 애플리케이션의 Modal 컴포넌트(src/components/common/modal/Modal.tsx)를 사용하는 것이 선호됩니다.
Learnt from: YouD0313
PR: devpalsPlus/frontend#278
File: src/pages/login/LoginSuccess.tsx:13-24
Timestamp: 2025-05-11T09:39:02.041Z
Learning: 사용자 알림은 브라우저의 기본 alert() 대신 애플리케이션의 Modal 컴포넌트(src/components/common/modal/Modal.tsx)를 사용하는 것이 선호됩니다.
src/components/admin/banner/toggleSwitch/ToggleSwitch.styled.ts (1)
Learnt from: YouD0313
PR: devpalsPlus/frontend#255
File: src/components/common/customerService/CustomerServiceHeader.styled.ts:45-62
Timestamp: 2025-04-26T17:24:42.853Z
Learning: Interactive elements like buttons should not be nested inside other interactive elements like links (a tags). When creating a clickable UI inside a Link component, using a div instead of a button is more semantically correct.
src/hooks/admin/useBannerManagement.ts (4)
Learnt from: YouD0313
PR: devpalsPlus/frontend#278
File: src/pages/login/LoginSuccess.tsx:13-24
Timestamp: 2025-05-11T09:39:02.041Z
Learning: useEffect 내에서 사용하는 모든 외부 변수(예: searchParams, storeLogin, navigate)는 의존성 배열에 포함되어야 합니다.
Learnt from: YouD0313
PR: devpalsPlus/frontend#278
File: src/pages/login/LoginSuccess.tsx:13-24
Timestamp: 2025-05-11T09:39:02.041Z
Learning: useEffect 내에서 사용하는 모든 외부 변수(예: searchParams, storeLogin, navigate)는 의존성 배열에 포함되어야 합니다.
Learnt from: YouD0313
PR: devpalsPlus/frontend#278
File: src/pages/login/LoginSuccess.tsx:13-24
Timestamp: 2025-05-11T09:39:02.041Z
Learning: useEffect 내에서 사용하는 모든 외부 변수(예: searchParams, storeLogin, navigate)는 의존성 배열에 포함되어야 합니다.
Learnt from: YouD0313
PR: devpalsPlus/frontend#278
File: src/pages/login/LoginSuccess.tsx:13-24
Timestamp: 2025-05-11T09:39:02.041Z
Learning: useEffect 내에서 사용하는 모든 외부 변수(예: searchParams, storeLogin, navigate)는 의존성 배열에 포함되어야 합니다.
🧬 Code Graph Analysis (21)
src/pages/admin/adminBanner/AdminBanner.styled.ts (1)
src/components/admin/banner/BannerList.styled.ts (1)
Container(8-15)
src/mock/browser.ts (2)
src/api/admin/banner.api.ts (4)
getBannerList(4-13)postBanner(15-35)patchBanner(37-45)deleteBanner(47-55)src/mock/adminMock.ts (4)
getBannerList(66-85)postBanner(87-145)patchBanner(147-212)deleteBanner(214-246)
src/components/admin/banner/tableHeader/TableHeader.tsx (1)
src/components/admin/banner/tableHeader/TableHeader.styled.ts (1)
TableHeader(3-5)
src/components/admin/banner/radioGroup/RadioGroup.styled.ts (1)
src/components/admin/banner/BannerList.styled.ts (1)
RadioGroup(160-202)
src/hooks/admin/useGetAllBannerList.ts (3)
src/hooks/queries/keys.ts (1)
Banners(85-87)src/api/admin/banner.api.ts (1)
getBannerList(4-13)src/mock/adminMock.ts (1)
getBannerList(66-85)
src/components/admin/banner/radioGroup/RadioGroup.tsx (2)
src/components/admin/banner/BannerList.styled.ts (1)
RadioGroup(160-202)src/components/admin/banner/radioGroup/RadioGroup.styled.ts (1)
RadioGroup(3-7)
src/components/admin/banner/toggleSwitch/ToggleSwitch.tsx (2)
src/components/admin/banner/BannerList.styled.ts (1)
ToggleSwitch(118-158)src/components/admin/banner/toggleSwitch/ToggleSwitch.styled.ts (1)
ToggleSwitch(3-8)
src/main.tsx (1)
src/mock/browser.ts (1)
worker(76-76)
src/components/admin/banner/BannerList.tsx (2)
src/hooks/useModal.ts (1)
useModal(3-36)src/hooks/admin/useBannerManagement.ts (1)
useBannerManagement(35-214)
src/components/admin/banner/newBannerRow/NewBannerRow.styled.ts (3)
src/components/admin/banner/BannerList.styled.ts (4)
TableRow(29-34)TableCell(36-42)ImageCell(44-49)CreateButton(250-250)src/components/admin/banner/bannerRow/BannerRow.styled.ts (4)
TableRow(3-9)TableCell(11-17)ImageCell(19-29)Placeholder(72-75)src/components/admin/banner/tableHeader/TableHeader.styled.ts (1)
TableRow(7-13)
src/hooks/admin/useBannerMutations.ts (4)
src/constants/admin/adminModal.ts (1)
ADMIN_MODAL_MESSAGE(1-10)src/api/admin/banner.api.ts (3)
postBanner(15-35)patchBanner(37-45)deleteBanner(47-55)src/mock/adminMock.ts (3)
postBanner(87-145)patchBanner(147-212)deleteBanner(214-246)src/hooks/queries/keys.ts (1)
Banners(85-87)
src/components/admin/banner/toggleSwitch/ToggleSwitch.styled.ts (1)
src/components/admin/banner/BannerList.styled.ts (1)
ToggleSwitch(118-158)
src/models/admin/banner.ts (1)
src/models/apiCommon.ts (1)
ApiCommonType(1-4)
src/components/admin/banner/imageUploadArea/ImageUploadArea.styled.ts (1)
src/components/admin/banner/BannerList.styled.ts (5)
ImageUploadArea(51-58)Thumbnail(60-65)ImageOverlay(67-78)EditIcon(80-85)PlusButton(98-116)
src/components/admin/banner/newBannerRow/NewBannerRow.tsx (4)
src/models/admin/banner.ts (1)
BannerFormData(12-18)src/components/admin/banner/imageUploadArea/ImageUploadArea.styled.ts (1)
ImageUploadArea(3-18)src/components/admin/banner/toggleSwitch/ToggleSwitch.styled.ts (1)
ToggleSwitch(3-8)src/components/admin/banner/radioGroup/RadioGroup.styled.ts (1)
RadioGroup(3-7)
src/components/admin/banner/dateRange/DateRange.tsx (1)
src/components/admin/banner/BannerList.styled.ts (1)
DateRange(204-213)
src/components/admin/banner/tableHeader/TableHeader.styled.ts (3)
src/components/admin/banner/BannerList.styled.ts (3)
TableHeader(17-19)TableRow(29-34)TableHeaderCell(21-25)src/components/admin/banner/bannerRow/BannerRow.styled.ts (1)
TableRow(3-9)src/components/admin/banner/newBannerRow/NewBannerRow.styled.ts (1)
TableRow(3-10)
src/components/admin/banner/imageUploadArea/ImageUploadArea.tsx (1)
src/components/admin/banner/imageUploadArea/ImageUploadArea.styled.ts (1)
ImageUploadArea(3-18)
src/components/admin/banner/bannerRow/BannerRow.styled.ts (3)
src/components/admin/banner/BannerList.styled.ts (5)
TableRow(29-34)TableCell(36-42)ImageCell(44-49)EditButton(231-235)DeleteButton(237-240)src/components/admin/banner/newBannerRow/NewBannerRow.styled.ts (4)
TableRow(3-10)TableCell(12-18)ImageCell(20-27)Placeholder(49-52)src/components/admin/banner/tableHeader/TableHeader.styled.ts (1)
TableRow(7-13)
src/mock/adminMock.ts (2)
src/models/admin/banner.ts (1)
BannerItem(3-10)src/api/admin/banner.api.ts (4)
getBannerList(4-13)postBanner(15-35)patchBanner(37-45)deleteBanner(47-55)
src/components/admin/banner/BannerList.styled.ts (6)
src/pages/admin/adminBanner/AdminBanner.styled.ts (1)
Container(3-5)src/components/admin/banner/tableHeader/TableHeader.styled.ts (3)
TableHeader(3-5)TableHeaderCell(15-41)TableRow(7-13)src/components/admin/banner/bannerRow/BannerRow.styled.ts (5)
TableRow(3-9)TableCell(11-17)ImageCell(19-29)EditButton(31-50)DeleteButton(52-65)src/components/admin/banner/imageUploadArea/ImageUploadArea.styled.ts (5)
ImageUploadArea(3-18)Thumbnail(20-28)ImageOverlay(30-41)EditIcon(43-48)PlusButton(50-69)src/components/admin/banner/toggleSwitch/ToggleSwitch.styled.ts (1)
ToggleSwitch(3-8)src/components/admin/banner/radioGroup/RadioGroup.styled.ts (1)
RadioGroup(3-7)
⏰ Context from checks skipped due to timeout of 90000ms (1)
- GitHub Check: accessibility-test
🔇 Additional comments (37)
src/main.tsx (1)
5-8: 개발 환경에서 Mock Service Worker 활성화 승인새로운 배너 관리 API 모킹을 지원하기 위해 개발 환경에서 Mock Service Worker를 활성화하는 것이 적절합니다. 조건부 import와 비동기 초기화 패턴이 올바르게 구현되어 있습니다.
src/pages/admin/adminBanner/AdminBanner.tsx (1)
1-12: 깔끔한 페이지 컴포넌트 구현AdminBanner 컴포넌트가 적절한 구조로 구현되어 있습니다. AdminTitle과 BannerList 컴포넌트를 조합하여 관리자 배너 페이지를 구성하는 방식이 명확하고 유지보수하기 좋습니다.
src/mock/browser.ts (2)
34-37: 배너 관련 Mock 핸들러 import 승인새로운 배너 관리 기능을 위한 4개의 Mock 핸들러가 올바르게 import되었습니다. CRUD 연산을 모두 지원하는 완전한 구성입니다.
68-71: Mock 핸들러 등록 완료배너 관련 Mock 핸들러들이 handlers 배열에 올바르게 추가되었습니다. 이로써 개발 환경에서 배너 관리 기능을 완전히 테스트할 수 있게 되었습니다.
src/hooks/admin/useGetAllBannerList.ts (1)
5-17: React Query 패턴 우수 구현React Query 훅이 올바른 패턴으로 구현되어 있습니다. 특히
queryFn: () => getBannerList()와 같이 불필요한 async/await 없이 Promise를 직접 반환하는 형태로 작성되어 효율적입니다. 필요한 상태(isLoading,isFetching)와 함수(refetch)를 적절히 반환하고 있습니다.src/hooks/queries/keys.ts (2)
54-54: 새로운 쿼리 키 추가가 적절합니다.기존 Inquiries 상수에
inquiriesPreview키를 추가한 것이 구조적으로 일관성이 있습니다.
85-87: 배너 관리를 위한 쿼리 키 구조가 올바릅니다.새로운
Banners상수와allBanners키가 다른 쿼리 키들과 일관된 패턴을 따르고 있어 좋습니다.src/components/admin/banner/tableHeader/TableHeader.tsx (1)
3-14: 테이블 헤더 컴포넌트 구현이 깔끔합니다.시맨틱 HTML 구조를 잘 활용하고 있으며, 한국어 레이블이 배너 관리 기능에 적합합니다.
src/components/admin/banner/radioGroup/RadioGroup.tsx (1)
3-7: TypeScript 인터페이스가 잘 정의되어 있습니다.controlled component에 필요한 props가 적절히 정의되어 있습니다.
src/components/admin/banner/toggleSwitch/ToggleSwitch.tsx (2)
3-7: TypeScript 인터페이스가 명확하게 정의되어 있습니다.토글 스위치에 필요한 props가 적절히 타입 정의되어 있습니다.
9-19: 토글 스위치 구현이 올바릅니다.controlled component 패턴을 잘 따르고 있으며, 접근성을 위한 htmlFor 속성과 적절한 onChange 핸들러를 제공하고 있습니다.
src/components/admin/banner/radioGroup/RadioGroup.styled.ts (2)
3-7: RadioGroup 컨테이너 스타일링이 적절합니다.flex 레이아웃과 간격, 정렬이 잘 설정되어 있습니다.
9-15: 네이티브 라디오 입력 스타일링이 간결합니다.기본 스타일링이 적절하게 적용되어 있습니다.
src/models/admin/banner.ts (3)
3-10: 인터페이스 정의가 잘 구성되어 있습니다.배너 엔티티의 모든 필요한 속성이 포함되어 있고, 타입 정의가 명확합니다. 날짜 필드를 문자열로 처리하는 것은 API 통신에 적절합니다.
12-18: 폼 데이터 인터페이스가 실용적으로 설계되었습니다.
imageUrl을 선택적 File 타입으로 정의한 것은 이미지 업로드 시나리오를 잘 고려한 설계입니다.
20-22: API 응답 타입이 일관성 있게 확장되었습니다.
ApiCommonType을 적절히 확장하여 공통 API 응답 패턴을 유지하고 있습니다.src/components/admin/banner/newBannerRow/NewBannerRow.styled.ts (3)
12-18: 테이블 셀 스타일링이 일관성 있게 적용되었습니다.다른 배너 관련 컴포넌트들과 일관된 스타일링을 유지하고 있습니다.
29-47: 버튼 스타일링이 잘 구현되었습니다.hover 상태와 disabled 상태가 적절히 처리되어 있고, 색상 선택도 직관적입니다.
49-52: 플레이스홀더 스타일이 적절합니다.다른 컴포넌트들과 일관된 플레이스홀더 스타일을 유지하고 있습니다.
src/components/admin/banner/dateRange/DateRange.tsx (1)
4-13: Props 인터페이스가 명확하게 정의되었습니다.타입 정의가 적절하고 선택적 placeholder 속성이 유연성을 제공합니다.
src/components/admin/banner/dateRange/DateRange.styled.ts (3)
3-7: 날짜 범위 컨테이너 스타일링이 적절합니다.플렉스 레이아웃과 적절한 간격을 사용하여 날짜 입력 필드들을 잘 정렬하고 있습니다.
9-12: 구분자 스타일링이 일관성 있게 적용되었습니다.다른 컴포넌트들과 일관된 색상과 폰트 크기를 사용하고 있습니다.
14-30: 날짜 입력 필드의 접근성 고려사항이 우수합니다.focus 상태에서 적절한 시각적 피드백을 제공하고 있으며, box-shadow를 사용한 포커스 링이 접근성 가이드라인을 잘 따르고 있습니다. hover 상태도 적절히 처리되어 있습니다.
src/hooks/admin/useBannerMutations.ts (3)
20-32: 상태 처리 함수가 잘 구성되어 있습니다.switch 문을 사용하여 명확하게 상태를 처리하고 있습니다.
62-73: 삭제 뮤테이션의 오류 처리가 적절합니다.성공과 실패 상황 모두에서 사용자에게 적절한 피드백을 제공하고 있습니다.
75-80: 반환 객체가 명확하게 구성되어 있습니다.세 개의 뮤테이션 객체를 명확하게 반환하여 사용성이 좋습니다.
src/components/admin/banner/toggleSwitch/ToggleSwitch.styled.ts (1)
10-14: 시각적으로 숨겨진 입력 요소 구현 승인접근성을 고려한 올바른 숨김 처리 방식입니다.
display: none대신opacity: 0과 크기 조정을 사용하여 스크린 리더가 여전히 요소에 접근할 수 있도록 했습니다.src/components/admin/banner/BannerList.tsx (1)
10-29: 훅 사용과 관심사 분리 잘 구현됨
useModal과useBannerManagement훅을 활용한 관심사 분리가 잘 되어 있습니다. 컴포넌트는 UI 렌더링에 집중하고, 비즈니스 로직은 커스텀 훅으로 분리되어 있어 유지보수성이 좋습니다.src/components/admin/banner/imageUploadArea/ImageUploadArea.tsx (1)
4-18: 깔끔한 인터페이스 설계와 타입 정의컴포넌트 인터페이스가 잘 설계되어 있고, 선택적 핸들러들이 적절히 정의되어 있습니다. 프롭 타입이 명확하고 기본값 설정도 적절합니다.
src/mock/adminMock.ts (2)
36-43: 파일 변환 함수가 잘 구현되었습니다.
fileToBase64함수는 Promise를 적절히 사용하여 파일을 base64로 변환하고 있습니다.
169-177: 404 에러 처리가 적절히 구현되었습니다.존재하지 않는 배너에 대한 적절한 오류 응답을 반환하고 있습니다.
src/components/admin/banner/imageUploadArea/ImageUploadArea.styled.ts (2)
3-18: 이미지 업로드 영역 스타일링이 잘 구현되었습니다.적절한 크기, 대시 보더, 호버 효과가 구현되어 있어 사용자 경험이 좋습니다.
50-69: 버튼 스타일링의 일관성이 좋습니다.ImageUploadArea와 동일한 크기와 스타일을 유지하면서 호버 효과도 적절히 구현되었습니다.
src/components/admin/banner/bannerRow/BannerRow.styled.ts (2)
31-50: 버튼 스타일링이 일관되게 구현되었습니다.EditButton의 호버 효과와 비활성화 상태 처리가 적절히 구현되었습니다.
26-29: ImageCell의 flexbox 사용이 적절합니다.이미지 컨테이너의 중앙 정렬을 위한 flexbox 사용이 적절합니다.
src/components/admin/banner/bannerRow/BannerRow.tsx (1)
1-126: 전반적으로 잘 구성된 컴포넌트입니다TypeScript 타입 정의가 명확하고, 책임 분리가 잘 되어 있으며, 컴포넌트 구조가 깔끔합니다.
src/components/admin/banner/BannerList.styled.ts (1)
14-14: 과도한 margin-top 값 확인 필요
margin-top: 10rem은 160px로 매우 큰 값입니다. 고정된 관리자 헤더를 위한 것인지 확인이 필요합니다.이 값이 의도적인 것인지, 아니면 조정이 필요한지 확인해주세요. 일반적으로 헤더 높이 + 추가 여백 정도면 충분합니다.
| const RadioGroup = ({ name, value, onChange }: RadioGroupProps) => ( | ||
| <S.RadioGroup> | ||
| <S.RadioInput | ||
| type='radio' | ||
| id={`always-${name}`} | ||
| name={`expType-${name}`} | ||
| checked={value} | ||
| onChange={() => onChange(true)} | ||
| /> | ||
| <S.RadioLabel htmlFor={`always-${name}`}>상시 노출</S.RadioLabel> | ||
| <S.RadioInput | ||
| type='radio' | ||
| id={`period-${name}`} | ||
| name={`expType-${name}`} | ||
| checked={!value} | ||
| onChange={() => onChange(false)} | ||
| /> | ||
| <S.RadioLabel htmlFor={`period-${name}`}>기간 노출</S.RadioLabel> | ||
| </S.RadioGroup> | ||
| ); |
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.
💡 Verification agent
🧩 Analysis chain
RadioGroup 컴포넌트 스타일링 중복을 확인해주세요.
컴포넌트 로직은 올바르게 구현되었지만, src/components/admin/banner/BannerList.styled.ts에 이미 다른 RadioGroup 스타일이 정의되어 있어 중복이나 혼동이 있을 수 있습니다. 네이티브 라디오 입력을 사용하는 현재 방식과 커스텀 스타일링을 사용하는 기존 방식 중 어느 것을 사용할지 일관성을 확인해주세요.
다음 스크립트로 라디오 그룹 스타일 중복을 확인해보겠습니다:
🏁 Script executed:
#!/bin/bash
# 라디오 그룹 스타일 중복 확인
echo "=== RadioGroup 스타일 컴포넌트 검색 ==="
rg -A 10 "export const RadioGroup" --type ts
echo "=== RadioGroup 컴포넌트 사용 위치 검색 ==="
rg "RadioGroup" --type tsx --type ts -A 2 -B 2Length of output: 1828
RadioGroup 스타일 컴포넌트 네이밍 충돌 확인 필요
src/components/admin/banner/radioGroup/RadioGroup.styled.ts와
src/components/admin/banner/BannerList.styled.ts에서
둘 다 export const RadioGroup를 정의하고 있어 네이밍 충돌 및 혼동이 발생할 수 있습니다.
- 대상 파일
- src/components/admin/banner/radioGroup/RadioGroup.styled.ts
- src/components/admin/banner/BannerList.styled.ts
두 스타일 중 하나를 통합하거나, 각 용도에 맞게 컴포넌트 이름(예: BannerRadioGroup, ExpTypeRadioGroup 등)을 구체화해 주세요.
🤖 Prompt for AI Agents
In src/components/admin/banner/radioGroup/RadioGroup.tsx lines 9 to 28, the
styled component RadioGroup imported from RadioGroup.styled.ts conflicts in name
with another RadioGroup styled component in BannerList.styled.ts. To fix this,
rename one or both styled components to more specific names reflecting their
usage, such as BannerRadioGroup or ExpTypeRadioGroup, and update all references
accordingly to avoid naming collisions and confusion.
| export const RadioLabel = styled.label` | ||
| font-size: 14px; | ||
| color: #495057; | ||
| cursor: pointer; | ||
| white-space: nowrap; | ||
|
|
||
| &:hover { | ||
| color: #007bff; | ||
| } | ||
| `; |
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.
💡 Verification agent
🛠️ Refactor suggestion
🧩 Analysis chain
라디오 레이블 스타일링에서 일관성을 확인해주세요.
현재 파일의 RadioGroup은 네이티브 라디오 입력을 사용하는 반면, src/components/admin/banner/BannerList.styled.ts의 RadioGroup은 커스텀 스타일링을 사용합니다. 두 접근 방식이 혼재되어 있어 UI 일관성 문제가 있을 수 있습니다. 프로젝트 전반에서 하나의 접근 방식으로 통일하는 것을 고려해주세요.
다음 스크립트로 RadioGroup 스타일 사용 패턴을 확인해보겠습니다:
🏁 Script executed:
#!/bin/bash
# RadioGroup 스타일 사용 패턴 분석
echo "=== 네이티브 라디오 입력 사용 패턴 ==="
rg -B 5 -A 10 "RadioInput.*type.*radio" --type ts --type tsx
echo "=== 커스텀 라디오 스타일 패턴 ==="
rg -B 5 -A 10 "display.*none.*input" --type ts --type tsx
echo "=== RadioGroup 컴포넌트 import 패턴 ==="
rg "import.*RadioGroup" --type ts --type tsxLength of output: 446
기존 스크립트에서 --type tsx 필터가 적용되지 않아 검색이 모두 실패했습니다. 타입 필터 없이 전체 파일을 대상으로 다시 검색해 BannerList.styled.ts 경로와 styled.input 혹은 display: none 스타일 패턴을 확인해보세요.
🏁 Script executed:
#!/bin/bash
# BannerList.styled.ts 경로 및 내용 확인
echo "🔍 파일 찾기: BannerList.styled.ts"
fd BannerList.styled.ts || true
echo ""
echo "🔍 styled.input 사용 패턴 검색"
rg "styled\.input" -n || true
echo ""
echo "🔍 input 요소에 display: none 검색"
rg "display\s*:\s*none" -n || trueLength of output: 4042
라디오 그룹 스타일링 일관성 확인 필요
프로젝트 전반에 두 가지 방식이 혼용되고 있습니다:
- 네이티브
<input type="radio">스타일링 (예:src/components/admin/banner/radioGroup/RadioGroup.styled.ts) - 라벨 및 가상 요소로 커스텀 스타일 적용 (예:
src/components/admin/banner/BannerList.styled.ts에서input { display: none; }패턴)
UI 일관성과 유지보수를 위해 아래 파일을 참고하여 하나의 패턴으로 통일하는 것을 권장드립니다:
• src/components/admin/banner/radioGroup/RadioGroup.styled.ts
• src/components/admin/banner/BannerList.styled.ts (라인 ~167, display: none; 처리된 input 및 커스텀 레이블)
어느 방식을 표준으로 채택할지 결정 후, 나머지 컴포넌트에도 동일하게 적용해주세요.
🤖 Prompt for AI Agents
In src/components/admin/banner/radioGroup/RadioGroup.styled.ts lines 17 to 26,
the current radio button styling uses native input styling, which is
inconsistent with the custom styled approach using hidden inputs and styled
labels found in src/components/admin/banner/BannerList.styled.ts around line
167. Decide on one standard styling method—either native input styling or custom
hidden inputs with styled labels—and refactor RadioGroup.styled.ts to match that
pattern for UI consistency and easier maintenance. Then apply the chosen pattern
uniformly across all related components.
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.
🧩 Analysis chain
@layout-SY 두 가지 라디오 버튼 스타일링 방식의 차이를 명확히 보여드리기 위해 실제 코드를 비교해보겠습니다.
🏁 Script executed:
#!/bin/bash
echo "=== 현재 파일 (네이티브 방식) ==="
echo "File: src/components/admin/banner/radioGroup/RadioGroup.styled.ts"
cat src/components/admin/banner/radioGroup/RadioGroup.styled.ts
echo ""
echo "=== BannerList.styled.ts 파일 (커스텀 방식) ==="
echo "File: src/components/admin/banner/BannerList.styled.ts"
cat src/components/admin/banner/BannerList.styled.ts | sed -n '160,180p'
echo ""
echo "=== RadioGroup 사용 부분 찾기 ==="
rg -A 5 -B 5 "RadioGroup" src/components/admin/banner/BannerList.styled.ts
Length of output: 1629
@layout-SY 이제 두 방식의 차이를 명확히 설명해드리겠습니다!
🔍 두 가지 라디오 버튼 스타일링 방식 비교
1️⃣ 네이티브 방식 (현재 파일 - RadioGroup.styled.ts)
export const RadioInput = styled.input`
margin: 0;
margin-right: 4px;
width: 16px;
height: 16px;
cursor: pointer;
`;특징:
- 브라우저의 기본 라디오 버튼을 그대로 사용
- 크기, 여백, 커서만 조정
- 실제
<input type="radio">요소가 보임 - 브라우저마다 모양이 약간 다를 수 있음
2️⃣ 커스텀 방식 (BannerList.styled.ts)
export const RadioGroup = styled.div`
input {
display: none; // 👈 실제 input을 완전히 숨김
}
label {
position: relative;
padding-left: 22px;
&::before { // 👈 바깥쪽 원 (테두리)
content: '';
position: absolute;
left: 0;
top: 2px;
width: 16px;
height: 16px;
border: 2px solid #666;
border-radius: 50%;
}
&::after { // 👈 안쪽 점 (선택 표시)
content: '';
position: absolute;
left: 4px;
top: 6px;
width: 8px;
height: 8px;
background-color: #6c5ce7;
border-radius: 50%;
opacity: 0; // 기본적으로 숨김
}
}
input:checked + label::after {
opacity: 1; // 👈 선택되면 안쪽 점 표시
}
`;특징:
- 실제
<input>요소를 완전히 숨김 (display: none) label의::before,::after가상 요소로 완전히 새로운 라디오 버튼 모양을 만듦- 모든 브라우저에서 동일한 모양 보장
- 디자인 컨트롤이 완전히 가능
🎨 시각적 차이
네이티브 방식:
○ 상시 노출 ○ 기간 노출
↳ 브라우저 기본 라디오 버튼 (Chrome, Firefox, Safari마다 조금씩 다름)
커스텀 방식:
○ 상시 노출 ● 기간 노출
↳ 완전히 커스텀 디자인된 라디오 버튼 (모든 브라우저에서 동일)
⚖️ 각 방식의 장단점
| 구분 | 네이티브 방식 | 커스텀 방식 |
|---|---|---|
| 구현 난이도 | ✅ 간단 | ❌ 복잡 |
| 접근성 | ✅ 우수 | |
| 브라우저 호환성 | ✅ 완벽 | ✅ 좋음 |
| 디자인 통일성 | ❌ 브라우저별 차이 | ✅ 완전 통일 |
| 유지보수 | ✅ 쉬움 | ❌ 복잡 |
🤔 현재 프로젝트의 문제점
프로젝트에서 두 방식이 혼재되어 있어:
- 사용자가 다른 페이지에서 다른 모양의 라디오 버튼을 보게 됨
- 디자인 일관성 부족
- 개발자가 어느 방식을 사용해야 할지 혼란
💡 권장사항
선택 기준:
- 디자인 통일성이 중요하다면 → 커스텀 방식
- 개발 속도와 접근성이 중요하다면 → 네이티브 방식
둘 중 하나를 선택해서 프로젝트 전체에 일관되게 적용하는 것이 좋습니다!
어떤 방식을 선호하시나요? 🤔
🧬 Code Graph Analysis Results
Relevant snippet from src/components/admin/banner/BannerList.styled.ts (lines 155-197):
export const RadioGroup = styled.div`
display: flex;
justify-content: center;
gap: 12px;
font-size: 14px;
input {
display: none;
}
label {
position: relative;
padding-left: 22px;
cursor: pointer;
&::before {
content: '';
position: absolute;
left: 0;
top: 2px;
width: 16px;
height: 16px;
border: 2px solid #666;
border-radius: 50%;
}
&::after {
content: '';
position: absolute;
left: 4px;
top: 6px;
width: 8px;
height: 8px;
background-color: #6c5ce7;
border-radius: 50%;
opacity: 0;
}
}
input:checked + label::after {
opacity: 1;
}
`;This snippet defines a styled RadioGroup component with custom radio button styling using hidden native inputs and styled labels with pseudo-elements ::before and ::after to create the radio circle and the checked indicator. It contrasts with the simpler styling in RadioGroup.styled.ts where the native radio input is visible and styled directly.
🧠 Learnings used
Learnt from: YouD0313
PR: devpalsPlus/frontend#334
File: src/components/common/admin/title/AdminTitle.styled.ts:4-10
Timestamp: 2025-06-05T15:59:56.230Z
Learning: YouD0313 prefers using position: fixed for admin headers due to layout requirements, even if it requires viewport-relative width calculations like calc(100vw - 20rem).
| export const ToggleSwitch = styled.div` | ||
| position: relative; | ||
| display: inline-block; | ||
| width: 48px; | ||
| height: 24px; | ||
| `; | ||
|
|
||
| export const HiddenInput = styled.input` | ||
| opacity: 0; | ||
| width: 0; | ||
| height: 0; | ||
| `; | ||
|
|
||
| export const ToggleLabel = styled.label` | ||
| position: absolute; | ||
| cursor: pointer; | ||
| top: 0; | ||
| left: 0; | ||
| right: 0; | ||
| bottom: 0; | ||
| background-color: #ccc; | ||
| border-radius: 24px; | ||
| transition: 0.3s; | ||
|
|
||
| &:before { | ||
| position: absolute; | ||
| content: ''; | ||
| height: 18px; | ||
| width: 18px; | ||
| left: 3px; | ||
| bottom: 3px; | ||
| background-color: white; | ||
| border-radius: 50%; | ||
| transition: 0.3s; | ||
| } | ||
|
|
||
| ${HiddenInput}:checked + & { | ||
| background-color: #007bff; | ||
| } | ||
|
|
||
| ${HiddenInput}:checked + &:before { | ||
| transform: translateX(24px); | ||
| } | ||
| `; |
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
토글 스위치 중복 구현 통합 필요
BannerList.styled.ts에 이미 토글 스위치 스타일이 정의되어 있습니다. 두 구현 간에 색상과 크기가 다르게 설정되어 있어 UI 일관성 문제가 발생할 수 있습니다.
하나의 통합된 토글 스위치 컴포넌트로 정리하여 일관성을 유지하세요:
// 현재 파일의 구현 (#007bff 색상, 48px 너비)
// vs BannerList.styled.ts의 구현 (#6c5ce7 색상, 50px 너비)
// 공통 토글 스위치 컴포넌트로 분리하거나
// 디자인 시스템에서 정의된 표준 색상과 크기를 사용하세요Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In src/components/admin/banner/toggleSwitch/ToggleSwitch.styled.ts lines 3 to
46, the toggle switch styles are duplicated with those in BannerList.styled.ts,
causing inconsistent colors and sizes in the UI. To fix this, consolidate the
toggle switch styles into a single shared styled component used across both
files. Refactor the styles to unify color schemes, dimensions, and transitions,
then replace the duplicated code in both locations with imports from this shared
component to maintain UI consistency.
|
|
||
| label { | ||
| position: absolute; | ||
| background: #ccc; |
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.
하드코딩된 색상 대신 테마 변수 사용
일관된 테마 관리를 위해 하드코딩된 색상값을 테마 변수로 교체해야 합니다.
label {
position: absolute;
- background: #ccc;
+ background: ${({ theme }) => theme.color.gray300};
border-radius: 999px;
width: 100%;
height: 100%;
cursor: pointer;
transition: background 0.2s ease-in-out;
}
input:checked + label {
- background-color: #6c5ce7;
+ background-color: ${({ theme }) => theme.buttonScheme.primary.bg};
}Also applies to: 152-152
🤖 Prompt for AI Agents
In src/components/admin/banner/BannerList.styled.ts at lines 131 and 152,
replace the hardcoded background color #ccc with the appropriate theme variable
to ensure consistent theming. Identify the correct color variable from the theme
object and use it instead of the literal color code in the styled component.
| top: 2px; | ||
| width: 16px; | ||
| height: 16px; | ||
| border: 2px solid #666; |
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.
RadioGroup에서도 테마 변수 사용 필요
RadioGroup 컴포넌트에서도 하드코딩된 색상을 테마 변수로 교체해주세요.
width: 16px;
height: 16px;
- border: 2px solid #666;
+ border: 2px solid ${({ theme }) => theme.color.gray600};
border-radius: 50%;
}
&::after {
content: '';
position: absolute;
left: 4px;
top: 6px;
width: 8px;
height: 8px;
- background-color: #6c5ce7;
+ background-color: ${({ theme }) => theme.buttonScheme.primary.bg};
border-radius: 50%;
opacity: 0;
}Also applies to: 193-193
🤖 Prompt for AI Agents
In src/components/admin/banner/BannerList.styled.ts at lines 182 and 193,
replace the hardcoded color value #666 used in the border property of the
RadioGroup component with the appropriate color variable from the theme. Access
the theme object and use its color property instead of the fixed hex code to
ensure consistent theming.
| padding: 0 12px; | ||
| font-size: 14px; | ||
| border-radius: 18px; | ||
| border: 1px solid #ccc; |
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
DateInput 테두리 색상도 테마 변수 사용
날짜 입력 필드의 테두리 색상도 테마를 사용해주세요.
font-size: 14px;
border-radius: 18px;
- border: 1px solid #ccc;
+ border: 1px solid ${({ theme }) => theme.color.gray300};📝 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.
| border: 1px solid #ccc; | |
| font-size: 14px; | |
| border-radius: 18px; | |
| border: 1px solid ${({ theme }) => theme.color.gray300}; |
🤖 Prompt for AI Agents
In src/components/admin/banner/BannerList.styled.ts at line 220, the border
color for the DateInput is hardcoded as #ccc. Replace this hardcoded color with
the appropriate theme variable for border color to ensure consistent theming
across the application.
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 (2)
src/components/admin/banner/BannerList.tsx (2)
30-30: 브라우저 기본 confirm 대신 Modal 컴포넌트 사용 필요학습된 선호 사항에 따르면 브라우저의 기본
window.confirm대신 애플리케이션의 Modal 컴포넌트를 사용해야 합니다.
53-57: 테이블 구조 문제 수정 필요
colgroup이table요소 없이 직접S.Container안에 정의되어 있습니다. 이는 유효하지 않은 HTML 구조입니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (11)
src/api/admin/banner.api.ts(1 hunks)src/components/admin/banner/BannerList.styled.ts(1 hunks)src/components/admin/banner/BannerList.tsx(1 hunks)src/components/admin/banner/bannerRow/BannerRow.styled.ts(1 hunks)src/components/admin/banner/bannerRow/BannerRow.tsx(1 hunks)src/components/admin/banner/bannerRow/useBannerRow.ts(1 hunks)src/components/admin/banner/newBannerRow/NewBannerRow.tsx(1 hunks)src/components/admin/banner/newBannerRow/useNewBannerRow.ts(1 hunks)src/hooks/admin/useBannerMutations.ts(1 hunks)src/hooks/admin/useGetAllBannerList.ts(1 hunks)src/models/admin/banner.ts(1 hunks)
✅ Files skipped from review due to trivial changes (1)
- src/components/admin/banner/BannerList.styled.ts
🚧 Files skipped from review as they are similar to previous changes (5)
- src/hooks/admin/useGetAllBannerList.ts
- src/hooks/admin/useBannerMutations.ts
- src/components/admin/banner/newBannerRow/NewBannerRow.tsx
- src/api/admin/banner.api.ts
- src/components/admin/banner/bannerRow/BannerRow.styled.ts
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: YouD0313
PR: devpalsPlus/frontend#334
File: src/components/common/admin/title/AdminTitle.styled.ts:4-10
Timestamp: 2025-06-05T15:59:56.230Z
Learning: YouD0313 prefers using position: fixed for admin headers due to layout requirements, even if it requires viewport-relative width calculations like calc(100vw - 20rem).
src/components/admin/banner/BannerList.tsx (4)
Learnt from: YouD0313
PR: devpalsPlus/frontend#278
File: src/pages/login/LoginSuccess.tsx:13-24
Timestamp: 2025-05-11T09:39:02.041Z
Learning: 사용자 알림은 브라우저의 기본 alert() 대신 애플리케이션의 Modal 컴포넌트(src/components/common/modal/Modal.tsx)를 사용하는 것이 선호됩니다.
Learnt from: YouD0313
PR: devpalsPlus/frontend#278
File: src/pages/login/LoginSuccess.tsx:13-24
Timestamp: 2025-05-11T09:39:02.041Z
Learning: 사용자 알림은 브라우저의 기본 alert() 대신 애플리케이션의 Modal 컴포넌트(src/components/common/modal/Modal.tsx)를 사용하는 것이 선호됩니다.
Learnt from: YouD0313
PR: devpalsPlus/frontend#278
File: src/pages/login/LoginSuccess.tsx:13-24
Timestamp: 2025-05-11T09:39:02.041Z
Learning: 로그인 오류와 같은 사용자 알림은 브라우저의 기본 alert() 대신 애플리케이션의 모달 컴포넌트를 사용하는 것이 선호됩니다.
Learnt from: YouD0313
PR: devpalsPlus/frontend#278
File: src/pages/login/LoginSuccess.tsx:13-24
Timestamp: 2025-05-11T09:39:02.041Z
Learning: 사용자 알림은 브라우저의 기본 alert() 대신 애플리케이션의 모달 컴포넌트를 사용하는 것이 선호됩니다.
🧬 Code Graph Analysis (1)
src/models/admin/banner.ts (1)
src/models/apiCommon.ts (1)
ApiCommonType(1-4)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: accessibility-test
🔇 Additional comments (3)
src/models/admin/banner.ts (1)
1-31: 타입 정의가 잘 구성되어 있습니다!배너 관리 기능에 필요한 인터페이스들이 명확하게 정의되어 있으며, API 응답 타입과 폼 데이터 타입이 적절히 분리되어 있습니다.
src/components/admin/banner/newBannerRow/useNewBannerRow.ts (1)
1-74: 새 배너 생성 훅이 잘 구현되었습니다!상태 관리, 입력 검증, 에러 처리가 적절히 구현되어 있으며, FormData를 활용한 이미지 업로드 처리가 올바르게 되어 있습니다.
src/components/admin/banner/bannerRow/BannerRow.tsx (1)
1-124: 배너 행 컴포넌트가 잘 구조화되어 있습니다!이미지 업로드 처리, 상태 관리, UI 컴포넌트 분리가 적절히 되어 있으며, 조건부 렌더링 로직이 명확합니다.
| imageUrl: changes.newImageFile | ||
| ? URL.createObjectURL(changes.newImageFile) | ||
| : banner.imageUrl, |
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.
URL.createObjectURL 메모리 누수 가능성
URL.createObjectURL로 생성된 URL은 메모리에서 해제되지 않으면 메모리 누수가 발생할 수 있습니다. 컴포넌트가 언마운트되거나 이미지가 변경될 때 URL.revokeObjectURL을 호출하여 메모리를 정리해야 합니다.
useEffect cleanup 함수에서 URL을 해제하는 방식으로 수정하세요:
+import { useState, useCallback, useEffect } from 'react';
export const useBannerRow = (banner: BannerItem) => {
const { handleModalOpen } = useModal();
const { patchBannerMutate } = useBannerMutations({ handleModalOpen });
const [changes, setChanges] = useState<BannerChanges>({});
+ const [objectUrl, setObjectUrl] = useState<string | null>(null);
+
+ useEffect(() => {
+ if (changes.newImageFile) {
+ const url = URL.createObjectURL(changes.newImageFile);
+ setObjectUrl(url);
+ return () => URL.revokeObjectURL(url);
+ }
+ }, [changes.newImageFile]);
// 현재 표시할 값들 계산
const currentValues: CurrentValues = {
visible: changes.visible ?? banner.visible,
always: changes.always ?? banner.always,
startDate: changes.startDate ?? banner.startDate,
endDate: changes.endDate ?? banner.endDate,
- imageUrl: changes.newImageFile
- ? URL.createObjectURL(changes.newImageFile)
- : banner.imageUrl,
+ imageUrl: objectUrl ?? banner.imageUrl,
};📝 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.
| imageUrl: changes.newImageFile | |
| ? URL.createObjectURL(changes.newImageFile) | |
| : banner.imageUrl, | |
| import { useState, useCallback, useEffect } from 'react'; | |
| export const useBannerRow = (banner: BannerItem) => { | |
| const { handleModalOpen } = useModal(); | |
| const { patchBannerMutate } = useBannerMutations({ handleModalOpen }); | |
| const [changes, setChanges] = useState<BannerChanges>({}); | |
| const [objectUrl, setObjectUrl] = useState<string | null>(null); | |
| useEffect(() => { | |
| if (changes.newImageFile) { | |
| const url = URL.createObjectURL(changes.newImageFile); | |
| setObjectUrl(url); | |
| return () => { | |
| URL.revokeObjectURL(url); | |
| }; | |
| } | |
| }, [changes.newImageFile]); | |
| // 현재 표시할 값들 계산 | |
| const currentValues: CurrentValues = { | |
| visible: changes.visible ?? banner.visible, | |
| always: changes.always ?? banner.always, | |
| startDate: changes.startDate ?? banner.startDate, | |
| endDate: changes.endDate ?? banner.endDate, | |
| imageUrl: objectUrl ?? banner.imageUrl, | |
| }; | |
| // …rest of hook logic… | |
| }; |
🤖 Prompt for AI Agents
In src/components/admin/banner/bannerRow/useBannerRow.ts around lines 26 to 28,
the code creates an object URL using URL.createObjectURL without releasing it,
which can cause memory leaks. Modify the code to store the created URL in state
or a ref, and use a useEffect hook with a cleanup function to call
URL.revokeObjectURL when the component unmounts or when the image changes,
ensuring proper memory cleanup.
|
|
||
| export const CreateButton = styled(Button)``; | ||
|
|
||
| export const CancelButton = styled(Button)``; |
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.
250과 중복입니다!
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.
감사합니다
| @@ -0,0 +1,125 @@ | |||
| import * as S from './BannerRow.styled'; | |||
| import { BannerItem } from '../../../../models/admin/banner'; | |||
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.
type
구현내용
연관이슈
close #347
Summary by CodeRabbit
신규 기능
버그 수정