Skip to content

Commit 8ac62a6

Browse files
authored
Merge pull request #41 from Wedvice/r/commentBox-refactor/suji_chae
[refactor] 댓글 입력 영역 컴포넌트 리팩토링
2 parents 4f1a108 + a2d4599 commit 8ac62a6

11 files changed

Lines changed: 308 additions & 140 deletions

File tree

.storybook/preview.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { Preview } from '@storybook/react';
2-
import '../src/app/globals.css';
2+
import './storybook-globals.css';
33

44
const preview: Preview = {
55
parameters: {

.storybook/storybook-globals.css

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
@tailwind base;
2+
@tailwind components;
3+
@tailwind utilities;
4+
5+
:root {
6+
--layout-max-w: 100%;
7+
}
8+
9+
@layer base {
10+
.layout {
11+
@apply mx-auto min-h-[100dvh] w-full;
12+
max-width: var(--layout-max-w);
13+
}
14+
}

src/app/globals.css

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,13 @@
22
@tailwind components;
33
@tailwind utilities;
44

5+
:root {
6+
--layout-max-w: 480px;
7+
}
8+
59
@layer base {
610
.layout {
7-
@apply max-w-[480px] min-h-[100dvh] w-full mx-auto;
11+
@apply mx-auto min-h-[100dvh] w-full;
12+
max-width: var(--layout-max-w);
813
}
914
}

src/assets/close_20.svg

Lines changed: 4 additions & 0 deletions
Loading

src/components/atoms/bottomSheet/style.css

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,10 @@
22
--rsbs-max-w: var(--layout-max-w);
33
--rsbs-content-opacity: 1;
44
--rsbs-backdrop-bg: rgba(0, 0, 0, 0.6);
5-
--rsbs-bg: #1F2028;
6-
--rsbs-handle-bg: #8C8EA6;
7-
--rsbs-max-w: auto;
8-
--rsbs-ml: env(safe-area-inset-left);
9-
--rsbs-mr: env(safe-area-inset-right);
5+
--rsbs-bg: #2c2e3a;
6+
--rsbs-handle-bg: #8c8ea6;
7+
--rsbs-ml: auto;
8+
--rsbs-mr: auto;
109
--rsbs-overlay-rounded: 16px;
1110

1211
/* 스크롤바 없음 */
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
import React, { useEffect, useRef, ChangeEvent } from 'react';
2+
import { BottomSheet } from '@/components/atoms/bottomSheet';
3+
import Close from '@/assets/close_20.svg';
4+
5+
const DEFAULT_BOTTOM_SHEET_HEIGHT = 290;
6+
const MAX_IMAGE = 9;
7+
8+
interface BottomContentProps {
9+
images: string[];
10+
setImages: (images: string[]) => void;
11+
showGallery: boolean;
12+
setShowGallery: (value: boolean) => void;
13+
setBottomSheetHeight: (height: number) => void;
14+
}
15+
16+
export const BottomContent = ({
17+
images,
18+
showGallery,
19+
setImages,
20+
setShowGallery,
21+
setBottomSheetHeight,
22+
}: BottomContentProps) => {
23+
const observerRef = useRef<MutationObserver | null>(null);
24+
const isFirstOpenRef = useRef(true);
25+
26+
useEffect(() => {
27+
if (!showGallery && observerRef.current) {
28+
isFirstOpenRef.current = true;
29+
observerRef.current.disconnect();
30+
observerRef.current = null;
31+
}
32+
}, [showGallery]);
33+
34+
const observeHeightChanges = () => {
35+
const bottomSheetElement = document.querySelector(
36+
'[data-rsbs-root="true"]',
37+
) as HTMLElement;
38+
39+
if (!bottomSheetElement) {
40+
return;
41+
}
42+
43+
observerRef.current?.disconnect();
44+
45+
observerRef.current = new MutationObserver((mutations) => {
46+
mutations.forEach((mutation) => {
47+
if (mutation.attributeName === 'style') {
48+
if (isFirstOpenRef.current) {
49+
setBottomSheetHeight(DEFAULT_BOTTOM_SHEET_HEIGHT);
50+
return;
51+
}
52+
53+
const computedStyle = getComputedStyle(bottomSheetElement);
54+
const newHeight = parseInt(
55+
computedStyle.getPropertyValue('--rsbs-overlay-h'),
56+
10,
57+
);
58+
const translateY = parseInt(
59+
computedStyle.getPropertyValue('--rsbs-overlay-translate-y'),
60+
10,
61+
);
62+
63+
setBottomSheetHeight(
64+
newHeight === DEFAULT_BOTTOM_SHEET_HEIGHT
65+
? newHeight - translateY
66+
: newHeight,
67+
);
68+
}
69+
});
70+
});
71+
72+
observerRef.current.observe(bottomSheetElement, {
73+
attributes: true,
74+
attributeOldValue: true,
75+
});
76+
};
77+
78+
const handleSpringStart = () => {
79+
observeHeightChanges();
80+
};
81+
82+
const handleSpringEnd = () => {
83+
if (showGallery) {
84+
isFirstOpenRef.current = false;
85+
}
86+
};
87+
88+
return (
89+
<BottomSheet
90+
open={showGallery}
91+
onSpringStart={handleSpringStart}
92+
onSpringEnd={handleSpringEnd}
93+
onDismiss={() => setShowGallery(false)}
94+
snapPoints={({ maxHeight }) => [DEFAULT_BOTTOM_SHEET_HEIGHT, maxHeight]}
95+
defaultSnap={() => DEFAULT_BOTTOM_SHEET_HEIGHT}
96+
blocking={false}
97+
header={
98+
<BottomSheetHeader
99+
images={images}
100+
setImages={setImages}
101+
setShowGallery={setShowGallery}
102+
/>
103+
}
104+
>
105+
<BottomSheetContent images={images} setImages={setImages} />
106+
</BottomSheet>
107+
);
108+
};
109+
110+
const BottomSheetHeader = ({
111+
images,
112+
setImages,
113+
setShowGallery,
114+
}: {
115+
images: string[];
116+
setImages: (images: string[]) => void;
117+
setShowGallery: (value: boolean) => void;
118+
}) => {
119+
const inputRef = useRef<HTMLInputElement>(null);
120+
const openNativeGallery = () => {
121+
inputRef.current?.click();
122+
};
123+
124+
const handleImageUpload = (event: ChangeEvent<HTMLInputElement>) => {
125+
if (!event.target.files) return;
126+
127+
const fileArray = Array.from(event.target.files).map((file) =>
128+
URL.createObjectURL(file),
129+
);
130+
setImages(images ? [...images, ...fileArray] : [...fileArray]);
131+
setShowGallery(true);
132+
};
133+
134+
return (
135+
<div className='flex items-center justify-between px-1 text-lg font-medium'>
136+
<input
137+
ref={inputRef}
138+
type='file'
139+
accept='image/*'
140+
multiple
141+
className='hidden'
142+
onChange={handleImageUpload}
143+
/>
144+
145+
<div>
146+
<span className='text-white'>사진</span>
147+
<span className='ml-3 tracking-[2px] text-gray-700'>
148+
<span className='text-primary-500'>{images?.length ?? 0}</span>/
149+
{MAX_IMAGE}
150+
</span>
151+
</div>
152+
153+
<button
154+
className='h-[34px] w-[83px] rounded-md bg-gray-300 text-sm text-gray-800'
155+
onClick={openNativeGallery}
156+
>
157+
사진 찾기
158+
</button>
159+
</div>
160+
);
161+
};
162+
163+
const BottomSheetContent = ({
164+
images,
165+
setImages,
166+
}: {
167+
images: string[];
168+
setImages: (images: string[]) => void;
169+
}) => {
170+
return images.length ? (
171+
<div className='mt-2 grid grid-cols-3 gap-[3px]'>
172+
{images.map((src, index) => (
173+
<div key={index} className='relative'>
174+
<img
175+
src={src}
176+
alt={`User Update Image ${src}`}
177+
className='h-32 w-full object-cover'
178+
/>
179+
<button
180+
className='absolute right-2 top-2 flex h-[28px] w-[28px] items-center justify-center rounded-full bg-gray-50'
181+
onClick={() => setImages(images.filter((_, i) => i !== index))}
182+
>
183+
<Close />
184+
</button>
185+
</div>
186+
))}
187+
</div>
188+
) : (
189+
<div className='mt-[74px] text-center'>
190+
<span className='text-base text-gray-600'>
191+
앨범에서 사진을 찾아보세요.
192+
</span>
193+
</div>
194+
);
195+
};

src/features/comment/component/CommentBox.stories.tsx

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { useState } from 'react';
21
import type { Meta, StoryObj } from '@storybook/react';
32
import { CommentBox } from './CommentBox';
43

@@ -14,11 +13,7 @@ export default meta;
1413
type Story = StoryObj<typeof CommentBox>;
1514

1615
export const Default: Story = {
17-
args: {
18-
text: '',
19-
},
20-
render: (args) => {
21-
const [text, setText] = useState(args.text);
22-
return <CommentBox {...args} text={text} setText={setText} />;
16+
render: () => {
17+
return <CommentBox />;
2318
},
2419
};
Lines changed: 34 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,44 @@
1-
import Upload from '@/assets/upload.svg';
2-
import UploadActive from '@/assets/upload_active.svg';
3-
import { CommentInput } from '@/components/molecules/comment';
4-
import { ImageUploader } from './ImageUploader';
1+
import { useState } from 'react';
2+
import { BottomContent } from './BottomContent';
3+
import { TextFieldBottom } from './TextFieldBottom';
54

6-
interface CommentBoxProps {
7-
text: string;
8-
setText: (text: string) => void;
9-
}
5+
export const CommentBox = () => {
6+
const [text, setText] = useState<string>('');
7+
const [images, setImages] = useState<string[]>([]);
8+
const [showGallery, setShowGallery] = useState(false);
9+
const [bottomSheetHeight, setBottomSheetHeight] = useState(0);
1010

11-
export const CommentBox = ({ text, setText }: CommentBoxProps) => {
12-
const handleUploadText = () => {
13-
// text upload
14-
setText('');
11+
const handleUpload = () => {
12+
// text, image upload
13+
if (text.trim()) {
14+
setText('');
15+
}
16+
if (showGallery && images.length > 0) {
17+
setImages([]);
18+
setShowGallery(false);
19+
}
1520
};
1621

1722
return (
18-
<div className='absolute bottom-0 flex w-full items-center justify-between gap-3 bg-gray-100 px-5 py-2'>
19-
<ImageUploader />
20-
21-
<CommentInput
22-
value={text}
23-
onChange={setText}
24-
placeholder='댓글 입력'
25-
minHeight={20}
26-
maxHeight={42}
27-
maxLineCount={3}
23+
<div
24+
className='absolute bottom-[34px] w-full'
25+
style={{ bottom: showGallery ? `${bottomSheetHeight}px` : '34px' }}
26+
>
27+
<TextFieldBottom
28+
text={text}
29+
setText={setText}
30+
showGallery={showGallery}
31+
setShowGallery={setShowGallery}
32+
handleUpload={handleUpload}
2833
/>
2934

30-
<div
31-
className={`cursor-pointer rounded-full p-2 ${text.trim() ? 'bg-primary-400' : 'bg-primary-100'}`}
32-
onClick={handleUploadText}
33-
>
34-
{text.trim() ? <UploadActive /> : <Upload />}
35-
</div>
35+
<BottomContent
36+
images={images}
37+
setImages={setImages}
38+
showGallery={showGallery}
39+
setShowGallery={setShowGallery}
40+
setBottomSheetHeight={setBottomSheetHeight}
41+
/>
3642
</div>
3743
);
3844
};

0 commit comments

Comments
 (0)