Skip to content

Commit

Permalink
Merge branch 'feat/#374_data_ui' of https://github.com/ssu-student-un…
Browse files Browse the repository at this point in the history
…ion/homepage-frontend into feat/#374_data_ui
  • Loading branch information
jongse7 committed Feb 5, 2025
2 parents 7b534b1 + 93b7d01 commit 2d5aa95
Show file tree
Hide file tree
Showing 28 changed files with 1,175 additions and 1,602 deletions.
201 changes: 201 additions & 0 deletions src/components/BoardNew/edit/FileInputWithType.tsx
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>
);
};
79 changes: 79 additions & 0 deletions src/components/BoardNew/edit/FileInputsWithType.tsx
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>
);
}
2 changes: 1 addition & 1 deletion src/components/FilterDropDown/FilterDropDown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ interface FilterDropDownProps extends React.HTMLAttributes<HTMLDivElement> {
}

export function FilterDropDown({
className,
className = '',
defaultValue,
optionValue,
onValueChange,
Expand Down
2 changes: 1 addition & 1 deletion src/components/ui/input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
return (
<input
type={type}
className={cn(className, InputVariants({ variant: isInvalid ? 'error' : 'default' }))}
className={cn(InputVariants({ variant: isInvalid ? 'error' : 'default' }), className)}
ref={ref}
disabled={isDisabled}
{...props}
Expand Down
4 changes: 2 additions & 2 deletions src/containers/common/Header/component/HeaderSheet.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { menuItems } from '@/containers/common/Header/const/pathData';
import { dataPath, menuItems } from '@/containers/common/Header/const/pathData';
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
import { CaretDown } from '@phosphor-icons/react';
import { ReactNode, useState, useEffect } from 'react';
Expand Down Expand Up @@ -93,7 +93,7 @@ export function HeaderSheet({ trigger, state: initialState = State.Logout }: Hea
))}
{/*임시 자료집(구글 드라이브 링크)*/}
<div
onClick={() => window.open(import.meta.env.VITE_TEMP_DATA_URL, '_blank')}
onClick={() => navigate(dataPath)}
className={`flex h-[64px] cursor-pointer items-center border-b border-[#E5E7EB] pl-10 text-gray-800`}
>
자료집
Expand Down
12 changes: 7 additions & 5 deletions src/containers/common/Header/component/Navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,20 @@ import {
NavigationMenuTrigger,
NavigationMenuContent,
} from '@/components/ui/navigation-menu';
import { menuItems } from '../const/pathData';
import { dataPath, menuItems } from '../const/pathData';
import { getStyles } from '../const/style';
import { State } from '../const/state';
import { cn } from '@/libs/utils';
import DropDownMenu from './DropDownMenu';
import { useNavigate } from 'react-router-dom';

interface NavigationProps {
state?: State;
}

export function Navigation({ state = State.Onboarding }: NavigationProps) {
const styles = getStyles(state);
const navigate = useNavigate();
return (
<NavigationMenu className="h-full xs:hidden sm:hidden md:hidden lg:hidden">
<NavigationMenuList className="h-full">
Expand All @@ -33,11 +35,11 @@ export function Navigation({ state = State.Onboarding }: NavigationProps) {
</NavigationMenuContent>
</NavigationMenuItem>
))}
<<<<<<< HEAD
=======
{/*자료집 임시 제거*/}
<NavigationMenuItem
className="relative h-full min-w-fit text-[20px]"
onClick={() => window.open(import.meta.env.VITE_TEMP_DATA_URL, '_blank')}
>
>>>>>>> temp-branch
<NavigationMenuItem className="relative h-full min-w-fit text-[20px]" onClick={() => navigate(dataPath)}>
<NavigationMenuTrigger isData={true} className={cn(styles.headerItemStyle)}>
<p>자료집</p>
</NavigationMenuTrigger>
Expand Down
1 change: 1 addition & 0 deletions src/containers/new/Container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export function Container({ className, children }: ContainerProps) {
<section
className={cn('flex justify-center px-10 md:px-[72px] lg:px-[200px] xl:px-[200px] xxl:px-[200px]', className)}
>
{/*px값은 section, py값은 div에 설정해야하는 이유가 있나요??*/}
<div className="w-full max-w-[1040px] py-20">{children}</div>
</section>
);
Expand Down
20 changes: 20 additions & 0 deletions src/pages/data/[id]/const/mockupData.ts
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 올림
`;
Loading

0 comments on commit 2d5aa95

Please sign in to comment.