Skip to content

Commit

Permalink
Merge branch 'feat/#35' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
seoyeon5117 committed Feb 3, 2025
2 parents ad026e5 + 9d24da2 commit bff0934
Show file tree
Hide file tree
Showing 16 changed files with 553 additions and 129 deletions.
6 changes: 6 additions & 0 deletions src/assets/imgs/ImgX.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/assets/imgs/camera.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions src/assets/imgs/pencil.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 3 additions & 2 deletions src/components/board/FilterBox.jsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { useState } from "react"
import { DropBox } from "../common/DropBox"
import { Categories } from "@/constants/categories"
export const FilterBox = () => {
const [category, setCategory] = useState("All") // 선택된 카테고리 값
const [sortOrder, setSortOrder] = useState("All") // 선택된 정렬 기준 값
const [isCategoryOpen, setIsCategoryOpen] = useState(false)
const [isSortOpen, setIsSortOpen] = useState(false)
const categories = ["All", "Hospital", "University", "Restaurant"];
const categoriesWithAll = ["All", ...Categories];
const sortOptions = ["All", "Newest", "Registered", "Popularity"];
const toggleCategory_Sort = (type) => { // 카테고리랑 정렬 토글
if (type === "category") {
Expand All @@ -28,7 +29,7 @@ export const FilterBox = () => {
};
return (
<div className="flex gap-[0.875rem]">
<DropBox dropList={categories} state={isCategoryOpen} content={'Category'} toggle={() => toggleCategory_Sort("category")} select={handleCategorySelect} selected={category} />
<DropBox dropList={categoriesWithAll} state={isCategoryOpen} content={'Category'} toggle={() => toggleCategory_Sort("category")} select={handleCategorySelect} selected={category} />
<DropBox dropList={sortOptions} state={isSortOpen} content={'Sort by'} toggle={() => toggleCategory_Sort("sortOrder")} select={handleSortSelect} selected={sortOrder} />
</div>
)
Expand Down
34 changes: 34 additions & 0 deletions src/components/board/write/SelectCategory.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Categories } from '@/constants/categories';

export const SelectCategory = ({ selectedCategory, setSelectedCategory }) => {
const CategoryList = [...Categories];
const handleOnChange = (e) => {
setSelectedCategory(e.target.id);
};

return (
<div className="flex flex-col gap-3">
<div className="text-subTitle">Category</div>
<div className="flex w-full gap-3">
{CategoryList.map((category) => (
<div className="box-border flex" key={category}>
<input
type="radio"
name="category"
id={category}
className="hidden peer"
onChange={handleOnChange}
checked={selectedCategory === category}
/>
<label
htmlFor={category}
className="flex items-center px-3 py-[.375rem] border rounded-full cursor-pointer border-neutral-border-30 text-neutral-border-50 border-box peer-checked:border-primary-30 peer-checked:bg-primary-30 peer-checked:text-white"
>
{category}
</label>
</div>
))}
</div>
</div>
);
};
103 changes: 103 additions & 0 deletions src/components/board/write/UploadPics.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import Camera from '@/assets/imgs/camera.svg';
import ImgX from '@/assets/imgs/ImgX.svg?react';
import { useState } from 'react';

export const UploadPics = ({ onChange }) => {
const [previewImages, setPreviewImages] = useState([]);
const [files, setFiles] = useState([]);

const getImageFiles = async (e) => {
const newFiles = Array.from(e.target.files);

if (newFiles.length + files.length > 10) {
alert('이미지는 최대 10장까지 업로드가 가능합니다.');
return;
}

const validFiles = newFiles.filter((file) => file.type.match('image/.*'));
if (validFiles.length !== newFiles.length) {
alert('이미지 파일만 업로드가 가능합니다.');
}

try {
// Promise로 모든 미리보기 URL 생성
const newPreviews = await Promise.all(
validFiles.map((file) => {
// URL 생성 중 오류 가능성 대비
return new Promise((resolve, reject) => {
try {
resolve(URL.createObjectURL(file));
} catch (error) {
reject(error);
}
});
}),
);

// 모든 파일과 미리보기가 준비된 후 상태 업데이트
if (validFiles.length > 0 && newPreviews.length === validFiles.length) {
const updatedFiles = [...files, ...validFiles];
const updatedPreviews = [...previewImages, ...newPreviews];

setFiles(updatedFiles);
setPreviewImages(updatedPreviews);
onChange(updatedFiles); // 부모 컴포넌트에 전달
}
} catch (error) {
alert('파일 처리 중 오류가 발생했습니다.');
return null;
}
};

const removeImage = (index) => {
const urlToRevoke = previewImages[index];

const updatedFiles = files.filter((_, i) => i !== index);
const updatedPreviews = previewImages.filter((_, i) => i !== index);

setFiles(updatedFiles);
setPreviewImages(updatedPreviews);
onChange(updatedFiles); // 부모 컴포넌트에 전달

URL.revokeObjectURL(urlToRevoke); // URL 해제
};

return (
<div className="flex flex-row items-end gap-2">
<label
htmlFor="selectImages"
className="flex flex-col flex-shrink-0 items-center justify-center w-20 h-20 bg-neutral-border-30 rounded-[.3125rem] mt-[.5625rem]"
>
<img src={Camera} alt="" className="w-[2.375rem] h-[2.375rem]" />
<span className="text-small text-neutral-border-50">
{files.length}/10
</span>
</label>
<input
type="file"
accept="image/*"
id="selectImages"
className="hidden"
multiple
onChange={getImageFiles}
/>
<div className="grid grid-flow-col gap-[.875rem] overflow-x-scroll scrollbar-hide pr-[.5625rem] pt-[.5625rem]">
{previewImages.map((src, index) => (
<div className="relative w-20 h-20" key={index}>
<img
src={src}
alt={`Preview ${index + 1}`}
className="object-cover w-20 h-20"
/>
<button
onClick={() => removeImage(index)}
className="absolute top-0 right-0 z-10 translate-x-1/2 -translate-y-1/2"
>
<ImgX className="w-[1.125rem] h-[1.125rem] text-neutral-border-50" />
</button>
</div>
))}
</div>
</div>
);
};
16 changes: 16 additions & 0 deletions src/components/board/write/WriteButton.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { useNavigate } from 'react-router-dom';
import Pencil from '@/assets/imgs/pencil.svg'

export const WriteButton = () => {
const navigate = useNavigate();
return (
<button
type="button"
className="fixed flex items-center py-2 px-[1.875rem] bg-primary-base rounded-full text-white shadow-[.1875rem_.1875rem_.25rem_0rem_rgba(0,0,0,0.2)] cursor-pointer bottom-9 left-1/2 -translate-x-1/2 gap-[.375rem]"
onClick={() => navigate('./write')}
>
<span>Write</span>
<img src={Pencil} alt="" className='w-4 h-4'/>
</button>
);
};
54 changes: 54 additions & 0 deletions src/components/board/write/WriteContent.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { TranslateButton } from '@/components/common/TranslateButton';

export const WriteContent = ({
content,
setContent,
translatedContent,
setTranslatedContent,
}) => {
const handleOnChange = (e) => {
setContent(e.target.value);
};

return (
<div className="flex flex-col gap-[2.5rem]">
<div className="flex flex-col gap-3">
<label htmlFor="contentInput" className="text-subTitle">
Content
</label>
<div className="box-border flex flex-row w-full px-[.875rem] py-[1.125rem] border border-neutral-border-30 rounded-lg gap-[.625rem] items-start">
<textarea
id="contentInput"
rows={16}
placeholder="Add a content."
value={content}
onChange={handleOnChange}
className="w-full leading-none resize-none placeholder-neutral-border-50 scrollbar-hide"
required
/>
<TranslateButton
translateType="content"
value={content}
setValue={setTranslatedContent}
/>
</div>
</div>
{translatedContent && (
<div className="flex flex-col gap-3">
<label htmlFor="translatedContent" className="text-subTitle">
Translation of Content
</label>
<div className="box-border flex flex-row w-full px-[.875rem] py-[1.125rem] border border-neutral-border-30 rounded-lg gap-[.625rem] items-start">
<textarea
id="translatedContent"
rows={16}
value={translatedContent}
className="w-full leading-none bg-transparent resize-none placeholder-neutral-border-50 scrollbar-hide"
disabled
/>
</div>
</div>
)}
</div>
);
};
28 changes: 28 additions & 0 deletions src/components/board/write/WriteTitle.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { TranslateButton } from '@/components/common/TranslateButton';

export const WriteTitle = ({ title, setTitle }) => {
const handleOnChange = (e) => {
setTitle(e.target.value);
};

return (
<div className="flex flex-col gap-3">
<label htmlFor="writeTitle" className="text-subTitle">
Title
</label>
<div className="box-border flex flex-row w-full px-[.875rem] py-[1.125rem] border border-neutral-border-30 rounded-lg gap-[.625rem]">
<input
id="writeTitle"
type="text"
placeholder="Please add a title."
value={title}
onChange={handleOnChange}
autoComplete="off"
className="w-full leading-none placeholder-neutral-border-50"
required
/>
<TranslateButton translateType="text" value={title} setValue={setTitle}/>
</div>
</div>
);
};
32 changes: 32 additions & 0 deletions src/components/common/TranslateButton.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import TranslateImg from '@/assets/imgs/translate.svg';

/** 번역 컴포넌트
* @param {Object} props - 컴포넌트의 props
* @param {'post' | 'content' | 'text'} props.translateType - 번역 타입 (게시된 post, 작성 중인 content, 단순 text)
* @param {number | string} props.value - post 번역 시 number, 작성 중인 글 본문이나 단순 text 번역 시 string
* @param {function} props.setValue - 해당 함수로 번역된 텍스트 전달
*/
export const TranslateButton = ({ translateType, value, setValue }) => {
const translate = () => {
if (value || value === 0) {
if (translateType === 'post') {
// postId 전달 시 게시글 번역 - value는 number
console.log(value);
} else if (translateType === 'content') {
// 작성 중인 게시글 본문 번역 - value는 string
// 모달 구현해야함
// 사용자가 선택한 언어 있으면 백에 알려주기
setValue(value); // 현재는 번역 안된 상태로 전달
} else if (translateType === 'text') {
// 영어로 번역 - value는 string
setValue(value); // 현재는 번역 안된 상태로 전달
}
}
};

return (
<button type="button" onClick={translate}>
<img src={TranslateImg} alt="" className="w-[1.375rem] h-[1.375rem]" />
</button>
);
};
1 change: 1 addition & 0 deletions src/constants/categories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const Categories = ["Hospital", "University", "Restaurant"];
Loading

0 comments on commit bff0934

Please sign in to comment.