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
115 changes: 115 additions & 0 deletions src/hooks/useSugangInwonMasterDetail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// hooks/useSugangInwonMasterDetail.ts
import { useState, useMemo } from 'react';
import type { SugangInwon, GroupedSugangInwon, SortDirection } from '@/types/types';

export function useSugangInwonMasterDetailTable(sugangs: SugangInwon[]) {
// Master 정렬 및 필터링 상태
const [sortColumnMaster, setSortColumnMaster] = useState<keyof SugangInwon | null>(null);
const [sortDirectionMaster, setSortDirectionMaster] = useState<SortDirection>('asc');
const [filtersMaster, setFiltersMaster] = useState<Record<string, Set<string>>>({});

// 확장된 행 상태
const [expandedSet, setExpandedSet] = useState<Set<string>>(new Set());

// 데이터를 그룹화
const groupedData: GroupedSugangInwon[] = useMemo(() => {
const map = new Map<string, SugangInwon[]>();
sugangs.forEach((subj) => {
const list = map.get(subj.gwamokcode) || [];
list.push(subj);
map.set(subj.gwamokcode, list);
});
return Array.from(map.entries()).map(([gwamokcode, subjects]) => ({
gwamokcode,
master: subjects[0],
details: subjects,
}));
}, [sugangs]);

// Master 정렬 핸들러
const handleMasterSort = (colId: keyof SugangInwon, sortable: boolean) => {
if (!sortable) return;
if (sortColumnMaster !== colId) {
setSortColumnMaster(colId);
setSortDirectionMaster('asc');
} else {
setSortDirectionMaster((prev) => (prev === 'asc' ? 'desc' : 'asc'));
}
};

// Master 필터링 핸들러
const handleMasterCheckboxFilterChange = (colId: string, value: string) => {
setFiltersMaster((prev) => {
const newSet = new Set(prev[colId] || []);
if (newSet.has(value)) {
newSet.delete(value);
} else {
newSet.add(value);
}
return { ...prev, [colId]: newSet };
});
};

// Master 필터 초기화 핸들러
const clearMasterFilters = (colId: string) => {
setFiltersMaster((prev) => ({ ...prev, [colId]: new Set() }));
};

// 행 확장/축소 핸들러
const toggleExpand = (gwamokcode: string) => {
setExpandedSet((prev) => {
const newSet = new Set(prev);
if (newSet.has(gwamokcode)) {
newSet.delete(gwamokcode);
} else {
newSet.add(gwamokcode);
}
return newSet;
});
};

// 필터링 및 정렬된 데이터 계산
const filteredAndSorted = useMemo(() => {
// Master 필터링
let data = groupedData.filter((group) => {
return Object.entries(filtersMaster).every(([colId, setOfVals]) => {
if (setOfVals.size === 0) return true;
const cellValue = String(group.master[colId as keyof SugangInwon] || '');
return setOfVals.has(cellValue);
});
});

// Master 정렬
if (sortColumnMaster) {
data = [...data].sort((a, b) => {
const valA = a.master[sortColumnMaster];
const valB = b.master[sortColumnMaster];

if (typeof valA === 'number' && typeof valB === 'number') {
return sortDirectionMaster === 'asc' ? valA - valB : valB - valA;
}
const strValA = String(valA || '');
const strValB = String(valB || '');
return sortDirectionMaster === 'asc' ? strValA.localeCompare(strValB) : strValB.localeCompare(strValA);
});
}

return data;
}, [groupedData, filtersMaster, sortColumnMaster, sortDirectionMaster]);

return {
// Master 상태 및 핸들러
sortColumnMaster,
sortDirectionMaster,
filtersMaster,
handleMasterSort,
handleMasterCheckboxFilterChange,
clearMasterFilters,

// 기타 상태
expandedSet,
groupedData,
filteredAndSorted,
toggleExpand,
};
}
59 changes: 18 additions & 41 deletions src/hooks/useSugangInwonTable.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,10 @@
import { useState, useMemo } from 'react';
import type { SugangInwon, GroupedSugangInwon, SortDirection } from '@/types/types';
import type { SugangInwon, SortDirection } from '@/types/types';

export function useSugangInwonTable(sugangs: SugangInwon[]) {
const [sortColumn, setSortColumn] = useState<keyof SugangInwon | null>(null);
const [sortDirection, setSortDirection] = useState<SortDirection>('asc');
const [filters, setFilters] = useState<Record<string, Set<string>>>({});
const [expandedSet, setExpandedSet] = useState<Set<string>>(new Set());

const groupedData: GroupedSugangInwon[] = useMemo(() => {
const map = new Map<string, SugangInwon[]>();
setFilters({});
sugangs.forEach((subj) => {
const list = map.get(subj.gwamokcode) || [];
list.push(subj);
map.set(subj.gwamokcode, list);
});
return Array.from(map.entries()).map(([gwamokcode, subjects]) => ({
gwamokcode,
master: subjects[0],
details: subjects,
}));
}, [sugangs]);

const handleSort = (colId: keyof SugangInwon, sortable: boolean) => {
if (!sortable) return;
Expand Down Expand Up @@ -48,55 +32,48 @@ export function useSugangInwonTable(sugangs: SugangInwon[]) {
setFilters((prev) => ({ ...prev, [colId]: new Set() }));
};

const toggleExpand = (gwamokcode: string) => {
setExpandedSet((prev) => {
const newSet = new Set(prev);
if (newSet.has(gwamokcode)) {
newSet.delete(gwamokcode);
} else {
newSet.add(gwamokcode);
}
return newSet;
});
};

const filteredAndSorted = useMemo(() => {
const data = groupedData.filter((group) => {
let data = sugangs.filter((item) => {
return Object.entries(filters).every(([colId, setOfVals]) => {
if (setOfVals.size === 0) return true;
const cellValue = String(group.master[colId as keyof SugangInwon] || '');
const cellValue = String(item[colId as keyof SugangInwon] || '');
return setOfVals.has(cellValue);
});
});

if (sortColumn) {
data.sort((a, b) => {
const valA = a.master[sortColumn];
const valB = b.master[sortColumn];
data = [...data].sort((a, b) => {
const valA = a[sortColumn];
const valB = b[sortColumn];

// 숫자와 문자열을 구분하여 정렬
if (typeof valA === 'number' && typeof valB === 'number') {
return sortDirection === 'asc' ? valA - valB : valB - valA;
// 숫자 변환 가능 여부 확인
const numA = Number(valA);
const numB = Number(valB);
const isNumA = !isNaN(numA);
const isNumB = !isNaN(numB);

if (isNumA && isNumB) {
// 둘 다 숫자인 경우 숫자 비교
return sortDirection === 'asc' ? numA - numB : numB - numA;
}

// 둘 다 문자열이거나 하나만 숫자인 경우 문자열 비교
const strValA = String(valA || '');
const strValB = String(valB || '');
return sortDirection === 'asc' ? strValA.localeCompare(strValB) : strValB.localeCompare(strValA);
});
}

return data;
}, [groupedData, filters, sortColumn, sortDirection]);
}, [sugangs, filters, sortColumn, sortDirection]);

return {
sortColumn,
sortDirection,
filters,
expandedSet,
groupedData,
filteredAndSorted,
handleSort,
handleCheckboxFilterChange,
toggleExpand,
clearAllFilters,
};
}
2 changes: 1 addition & 1 deletion src/sugang_inwon/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export default function App() {
}, []);

return (
<div className="font-sans p-6 max-w-5xl mx-auto bg-white space-y-6">
<div className="font-sans p-6 max-w-7xl mx-auto bg-white space-y-6">
<motion.div
initial="hidden"
animate="visible"
Expand Down
61 changes: 34 additions & 27 deletions src/sugang_inwon/DepartmentSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import React, { useState, useEffect } from 'react';
import { Department, Semester, Subject, SugangInwon } from '@/types/types';
import { Department, SugangInwon } from '@/types/types';
import { Input } from '@/components/ui/input';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { ScrollArea } from '@/components/ui/scroll-area';
import { fetchDepartmentData, fetchSemesterData, fetchSubjectsData, fetchSugangInwonData } from '@/hooks/fetchAPI';
import { fetchSugangInwonData } from '@/hooks/fetchAPI';
import { Label } from '@/components/ui/label';
import { Popover } from '@/components/ui/popover';
import { PopoverContent, PopoverTrigger } from '@radix-ui/react-popover';
import { Button } from '@/components/ui/button';
import { loadDataFromStorage } from '@/hooks/storage';
import { Check, ChevronDown } from 'lucide-react';
import SugangInwonTable from './SugangInwonTable';

export type ViewType = 'total' | 'master-detail';
interface DepartmentSearchResult extends Department {
university: string;
faculty: string;
Expand All @@ -24,7 +24,6 @@ const DepartmentSelector: React.FC<GroupedDataProp> = (groupedDataProp: GroupedD
const groupedData = groupedDataProp.groupedData;
const [selectedUniversity, setSelectedUniversity] = useState<string | null>(null);
const [selectedFaculty, setSelectedFaculty] = useState<string | null>(null);
const [subjectsData, setSubjectsData] = useState<Subject[]>([]);
const [selectedDepartment, setSelectedDepartment] = useState<DepartmentSearchResult | null>(null);
const [selectedTrack, setSelectedTrack] = useState<string | null>(null);
const [searchTerm, setSearchTerm] = useState<string>('');
Expand All @@ -37,23 +36,52 @@ const DepartmentSelector: React.FC<GroupedDataProp> = (groupedDataProp: GroupedD

const [sugangInwon, setSugangInwon] = useState<SugangInwon[]>([]);

const [view, setView] = useState<ViewType>('master-detail');

useEffect(() => {
if (!selectedDepartment) return;
const fetchAndGroupXML = async () => {
try {
/**
* TODO 나중에 동적으로 가져올 것! 🚨
*
*
*/
await fetchSugangInwonData({ code: 'L11E', setSugangInwon: setSugangInwon });

await fetchSugangInwonData({
code: selectedDepartment.tcd,
setSugangInwon: setSugangInwon,
});
setLoading(false);
} catch (err) {
setError((err as Error).message);
setLoading(false);
}
};
fetchAndGroupXML();
}, []);
}, [selectedDepartment]);

useEffect(() => {
if (isInitialized || !groupedData || Object.keys(groupedData).length === 0) return;

setIsInitialized(true);
loadDataFromStorage('department', (data: string | null) => {
if (!data) data = '교양필수';
Object.entries(groupedData).forEach(([university, faculties]) => {
Object.entries(faculties).forEach(([faculty, departments]) => {
departments.forEach((dept) => {
if (dept.tnm.toLowerCase().includes(data)) {
setSelectedDepartment({ university: university, faculty: faculty, tcd: dept.tcd, tnm: dept.tnm });
setSelectedUniversity(university);
setSelectedFaculty(faculty);
setSelectedTrack(dept.tnm);
return;
}
});
});
});
});
}, [groupedData, isInitialized]);

useEffect(() => {
if (searchTerm.trim() === '') {
Expand All @@ -80,27 +108,6 @@ const DepartmentSelector: React.FC<GroupedDataProp> = (groupedDataProp: GroupedD
setSearchResults(results);
}, [searchTerm, groupedData]);

useEffect(() => {
if (isInitialized || !groupedData || Object.keys(groupedData).length === 0) return;

setIsInitialized(true);
loadDataFromStorage('department', (data: string | null) => {
if (!data) return;
Object.entries(groupedData).forEach(([university, faculties]) => {
Object.entries(faculties).forEach(([faculty, departments]) => {
departments.forEach((dept) => {
if (dept.tnm.toLowerCase().includes(data)) {
setSelectedDepartment({ university: university, faculty: faculty, tcd: dept.tcd, tnm: dept.tnm });
setSelectedUniversity(university);
setSelectedFaculty(faculty);
setSelectedTrack(dept.tnm);
}
});
});
});
});
}, [groupedData, isInitialized]);

// 학과 검색 결과 선택 핸들러
const handleSearchSelect = (result: DepartmentSearchResult) => {
setSelectedUniversity(result.university);
Expand Down
Loading
Loading