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
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ export const AuthorInfo = styled.div({
marginRight: '1rem',
});

export const RatingWrapper = styled.div({
marginLeft: '0.5rem',
display: 'flex',
alignItems: 'center',
});

export const NameRatingGroup = styled.div({
display: 'flex',
alignItems: 'center',
Expand Down Expand Up @@ -67,3 +73,10 @@ export const EditButtonContainer = styled.div({
justifyContent: 'flex-end',
gap: '0.5rem',
});

export const ButtonGroup = styled.div({
display: 'flex',
gap: '0.5rem',
justifyContent: 'flex-end',
marginTop: '1.5rem',
});
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import styled from '@emotion/styled';
import { useState } from 'react';
import { useParams } from 'react-router-dom';
import { useAuth } from '@/app/providers/auth';
import { ApplicantStarRating } from '@/pages/admin/ApplicationDetail/components/CommentSection/ApplicantStarRating';
import { useComments } from '@/pages/admin/ApplicationDetail/hooks/useComments';
import { Button } from '@/shared/components/Button';
import { Modal } from '@/shared/components/Modal';
import { Text } from '@/shared/components/Text';
import { useModal } from '@/shared/hooks/useModal';
import { CommentEditForm } from './CommentEditForm';
import * as S from './CommentItem.styles';
import type { Comment, CommentFormData } from '@/pages/admin/ApplicationDetail/types/comments';
Expand All @@ -18,6 +20,8 @@ export const CommentItem = ({ author, commentId, content, createdAt, rating }: P
const [isEditing, setIsEditing] = useState(false);
const [editableRating, setEditableRating] = useState(rating);

const { isOpen, openModal, closeModal } = useModal();

const handleEdit = () => {
setIsEditing(true);
setEditableRating(rating);
Expand All @@ -39,54 +43,60 @@ export const CommentItem = ({ author, commentId, content, createdAt, rating }: P
};

const handleDelete = () => {
deleteComment(commentId);
openModal();
};

return (
<S.Layout>
<S.Header>
<S.AuthorInfo>
<S.NameRatingGroup>
<Text size={'base'} weight={'medium'}>
{author.name}
</Text>
{rating !== undefined && (
<RatingWrapper>
<ApplicantStarRating
rating={isEditing ? editableRating : rating}
readOnly={!isEditing}
onRatingChange={setEditableRating}
/>
</RatingWrapper>
<>
<S.Layout>
<S.Header>
<S.AuthorInfo>
<S.NameRatingGroup>
<Text size={'base'} weight={'medium'}>
{author.name}
</Text>
{rating !== undefined && (
<S.RatingWrapper>
<ApplicantStarRating
rating={isEditing ? editableRating : rating}
readOnly={!isEditing}
onRatingChange={setEditableRating}
/>
</S.RatingWrapper>
)}
</S.NameRatingGroup>
{!isEditing && user?.userId === author.id && (
<S.ButtonContainer>
<S.ActionButton onClick={handleEdit}>수정</S.ActionButton>
<S.Divider>|</S.Divider>
<S.ActionButton onClick={handleDelete}>삭제</S.ActionButton>
</S.ButtonContainer>
)}
</S.NameRatingGroup>
{!isEditing && user?.userId === author.id && (
<S.ButtonContainer>
<S.ActionButton onClick={handleEdit}>수정</S.ActionButton>
<S.Divider>|</S.Divider>
<S.ActionButton onClick={handleDelete}>삭제</S.ActionButton>
</S.ButtonContainer>
)}
</S.AuthorInfo>
</S.AuthorInfo>

<Text size={'xs'} weight={'medium'} color={'#616677'}>
{createdAt}
</Text>
</S.Header>

<Text size={'xs'} weight={'medium'} color={'#616677'}>
{createdAt}
</Text>
</S.Header>
{isEditing ? (
<CommentEditForm content={content} onSave={handleSave} onCancel={handleCancel} />
) : (
<S.CommentContent>
<Text size={'sm'}>{content}</Text>
</S.CommentContent>
)}
</S.Layout>

{isEditing ? (
<CommentEditForm content={content} onSave={handleSave} onCancel={handleCancel} />
) : (
<S.CommentContent>
<Text size={'sm'}>{content}</Text>
</S.CommentContent>
)}
</S.Layout>
<Modal isOpen={isOpen} onClose={closeModal} title='댓글 삭제' size='sm'>
<Text>댓글을 완전히 삭제할까요?</Text>
<S.ButtonGroup>
<Button onClick={closeModal} variant='outline'>
취소
</Button>
<Button onClick={() => deleteComment(commentId)}>삭제</Button>
</S.ButtonGroup>
</Modal>
</>
);
};

const RatingWrapper = styled.div({
marginLeft: '0.5rem',
display: 'flex',
alignItems: 'center',
});
135 changes: 135 additions & 0 deletions src/shared/components/Modal/index.styled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { keyframes } from '@emotion/react';
import styled from '@emotion/styled';

const fadeIn = keyframes`
from {
opacity: 0;
}
to {
opacity: 1;
}
`;

const scaleIn = keyframes`
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
`;

type OverlayProps = {
$variant?: 'modal' | 'popover';
};

export const Overlay = styled.div<OverlayProps>(({ $variant = 'modal' }) => ({
position: 'fixed',
inset: 0,
backgroundColor: $variant === 'popover' ? 'transparent' : 'rgba(0, 0, 0, 0.3)',
display: $variant === 'popover' ? 'block' : 'flex',
alignItems: $variant === 'popover' ? undefined : 'center',
justifyContent: $variant === 'popover' ? undefined : 'center',
zIndex: 50,
padding: $variant === 'popover' ? 0 : '1rem',
animation: `${fadeIn} 0.2s ease-out`,
}));

type ContentProps = {
$size?: 'sm' | 'md' | 'lg';
$variant?: 'modal' | 'popover';
$position?: { top: number; left: number };
};

const sizeMap = {
sm: '13rem',
md: '20rem',
lg: '30rem',
};

export const Content = styled.div<ContentProps>(
({ theme, $size = 'md', $variant = 'modal', $position }) => ({
backgroundColor: '#fff',
borderRadius: theme.radius.lg,
boxShadow:
$variant === 'popover'
? '0 4px 20px rgba(0, 0, 0, 0.15), 0 0 1px rgba(0, 0, 0, 0.1)'
: '0 25px 50px -12px rgba(0, 0, 0, 0.25)',
width: sizeMap[$size],
maxWidth: '100%',
maxHeight: $variant === 'popover' ? '80vh' : '90vh',
overflow: 'auto',
padding: $variant === 'popover' ? '1.5rem' : '2rem',
animation: `${scaleIn} 0.2s ease-out`,
display: 'flex',
flexDirection: 'column',

// popover일 때 position 계산 전에는 숨김
...($variant === 'popover' &&
!$position && {
visibility: 'hidden',
position: 'fixed',
}),

// popover일 때 위치 지정 (fixed로 스크롤 시 따라감)
...($variant === 'popover' &&
$position && {
position: 'fixed',
top: $position.top - 10,
left: $position.left - 30,
Comment on lines +80 to +81
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

popover의 위치를 조정하기 위해 하드코딩된 -10, -30 같은 매직 넘버를 사용하고 있습니다. 이렇게 하면 컴포넌트의 재사용성이 떨어지고, 다른 위치에 popover를 표시하고 싶을 때 스타일 파일을 수정해야 하는 불편함이 있습니다. 이 로직을 스타일 컴포넌트에서 제거하고, Modal/index.tsx 컴포넌트 내에서 위치를 계산할 때 offset을 적용하는 것이 좋습니다. offset 값을 prop으로 받아 더욱 유연하게 만드는 것도 좋은 방법입니다.

Suggested change
top: $position.top - 10,
left: $position.left - 30,
top: $position.top,
left: $position.left,

}),

[`@media (max-width: ${theme.breakpoints.mobile})`]: {
padding: '1.5rem',
maxHeight: '85vh',
},
}),
);

export const Title = styled.h2(({ theme }) => ({
fontSize: theme.font.size.base,
fontWeight: 700,
color: theme.colors.gray900,
marginBottom: '0.5rem',

'&:focus': {
outline: 'none',
},
}));

export const Description = styled.p(({ theme }) => ({
fontSize: '1rem',
color: theme.colors.gray500,
marginBottom: '1.5rem',
}));

export const CloseButton = styled.button(({ theme }) => ({
position: 'absolute',
top: '1rem',
right: '1rem',
background: 'none',
border: 'none',
padding: '0.5rem',
cursor: 'pointer',
color: theme.colors.gray400,
borderRadius: theme.radius.sm,
transition: 'color 0.2s, background-color 0.2s',

'&:hover': {
color: theme.colors.gray600,
backgroundColor: theme.colors.gray100,
},
}));

export const Header = styled.div({
position: 'relative',
marginBottom: '1rem',
});

export const Body = styled.div({
flex: 1,
display: 'flex',
flexDirection: 'column',
});
Loading
Loading