Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
swgvenghy committed Nov 5, 2024
2 parents bc2ae38 + d8fcce9 commit 78c5e11
Show file tree
Hide file tree
Showing 18 changed files with 945 additions and 79 deletions.
433 changes: 412 additions & 21 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
"private": true,
"dependencies": {
"@ant-design/icons": "^5.3.7",
"@radix-ui/react-dropdown-menu": "^2.1.2",
"@radix-ui/react-icons": "^1.3.1",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
Expand Down
23 changes: 9 additions & 14 deletions src/api/admin/admin-admission-type-detail.query.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,13 @@
import { AdmissionDetailTypeState } from '../../store/admin/admission-detail-type-store';
import { server_axiosInstance } from '../../utils/axios';
import { getCookie } from '../../utils/cookies';
import { TypeStatusProps } from './question-type-status/change-type-status';

export interface AdmissionTypeDetailProps extends TypeStatusProps {
detailTypeId: number;
detailTypeName: string;
}

export const updateAdmissionTypeDetail = async ({ detailTypeId, detailTypeName }: AdmissionTypeDetailProps) => {
export const updateAdmissionTypeDetail = async ({ id, name }: AdmissionDetailTypeState) => {
try {
const response = server_axiosInstance.put(
`/api/admin/admissions/${detailTypeId}`,
`/api/admin/admissions/${id}`,
{
detailTypeName,
name,
},
{
headers: {
Expand All @@ -26,9 +21,9 @@ export const updateAdmissionTypeDetail = async ({ detailTypeId, detailTypeName }
}
};

export const deleteAdmissionTypeDetail = async ({ detailTypeId }: AdmissionTypeDetailProps) => {
export const deleteAdmissionTypeDetail = async (id: number) => {
try {
const response = server_axiosInstance.delete(`/api/admin/admission/${detailTypeId}`, {
const response = server_axiosInstance.delete(`/api/admin/admissions/${id}`, {
headers: {
Authorization: `Bearer ${getCookie('accessToken')}`,
},
Expand All @@ -39,12 +34,12 @@ export const deleteAdmissionTypeDetail = async ({ detailTypeId }: AdmissionTypeD
}
};

export const generateAdmissionTypeDetail = async ({ type, detailTypeName }: AdmissionTypeDetailProps) => {
export const generateAdmissionTypeDetail = async ({ type, name }: AdmissionDetailTypeState) => {
try {
const response = server_axiosInstance.put(
const response = server_axiosInstance.post(
`/api/admin/admissions/detail`,
{
detail: detailTypeName,
detail: name,
type: type,
},
{
Expand Down
2 changes: 1 addition & 1 deletion src/api/get-admission-detail-type.query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const getAllDetailType = async () => {

export const getDetailType = async ({ type }: TypeStatusProps) => {
try {
const response = await server_axiosInstance.get(`/api/admissions/detail/${type}`);
const response = await server_axiosInstance.get(`/api/admissions/details/${type}`);
return response.data;
} catch (error: any) {
throw new Error('get all detail type fail', error);
Expand Down
3 changes: 2 additions & 1 deletion src/hooks/use-chat-section.hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import useTypeStore, { TypeCategoryState } from '../store/type-category-store';
const useChatSection = () => {
const { setSelectedType, setSelectedCategory } = useTypeStore();
const [selectedTypeButton, setSelectedTypeButton] = React.useState<TypeCategoryState['type']>(undefined);
const [selectedCategoryButton, setSelectedCategoryButton] = React.useState<TypeCategoryState['category']>(undefined);
const [selectedCategoryButton, setSelectedCategoryButton] =
React.useState<TypeCategoryState['category']>('ADMISSION_GUIDELINE');
const handleTypeButtonClick = (selectedType: TypeCategoryState['type']) => {
setSelectedType(selectedType);
setSelectedTypeButton(selectedType);
Expand Down
35 changes: 35 additions & 0 deletions src/hooks/use-detail-type-prompt.hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { useUserDetailTypeStore } from '../store/user-detail-type-store';

interface DetailTypeItem {
middleName: string;
lastNames: string[];
}

export const useDetailTypePrompt = () => {
const { detailTypeData } = useUserDetailTypeStore();

const itemMap = new Map<string, Set<string>>();

detailTypeData.forEach((item) => {
const [middle, lastWithBracket] = item.name.split('(');
const middleTrimmed = middle.trim();
const lastTrimmed = lastWithBracket ? lastWithBracket.replace(/\)$/, '').trim() : '';

if (!itemMap.has(middleTrimmed)) {
itemMap.set(middleTrimmed, new Set<string>());
}

if (lastTrimmed) {
itemMap.get(middleTrimmed)?.add(lastTrimmed);
}
});

const itemList: DetailTypeItem[] = Array.from(itemMap.entries()).map(([middleName, lastNamesSet]) => ({
middleName,
lastNames: Array.from(lastNamesSet),
}));

return {
itemList,
};
};
2 changes: 2 additions & 0 deletions src/routes/admin-routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ import { getCookie } from '../utils/cookies';
import QuestionCheck from '../ui/pages/admin/question-check';
import GenerateQuestion from '../ui/pages/admin/generate-question';
import TypeDisabled from '../ui/pages/admin/type-disabled';
import DetailTypeCheck from '../ui/pages/admin/detail-type-check/detail-type-check';
const AdminRoutes: React.FC = () => {
const token = getCookie('accessToken');
return (
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/" element={<PrivateRoute component={<AdminLayout />} authenticated={token} />}>
<Route path="setting/file" element={<FileList />} />
<Route path="setting/detail/type" element={<DetailTypeCheck />} />
<Route path="question/list" element={<QuestionCheck />} />
<Route path="generate/question" element={<GenerateQuestion />} />
<Route path="setting/type" element={<TypeDisabled />} />
Expand Down
28 changes: 28 additions & 0 deletions src/store/admin/admission-detail-type-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { create } from 'zustand';

export interface AdmissionDetailTypeState {
type?: 'SUSI' | 'PYEONIP' | 'JEONGSI';
id?: number;
name?: string;
}

interface AdmissionDetailTypeStoreState {
detailTypeData: AdmissionDetailTypeState[];
updateDetailTypeData: (data: AdmissionDetailTypeState[]) => void;
deleteDetailType: (detailTypeId: number) => void;
loading: boolean;
setLoading: (load: boolean) => void;
}

const useAdmissionDetailTypeStore = create<AdmissionDetailTypeStoreState>((set) => ({
detailTypeData: [],
updateDetailTypeData: (data) => set({ detailTypeData: data }),
deleteDetailType: (id) =>
set((state) => ({
detailTypeData: state.detailTypeData.filter((data) => data.id !== id),
})),
loading: false,
setLoading: (load) => set({ loading: load }),
}));

export default useAdmissionDetailTypeStore;
4 changes: 2 additions & 2 deletions src/store/type-category-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import { create } from 'zustand';

export interface TypeCategoryState {
type: undefined | 'SUSI' | 'PYEONIP' | 'JEONGSI';
category: undefined | 'ADMISSION_GUIDELINE' | 'PASSING_RESULT' | 'PAST_QUESTIONS' | 'INTERVIEW_PRACTICAL_TEST';
category: 'ADMISSION_GUIDELINE' | 'PASSING_RESULT' | 'PAST_QUESTIONS' | 'INTERVIEW_PRACTICAL_TEST';
setSelectedType: (button: TypeCategoryState['type']) => void;
setSelectedCategory: (button: TypeCategoryState['category']) => void;
}

const useTypeStore = create<TypeCategoryState>()((set) => ({
type: undefined,
category: undefined,
category: 'ADMISSION_GUIDELINE',
setSelectedType: (button) => set({ type: button }),
setSelectedCategory: (button) => set({ category: button }),
}));
Expand Down
34 changes: 34 additions & 0 deletions src/store/user-detail-type-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { create } from 'zustand';
export interface DetailTypeItem {
middleName: string;
lastNames: string[];
}

//TSK-53 admission-detail-type-store.ts와 일부 겹침 나중에 병합 필요성
export interface UserDetailTypeState {
type: 'SUSI' | 'JEONGSI' | 'PYEONIP';
id: number;
name: string;
}

interface UserDetailTypeStoreState {
detailTypeData: UserDetailTypeState[];
updateDetailTypeData: (data: UserDetailTypeState[]) => void;
loading: boolean;
selectedName: string;
setSelectedName: (name: string) => void;
setLoading: (load: boolean) => void;
itemsArray: DetailTypeItem[];
setItemArray: (dataArray: DetailTypeItem[]) => void;
}

export const useUserDetailTypeStore = create<UserDetailTypeStoreState>((set) => ({
detailTypeData: [],
loading: false,
itemsArray: [],
selectedName: '',
setSelectedName: (name) => set({ selectedName: name }),
updateDetailTypeData: (data) => set({ detailTypeData: data }),
setLoading: (load) => set({ loading: load }),
setItemArray: (data) => set({ itemsArray: data }),
}));
2 changes: 2 additions & 0 deletions src/ui/components/admin/slide-menu/slide-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export default function SlideMenu(props: MenuProps) {
{ key: '/admin/generate/question', label: '질문답변 생성' },
{ type: 'divider' },
{ key: '/admin/setting/type', label: '전형 활성화 선택' },
{ type: 'divider' },
{ key: '/admin/setting/detail/type', label: '세부 전형 설정' },
];
}, []);

Expand Down
4 changes: 3 additions & 1 deletion src/ui/components/atom/chat-card/chat-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import Loader from '../loader/loader';
interface ChatCardProps {
content: string;
role: 'user' | 'system';
children?: React.ReactNode;
}
const ChatCard = ({ content, role }: ChatCardProps) => {
const ChatCard = ({ content, role, children }: ChatCardProps) => {
return (
<div>
{role === 'user' ? null : <img src={maru} className="mt-2 h-8 w-8" />}
Expand All @@ -26,6 +27,7 @@ const ChatCard = ({ content, role }: ChatCardProps) => {
>
<div className="text-md max-w-full font-pretendard font-normal">
{content === 'loading' ? <Loader /> : <ReactMarkdown remarkPlugins={[remarkGfm]}>{content}</ReactMarkdown>}
{children}
</div>
</div>
</div>
Expand Down
67 changes: 67 additions & 0 deletions src/ui/components/atom/dropdown/dropdown.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import React from 'react';
import { Meta, StoryFn } from '@storybook/react';
import Dropdown from './dropdown';

export default {
title: 'Components/Dropdown',
component: Dropdown,
argTypes: {
type: {
control: { type: 'select' },
options: ['SUSI', 'JEONGSI', 'PYEONIP'],
},
},
} as Meta<typeof Dropdown>;

const Template: StoryFn<typeof Dropdown> = (args) => <Dropdown {...args} />;

export const SUSI = Template.bind({});
SUSI.args = {
type: 'SUSI',
items: [
{
middleName: '학생부교과',
lastNames: ['학교장추천', '교과면접', '기회균형', '특성화고교', '만학도', '특성화고등졸재직자', '특수교육대상자'],
},
{
middleName: '학생부종합',
lastNames: ['명지인재면접', '명지인재서류', '크리스천리더', '사회적배려대상자', '농어촌학생'],
},
{
middleName: '실기/실적',
lastNames: ['실기우수자', '특기자-문학/체육'],
},
],
};

export const JEONGSI = Template.bind({});
JEONGSI.args = {
type: 'JEONGSI',
items: [
{
middleName: '수능',
lastNames: ['일반-가/나/다', '실기-가/나/다', '농어촌학생', '특성화고교'],
},
{
middleName: '실기/실적',
lastNames: ['실기우수자'],
},
{
middleName: '학생부교과',
lastNames: ['만학도', '특성화고등졸재직자'],
},
],
};

export const PYEONIP = Template.bind({});
PYEONIP.args = {
type: 'PYEONIP',
items: [
{ middleName: '일반', lastNames: [] },
{ middleName: '학사', lastNames: [] },
{ middleName: '농어촌학생', lastNames: [] },
{ middleName: '특성화고교', lastNames: [] },
{ middleName: '재외국민', lastNames: [] },
{ middleName: '특성화고등졸재직자', lastNames: [] },
],
};
69 changes: 69 additions & 0 deletions src/ui/components/atom/dropdown/dropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import React from 'react';
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
import { ChevronRightIcon } from '@radix-ui/react-icons';
import { useDetailTypePrompt } from '../../../../hooks/use-detail-type-prompt.hooks';
import { useUserDetailTypeStore } from '../../../../store/user-detail-type-store';
import usePresetButton from '../../../../hooks/use-preset-button.hooks';

interface DropdownProps {
type: 'SUSI' | 'JEONGSI' | 'PYEONIP';
items: { middleName: string; lastNames: string[] }[];
}

const Dropdown: React.FC<DropdownProps> = ({ type }) => {
const { itemList: items } = useDetailTypePrompt();
const { setSelectedName } = useUserDetailTypeStore();
const { handleDetailTypeButtonClick } = usePresetButton();

const handleNameClick = (name: string) => {
setSelectedName(name);
handleDetailTypeButtonClick(name);
};

return (
<DropdownMenu.Root open>
<DropdownMenu.Trigger asChild>
<button style={{ display: 'hidden' }}></button>
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content className="rounded-md border-gray-300 bg-white" align="start">
{type === 'PYEONIP'
? // 편입의 경우, 한 번에 전체 목록 표시
items.map((item, index) => (
<DropdownMenu.Item
key={index}
className="cursor-pointer px-4 py-2 hover:bg-gray-100"
onClick={() => handleNameClick(item.middleName)}
>
{item.middleName}
</DropdownMenu.Item>
))
: // 수시, 정시의 경우, 중간 이름을 상위 메뉴로, 마지막 이름을 하위 메뉴로 표시
items.map((item, index) => (
<DropdownMenu.Sub key={index}>
<DropdownMenu.SubTrigger className="flex cursor-pointer items-center justify-between px-4 py-2 hover:bg-gray-100">
{item.middleName}
<ChevronRightIcon />
</DropdownMenu.SubTrigger>
<DropdownMenu.Portal>
<DropdownMenu.SubContent className="rounded-md border border-gray-300 bg-white shadow-lg">
{item.lastNames.map((lastName, subIndex) => (
<DropdownMenu.Item
key={subIndex}
className="cursor-pointer px-4 py-2 hover:bg-gray-100"
onClick={() => handleNameClick(lastName)}
>
{lastName}
</DropdownMenu.Item>
))}
</DropdownMenu.SubContent>
</DropdownMenu.Portal>
</DropdownMenu.Sub>
))}
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
);
};

export default Dropdown;
Loading

0 comments on commit 78c5e11

Please sign in to comment.