Skip to content
Open
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
20 changes: 20 additions & 0 deletions src/features/portfolio/components/portfolio-filters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ type PortfolioFiltersProps = {
dreamJobFilter: string
onDreamJobChange: (value: string) => void
dreamJobs: string[]
generationFilter: string
onGenerationChange: (value: string) => void
generations: number[]
sortBy: string
onSortChange: (value: string) => void
viewMode: 'grid' | 'list'
Expand All @@ -27,6 +30,9 @@ export const PortfolioFilters = ({
dreamJobFilter,
onDreamJobChange,
dreamJobs,
generationFilter,
onGenerationChange,
generations,
sortBy,
onSortChange,
viewMode,
Expand Down Expand Up @@ -79,6 +85,20 @@ export const PortfolioFilters = ({
</SelectContent>
</Select>

<Select value={generationFilter} onValueChange={onGenerationChange}>
<SelectTrigger className="w-full sm:w-32">
<SelectValue placeholder="기수" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">전체 기수</SelectItem>
{generations.map((gen) => (
<SelectItem key={gen} value={gen.toString()}>
{gen}κΈ°
</SelectItem>
))}
</SelectContent>
</Select>

<div className="flex items-center gap-2 sm:ml-auto">
<ArrowUpDown className="h-4 w-4 text-muted-foreground" />
<Select value={sortBy} onValueChange={onSortChange}>
Expand Down
8 changes: 6 additions & 2 deletions src/features/portfolio/components/student-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,12 @@ export const StudentCard = ({
</CardHeader>

<CardContent className="flex-1 space-y-3 overflow-hidden">
<p className="text-sm text-muted-foreground line-clamp-2 min-h-[40px]">
{description || 'ν•œμ€„μ†Œκ°œκ°€ μž‘μ„±λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.'}
<p className="text-sm text-muted-foreground min-h-[20px]">
{description
? description.length > 30
? `${description.slice(0, 30)}...`
: description
: 'ν•œμ€„μ†Œκ°œκ°€ μž‘μ„±λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.'}
</p>

{email && (
Expand Down
8 changes: 6 additions & 2 deletions src/features/portfolio/components/student-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,12 @@ export const StudentTable = ({
{student.name}
</TableCell>
<TableCell className='px-2 py-3'>
<div className='truncate text-sm text-muted-foreground'>
{student.description || 'ν•œμ€„μ†Œκ°œκ°€ μž‘μ„±λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.'}
<div className='text-sm text-muted-foreground'>
{student.description
? student.description.length > 30
? `${student.description.slice(0, 30)}...`
: student.description
: 'ν•œμ€„μ†Œκ°œκ°€ μž‘μ„±λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.'}
</div>
</TableCell>
<TableCell className='whitespace-nowrap px-2 py-3'>
Expand Down
88 changes: 22 additions & 66 deletions src/features/portfolio/hooks/use-all-students.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,20 @@
import { useQuery } from '@tanstack/react-query'
import supabase from '@/utils/supabase/client'
import { StudentPortfolio } from '../types/student-portfolio-types'

type ProfileLink = {
link: string
alt: string | null
}
import { getGenerationFromJoinAt } from '../utils/generation'

type StudentWithDepartment = StudentPortfolio & {
student_id: string
department: string
generation: number | null
}

export const useAllStudents = () => {
const fetchAllStudents = async (): Promise<StudentWithDepartment[]> => {
// Fetch all data in parallel with optimized queries
const [studentsResult, profilesResult, studentJobsResult] = await Promise.all([
supabase
.from('student')
.select('student_id, name, email, departments(department_name)')
.select('student_id, name, email, join_at, departments(department_name)')
.order('name'),
supabase
.from('profile')
Expand All @@ -39,14 +35,13 @@ export const useAllStudents = () => {
if (studentsResult.error) throw studentsResult.error
if (!studentsResult.data) return []

// Create lookup maps for O(1) access
const profileMap = new Map()
profilesResult.data?.forEach((profile) => {
profileMap.set(profile.owner, profile)
})

const jobsMap = new Map()
studentJobsResult.data?.forEach((sj: { student_id: string, jobs: { job_name: string } | null }) => {
studentJobsResult.data?.forEach((sj) => {
if (!jobsMap.has(sj.student_id)) {
jobsMap.set(sj.student_id, [])
}
Expand All @@ -55,71 +50,32 @@ export const useAllStudents = () => {
}
})

// Map students with their data
const studentsWithProfiles = studentsResult.data.map((student) => {
type ProfileSkill = {
skills: {
skill_name: string
}
}

type ProfileCompetition = {
competition: {
competition_name: string
}
prize: string
}

type ProjectContributor = {
project: {
project_id: number
project_name: string
}
}

type RawProfile = {
description?: string
profile_skills?: ProfileSkill[]
profile_competitions?: ProfileCompetition[]
project_contributors?: ProjectContributor[]
profile_link?: ProfileLink[]
}

type Department = {
department_name: string
}

const rawProfile = (profileMap.get(student.student_id) as RawProfile) || null
return studentsResult.data.map((student) => {
const profile = profileMap.get(student.student_id)
const dreamJobs = jobsMap.get(student.student_id) || []

return {
student_id: student.student_id,
name: student.name,
description: rawProfile?.description ?? null,
description: profile?.description ?? null,
email: student.email,
department:
(student.departments as Department | null)?.department_name ||
'λ―Έμ§€μ •',
department: student.departments?.department_name || 'λ―Έμ§€μ •',
dreamJob: dreamJobs.length > 0 ? dreamJobs.join(', ') : null,
skills:
rawProfile?.profile_skills?.map((s) => ({
skill_name: s.skills.skill_name,
})) || [],
awards:
rawProfile?.profile_competitions?.map((c) => ({
competition_name: c.competition.competition_name,
prize: c.prize,
})) || [],
projects:
rawProfile?.project_contributors?.map((p) => ({
project_id: p.project.project_id,
project_name: p.project.project_name,
})) || [],
links: rawProfile?.profile_link || [],
} as StudentWithDepartment
generation: getGenerationFromJoinAt(student.join_at),
skills: profile?.profile_skills?.map((s) => ({
skill_name: s.skills.skill_name,
})) || [],
awards: profile?.profile_competitions?.map((c) => ({
competition_name: c.competition.competition_name,
prize: c.prize,
})) || [],
projects: profile?.project_contributors?.map((p) => ({
project_id: p.project.project_id,
project_name: p.project.project_name,
})) || [],
links: profile?.profile_link || [],
}
})

return studentsWithProfiles
}

return useQuery({
Expand Down
71 changes: 28 additions & 43 deletions src/features/portfolio/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export default function Portfolio() {
const [searchQuery, setSearchQuery] = useState('')
const [departmentFilter, setDepartmentFilter] = useState('all')
const [dreamJobFilter, setDreamJobFilter] = useState('all')
const [generationFilter, setGenerationFilter] = useState('all')
const [sortBy, setSortBy] = useState('name')
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid')

Expand Down Expand Up @@ -58,53 +59,34 @@ export default function Portfolio() {
return Array.from(new Set(allJobs)).sort() as string[]
}, [students])

// Filter students based on search, department, and dream job
// Get unique generations
const generations = useMemo((): number[] => {
if (!students) return []
return [...new Set(students.map((s) => s.generation).filter(Boolean))].sort((a, b) => b - a)
}, [students])

// Filter students
const filteredStudents = useMemo(() => {
if (!students) return []

return students.filter(
(student: {
department: string
dreamJob: string | null
name: string
email: string | null
skills: { skill_name: string }[]
}) => {
// Department filter
if (
departmentFilter !== 'all' &&
student.department !== departmentFilter
) {
return false
}

// Dream job filter
if (dreamJobFilter !== 'all') {
if (!student.dreamJob || !student.dreamJob.includes(dreamJobFilter)) {
return false
}
}

// Search filter
if (searchQuery) {
const query = searchQuery.toLowerCase()
const matchesName = student.name.toLowerCase().includes(query)
const matchesEmail = student.email?.toLowerCase().includes(query)
const matchesSkills = student.skills.some(
(skill: { skill_name: string }) =>
skill.skill_name.toLowerCase().includes(query)
)
const matchesDreamJob = student.dreamJob
?.toLowerCase()
.includes(query)

return matchesName || matchesEmail || matchesSkills || matchesDreamJob
}

return true
return students.filter((student) => {
if (departmentFilter !== 'all' && student.department !== departmentFilter) return false
if (dreamJobFilter !== 'all' && !student.dreamJob?.includes(dreamJobFilter)) return false
if (generationFilter !== 'all' && student.generation?.toString() !== generationFilter) return false

if (searchQuery) {
const query = searchQuery.toLowerCase()
return [
student.name,
student.email,
student.dreamJob,
...student.skills.map((s) => s.skill_name)
].some((field) => field?.toLowerCase().includes(query))
}
)
}, [students, searchQuery, departmentFilter, dreamJobFilter])

return true
})
}, [students, searchQuery, departmentFilter, dreamJobFilter, generationFilter])

// Sort students
const sortedStudents = useMemo(() => {
Expand Down Expand Up @@ -149,6 +131,9 @@ export default function Portfolio() {
dreamJobFilter={dreamJobFilter}
onDreamJobChange={setDreamJobFilter}
dreamJobs={dreamJobs}
generationFilter={generationFilter}
onGenerationChange={setGenerationFilter}
generations={generations}
sortBy={sortBy}
onSortChange={setSortBy}
viewMode={viewMode}
Expand Down
5 changes: 5 additions & 0 deletions src/features/portfolio/utils/generation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const getGenerationFromJoinAt = (joinAt: string | null | undefined): number | null => {
if (!joinAt) return null
const year = new Date(joinAt).getFullYear()
return year - 2020
}