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
2 changes: 1 addition & 1 deletion .storybook/preview.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Preview } from '@storybook/react';
import '../src/app/globals.css';
import './storybook-globals.css';

const preview: Preview = {
parameters: {
Expand Down
14 changes: 14 additions & 0 deletions .storybook/storybook-globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

:root {
--layout-max-w: 100%;
}

@layer base {
.layout {
@apply mx-auto min-h-[100dvh] w-full;
max-width: var(--layout-max-w);
}
}
7 changes: 6 additions & 1 deletion src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@
@tailwind components;
@tailwind utilities;

:root {
--layout-max-w: 480px;
}

@layer base {
.layout {
@apply max-w-[480px] min-h-[100dvh] w-full mx-auto;
@apply mx-auto min-h-[100dvh] w-full;
max-width: var(--layout-max-w);
}
}
4 changes: 4 additions & 0 deletions src/assets/close_20.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
File renamed without changes
9 changes: 4 additions & 5 deletions src/components/atoms/bottomSheet/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@
--rsbs-max-w: var(--layout-max-w);
--rsbs-content-opacity: 1;
--rsbs-backdrop-bg: rgba(0, 0, 0, 0.6);
--rsbs-bg: #1F2028;
--rsbs-handle-bg: #8C8EA6;
--rsbs-max-w: auto;
--rsbs-ml: env(safe-area-inset-left);
--rsbs-mr: env(safe-area-inset-right);
--rsbs-bg: #2c2e3a;
--rsbs-handle-bg: #8c8ea6;
--rsbs-ml: auto;
--rsbs-mr: auto;
--rsbs-overlay-rounded: 16px;

/* 스크롤바 없음 */
Expand Down
195 changes: 195 additions & 0 deletions src/features/comment/component/BottomContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import React, { useEffect, useRef, ChangeEvent } from 'react';
import { BottomSheet } from '@/components/atoms/bottomSheet';
import Close from '@/assets/close_20.svg';

const DEFAULT_BOTTOM_SHEET_HEIGHT = 290;
const MAX_IMAGE = 9;

interface BottomContentProps {
images: string[];
setImages: (images: string[]) => void;
showGallery: boolean;
setShowGallery: (value: boolean) => void;
setBottomSheetHeight: (height: number) => void;
}

export const BottomContent = ({
images,
showGallery,
setImages,
setShowGallery,
setBottomSheetHeight,
}: BottomContentProps) => {
const observerRef = useRef<MutationObserver | null>(null);
const isFirstOpenRef = useRef(true);

useEffect(() => {
if (!showGallery && observerRef.current) {
isFirstOpenRef.current = true;
observerRef.current.disconnect();
observerRef.current = null;
}
}, [showGallery]);

const observeHeightChanges = () => {
const bottomSheetElement = document.querySelector(
'[data-rsbs-root="true"]',
) as HTMLElement;

if (!bottomSheetElement) {
return;
}

observerRef.current?.disconnect();

observerRef.current = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.attributeName === 'style') {
if (isFirstOpenRef.current) {
setBottomSheetHeight(DEFAULT_BOTTOM_SHEET_HEIGHT);
return;
}

const computedStyle = getComputedStyle(bottomSheetElement);
const newHeight = parseInt(
computedStyle.getPropertyValue('--rsbs-overlay-h'),
10,
);
const translateY = parseInt(
computedStyle.getPropertyValue('--rsbs-overlay-translate-y'),
10,
);

setBottomSheetHeight(
newHeight === DEFAULT_BOTTOM_SHEET_HEIGHT
? newHeight - translateY
: newHeight,
);
}
});
});

observerRef.current.observe(bottomSheetElement, {
attributes: true,
attributeOldValue: true,
});
};

const handleSpringStart = () => {
observeHeightChanges();
};

const handleSpringEnd = () => {
if (showGallery) {
isFirstOpenRef.current = false;
}
};

return (
<BottomSheet
open={showGallery}
onSpringStart={handleSpringStart}
onSpringEnd={handleSpringEnd}
onDismiss={() => setShowGallery(false)}
snapPoints={({ maxHeight }) => [DEFAULT_BOTTOM_SHEET_HEIGHT, maxHeight]}
defaultSnap={() => DEFAULT_BOTTOM_SHEET_HEIGHT}
blocking={false}
header={
<BottomSheetHeader
images={images}
setImages={setImages}
setShowGallery={setShowGallery}
/>
}
>
<BottomSheetContent images={images} setImages={setImages} />
</BottomSheet>
);
};

const BottomSheetHeader = ({
images,
setImages,
setShowGallery,
}: {
images: string[];
setImages: (images: string[]) => void;
setShowGallery: (value: boolean) => void;
}) => {
const inputRef = useRef<HTMLInputElement>(null);
const openNativeGallery = () => {
inputRef.current?.click();
};

const handleImageUpload = (event: ChangeEvent<HTMLInputElement>) => {
if (!event.target.files) return;

const fileArray = Array.from(event.target.files).map((file) =>
URL.createObjectURL(file),
);
setImages(images ? [...images, ...fileArray] : [...fileArray]);
setShowGallery(true);
};

return (
<div className='flex items-center justify-between px-1 text-lg font-medium'>
<input
ref={inputRef}
type='file'
accept='image/*'
multiple
className='hidden'
onChange={handleImageUpload}
/>

<div>
<span className='text-white'>사진</span>
<span className='ml-3 tracking-[2px] text-gray-700'>
<span className='text-primary-500'>{images?.length ?? 0}</span>/
{MAX_IMAGE}
</span>
</div>

<button
className='h-[34px] w-[83px] rounded-md bg-gray-300 text-sm text-gray-800'
onClick={openNativeGallery}
>
사진 찾기
</button>
</div>
);
};

const BottomSheetContent = ({
images,
setImages,
}: {
images: string[];
setImages: (images: string[]) => void;
}) => {
return images.length ? (
<div className='mt-2 grid grid-cols-3 gap-[3px]'>
{images.map((src, index) => (
<div key={index} className='relative'>
<img
src={src}
alt={`User Update Image ${src}`}
className='h-32 w-full object-cover'
/>
<button
className='absolute right-2 top-2 flex h-[28px] w-[28px] items-center justify-center rounded-full bg-gray-50'
onClick={() => setImages(images.filter((_, i) => i !== index))}
>
<Close />
</button>
</div>
))}
</div>
) : (
<div className='mt-[74px] text-center'>
<span className='text-base text-gray-600'>
앨범에서 사진을 찾아보세요.
</span>
</div>
);
};
9 changes: 2 additions & 7 deletions src/features/comment/component/CommentBox.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { useState } from 'react';
import type { Meta, StoryObj } from '@storybook/react';
import { CommentBox } from './CommentBox';

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

export const Default: Story = {
args: {
text: '',
},
render: (args) => {
const [text, setText] = useState(args.text);
return <CommentBox {...args} text={text} setText={setText} />;
render: () => {
return <CommentBox />;
},
};
62 changes: 34 additions & 28 deletions src/features/comment/component/CommentBox.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,44 @@
import Upload from '@/assets/upload.svg';
import UploadActive from '@/assets/upload_active.svg';
import { CommentInput } from '@/components/molecules/comment';
import { ImageUploader } from './ImageUploader';
import { useState } from 'react';
import { BottomContent } from './BottomContent';
import { TextFieldBottom } from './TextFieldBottom';

interface CommentBoxProps {
text: string;
setText: (text: string) => void;
}
export const CommentBox = () => {
const [text, setText] = useState<string>('');
const [images, setImages] = useState<string[]>([]);
const [showGallery, setShowGallery] = useState(false);
const [bottomSheetHeight, setBottomSheetHeight] = useState(0);

export const CommentBox = ({ text, setText }: CommentBoxProps) => {
const handleUploadText = () => {
// text upload
setText('');
const handleUpload = () => {
// text, image upload
if (text.trim()) {
setText('');
}
if (showGallery && images.length > 0) {
setImages([]);
setShowGallery(false);
}
};

return (
<div className='absolute bottom-0 flex w-full items-center justify-between gap-3 bg-gray-100 px-5 py-2'>
<ImageUploader />

<CommentInput
value={text}
onChange={setText}
placeholder='댓글 입력'
minHeight={20}
maxHeight={42}
maxLineCount={3}
<div
className='absolute bottom-[34px] w-full'
style={{ bottom: showGallery ? `${bottomSheetHeight}px` : '34px' }}
>
<TextFieldBottom
text={text}
setText={setText}
showGallery={showGallery}
setShowGallery={setShowGallery}
handleUpload={handleUpload}
/>

<div
className={`cursor-pointer rounded-full p-2 ${text.trim() ? 'bg-primary-400' : 'bg-primary-100'}`}
onClick={handleUploadText}
>
{text.trim() ? <UploadActive /> : <Upload />}
</div>
<BottomContent
images={images}
setImages={setImages}
showGallery={showGallery}
setShowGallery={setShowGallery}
setBottomSheetHeight={setBottomSheetHeight}
/>
</div>
);
};
Loading