Skip to content

Commit 9e46e9c

Browse files
authored
Merge pull request #413 from kakao-tech-campus-3rd-step3/feat/common-modal#248
[FEAT] 공통 모달 구현 (#248)
2 parents e64d970 + a3c9b5d commit 9e46e9c

File tree

5 files changed

+464
-44
lines changed

5 files changed

+464
-44
lines changed

src/pages/admin/ApplicationDetail/components/CommentSection/CommentItem/CommentItem.styles.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ export const AuthorInfo = styled.div({
2121
marginRight: '1rem',
2222
});
2323

24+
export const RatingWrapper = styled.div({
25+
marginLeft: '0.5rem',
26+
display: 'flex',
27+
alignItems: 'center',
28+
});
29+
2430
export const NameRatingGroup = styled.div({
2531
display: 'flex',
2632
alignItems: 'center',
@@ -67,3 +73,10 @@ export const EditButtonContainer = styled.div({
6773
justifyContent: 'flex-end',
6874
gap: '0.5rem',
6975
});
76+
77+
export const ButtonGroup = styled.div({
78+
display: 'flex',
79+
gap: '0.5rem',
80+
justifyContent: 'flex-end',
81+
marginTop: '1.5rem',
82+
});

src/pages/admin/ApplicationDetail/components/CommentSection/CommentItem/CommentItem.tsx

Lines changed: 54 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
import styled from '@emotion/styled';
21
import { useState } from 'react';
32
import { useParams } from 'react-router-dom';
43
import { useAuth } from '@/app/providers/auth';
54
import { ApplicantStarRating } from '@/pages/admin/ApplicationDetail/components/CommentSection/ApplicantStarRating';
65
import { useComments } from '@/pages/admin/ApplicationDetail/hooks/useComments';
6+
import { Button } from '@/shared/components/Button';
7+
import { Modal } from '@/shared/components/Modal';
78
import { Text } from '@/shared/components/Text';
9+
import { useModal } from '@/shared/hooks/useModal';
810
import { CommentEditForm } from './CommentEditForm';
911
import * as S from './CommentItem.styles';
1012
import type { Comment, CommentFormData } from '@/pages/admin/ApplicationDetail/types/comments';
@@ -18,6 +20,8 @@ export const CommentItem = ({ author, commentId, content, createdAt, rating }: P
1820
const [isEditing, setIsEditing] = useState(false);
1921
const [editableRating, setEditableRating] = useState(rating);
2022

23+
const { isOpen, openModal, closeModal } = useModal();
24+
2125
const handleEdit = () => {
2226
setIsEditing(true);
2327
setEditableRating(rating);
@@ -39,54 +43,60 @@ export const CommentItem = ({ author, commentId, content, createdAt, rating }: P
3943
};
4044

4145
const handleDelete = () => {
42-
deleteComment(commentId);
46+
openModal();
4347
};
4448

4549
return (
46-
<S.Layout>
47-
<S.Header>
48-
<S.AuthorInfo>
49-
<S.NameRatingGroup>
50-
<Text size={'base'} weight={'medium'}>
51-
{author.name}
52-
</Text>
53-
{rating !== undefined && (
54-
<RatingWrapper>
55-
<ApplicantStarRating
56-
rating={isEditing ? editableRating : rating}
57-
readOnly={!isEditing}
58-
onRatingChange={setEditableRating}
59-
/>
60-
</RatingWrapper>
50+
<>
51+
<S.Layout>
52+
<S.Header>
53+
<S.AuthorInfo>
54+
<S.NameRatingGroup>
55+
<Text size={'base'} weight={'medium'}>
56+
{author.name}
57+
</Text>
58+
{rating !== undefined && (
59+
<S.RatingWrapper>
60+
<ApplicantStarRating
61+
rating={isEditing ? editableRating : rating}
62+
readOnly={!isEditing}
63+
onRatingChange={setEditableRating}
64+
/>
65+
</S.RatingWrapper>
66+
)}
67+
</S.NameRatingGroup>
68+
{!isEditing && user?.userId === author.id && (
69+
<S.ButtonContainer>
70+
<S.ActionButton onClick={handleEdit}>수정</S.ActionButton>
71+
<S.Divider>|</S.Divider>
72+
<S.ActionButton onClick={handleDelete}>삭제</S.ActionButton>
73+
</S.ButtonContainer>
6174
)}
62-
</S.NameRatingGroup>
63-
{!isEditing && user?.userId === author.id && (
64-
<S.ButtonContainer>
65-
<S.ActionButton onClick={handleEdit}>수정</S.ActionButton>
66-
<S.Divider>|</S.Divider>
67-
<S.ActionButton onClick={handleDelete}>삭제</S.ActionButton>
68-
</S.ButtonContainer>
69-
)}
70-
</S.AuthorInfo>
75+
</S.AuthorInfo>
76+
77+
<Text size={'xs'} weight={'medium'} color={'#616677'}>
78+
{createdAt}
79+
</Text>
80+
</S.Header>
7181

72-
<Text size={'xs'} weight={'medium'} color={'#616677'}>
73-
{createdAt}
74-
</Text>
75-
</S.Header>
82+
{isEditing ? (
83+
<CommentEditForm content={content} onSave={handleSave} onCancel={handleCancel} />
84+
) : (
85+
<S.CommentContent>
86+
<Text size={'sm'}>{content}</Text>
87+
</S.CommentContent>
88+
)}
89+
</S.Layout>
7690

77-
{isEditing ? (
78-
<CommentEditForm content={content} onSave={handleSave} onCancel={handleCancel} />
79-
) : (
80-
<S.CommentContent>
81-
<Text size={'sm'}>{content}</Text>
82-
</S.CommentContent>
83-
)}
84-
</S.Layout>
91+
<Modal isOpen={isOpen} onClose={closeModal} title='댓글 삭제' size='sm'>
92+
<Text>댓글을 완전히 삭제할까요?</Text>
93+
<S.ButtonGroup>
94+
<Button onClick={closeModal} variant='outline'>
95+
취소
96+
</Button>
97+
<Button onClick={() => deleteComment(commentId)}>삭제</Button>
98+
</S.ButtonGroup>
99+
</Modal>
100+
</>
85101
);
86102
};
87-
88-
const RatingWrapper = styled.div({
89-
marginLeft: '0.5rem',
90-
display: 'flex',
91-
alignItems: 'center',
92-
});
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import { keyframes } from '@emotion/react';
2+
import styled from '@emotion/styled';
3+
4+
const fadeIn = keyframes`
5+
from {
6+
opacity: 0;
7+
}
8+
to {
9+
opacity: 1;
10+
}
11+
`;
12+
13+
const scaleIn = keyframes`
14+
from {
15+
opacity: 0;
16+
transform: scale(0.95);
17+
}
18+
to {
19+
opacity: 1;
20+
transform: scale(1);
21+
}
22+
`;
23+
24+
type OverlayProps = {
25+
$variant?: 'modal' | 'popover';
26+
};
27+
28+
export const Overlay = styled.div<OverlayProps>(({ $variant = 'modal' }) => ({
29+
position: 'fixed',
30+
inset: 0,
31+
backgroundColor: $variant === 'popover' ? 'transparent' : 'rgba(0, 0, 0, 0.3)',
32+
display: $variant === 'popover' ? 'block' : 'flex',
33+
alignItems: $variant === 'popover' ? undefined : 'center',
34+
justifyContent: $variant === 'popover' ? undefined : 'center',
35+
zIndex: 50,
36+
padding: $variant === 'popover' ? 0 : '1rem',
37+
animation: `${fadeIn} 0.2s ease-out`,
38+
}));
39+
40+
type ContentProps = {
41+
$size?: 'sm' | 'md' | 'lg';
42+
$variant?: 'modal' | 'popover';
43+
$position?: { top: number; left: number };
44+
};
45+
46+
const sizeMap = {
47+
sm: '13rem',
48+
md: '20rem',
49+
lg: '30rem',
50+
};
51+
52+
export const Content = styled.div<ContentProps>(
53+
({ theme, $size = 'md', $variant = 'modal', $position }) => ({
54+
backgroundColor: '#fff',
55+
borderRadius: theme.radius.lg,
56+
boxShadow:
57+
$variant === 'popover'
58+
? '0 4px 20px rgba(0, 0, 0, 0.15), 0 0 1px rgba(0, 0, 0, 0.1)'
59+
: '0 25px 50px -12px rgba(0, 0, 0, 0.25)',
60+
width: sizeMap[$size],
61+
maxWidth: '100%',
62+
maxHeight: $variant === 'popover' ? '80vh' : '90vh',
63+
overflow: 'auto',
64+
padding: $variant === 'popover' ? '1.5rem' : '2rem',
65+
animation: `${scaleIn} 0.2s ease-out`,
66+
display: 'flex',
67+
flexDirection: 'column',
68+
69+
// popover일 때 position 계산 전에는 숨김
70+
...($variant === 'popover' &&
71+
!$position && {
72+
visibility: 'hidden',
73+
position: 'fixed',
74+
}),
75+
76+
// popover일 때 위치 지정 (fixed로 스크롤 시 따라감)
77+
...($variant === 'popover' &&
78+
$position && {
79+
position: 'fixed',
80+
top: $position.top - 10,
81+
left: $position.left - 30,
82+
}),
83+
84+
[`@media (max-width: ${theme.breakpoints.mobile})`]: {
85+
padding: '1.5rem',
86+
maxHeight: '85vh',
87+
},
88+
}),
89+
);
90+
91+
export const Title = styled.h2(({ theme }) => ({
92+
fontSize: theme.font.size.base,
93+
fontWeight: 700,
94+
color: theme.colors.gray900,
95+
marginBottom: '0.5rem',
96+
97+
'&:focus': {
98+
outline: 'none',
99+
},
100+
}));
101+
102+
export const Description = styled.p(({ theme }) => ({
103+
fontSize: '1rem',
104+
color: theme.colors.gray500,
105+
marginBottom: '1.5rem',
106+
}));
107+
108+
export const CloseButton = styled.button(({ theme }) => ({
109+
position: 'absolute',
110+
top: '1rem',
111+
right: '1rem',
112+
background: 'none',
113+
border: 'none',
114+
padding: '0.5rem',
115+
cursor: 'pointer',
116+
color: theme.colors.gray400,
117+
borderRadius: theme.radius.sm,
118+
transition: 'color 0.2s, background-color 0.2s',
119+
120+
'&:hover': {
121+
color: theme.colors.gray600,
122+
backgroundColor: theme.colors.gray100,
123+
},
124+
}));
125+
126+
export const Header = styled.div({
127+
position: 'relative',
128+
marginBottom: '1rem',
129+
});
130+
131+
export const Body = styled.div({
132+
flex: 1,
133+
display: 'flex',
134+
flexDirection: 'column',
135+
});

0 commit comments

Comments
 (0)