diff --git a/src/actions/planner/list.ts b/src/actions/planner/list.ts index 477b65a..d6a3f26 100644 --- a/src/actions/planner/list.ts +++ b/src/actions/planner/list.ts @@ -4,7 +4,6 @@ export const RESET = `${BASE_STRING}RESET` as const; export const SET_SELECTED_LIST_CODE = `${BASE_STRING}SER_SELECTED_LIST_CODE` as const; export const SET_LIST_COURSES = `${BASE_STRING}SET_LIST_COURSES` as const; export const CLEAR_SEARCH_LIST_COURSES = `${BASE_STRING}CLEAR_SEARCH_LIST_COURSES` as const; -export const ADD_COURSE_READ = `${BASE_STRING}ADD_COURSE_READ` as const; import { CourseListCode } from '@/shapes/enum'; import Course from '@/shapes/model/subject/Course'; @@ -36,16 +35,8 @@ export function clearSearchListCourses() { }; } -export function addCourseRead(course: Course) { - return { - type: ADD_COURSE_READ, - course: course, - }; -} - export type ListAction = | ReturnType | ReturnType | ReturnType - | ReturnType - | ReturnType; + | ReturnType; diff --git a/src/reducers/planner/index.js b/src/reducers/planner/index.ts similarity index 83% rename from src/reducers/planner/index.js rename to src/reducers/planner/index.ts index d50e0ae..33f4abb 100644 --- a/src/reducers/planner/index.js +++ b/src/reducers/planner/index.ts @@ -11,4 +11,5 @@ const CombinedReducer = combineReducers({ search: search, }); +export type PlannerState = ReturnType; export default CombinedReducer; diff --git a/src/reducers/planner/itemFocus.js b/src/reducers/planner/itemFocus.js deleted file mode 100644 index b8cead9..0000000 --- a/src/reducers/planner/itemFocus.js +++ /dev/null @@ -1,80 +0,0 @@ -import { - RESET, - SET_ITEM_FOCUS, - CLEAR_ITEM_FOCUS, - SET_CATEGORY_FOCUS, - CLEAR_CATEGORY_FOCUS, - SET_REVIEWS, - SET_LECTURES, -} from '../../actions/planner/itemFocus'; - -import { ItemFocusFrom } from '@/shapes/enum'; - -const initialState = { - from: ItemFocusFrom.NONE, - clicked: false, - item: null, - course: null, - category: null, - reviews: null, - lectures: null, -}; - -const itemFocus = (state = initialState, action) => { - switch (action.type) { - case RESET: { - return initialState; - } - case SET_ITEM_FOCUS: { - const courseChanged = !state.course || state.course.id !== action.course.id; - return Object.assign( - {}, - state, - { - from: action.from, - clicked: action.clicked, - item: action.item, - course: action.course, - }, - courseChanged ? { reviews: null, lectures: null } : {}, - ); - } - case CLEAR_ITEM_FOCUS: { - return Object.assign({}, state, { - from: ItemFocusFrom.NONE, - clicked: false, - item: null, - course: null, - reviews: null, - lectures: null, - }); - } - case SET_CATEGORY_FOCUS: { - return Object.assign({}, state, { - from: ItemFocusFrom.CATEGORY, - category: action.category, - }); - } - case CLEAR_CATEGORY_FOCUS: { - return Object.assign({}, state, { - from: ItemFocusFrom.NONE, - category: null, - }); - } - case SET_REVIEWS: { - return Object.assign({}, state, { - reviews: action.reviews, - }); - } - case SET_LECTURES: { - return Object.assign({}, state, { - lectures: action.lectures, - }); - } - default: { - return state; - } - } -}; - -export default itemFocus; diff --git a/src/reducers/planner/itemFocus.ts b/src/reducers/planner/itemFocus.ts new file mode 100644 index 0000000..eafdbcd --- /dev/null +++ b/src/reducers/planner/itemFocus.ts @@ -0,0 +1,112 @@ +import { + RESET, + SET_ITEM_FOCUS, + CLEAR_ITEM_FOCUS, + SET_CATEGORY_FOCUS, + CLEAR_CATEGORY_FOCUS, + SET_REVIEWS, + SET_LECTURES, + ItemFocusAction, +} from '@/actions/planner/itemFocus'; +import { ItemFocusFrom } from '@/shapes/enum'; +import ItemFocus from '@/shapes/state/planner/ItemFocus'; + +const initialState: ItemFocus = { + from: ItemFocusFrom.NONE, + clicked: false, + item: null, + course: null, + category: null, + reviews: null, + lectures: null, +}; + +/* + 모든 ItemFocus 는 아래 5가지 중 하나의 타입을 가짐 + type ItemFocus = NoneItem | ListItem | AddingItem | TableItem | CategoryItem; + + CategoryItem 에 focusing 할 경우에는 SET_CATEGORY_FOCUS 액션을, + ListItem | AddingItem | TableItem 에 focusing 할 경우에는 SET_ITEM_FOCUS 액션을 사용함. + + (구체적인 사진은 notion과 PR 에 첨부된 사진을 참고.) + 1. NoneItem + 포커싱된 아이템이 없을 때. + 2. ListItem + Planner 테이블 아래의 course list 중 하나의 항목을 클릭했을 때 포커싱되는 상태를 나타냄. + 3. AddingItem + Planner 테이블에 아이템을 추가할 때 + 4. TableItem + Planner 테이블에서 각 항목을 클릭했을 때 포커싱되는 상태를 나타냄. + Future, Taken, Arbitrary 중하나임. + 5. CategoryItem + 플래너 우측의 기초/전공/교양 과목의 이수 상황을 보여주는 바에서 특정 카테고리를 호버하면 Focus, 언호버하면 Focus 해제됨. + [CategoryIndex, N, M] + CategoryIndex: 0, 1, 2, 3, 4 중 하나. + 순서대로 기초, 전공, 연구, 교양, 기타. + N: + 세부 전공을 의미. 전공/부전/복수/심화/자유 등을 가르킴. + M: + 0, 1 중 하나. 0이면 필수 과목, 1이면 선택 과목. + (ex. [0, 1, 0] => 기초 1번째 필수 과목) +*/ +const itemFocus = (state: ItemFocus = initialState, action: ItemFocusAction) => { + switch (action.type) { + case RESET: { + return initialState; + } + case SET_ITEM_FOCUS: { + const courseChanged = !state.course || state.course.id !== action.course.id; + const changedItem = courseChanged ? { reviews: null, lectures: null } : {}; + return { + ...state, + from: action.from, + clicked: action.clicked, + item: action.item, + course: action.course, + changedItem, + }; + } + case CLEAR_ITEM_FOCUS: { + return { + ...state, + from: ItemFocusFrom.NONE, + clicked: false, + item: null, + course: null, + reviews: null, + lectures: null, + }; + } + case SET_CATEGORY_FOCUS: { + return { + ...state, + from: ItemFocusFrom.CATEGORY, + category: action.category, + }; + } + case CLEAR_CATEGORY_FOCUS: { + return { + ...state, + from: ItemFocusFrom.NONE, + category: null, + }; + } + case SET_REVIEWS: { + return { + ...state, + reviews: action.reviews, + }; + } + case SET_LECTURES: { + return { + ...state, + lectures: action.lectures, + }; + } + default: { + return state; + } + } +}; + +export default itemFocus; diff --git a/src/reducers/planner/list.js b/src/reducers/planner/list.js deleted file mode 100644 index 67b0fa5..0000000 --- a/src/reducers/planner/list.js +++ /dev/null @@ -1,66 +0,0 @@ -import { - RESET, - SET_SELECTED_LIST_CODE, - SET_LIST_COURSES, - CLEAR_SEARCH_LIST_COURSES, - ADD_COURSE_READ, -} from '../../actions/planner/list'; - -import { CourseListCode } from '@/shapes/enum'; - -const initialState = { - selectedListCode: CourseListCode.SEARCH, - lists: { - [CourseListCode.SEARCH]: { - courses: [], - }, - [CourseListCode.BASIC]: { - courses: null, - }, - [CourseListCode.HUMANITY]: { - courses: null, - }, - [CourseListCode.TAKEN]: { - courses: null, - }, - }, - readCourses: [], -}; - -const list = (state = initialState, action) => { - switch (action.type) { - case RESET: { - return initialState; - } - case SET_SELECTED_LIST_CODE: { - return Object.assign({}, state, { - selectedListCode: action.listCode, - }); - } - case SET_LIST_COURSES: { - const newState = { ...state }; - newState.lists = { ...newState.lists }; - newState.lists[action.code] = { ...newState.lists[action.code] }; - newState.lists[action.code].courses = action.courses; - return Object.assign({}, state, newState); - } - case CLEAR_SEARCH_LIST_COURSES: { - const newState = { ...state }; - newState.lists = { ...newState.lists }; - newState.lists[CourseListCode.SEARCH] = { ...newState.lists[CourseListCode.SEARCH] }; - newState.lists[CourseListCode.SEARCH].courses = null; - return Object.assign({}, state, newState); - } - case ADD_COURSE_READ: { - const newState = { - readCourses: [...state.readCourses, action.course], - }; - return Object.assign({}, state, newState); - } - default: { - return state; - } - } -}; - -export default list; diff --git a/src/reducers/planner/list.ts b/src/reducers/planner/list.ts new file mode 100644 index 0000000..3b16782 --- /dev/null +++ b/src/reducers/planner/list.ts @@ -0,0 +1,82 @@ +import { + RESET, + SET_SELECTED_LIST_CODE, + SET_LIST_COURSES, + CLEAR_SEARCH_LIST_COURSES, + ListAction, +} from '../../actions/planner/list'; + +import { CourseListCode, DepartmentCode } from '@/shapes/enum'; +import Course from '@/shapes/model/subject/Course'; + +type CourseDepartmentLists = { + [K in CourseListCode]: { + courses: Course[] | null; + }; +} & { + [K in DepartmentCode]?: { + courses: Course[] | null; + }; +}; + +interface ListState { + selectedListCode: CourseListCode | DepartmentCode; + lists: CourseDepartmentLists; +} + +const initialState: ListState = { + selectedListCode: CourseListCode.SEARCH, + lists: { + [CourseListCode.SEARCH]: { + courses: [], + }, + [CourseListCode.BASIC]: { + courses: null, + }, + [CourseListCode.HUMANITY]: { + courses: null, + }, + [CourseListCode.TAKEN]: { + courses: null, + }, + }, +}; + +const list = (state = initialState, action: ListAction) => { + switch (action.type) { + case RESET: { + return initialState; + } + case SET_SELECTED_LIST_CODE: { + return { ...state, selectedListCode: action.listCode }; + } + case SET_LIST_COURSES: { + return { + ...state, + lists: { + ...state.lists, + [action.code]: { + ...state.lists[action.code], + courses: action.courses, + }, + }, + }; + } + case CLEAR_SEARCH_LIST_COURSES: + return { + ...state, + lists: { + ...state.lists, + [CourseListCode.SEARCH]: { + ...state.lists[CourseListCode.SEARCH], + courses: null, + }, + }, + }; + default: { + return state; + } + } +}; + +export default list; diff --git a/src/reducers/planner/planner.js b/src/reducers/planner/planner.ts similarity index 61% rename from src/reducers/planner/planner.js rename to src/reducers/planner/planner.ts index a49dee7..377f6e0 100644 --- a/src/reducers/planner/planner.js +++ b/src/reducers/planner/planner.ts @@ -12,9 +12,22 @@ import { REORDER_PLANNER, UPDATE_CELL_SIZE, SET_IS_TRACK_SETTINGS_SECTION_OPEN, -} from '../../actions/planner/planner'; + PlannerAction, +} from '@/actions/planner/planner'; +import Planner from '@/shapes/model/planner/Planner'; +import { PlannerItemType } from '@/shapes/enum'; -const initialState = { +interface PlannerState { + planners: Planner[] | null; + selectedPlanner: Planner | null; + cellWidth: number; + cellHeight: number; + isDragging: boolean; + isTrackSettingsSectionOpen: boolean; + isPlannerTabsOpenOnMobile: boolean; +} + +const initialState: PlannerState = { planners: null, selectedPlanner: null, cellWidth: 200, @@ -24,7 +37,7 @@ const initialState = { isPlannerTabsOpenOnMobile: false, }; -const getListNameOfType = (type) => { +const getListNameOfType = (type: PlannerItemType) => { switch (type) { case 'TAKEN': return 'taken_items'; @@ -33,52 +46,87 @@ const getListNameOfType = (type) => { case 'ARBITRARY': return 'arbitrary_items'; default: - return undefined; + throw new Error(`Unhandled planner item type: ${type}`); } }; -const planner = (state = initialState, action) => { +/** + 설명이 필요한 action 들에 대해서만 주석을 작성함. + SET_PLANNERS + 컴포넌트 마운트 시 실행되며, (a) user 가 없을 경우, []로 planners state 를 초기화함 + (b) user 가 있을 경우, 서버에서 user의 planner를 가져와서 state에 저장함. + ===> (a) 부분 삭제하고 planners initial state 를 null 에서 []로 변경하고자 함. + + UPDATE_PLANNER + planner 의 세부 정보, (입학연월 등) 을 수정하는 action + + ADD_ITEM_TO_PLANNER / UPDATE_ITEM_IN_PLANNER / REMOVE_ITEM_FROM_PLANNER + planner 에 과목 item 을 추가/수정/삭제 하는 action, + + REORDER_PLANNER + planner 들 간의 순서를 바꾸는 action, + planner list에 있는 planner 들의 "arrange_order" field 를 수정하고, "arrange_order" field 를 기준으로 정렬함. + + SET_IS_TRACK_SETTINGS_SECTION_OPEN + planner 관련 정보를 수정하는 팝업창이 열려있는지 여부를 변경하는 action +*/ +const planner = (state = initialState, action: PlannerAction) => { switch (action.type) { case RESET: { return initialState; } case SET_PLANNERS: { - return Object.assign({}, state, { + return { + ...state, planners: action.planners, - selectedPlanner: action.planners.length > 0 ? action.planners[0] : null, - }); + selectedPlanner: action.planners?.[0], + }; } case CLEAR_PLANNERS: { - return Object.assign({}, state, { - planners: null, + return { + ...state, + planners: [], selectedPlanner: null, - }); + }; } case SET_SELECTED_PLANNER: { - return Object.assign({}, state, { + return { + ...state, selectedPlanner: action.planner, - }); + }; } case CREATE_PLANNER: { - return Object.assign({}, state, { + if (!state.planners) { + return state; + } + return { + ...state, selectedPlanner: action.newPlanner, planners: [...state.planners, action.newPlanner], - }); + }; } case DELETE_PLANNER: { + if (!state.planners) { + return state; + } const indexOfPlanner = state.planners.findIndex((t) => t.id === action.planner.id); const newPlanners = state.planners.filter((t) => t.id !== action.planner.id); const newSelectedPlanner = indexOfPlanner !== state.planners.length - 1 ? newPlanners[indexOfPlanner] : newPlanners[indexOfPlanner - 1]; - return Object.assign({}, state, { + return { + ...state, selectedPlanner: newSelectedPlanner, planners: newPlanners, - }); + }; } case UPDATE_PLANNER: { - return Object.assign({}, state, { + if (!state.selectedPlanner || !state.planners) { + return state; + } + return { + ...state, selectedPlanner: state.selectedPlanner.id === action.updatedPlanner.id ? action.updatedPlanner @@ -86,21 +134,28 @@ const planner = (state = initialState, action) => { planners: state.planners.map((t) => t.id === action.updatedPlanner.id ? action.updatedPlanner : t, ), - }); + }; } case ADD_ITEM_TO_PLANNER: { + if (!state.selectedPlanner || !state.planners) { + return state; + } const listName = getListNameOfType(action.item.item_type); const newPlanner = { ...state.selectedPlanner, [listName]: state.selectedPlanner[listName].concat([action.item]), }; const newPlanners = state.planners.map((t) => (t.id === newPlanner.id ? newPlanner : t)); - return Object.assign({}, state, { + return { + ...state, selectedPlanner: newPlanner, planners: newPlanners, - }); + }; } case UPDATE_ITEM_IN_PLANNER: { + if (!state.selectedPlanner || !state.planners) { + return state; + } const listName = getListNameOfType(action.item.item_type); const newPlanner = { ...state.selectedPlanner, @@ -109,24 +164,32 @@ const planner = (state = initialState, action) => { ), }; const newPlanners = state.planners.map((t) => (t.id === newPlanner.id ? newPlanner : t)); - return Object.assign({}, state, { + return { + ...state, selectedPlanner: newPlanner, planners: newPlanners, - }); + }; } case REMOVE_ITEM_FROM_PLANNER: { + if (!state.selectedPlanner || !state.planners) { + return state; + } const listName = getListNameOfType(action.item.item_type); const newPlanner = { ...state.selectedPlanner, [listName]: state.selectedPlanner[listName].filter((i) => i.id !== action.item.id), }; const newPlanners = state.planners.map((t) => (t.id === newPlanner.id ? newPlanner : t)); - return Object.assign({}, state, { + return { + ...state, selectedPlanner: newPlanner, planners: newPlanners, - }); + }; } case REORDER_PLANNER: { + if (!state.planners) { + return state; + } const newPlanners = state.planners.map((t) => { if (t.id === action.planner.id) { return { @@ -154,24 +217,26 @@ const planner = (state = initialState, action) => { } return t; }); - newPlanners.sort((t1, t2) => t1.arrange_order - t2.arrange_order); - const updatedPlanner = newPlanners.find((t) => t.id === state.selectedPlanner.id); - return Object.assign({}, state, { + const updatedPlanner = newPlanners.find((t) => t.id === state.selectedPlanner?.id); + return { + ...state, planners: newPlanners, selectedPlanner: updatedPlanner, - }); + }; } case UPDATE_CELL_SIZE: { - return Object.assign({}, state, { + return { + ...state, cellWidth: action.width, cellHeight: action.height, - }); + }; } case SET_IS_TRACK_SETTINGS_SECTION_OPEN: { - return Object.assign({}, state, { + return { + ...state, isTrackSettingsSectionOpen: action.isTrackSettingsSectionOpen, - }); + }; } default: { return state; diff --git a/src/reducers/planner/search.js b/src/reducers/planner/search.ts similarity index 50% rename from src/reducers/planner/search.js rename to src/reducers/planner/search.ts index c7d10a1..24e308b 100644 --- a/src/reducers/planner/search.js +++ b/src/reducers/planner/search.ts @@ -3,35 +3,42 @@ import { OPEN_SEARCH, CLOSE_SEARCH, SET_LAST_SEARCH_OPTION, -} from '../../actions/planner/search'; + SearchAction, +} from '@/actions/planner/search'; +import LectureLastSearchOption from '@/shapes/state/timetable/LectureLastSearchOption'; -const initialState = { +interface SearchState { + open: boolean; + lastSearchOption: LectureLastSearchOption; +} + +const initialState: SearchState = { open: true, lastSearchOption: {}, }; -const search = (state = initialState, action) => { +const search = (state = initialState, action: SearchAction) => { switch (action.type) { case RESET: { return initialState; } case OPEN_SEARCH: { - return Object.assign({}, state, { + return { + ...state, open: true, - }); + }; } case CLOSE_SEARCH: { - return Object.assign({}, state, { + return { + ...state, open: false, - start: null, - end: null, - day: null, - }); + }; } case SET_LAST_SEARCH_OPTION: { - return Object.assign({}, state, { + return { + ...state, lastSearchOption: action.lastSearchOption, - }); + }; } default: { return state; diff --git a/src/shapes/enum.ts b/src/shapes/enum.ts index 06b28f9..df89218 100644 --- a/src/shapes/enum.ts +++ b/src/shapes/enum.ts @@ -77,3 +77,31 @@ export const enum Day { SAT, SUN, } + +/** Redux state 중에서 서버에서 course 또는 lecture를 fetch 해서 저장하는 + * planner.list , timetable.list , dictionary.list 에서 + 아래 enum 을 optional key 로 사용합니다.*/ +export const enum DepartmentCode { + ALL = 'ALL', + HSS = 'HSS', + CE = 'CE', + BTM = 'BTM', + ME = 'ME', + PH = 'PH', + BiS = 'BiS', + IE = 'IE', + ID = 'ID', + BS = 'BS', + MAS = 'MAS', + NQE = 'NQE', + EE = 'EE', + CS = 'CS', + AE = 'AE', + CH = 'CH', + CBE = 'CBE', + MS = 'MS', + TS = 'TS', + SS = 'SS', + BCS = 'BCS', + ETC = 'ETC', +} diff --git a/src/shapes/state/timetable/LectureFocus.ts b/src/shapes/state/timetable/LectureFocus.ts index bbfeabe..5460e3a 100644 --- a/src/shapes/state/timetable/LectureFocus.ts +++ b/src/shapes/state/timetable/LectureFocus.ts @@ -28,7 +28,7 @@ interface FromListOrTable { } interface FromMutliple { - from: LectureFocusFrom.TABLE; + from: LectureFocusFrom.MULTIPLE; clicked: false; lecture?: null; reviews?: null;