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
4 changes: 2 additions & 2 deletions apps/frontend/app/(client)/signup/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ export default function SignUpLayout({
style={{ backgroundImage: "url('/signup/background.png')" }}
/>
<div className="relative z-10 flex w-full flex-1">
<section className="relative hidden flex-1 lg:block">
<div className="fixed left-[254px] top-[414px] flex w-[260px] flex-col items-center gap-[15px]">
<section className="relative hidden flex-1 overflow-hidden lg:flex lg:items-start lg:justify-center">
<div className="sticky top-[414px] flex w-full max-w-[260px] flex-col items-center gap-[15px] px-4">
<Image
src={codedangLogoWhite}
alt="Codedang"
Expand Down
109 changes: 109 additions & 0 deletions apps/frontend/components/MajorSearchInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
'use client'

import { allMajors } from '@/libs/constants'
import { cn } from '@/libs/utils'
import { useState } from 'react'
import { IoSearchOutline } from 'react-icons/io5'

const getKoreanMajorName = (major: string) =>
major
.split(/\s*\/\s*/)
.at(-1)
?.trim() ?? major

interface MajorSearchInputProps {
value: string
onChange: (value: string) => void
placeholder?: string
disabled?: boolean
error?: string
inputClassName?: string
}

export function MajorSearchInput({
value,
onChange,
placeholder = '학과 검색',
disabled,
error,
inputClassName
}: MajorSearchInputProps) {
const [query, setQuery] = useState(value)
const [open, setOpen] = useState(false)

const filtered =
query.length > 0
? allMajors.filter((m) =>
getKoreanMajorName(m).toLowerCase().includes(query.toLowerCase())
)
: []

return (
<div
className="relative flex flex-col gap-1"
onBlur={(e) => {
if (!e.currentTarget.contains(e.relatedTarget as Node)) {
setOpen(false)
}
}}
tabIndex={-1}
>
<div className="relative">
<input
placeholder={placeholder}
value={query}
disabled={disabled}
onChange={(e) => {
setQuery(e.target.value)
setOpen(true)
if (value) {
onChange('')
}
}}
onFocus={() => setOpen(true)}
className={cn(
'focus:border-primary border-line h-[46px] w-full rounded-xl border bg-white px-5 py-[11px] pr-11 outline-none',
error && 'border-error focus:border-error',
inputClassName
)}
/>
<IoSearchOutline
className="text-color-neutral-30 absolute right-4 top-1/2 -translate-y-1/2"
size={18}
/>
</div>
{open && query.length > 0 && (
<ul className="border-line absolute top-[54px] z-10 max-h-[200px] w-full overflow-y-auto rounded-xl border bg-white shadow-md">
{filtered.length > 0 ? (
filtered.map((major) => {
const displayName = getKoreanMajorName(major)
return (
<li
key={major}
tabIndex={0}
onMouseDown={(e) => e.preventDefault()}
onClick={() => {
onChange(displayName)
setQuery(displayName)
setOpen(false)
}}
className={cn(
'text-body1_m_16 cursor-pointer px-5 py-[13px] hover:bg-gray-50',
value === displayName && 'bg-gray-50'
)}
>
{displayName}
</li>
)
})
) : (
<li className="text-body1_m_16 text-color-neutral-70 px-5 py-[13px]">
검색 결과가 없습니다
</li>
)}
</ul>
)}
{error && <p className="text-caption3_r_13 text-color-red-50">{error}</p>}
</div>
)
}
138 changes: 138 additions & 0 deletions apps/frontend/components/UniversitySearchInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
'use client'

import { CAMPUS_OVERRIDES, REGION_SHORT } from '@/libs/constants'
import { cn } from '@/libs/utils'
import { searchUniversities } from 'korea-universities'
import { useState } from 'react'
import { IoSearchOutline } from 'react-icons/io5'

type University = ReturnType<typeof searchUniversities>[number]

interface UniversitySearchInputProps {
value: string
onChange: (value: string) => void
placeholder?: string
disabled?: boolean
excludeNames?: string[]
error?: string
inputClassName?: string
}

const getDisplayName = (uni: University, allResults: University[]): string => {
const hasDuplicate =
allResults.filter((u) => u.nameKr === uni.nameKr).length > 1
if (!hasDuplicate) {
return uni.nameKr
}
const override = CAMPUS_OVERRIDES[uni.nameKr]?.[uni.campus ?? '']
if (override) {
return `${uni.nameKr} ${override}`
}
const sameRegion =
allResults.filter((u) => u.nameKr === uni.nameKr && u.region === uni.region)
.length > 1
if (sameRegion) {
return `${uni.nameKr} ${uni.campus}`
}
const short = REGION_SHORT[uni.region] ?? uni.region
return `${uni.nameKr} ${short}캠퍼스`
}

export function UniversitySearchInput({
value,
onChange,
placeholder = '대학교 검색',
disabled,
excludeNames = [],
error,
inputClassName
}: UniversitySearchInputProps) {
const [query, setQuery] = useState(value)
const [open, setOpen] = useState(false)

const filtered =
query.length > 0
? searchUniversities(query).filter((uni, idx, arr) => {
if (excludeNames.includes(uni.nameKr)) {
return false
}
if (CAMPUS_OVERRIDES[uni.nameKr]) {
return true
}
return (
arr.findIndex(
(u) => u.nameKr === uni.nameKr && u.region === uni.region
) === idx
)
})
: []

return (
<div
className="relative flex flex-col gap-1"
onBlur={(e) => {
if (!e.currentTarget.contains(e.relatedTarget as Node)) {
setOpen(false)
}
}}
tabIndex={-1}
>
<div className="relative">
<input
placeholder={placeholder}
value={query}
disabled={disabled}
onChange={(e) => {
setQuery(e.target.value)
setOpen(true)
if (value) {
onChange('')
}
}}
onFocus={() => setOpen(true)}
className={cn(
'focus:border-primary border-line h-[46px] w-full rounded-xl border bg-white px-5 py-[11px] pr-11 outline-none',
error && 'border-error focus:border-error',
inputClassName
)}
/>
<IoSearchOutline
className="text-color-neutral-30 absolute right-4 top-1/2 -translate-y-1/2"
size={18}
/>
</div>
{open && query.length > 0 && (
<ul className="border-line absolute top-[54px] z-10 max-h-[200px] w-full overflow-y-auto rounded-xl border bg-white shadow-md">
{filtered.length > 0 ? (
filtered.map((uni) => {
const displayName = getDisplayName(uni, filtered)
return (
<li
key={uni.id}
tabIndex={0}
onMouseDown={(e) => e.preventDefault()}
onClick={() => {
onChange(displayName)
setQuery(displayName)
setOpen(false)
}}
className={cn(
'text-body1_m_16 cursor-pointer px-5 py-[13px] hover:bg-gray-50',
value === displayName && 'bg-gray-50'
)}
>
{displayName}
</li>
)
})
) : (
<li className="text-body1_m_16 text-color-neutral-70 px-5 py-[13px]">
검색 결과가 없습니다
</li>
)}
</ul>
)}
{error && <p className="text-caption3_r_13 text-color-red-50">{error}</p>}
</div>
)
}
6 changes: 6 additions & 0 deletions apps/frontend/components/auth/HeaderAuthPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ interface HeaderAuthUser {
role: string
studentId: string
major: string
jobType: string
college: string
canCreateCourse: boolean
canCreateContest: boolean
}
Expand Down Expand Up @@ -68,8 +70,12 @@ export function HeaderAuthPanel({
.get('user')
.json()

const isSKKUStudent =
user.jobType === 'CollegeStudent' &&
user.college?.includes('성균관대학교')
const isUserInfoIncomplete =
user.role === 'User' &&
isSKKUStudent &&
(user.studentId === '0000000000' || user.major === 'none')

setIsUserInfoIncomplete(isUserInfoIncomplete)
Expand Down
Loading
Loading