-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'feat/#374_data_ui' of https://github.com/ssu-student-un…
…ion/homepage-frontend into feat/#374_data_ui
- Loading branch information
Showing
28 changed files
with
1,175 additions
and
1,602 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,201 @@ | ||
import { ChangeEvent, useEffect, useRef, useState } from 'react'; | ||
import { cn } from '@/libs/utils.ts'; | ||
import { FileText, Plus, Trash } from '@phosphor-icons/react'; | ||
import { FilterDropDown } from '@/components/FilterDropDown/FilterDropDown'; | ||
|
||
export type PostFile = UploadedPostFile | LocalPostFile; | ||
|
||
export interface UploadedPostFile { | ||
name: string; | ||
isUploaded: true; | ||
id: number; | ||
category?: string; | ||
} | ||
|
||
export interface LocalPostFile { | ||
name: string; | ||
isUploaded: false; | ||
file: File; | ||
category?: string; | ||
} | ||
|
||
interface FileInputsProps { | ||
className?: string; | ||
sizeLimit?: number; | ||
files?: PostFile[]; | ||
onChange?: (files: PostFile[]) => void; | ||
} | ||
|
||
export function FileInputsWithType({ className, files, onChange, sizeLimit }: FileInputsProps) { | ||
const [innerFiles, setInnerFiles] = useState<PostFile[]>([]); | ||
|
||
useEffect(() => { | ||
if (files) setInnerFiles(files); | ||
else setInnerFiles([]); | ||
}, [files]); | ||
|
||
function onNewFile(evt: ChangeEvent<HTMLInputElement>) { | ||
const file = evt.currentTarget.files?.item(0); | ||
if (file) { | ||
const postFile: PostFile = { | ||
name: file.name, | ||
isUploaded: false, | ||
file, | ||
category: '', // 기본 카테고리 설정 | ||
}; | ||
const newFiles = [...innerFiles, postFile]; | ||
setInnerFiles(newFiles); | ||
if (onChange) onChange(newFiles); | ||
evt.currentTarget.files = new DataTransfer().files; | ||
} | ||
} | ||
|
||
function onFileChange(idx: number, evt: ChangeEvent<HTMLInputElement>) { | ||
const file = evt.currentTarget.files?.item(0); | ||
const newFiles = [...innerFiles]; | ||
if (file) { | ||
newFiles[idx] = { | ||
...newFiles[idx], | ||
name: file.name, | ||
isUploaded: false, | ||
file, | ||
}; | ||
} else { | ||
newFiles.splice(idx, 1); | ||
} | ||
setInnerFiles(newFiles); | ||
if (onChange) onChange(newFiles); | ||
} | ||
|
||
function onCategoryChange(idx: number, category: string) { | ||
const newFiles = [...innerFiles]; | ||
newFiles[idx] = { | ||
...newFiles[idx], | ||
category, | ||
}; | ||
setInnerFiles(newFiles); | ||
if (onChange) onChange(newFiles); | ||
} | ||
|
||
return ( | ||
<div className={cn('flex flex-col gap-6', className)}> | ||
{innerFiles.map((file, idx) => ( | ||
<FileInputWithType | ||
key={idx} | ||
file={file} | ||
onChange={(evt) => onFileChange(idx, evt)} | ||
sizeLimit={sizeLimit} | ||
onCategoryChange={(category) => onCategoryChange(idx, category)} | ||
/> | ||
))} | ||
<FileInputWithType onChange={onNewFile} sizeLimit={sizeLimit} /> | ||
</div> | ||
); | ||
} | ||
|
||
interface FileItemProps { | ||
file?: PostFile; | ||
sizeLimit?: number; | ||
onChange?: (evt: ChangeEvent<HTMLInputElement>) => void; | ||
onCategoryChange?: (category: string) => void; | ||
} | ||
|
||
export const FileInputWithType = ({ file, sizeLimit, onChange, onCategoryChange }: FileItemProps) => { | ||
const [fileCategory, setFileCategory] = useState<string>(file?.category || ''); | ||
const fileInputRef = useRef<HTMLInputElement>(null); | ||
const [isDragging, setDragging] = useState(false); | ||
const [error, setError] = useState<string | null>(null); | ||
|
||
useEffect(() => { | ||
setFileCategory(file?.category || ''); | ||
}, [file]); | ||
|
||
function handleCategoryChange(category: string) { | ||
setFileCategory(category); | ||
if (onCategoryChange) onCategoryChange(category); | ||
} | ||
|
||
function fileChangeHandler(evt: ChangeEvent<HTMLInputElement>) { | ||
const fileSize = evt.currentTarget.files?.item(0)?.size ?? -1; | ||
if (fileSize >= 0 && sizeLimit && fileSize > sizeLimit) { | ||
evt.currentTarget.files = new DataTransfer().files; | ||
setError(`파일 크기가 ${sizeLimit}를 초과합니다.`); | ||
return; | ||
} | ||
setError(null); | ||
if (onChange) onChange(evt); | ||
} | ||
|
||
function triggerFileInput() { | ||
fileInputRef.current?.click(); | ||
} | ||
|
||
function fileDropHandler(evt: React.DragEvent<HTMLDivElement>) { | ||
evt.preventDefault(); | ||
setDragging(false); | ||
const file = evt.dataTransfer.files?.item(0); | ||
if (fileInputRef.current && file) { | ||
const dataTransfer = new DataTransfer(); | ||
dataTransfer.items.add(file); | ||
fileInputRef.current.files = dataTransfer.files; | ||
fileInputRef.current.dispatchEvent(new Event('change', { bubbles: true })); | ||
} | ||
} | ||
|
||
function dragOverHandler(evt: React.DragEvent<HTMLDivElement>) { | ||
evt.preventDefault(); | ||
setDragging(true); | ||
} | ||
|
||
function dragLeaveHandler(evt: React.DragEvent<HTMLDivElement>) { | ||
evt.preventDefault(); | ||
setDragging(false); | ||
} | ||
|
||
const fileCategories: string[] = ['결산안', '활동보고', '자료']; | ||
|
||
return ( | ||
<div | ||
className={cn('flex flex-row items-center gap-4 xs:items-start sm:flex-col sm:items-start')} | ||
onDrop={fileDropHandler} | ||
onDragOver={dragOverHandler} | ||
onDragLeave={dragLeaveHandler} | ||
> | ||
<div | ||
className={cn( | ||
'flex grow cursor-pointer items-center gap-4 rounded-[5px] border-2 border-[#CDCDCD] p-[8px] text-gray-400', | ||
error && 'border-red-800 bg-red-50 text-red-800', | ||
isDragging && 'border-dashed border-primary bg-blue-50 text-primary', | ||
file && 'text-gray-600' | ||
)} | ||
onClick={triggerFileInput} | ||
> | ||
<FileText | ||
className={cn( | ||
'select-none text-gray-600', | ||
error && 'text-red-800', | ||
isDragging && 'text-primary motion-safe:animate-bounce' | ||
)} | ||
size="32" | ||
/> | ||
<span>{file?.name || (isDragging ? '파일을 여기에 놓으세요' : '파일을 선택해주세요')}</span> | ||
</div> | ||
<div className="flex flex-row"> | ||
{' '} | ||
{file && ( | ||
<FilterDropDown | ||
className="flex h-[48px] w-[354px] justify-center rounded-[12px] border-gray-500 text-[19px] font-medium xs:w-[257px] sm:w-[187px]" | ||
defaultValue="파일종류 선택" | ||
optionValue={fileCategories} | ||
value={fileCategory} | ||
onValueChange={handleCategoryChange} | ||
/> | ||
)} | ||
<button className="p-2" onClick={triggerFileInput}> | ||
{file ? <Trash className="text-gray-600" size="32" /> : <Plus className="text-gray-600" size="32" />} | ||
</button> | ||
<input ref={fileInputRef} type="file" className="hidden" onChange={fileChangeHandler} /> | ||
</div> | ||
</div> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
import { ChangeEvent, useEffect, useState } from 'react'; | ||
import { PostFile } from '@/components/BoardNew/edit/FileInputWithType.tsx'; | ||
import { cn } from '@/libs/utils.ts'; | ||
import { FileInputWithType } from './FileInputWithType'; | ||
|
||
interface FileInputsProps { | ||
className?: string; | ||
sizeLimit?: number; | ||
files?: PostFile[]; | ||
onChange?: (files: PostFile[]) => void; | ||
} | ||
|
||
export function FileInputsWithType({ className, files, onChange, sizeLimit }: FileInputsProps) { | ||
const [innerFiles, setInnerFiles] = useState<PostFile[]>([]); | ||
|
||
useEffect(() => { | ||
// 외부에서 전달된 files가 있으면 상태 초기화 | ||
if (files) setInnerFiles(files); | ||
else setInnerFiles([]); | ||
}, [files]); | ||
|
||
function onNewFile(evt: ChangeEvent<HTMLInputElement>) { | ||
const file = evt.currentTarget.files?.item(0); | ||
if (file) { | ||
const postFile: PostFile = { | ||
name: file.name, | ||
isUploaded: false, | ||
file: file, | ||
category: '', // 기본 카테고리 설정 | ||
}; | ||
const newFiles = [...innerFiles, postFile]; | ||
setInnerFiles(newFiles); | ||
if (onChange) onChange(newFiles); | ||
evt.currentTarget.files = new DataTransfer().files; // 파일 초기화 | ||
} | ||
} | ||
|
||
function onFileChange(idx: number, evt: ChangeEvent<HTMLInputElement>) { | ||
const file = evt.currentTarget.files?.item(0); | ||
const newFiles = [...innerFiles]; | ||
if (file) { | ||
newFiles[idx] = { | ||
...newFiles[idx], | ||
name: file.name, | ||
isUploaded: false, | ||
file: file, | ||
}; | ||
} else { | ||
newFiles.splice(idx, 1); // 파일 삭제 | ||
} | ||
setInnerFiles(newFiles); | ||
if (onChange) onChange(newFiles); | ||
} | ||
|
||
function onCategoryChange(idx: number, category: string) { | ||
const newFiles = [...innerFiles]; | ||
newFiles[idx] = { | ||
...newFiles[idx], | ||
category: category, // 카테고리 업데이트 | ||
}; | ||
setInnerFiles(newFiles); | ||
if (onChange) onChange(newFiles); | ||
} | ||
|
||
return ( | ||
<div className={cn('flex flex-col gap-6', className)}> | ||
{innerFiles.map((file, idx) => ( | ||
<FileInputWithType | ||
key={idx} | ||
file={file} | ||
onChange={(evt) => onFileChange(idx, evt)} | ||
sizeLimit={sizeLimit} | ||
onCategoryChange={(category) => onCategoryChange(idx, category)} // 카테고리 변경 핸들러 추가 | ||
/> | ||
))} | ||
<FileInputWithType onChange={onNewFile} sizeLimit={sizeLimit} /> | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
export const breadcrumbItems: [string, string | null][] = [['자료집', null]]; | ||
export const content = ` | ||
존경하고 사랑하는 숭실대학교 학우 여러분 안녕하세요! | ||
함께 모여 빛나는 숭실 제64대 총학생회 US:SUM입니다. | ||
2024년 변화의 시기를 맞아 학생들의 권리를 되찾고 제도를 개선하겠다고 약속한 지 어느덧 1년이 지났습니다. | ||
학생 여러분의 불편한 점을 개선하기 위해 밤낮없이 고민하며 유관 부서와 끊임없이 논의하는 과정에서 때로는 힘든 순간도 있었지만, 여러분의 관심과 응원이 저희 총학생회에 큰 원동력이 되었습니다. | ||
숭실대학교에서 청춘을 빛내기 위해 노력하고 있는 여러분을 대표할 수 있어 진심으로 감사드립니다. | ||
저희의 노력이 여러분에게 모두 다르게 기억될 수 있지만, US:SUM과 함께한 순간들이 여러분들의 학교생활에 좋은 추억으로 간직되기를 소망합니다. | ||
여러분들과 함께한 모든 순간들이 모여 조금 더 빛나는 숭실을 만들 수 있었습니다. | ||
너무나 행복한 1년을 만들어 주셔서 감사드리며, 숭실을 함께 빛내 주셔서 진심으로 감사합니다. | ||
또한 앞으로의 학생사회가 더욱 빛날 수 있도록 2025년을 이끌어나갈 구성원들에게도 많은 응원해주시기 바라며 인사드리겠습니다. | ||
1년동안 진심으로 감사했습니다. | ||
제64대 총학생회 US:SUM 올림 | ||
`; |
Oops, something went wrong.