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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions src/api/admin/banner.api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import type { ApiBannerList } from '../../models/admin/banner';
import { httpClient } from '../http.api';

export const getBannerList = async () => {
try {
const response = await httpClient.get<ApiBannerList>('/banner');
console.log(response.data);
return response.data;
} catch (error) {
console.error(error);
throw error;
}
};

export const postBanner = async (formData: FormData) => {
try {
const response = await httpClient.post('/banner', formData);
return response.data;
} catch (error) {
console.error(error);
throw error;
}
};

export const patchBanner = async (formData: FormData, bannerId?: number) => {
try {
if (!bannerId) {
throw new Error('bannerId is required');
}

for (const [key, value] of formData.entries()) {
if (value instanceof File) {
console.log(`${key}:`, {
name: value.name,
size: value.size,
type: value.type,
lastModified: value.lastModified,
});
} else {
console.log(`${key}:`, value);
}
}
const response = await httpClient.patch(`/banner/${bannerId}`, formData);
return response.data;
} catch (error) {
console.error(error);
throw error;
}
};

export const deleteBanner = async (bannerId?: number) => {
try {
if (!bannerId) {
throw new Error('bannerId is required');
}

const response = await httpClient.delete(`/banner/${bannerId}`);
return response.data;
} catch (error) {
console.error(error);
throw error;
}
};
21 changes: 21 additions & 0 deletions src/components/admin/banner/BannerList.styled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import styled from 'styled-components';
import Button from '../../common/Button/Button';

export const Container = styled.table<{ $header?: boolean }>`
border-collapse: collapse;
table-layout: fixed;
border-radius: 8px;
overflow: hidden;
width: 100%;
margin-top: 10rem;
`;

export const TableBody = styled.tbody``;

export const ButtonContainer = styled.div`
display: flex;
justify-content: center;
margin-top: 20px;
`;

export const BannerButton = styled(Button)``;
102 changes: 102 additions & 0 deletions src/components/admin/banner/BannerList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { useState, useCallback } from 'react';
import * as S from './BannerList.styled';
import ScrollPreventor from '../../common/modal/ScrollPreventor';
import { useGetAllBannerList } from '../../../hooks/admin/useGetAllBannerList';
import { useBannerMutations } from '../../../hooks/admin/useBannerMutations';
import { useModal } from '../../../hooks/useModal';
import TableHeader from './tableHeader/TableHeader';
import BannerRow from './bannerRow/BannerRow';
import NewBannerRow from './newBannerRow/NewBannerRow';
import Modal from '../../common/modal/Modal';
import { Spinner } from '../../common/loadingSpinner/LoadingSpinner.styled';

const TABLE_COLUMNS = [
{ width: '50px' },
{ width: '280px' },
{ width: '100px' },
{ width: '160px' },
{ width: '240px' },
{ width: '140px' },
];

export default function BannerList() {
const { isOpen, message, handleModalOpen, handleModalClose } = useModal();
const { allBannersData, isLoading } = useGetAllBannerList();
const { deleteBannerMutate } = useBannerMutations({ handleModalOpen });
const [isCreating, setIsCreating] = useState(false);

const handleDelete = useCallback(
async (id: number) => {
if (window.confirm('정말 삭제하시겠습니까?')) {
try {
await deleteBannerMutate.mutateAsync(id);
} catch (error) {
console.error('삭제 실패:', error);
}
}
},
[deleteBannerMutate]
);

const toggleCreating = useCallback(() => {
setIsCreating((prev) => !prev);
}, []);

if (isLoading) {
return <Spinner />;
}

return (
<>
<ScrollPreventor />
<S.Container>
<colgroup>
{TABLE_COLUMNS.map((col, index) => (
<col key={index} style={{ width: col.width }} />
))}
</colgroup>

<TableHeader />

<S.TableBody>
{allBannersData?.data.map((banner, index) => (
<BannerRow
key={banner.id}
banner={banner}
index={index}
onDelete={handleDelete}
/>
))}

{isCreating && <NewBannerRow />}
</S.TableBody>
</S.Container>

<S.ButtonContainer>
{!isCreating ? (
<S.BannerButton
radius='primary'
schema='primary'
size='primary'
onClick={toggleCreating}
>
배너 생성하기
</S.BannerButton>
) : (
<S.BannerButton
radius='primary'
schema='primary'
size='primary'
onClick={toggleCreating}
>
취소하기
</S.BannerButton>
)}
</S.ButtonContainer>

<Modal isOpen={isOpen} onClose={handleModalClose}>
{message}
</Modal>
</>
);
}
76 changes: 76 additions & 0 deletions src/components/admin/banner/bannerRow/BannerRow.styled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import styled from 'styled-components';

export const TableRow = styled.tr`
border-bottom: 1px solid #e9ecef;

&:nth-child(even) {
background-color: ${({ theme }) => theme.color.lightgrey};
}
`;

export const TableCell = styled.td`
padding: 14px;
text-align: center;
vertical-align: middle;
font-size: 14px;
color: #333;
`;

export const ImageCell = styled.td`
position: relative;
height: 120px;
padding: 0;
width: 280px;
text-align: center;
vertical-align: middle;
display: flex;
align-items: center;
justify-content: center;
`;

export const EditButton = styled.button`
padding: 6px 12px;
margin-right: 8px;
border: none;
border-radius: 4px;
background-color: ${({ theme }) => theme.buttonScheme.primary.bg};
color: white;
font-size: 12px;
cursor: pointer;
transition: background-color 0.2s ease;

&:hover:not(:disabled) {
opacity: 0.8;
}

&:disabled {
background-color: ${({ theme }) => theme.color.deepGrey};
cursor: not-allowed;
opacity: 0.8;
}
`;

export const DeleteButton = styled.button`
padding: 6px 12px;
border: none;
border-radius: 4px;
background-color: ${({ theme }) => theme.color.red};
color: white;
font-size: 12px;
cursor: pointer;
transition: background-color 0.2s ease;

&:hover {
opacity: 0.8;
}
`;

export const DateDisplay = styled.span`
font-size: 14px;
color: #495057;
`;

export const Placeholder = styled.span`
font-size: 14px;
color: #6c757d;
`;
124 changes: 124 additions & 0 deletions src/components/admin/banner/bannerRow/BannerRow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { useState, useCallback } from 'react';
import * as S from './BannerRow.styled';
import type { BannerItem } from '../../../../models/admin/banner';
import { useBannerRow } from './useBannerRow';
import ImageUploadArea from '../imageUploadArea/ImageUploadArea';
import ToggleSwitch from '../toggleSwitch/ToggleSwitch';
import RadioGroup from '../radioGroup/RadioGroup';
import DateRange from '../dateRange/DateRange';
import { formatDate } from '../../../../util/formatDate';

interface BannerRowProps {
banner: BannerItem;
index: number;
onDelete: (id: number) => void;
}

const BannerRow = ({ banner, index, onDelete }: BannerRowProps) => {
const {
currentValues,
hasChanges,
handleToggle,
handleChangeType,
handleDateChange,
handleImageChange,
handleUpdate,
} = useBannerRow(banner);

const [hoveredImageId, setHoveredImageId] = useState<number | null>(null);
const showImageOverlay = hoveredImageId === banner.id;

// 이미지 관련 핸들러
const handleImageClick = useCallback(() => {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
input.style.display = 'none';

input.onchange = (event) => {
const file = (event.target as HTMLInputElement).files?.[0];
if (file) {
handleImageChange(file);
}
};

document.body.appendChild(input);
input.click();
document.body.removeChild(input);
}, [handleImageChange]);

// 날짜 표시 렌더링
const renderDateDisplay = () => {
if (currentValues.always) {
return <S.Placeholder>-</S.Placeholder>;
}

if (currentValues.startDate && currentValues.endDate) {
return (
<S.DateDisplay>
{formatDate(currentValues.startDate)} ~{' '}
{formatDate(currentValues.endDate)}
</S.DateDisplay>
);
}

return (
<DateRange
startDate={currentValues.startDate}
endDate={currentValues.endDate}
onStartDateChange={(date) =>
handleDateChange(date, currentValues.endDate)
}
onEndDateChange={(date) =>
handleDateChange(currentValues.startDate, date)
}
placeholder={{ start: '시작일', end: '종료일' }}
/>
);
};

return (
<S.TableRow>
<S.TableCell>{index + 1}</S.TableCell>

<S.ImageCell>
<ImageUploadArea
imageUrl={currentValues.imageUrl}
onImageClick={handleImageClick}
onImageHover={() => setHoveredImageId(banner.id)}
onImageLeave={() => setHoveredImageId(null)}
showOverlay={showImageOverlay}
/>
</S.ImageCell>

<S.TableCell>
<ToggleSwitch
id={`toggle-${banner.id}`}
checked={currentValues.visible}
onChange={handleToggle}
/>
</S.TableCell>

<S.TableCell>
<RadioGroup
name={banner.id.toString()}
value={currentValues.always}
onChange={handleChangeType}
/>
</S.TableCell>

<S.TableCell>{renderDateDisplay()}</S.TableCell>

<S.TableCell>
<S.EditButton onClick={handleUpdate} disabled={!hasChanges}>
수정
</S.EditButton>
<S.DeleteButton onClick={() => onDelete(banner.id)}>
삭제
</S.DeleteButton>
</S.TableCell>
</S.TableRow>
);
};

export default BannerRow;
Loading