diff --git a/src/app/dashboard/core-applications/page.tsx b/src/app/dashboard/core-applications/page.tsx index 3356439..2c44153 100644 --- a/src/app/dashboard/core-applications/page.tsx +++ b/src/app/dashboard/core-applications/page.tsx @@ -4,6 +4,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react' import Link from 'next/link' import Loader from '@/components/ui/common/Loader' +import { formatMajorLabel } from '@/constant/majorOptions' import { useAuthenticatedApi } from '@/hooks/useAuthenticatedApi' type RecruitCoreResultStatus = 'SUBMITTED' | 'IN_REVIEW' | 'ACCEPTED' | 'REJECTED' @@ -341,7 +342,7 @@ export default function DashboardCoreApplicationsPage() { > {application.name} {application.studentId} - {application.major} + {formatMajorLabel(application.major)} {application.team} {application.resultStatus} {formatDateTime(application.createdAt)} @@ -410,7 +411,7 @@ export default function DashboardCoreApplicationsPage() {

이름: {detail.snapshot.name}

학번: {detail.snapshot.studentId}

-

전공: {detail.snapshot.major}

+

전공: {formatMajorLabel(detail.snapshot.major)}

전화번호: {detail.snapshot.phone}

이메일: {detail.snapshot.email}

지원 팀: {detail.team}

diff --git a/src/app/dashboard/members/page.tsx b/src/app/dashboard/members/page.tsx index 75a2249..21d4967 100644 --- a/src/app/dashboard/members/page.tsx +++ b/src/app/dashboard/members/page.tsx @@ -6,6 +6,7 @@ import Link from 'next/link' import Loader from '@/components/ui/common/Loader' import UserDetailsModal from '@/components/admin/UserDetailsModal' import { GdgSegmentedButton } from '@/components/ui/design-system' +import { formatMajorLabel } from '@/constant/majorOptions' import { useAuthenticatedApi } from '@/hooks/useAuthenticatedApi' import { formatPhoneNumberDisplay } from '@/utils/phoneNumber' @@ -264,7 +265,7 @@ export default function DashboardMembersPage() { members.map((member) => ( {member.name} - {member.major} + {formatMajorLabel(member.major)} {member.studentId} {formatPhoneNumberDisplay(member.phoneNumber)} diff --git a/src/app/dashboard/users/page.tsx b/src/app/dashboard/users/page.tsx index 65cba77..e4be22f 100644 --- a/src/app/dashboard/users/page.tsx +++ b/src/app/dashboard/users/page.tsx @@ -4,6 +4,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react' import Link from 'next/link' import Loader from '@/components/ui/common/Loader' +import { formatMajorLabel } from '@/constant/majorOptions' import { useAuth } from '@/hooks/useAuth' import { useAuthenticatedApi } from '@/hooks/useAuthenticatedApi' @@ -350,7 +351,7 @@ export default function DashboardUsersPage() { {user.id} {user.name} {user.email} - {user.major || '-'} + {formatMajorLabel(user.major)} {user.studentId || '-'} {editable ? ( diff --git a/src/app/recruit/core/page.tsx b/src/app/recruit/core/page.tsx index ddf8ed7..f7d42a1 100644 --- a/src/app/recruit/core/page.tsx +++ b/src/app/recruit/core/page.tsx @@ -15,6 +15,7 @@ import { GdgInputField } from '@/components/ui/design-system' import { PrivacyPolicyNotice } from '@/components/ui/common/PrivacyPolicyNotice' +import { normalizeMajorCode } from '@/constant/majorOptions' import { useAuthenticatedApi } from '@/hooks/useAuthenticatedApi' import { usePhoneNumber } from '@/hooks/usePhoneNumber' import { unwrapApiResponse } from '@/utils/api/unwrap' @@ -272,7 +273,7 @@ export default function RecruitCore() { name: prev.name || payload.name || '', studentId: prev.studentId || payload.studentId || '', email: prev.email || payload.email || '', - major: prev.major || payload.major || '', + major: prev.major || normalizeMajorCode(payload.major) || '', phone: formatInput(prev.phone || payload.phone || '') })) } catch (error: any) { @@ -439,7 +440,7 @@ export default function RecruitCore() { name: formData.name, studentId: formData.studentId, phone: toDigits(formData.phone), - major: formData.major, + major: normalizeMajorCode(formData.major), email: formData.email }, team: formData.team, diff --git a/src/components/admin/AdminDashboard.tsx b/src/components/admin/AdminDashboard.tsx index 6ee70e5..9c42770 100644 --- a/src/components/admin/AdminDashboard.tsx +++ b/src/components/admin/AdminDashboard.tsx @@ -1,6 +1,7 @@ 'use client'; import React, { useMemo } from 'react'; +import { formatMajorLabel } from '@/constant/majorOptions'; import { ResponsiveContainer, PieChart, @@ -21,7 +22,7 @@ function normalizeMembers(rawMembers = []) { return rawMembers.map((u) => ({ id: u?.id ?? u?.member?.id, name: u?.name ?? u?.member?.name ?? '', - major: u?.major ?? u?.member?.major ?? u?.member?.majors?.main ?? '기타', + major: formatMajorLabel(u?.major ?? u?.member?.major ?? u?.member?.majors?.main ?? '기타'), admissionSemester: u?.admissionSemester ?? u?.member?.admissionSemester ?? '미상', studentId: String(u?.studentId ?? u?.member?.studentId ?? ''), isPayed: typeof (u?.isPayed ?? u?.member?.isPayed) === 'boolean' ? (u?.isPayed ?? u?.member?.isPayed) : false, @@ -207,4 +208,3 @@ export default function AdminDashboard({ members = [], totalCount }) { ); } - diff --git a/src/components/admin/AdminTableCell.tsx b/src/components/admin/AdminTableCell.tsx index 8f10642..1e80945 100644 --- a/src/components/admin/AdminTableCell.tsx +++ b/src/components/admin/AdminTableCell.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { User, Chip, Checkbox } from '@nextui-org/react'; +import { formatMajorLabel } from '@/constant/majorOptions'; const statusColorMap = { true: 'success', @@ -28,7 +29,7 @@ export default function AdminTableCell({ user, columnKey, onTogglePay }) { case 'major': return (
-

{user.major}

+

{formatMajorLabel(user.major)}

{user.studentId}

); @@ -54,4 +55,3 @@ export default function AdminTableCell({ user, columnKey, onTogglePay }) { } } - diff --git a/src/components/admin/UserDetailsModal.tsx b/src/components/admin/UserDetailsModal.tsx index 5787e14..6e50032 100644 --- a/src/components/admin/UserDetailsModal.tsx +++ b/src/components/admin/UserDetailsModal.tsx @@ -3,6 +3,7 @@ import React, { useEffect } from 'react'; import { Button } from '@nextui-org/react'; import clsx from 'clsx'; +import { formatMajorLabel } from '@/constant/majorOptions'; import { formatPhoneNumberDisplay } from '@/utils/phoneNumber'; const FIELD_LABELS = { @@ -79,6 +80,9 @@ const formatFieldValue = (key, value) => { if (key === 'isPayed' && typeof value === 'boolean') { return value ? '입금 완료' : '미입금'; } + if (key === 'major' && typeof value === 'string') { + return formatMajorLabel(value); + } return formatValue(value); }; diff --git a/src/components/auth/screen/AuthFindId.jsx b/src/components/auth/screen/AuthFindId.jsx index 7f1315d..9ebdc55 100644 --- a/src/components/auth/screen/AuthFindId.jsx +++ b/src/components/auth/screen/AuthFindId.jsx @@ -8,7 +8,7 @@ import axios from 'axios'; import TransparentInput from '@/components/ui/input/TransparentInput'; import { usePhoneNumber } from '@/hooks/usePhoneNumber'; -import { majorOptions } from '@/constant/majorOptions'; +import { majorOptions, normalizeMajorCode } from '@/constant/majorOptions'; export default function AuthFindId({ handleBackToLogin }) { const [name, setName] = useState(''); @@ -92,14 +92,14 @@ export default function AuthFindId({ handleBackToLogin }) { base: 'bg-[#1c1c1c] text-white', }, }} - selectedKeys={major} - onSelectionChange={setMajor} + selectedKeys={normalizeMajorCode(major)} + onSelectionChange={(value) => setMajor(String(value ?? ''))} > {majorOptions.map((major) => ( {major.items.map((item) => ( - - {item.value} + + {item.label} ))} diff --git a/src/components/study/ui/card/ApplicantInfoList.jsx b/src/components/study/ui/card/ApplicantInfoList.jsx index 906c44e..34693ba 100644 --- a/src/components/study/ui/card/ApplicantInfoList.jsx +++ b/src/components/study/ui/card/ApplicantInfoList.jsx @@ -2,6 +2,7 @@ // components import GreenTextButton from "@/components/ui/button/GreenTextButton"; +import { formatMajorLabel } from '@/constant/majorOptions'; export default function ApplicantInfoList({ applications, @@ -57,7 +58,7 @@ export default function ApplicantInfoList({ onClick={() => handleApplicantDetailPopup(app.id)} > {app.name} - {app.major} + {formatMajorLabel(app.major)} {app.studentId} {/* 이미 처리된 지원자가 있으면 상태 뱃지 표시 */} @@ -104,4 +105,4 @@ export default function ApplicantInfoList({
); -} \ No newline at end of file +} diff --git a/src/components/study/ui/modal/ApplicantDetailModal.jsx b/src/components/study/ui/modal/ApplicantDetailModal.jsx index f5bc167..558bbe7 100644 --- a/src/components/study/ui/modal/ApplicantDetailModal.jsx +++ b/src/components/study/ui/modal/ApplicantDetailModal.jsx @@ -2,6 +2,7 @@ import { useEffect } from 'react'; import { Spinner } from "@nextui-org/react"; +import { formatMajorLabel } from '@/constant/majorOptions'; // API Services import { useApplicantDetail } from '@/services/study/useApplicantDetail'; @@ -58,7 +59,7 @@ export default function ApplicantDetailModal({ apiClient, studyId, selectedAppli 학번 - {applicantDetail.major} + {formatMajorLabel(applicantDetail.major)} {applicantDetail.studentId} @@ -108,4 +109,4 @@ export default function ApplicantDetailModal({ apiClient, studyId, selectedAppli ) ); -} \ No newline at end of file +} diff --git a/src/components/ui/design-system/GdgMajorDropdown.tsx b/src/components/ui/design-system/GdgMajorDropdown.tsx index ef73a9f..3f20937 100644 --- a/src/components/ui/design-system/GdgMajorDropdown.tsx +++ b/src/components/ui/design-system/GdgMajorDropdown.tsx @@ -1,6 +1,6 @@ 'use client' -import { majorOptions } from '@/constant/majorOptions' +import { majorOptions, normalizeMajorCode } from '@/constant/majorOptions' import { GdgDropdown, type GdgDropdownOptionGroup } from './GdgDropdown' export type GdgMajorDropdownProps = { @@ -23,8 +23,8 @@ export function GdgMajorDropdown({ const groupedOptions: GdgDropdownOptionGroup[] = majorOptions.map((group) => ({ title: group.title, items: group.items.map((item) => ({ - id: item.value, - label: item.value + id: item.code, + label: item.label })) })) @@ -34,7 +34,7 @@ export function GdgMajorDropdown({ size="full" placeholder="학과를 입력해 주세요." optionGroups={groupedOptions} - value={value} + value={normalizeMajorCode(value)} onChange={onChangeAction} autoFocus={autoFocus} isInvalid={isInvalid ?? Boolean(errorMessage)} diff --git a/src/constant/majorOptions.ts b/src/constant/majorOptions.ts index 8411c3a..3f98996 100644 --- a/src/constant/majorOptions.ts +++ b/src/constant/majorOptions.ts @@ -1,139 +1,198 @@ -export const majorOptions = [ - { - title: '소프트웨어융합대학', - items: [ - { key: '인공지능공학과', value: '인공지능공학과' }, - { key: '데이터사이언스학과', value: '데이터사이언스학과' }, - { key: '스마트모빌리티공학과', value: '스마트모빌리티공학과' }, - { key: '디자인테크놀로지학과', value: '디자인테크놀로지학과' }, - { key: '컴퓨터공학과', value: '컴퓨터공학과' }, - ], - }, - { - title: '프런티어창의대학', - items: [ - { key: '자유전공융합학부', value: '자유전공융합학부' }, - { key: '공학융합학부', value: '공학융합학부' }, - { key: '자연과학융합학부', value: '자연과학융합학부' }, - { key: '경영융합학부', value: '경영융합학부' }, - { key: '사회과학융합학부', value: '사회과학융합학부' }, - { key: '인문융합학부', value: '인문융합학부' }, - ], - }, - { - title: '바이오시스템융합학부', - items: [ - { key: '생명공학과', value: '생명공학과' }, - { key: '생명과학과', value: '생명과학과' }, - { key: '첨단바이오의약학과', value: '첨단바이오의약학과' }, - ], - }, - { - title: '공과대학', - items: [ - { key: '기계공학과', value: '기계공학과' }, - { key: '항공우주공학과', value: '항공우주공학과' }, - { key: '조선해양공학과', value: '조선해양공학과' }, - { key: '산업경영공학과', value: '산업경영공학과' }, - { key: '화학공학과', value: '화학공학과' }, - { key: '고분자공학과', value: '고분자공학과' }, - { key: '신소재공학과', value: '신소재공학과' }, - { key: '사회인프라공학과', value: '사회인프라공학과' }, - { key: '환경공학과', value: '환경공학과' }, - { key: '공간정보공학과', value: '공간정보공학과' }, - { key: '건축공학전공', value: '건축공학전공' }, - { key: '건축학전공(5년제)', value: '건축학전공(5년제)' }, - { key: '에너지자원공학과', value: '에너지자원공학과' }, - { key: '전기전자공학부', value: '전기전자공학부' }, - { key: '반도체시스템공학과', value: '반도체시스템공학과' }, - { key: '이차전지융합학과', value: '이차전지융합학과' }, - ], - }, - { - title: '자연과학대학', - items: [ - { key: '수학과', value: '수학과' }, - { key: '통계학과', value: '통계학과' }, - { key: '물리학과', value: '물리학과' }, - { key: '화학과', value: '화학과' }, - { key: '해양과학과', value: '해양과학과' }, - { key: '식품영양학과', value: '식품영양학과' }, - ], - }, - { - title: '경영대학', - items: [ - { key: '경영학과', value: '경영학과' }, - { key: '파이낸스경영학과', value: '파이낸스경영학과' }, - { key: '아태물류학부', value: '아태물류학부' }, - { key: '국제통상학과', value: '국제통상학과' }, - ], - }, - { - title: '사범대학', - items: [ - { key: '국어교육과', value: '국어교육과' }, - { key: '영어교육과', value: '영어교육과' }, - { key: '사회교육과', value: '사회교육과' }, - { key: '체육교육과', value: '체육교육과' }, - { key: '교육학과', value: '교육학과' }, - { key: '수학교육과', value: '수학교육과' }, - ], - }, - { - title: '사회과학대학', - items: [ - { key: '행정학과', value: '행정학과' }, - { key: '정치외교학과', value: '정치외교학과' }, - { key: '경제학과', value: '경제학과' }, - { key: '소비자학과', value: '소비자학과' }, - { key: '아동심리학과', value: '아동심리학과' }, - { key: '사회복지학과', value: '사회복지학과' }, - { key: '미디어커뮤니케이션학과', value: '미디어커뮤니케이션학과' }, - ], - }, - { - title: '문과대학', - items: [ - { key: '한국어문학과', value: '한국어문학과' }, - { key: '사학과', value: '사학과' }, - { key: '철학과', value: '철학과' }, - { key: '중국학과', value: '중국학과' }, - { key: '일본언어문화학과', value: '일본언어문화학과' }, - { key: '영미유럽인문융합학부', value: '영미유럽인문융합학부' }, - { key: '문화콘텐츠문화경영학과', value: '문화콘텐츠문화경영학과' }, - ], - }, - { - title: '의과대학', - items: [ - { key: '의예과', value: '의예과' }, - ], - }, - { - title: '간호대학', - items: [ - { key: '간호학과', value: '간호학과' }, - ], - }, - { - title: '예술체육대학', - items: [ - { key: '조형예술학과', value: '조형예술학과' }, - { key: '스포츠과학과', value: '스포츠과학과' }, - { key: '의류디자인학과', value: '의류디자인학과' }, - { key: '디자인융합학과', value: '디자인융합학과' }, - { key: '연극영화학과', value: '연극영화학과' }, - ], - }, - { - title: '미래융합대학', - items: [ - { key: '메카트로닉스공학과', value: '메카트로닉스공학과' }, - { key: '산업경영학과', value: '산업경영학과' }, - { key: '반도체산업융합학과', value: '반도체산업융합학과' }, - { key: '소프트웨어융합공학과', value: '소프트웨어융합공학과' }, - { key: '금융투자학과', value: '금융투자학과' }, - ], - }, - ]; \ No newline at end of file +export type MajorOptionItem = { + code: string + label: string +} + +export type MajorOptionGroup = { + title: string + items: MajorOptionItem[] +} + +export const majorOptions: MajorOptionGroup[] = [ + { + title: '소프트웨어융합대학', + items: [ + { code: 'AIE', label: '인공지능공학과' }, + { code: 'DSE', label: '데이터사이언스학과' }, + { code: 'SME', label: '스마트모빌리티공학과' }, + { code: 'DTE', label: '디자인테크놀로지학과' }, + { code: 'CSE', label: '컴퓨터공학과' } + ] + }, + { + title: '공과대학', + items: [ + { code: 'ME', label: '기계공학과' }, + { code: 'AAE', label: '항공우주공학과' }, + { code: 'NAE', label: '조선해양공학과' }, + { code: 'IME', label: '산업경영공학과' }, + { code: 'CHE', label: '화학공학과' }, + { code: 'PSE', label: '고분자공학과' }, + { code: 'MSE', label: '신소재공학과' }, + { code: 'CIE', label: '사회인프라공학과' }, + { code: 'ENVE', label: '환경공학과' }, + { code: 'GIE', label: '공간정보공학과' }, + { code: 'ACE', label: '건축학부(건축공학)' }, + { code: 'ARCH', label: '건축학부(건축학)' }, + { code: 'ERE', label: '에너지자원공학과' }, + { code: 'MOT', label: '융합기술경영학부' }, + { code: 'EEE', label: '전기전자공학부' }, + { code: 'SSE', label: '반도체시스템공학과' }, + { code: 'BCE', label: '이차전지융합학과' } + ] + }, + { + title: '자연과학대학', + items: [ + { code: 'MATH', label: '수학과' }, + { code: 'STAT', label: '통계학과' }, + { code: 'PHYS', label: '물리학과' }, + { code: 'CHEM', label: '화학과' }, + { code: 'OCS', label: '해양과학과' }, + { code: 'FNS', label: '식품영양학과' } + ] + }, + { + title: '경영대학', + items: [ + { code: 'BUS', label: '경영학부(경영학과)' }, + { code: 'FIN', label: '경영학부(파이낸스경영학과)' }, + { code: 'APL', label: '아태물류학부' }, + { code: 'ITC', label: '국제통상학과' } + ] + }, + { + title: '예술체육대학', + items: [ + { code: 'FINEART', label: '조형예술학과' }, + { code: 'ID', label: '디자인융합학과' }, + { code: 'SPORTS', label: '스포츠과학과' }, + { code: 'TFA', label: '연극영화학과' }, + { code: 'FD', label: '의류디자인학과' } + ] + }, + { + title: '사회과학대학', + items: [ + { code: 'PAD', label: '행정학과' }, + { code: 'POL', label: '정치외교학과' }, + { code: 'MCS', label: '미디어커뮤니케이션학과' }, + { code: 'ECON', label: '경제학과' }, + { code: 'CONS', label: '소비자학과' }, + { code: 'CPSY', label: '아동심리학과' }, + { code: 'SW', label: '사회복지학과' } + ] + }, + { + title: '프런티어창의대학', + items: [ + { code: 'ULS', label: '자유전공융합학부' }, + { code: 'ECS', label: '공학융합학부' }, + { code: 'NCS', label: '자연과학융합학부' }, + { code: 'BCONV', label: '경영융합학부' }, + { code: 'SCS', label: '사회과학융합학부' }, + { code: 'HCS', label: '인문융합학부' } + ] + }, + { + title: '문과대학', + items: [ + { code: 'KLL', label: '한국어문학과' }, + { code: 'HIST', label: '사학과' }, + { code: 'PHIL', label: '철학과' }, + { code: 'CHIN', label: '중국학과' }, + { code: 'JLC', label: '일본언어문화학과' }, + { code: 'ELH', label: '영미유럽인문융합학부' }, + { code: 'CCM', label: '문화콘텐츠문화경영학과' } + ] + }, + { + title: '미래융합대학', + items: [ + { code: 'MTE', label: '메카트로닉스공학과' }, + { code: 'SWE', label: '소프트웨어융합공학과' }, + { code: 'IMGT', label: '산업경영학과' }, + { code: 'FI', label: '금융투자학과' } + ] + }, + { + title: '바이오시스템융합학부', + items: [ + { code: 'BIOE', label: '생명공학과' }, + { code: 'BPE', label: '바이오제약공학과' }, + { code: 'BIOS', label: '생명과학과' }, + { code: 'ABM', label: '첨단바이오의약학과' }, + { code: 'BFE', label: '바이오식품공학과' } + ] + }, + { + title: '국제학부', + items: [ + { code: 'IBT', label: 'IBT학과' }, + { code: 'ISE', label: 'ISE학과' }, + { code: 'KLC', label: 'KLC학과' } + ] + }, + { + title: '의과대학', + items: [ + { code: 'PREMED', label: '의예과' }, + { code: 'MED', label: '의학과' } + ] + }, + { + title: '간호대학', + items: [{ code: 'NURS', label: '간호학과' }] + }, + { + title: '사범대학', + items: [ + { code: 'KOR_EDU', label: '국어교육과' }, + { code: 'ENG_EDU', label: '영어교육과' }, + { code: 'SOC_EDU', label: '사회교육과' }, + { code: 'EDU', label: '교육학과' }, + { code: 'PE_EDU', label: '체육교육과' }, + { code: 'MATH_EDU', label: '수학교육과' } + ] + } +] + +const allMajorItems = majorOptions.flatMap((group) => group.items) + +export const majorLabelByCode = Object.freeze( + allMajorItems.reduce>((acc, item) => { + acc[item.code] = item.label + return acc + }, {}) +) + +const majorCodeByLabel = allMajorItems.reduce>((acc, item) => { + acc[item.label] = item.code + return acc +}, {}) + +const majorAliasToCode: Record = { + ...majorCodeByLabel, + 스마트모빌리티놀학과: 'SME', + 건축공학전공: 'ACE', + '건축학전공(5년제)': 'ARCH', + 경영학과: 'BUS', + 파이낸스경영학과: 'FIN', + '파이낸스경영학부(경영학과)': 'FIN', + '문화콘텐츠문화경영학부(경영학과)': 'CCM', + 문화컨텐츠경영학과: 'CCM' +} + +export const normalizeMajorCode = (value?: string | null): string => { + if (!value) return '' + const trimmed = value.trim() + if (!trimmed) return '' + if (majorLabelByCode[trimmed]) return trimmed + return majorAliasToCode[trimmed] ?? trimmed +} + +export const formatMajorLabel = (value?: string | null): string => { + if (!value) return '-' + const normalized = normalizeMajorCode(value) + return majorLabelByCode[normalized] ?? value +}