diff --git a/adminPage/components/dashboard/Dashboard.tsx b/adminPage/components/dashboard/Dashboard.tsx index 48ceda3..5c9a042 100644 --- a/adminPage/components/dashboard/Dashboard.tsx +++ b/adminPage/components/dashboard/Dashboard.tsx @@ -70,6 +70,11 @@ const boxes = ({ translateLabel }): Array => [ title: translateLabel("notices"), href: "/admin/resources/notices", }, + { + variant: "Folders", + title: translateLabel("linktrees"), + href: "/admin/resources/linktrees", + }, ]; const Card = styled(Box)` diff --git a/adminPage/components/index.ts b/adminPage/components/index.ts index cb1638d..fc7250b 100644 --- a/adminPage/components/index.ts +++ b/adminPage/components/index.ts @@ -21,5 +21,6 @@ export const Components = { notice_list: add('NoticeList', './notice/NoticeList.tsx'), notice_show: add('NoticeShow', './notice/NoticeShow.tsx'), notice_edit: add('NoticeEdit', './notice/NoticeEdit.tsx'), + linktree_edit: add('LinktreeEdit', './linktree/LinktreeEdit.tsx'), // custom: add('Custom', './custom/Custom.tsx') } diff --git a/adminPage/components/linktree/LinktreeEdit.tsx b/adminPage/components/linktree/LinktreeEdit.tsx new file mode 100644 index 0000000..30c9b62 --- /dev/null +++ b/adminPage/components/linktree/LinktreeEdit.tsx @@ -0,0 +1,87 @@ +import { Box, Button, DrawerContent, DrawerFooter, Icon } from "@adminjs/design-system"; +import { + ActionProps, + BasePropertyComponent, + RecordJSON, + useCurrentAdmin, + useRecord, + useTranslation +} from "adminjs"; +import React, { FC, useEffect } from "react"; +import { useNavigate } from "react-router"; + +const appendForceRefresh = (url: string, search?: string): string => { + const searchParamsIdx = url.lastIndexOf("?"); + const urlSearchParams = searchParamsIdx !== -1 ? url.substring(searchParamsIdx + 1) : null; + + const oldParams = new URLSearchParams(search ?? urlSearchParams ?? window.location.search ?? ""); + const shouldIgnoreOldParams = new URLSearchParams(urlSearchParams || "").get("ignore_params") === "true"; + const newParams = shouldIgnoreOldParams ? new URLSearchParams("") : new URLSearchParams(oldParams.toString()); + + newParams.set("refresh", "true"); + + const newUrl = searchParamsIdx !== -1 ? url.substring(0, searchParamsIdx) : url; + + return `${newUrl}?${newParams.toString()}`; +}; + +const Edit: FC = (props) => { + const { record: initialRecord, resource, action } = props; + const [currentAdmin, setCurrentAdmin] = useCurrentAdmin(); + const {role} = currentAdmin as any + const { record, handleChange, submit: handleSubmit, loading, setRecord } = useRecord(initialRecord, resource.id); + const { translateButton } = useTranslation(); + const navigate = useNavigate(); + const getActionElementCss = (resourceId: string, actionName: string, suffix: string) => `${resourceId}-${actionName}-${suffix}` + // const value = record.params?.[property.path] + // const error = record.errors && record.errors[property.path] + // console.log(props) + useEffect(() => { + if (initialRecord) { + setRecord(initialRecord); + } + }, [initialRecord]); + const majorProp = resource.editProperties.find(p => p.name == 'major'); + if (majorProp && majorProp.availableValues) { + majorProp.availableValues = majorProp.availableValues.filter(p => p.label == role) + } + console.log(resource.editProperties, {majorProp}); + const submit = (event: React.FormEvent): boolean => { + event.preventDefault(); + handleSubmit().then((response) => { + if (response.data.redirectUrl) { + navigate(appendForceRefresh(response.data.redirectUrl)); + } + }); + return false; + }; + + const contentTag = getActionElementCss(resource.id, action.name, "drawer-content"); + const formTag = getActionElementCss(resource.id, action.name, "form"); + const footerTag = getActionElementCss(resource.id, action.name, "drawer-footer"); + const buttonTag = getActionElementCss(resource.id, action.name, "drawer-submit"); + return ( + + + {resource.editProperties.map((property) => ( + + ))} + + + + + + ); +}; + +export default Edit; diff --git a/adminPage/handlers/linktree.ts b/adminPage/handlers/linktree.ts new file mode 100644 index 0000000..876d6b6 --- /dev/null +++ b/adminPage/handlers/linktree.ts @@ -0,0 +1,73 @@ +import { ActionHandler, Filter, SortSetter, flat, populator } from "adminjs"; + +const list: ActionHandler = async (request, response, context) => { + const { query } = request; // 요청 url의 query 부분 추출 + // console.log(query); + + const { resource, _admin, currentAdmin } = context; // db table + const { role } = currentAdmin; + const unflattenQuery = flat.unflatten( + query || {} + ); + let { page, perPage } = unflattenQuery; + // 진행중인 행사 탭에서는 시작일 내림차순 정렬 + // 종료된 행사 탭에서는 종료일 내림차순 정렬 + const { + sortBy = "order", + direction = "asc", + filters = { major: role }, + } = unflattenQuery; + + // adminOptions.settings.defaultPerPage, 한 페이지에 몇 행 보여줄 지 + if (perPage) { + perPage = +perPage > 500 ? 500 : +perPage; + } else { + perPage = _admin.options.settings?.defaultPerPage ?? 10; + } + page = +page || 1; + + // resource(DB table)에서 어떤 데이터를 가져올 지 filter 생성 + const listProperties = resource.decorate().getListProperties(); + const firstProperty = listProperties.find((p) => p.isSortable()); + let sort; + if (firstProperty) { + sort = SortSetter( + { sortBy, direction }, + firstProperty.name(), + resource.decorate().options + ); + } + const filter = await new Filter( + { ...filters }, + resource + ).populate(context); + const records = await resource.find( + filter, + { + limit: perPage, + offset: (page - 1) * perPage, + sort, + }, + context + ); + + const populatedRecords = await populator(records, context); + context.records = populatedRecords; + + // 메타데이터 및 가져온 데이터 return + const total = await resource.count(filter, context); + return { + meta: { + total, + perPage, + page, + direction: sort?.direction, + sortBy: sort?.sortBy, + }, + records: populatedRecords.map((r) => r.toJSON(currentAdmin)), + }; +}; + +export const LinktreeHandler = { + list, + } \ No newline at end of file diff --git a/adminPage/index.ts b/adminPage/index.ts index dd45074..0cae8ed 100644 --- a/adminPage/index.ts +++ b/adminPage/index.ts @@ -8,6 +8,8 @@ import { ADMIN } from './resources/admin.js'; import { COMMON, TEST } from "./resources/common.js"; import { EVENT } from "./resources/event.js"; import { NOTICE } from "./resources/notice.js"; +import Linktree from "../models/linktree.js"; +import { LINKTREE } from "./resources/linktree.js"; const authenticate = async (payload, context) => { const {email, role} = payload; @@ -69,6 +71,7 @@ export const adminOptions: AdminJSOptions = { labels: { events: '행사', notices: '공지', + linktrees: '링크트리', navigation: '', }, } @@ -92,6 +95,7 @@ export const adminOptions: AdminJSOptions = { // post { resource: Event, ...EVENT}, { resource: Notice, ...NOTICE}, + { resource: Linktree, ...LINKTREE}, // others { resource: Admin, ...ADMIN}, { resource: Read, ...COMMON}, diff --git a/adminPage/resources/common.ts b/adminPage/resources/common.ts index e4972fb..bf96cec 100644 --- a/adminPage/resources/common.ts +++ b/adminPage/resources/common.ts @@ -3,7 +3,11 @@ export const postTab = { icon: 'Edit', // icon name list: https://feathericons.com/ }; - +export const linktreeTab = { + name: '링크트리 관리', + icon: 'Edit', + // icon name list: https://feathericons.com/ +}; export const COMMON = { /** * @returns options: { navigation: false } diff --git a/adminPage/resources/linktree.ts b/adminPage/resources/linktree.ts new file mode 100644 index 0000000..7fb4073 --- /dev/null +++ b/adminPage/resources/linktree.ts @@ -0,0 +1,41 @@ +import { ResourceOptions } from "adminjs"; +import { linktreeTab } from "./common.js"; +import { Components } from "../components/index.js"; +import { LinktreeHandler } from "../handlers/linktree.js"; + +const linktreeOptions: ResourceOptions = { + navigation: linktreeTab, + + listProperties: ["order", "text", "src"], + showProperties: ["major", "text", "src", "order"], + editProperties: ["major", "text", "src", "order"], + filterProperties: ["major", "text", "src", "order"], + + properties: { + content: { + type: "richtext", + }, + major_advisor: { + isRequired: true, + }, + image: { + isArray: true, + }, + }, + actions: { + new: { + component: Components.linktree_edit, + }, + edit: { + component: Components.linktree_edit, + }, + list: { + handler: LinktreeHandler.list + } + } + }; + +export const LINKTREE = { + options: linktreeOptions, + }; + \ No newline at end of file diff --git a/controllers/common_method/utils.ts b/controllers/common_method/utils.ts index c287477..4d83f53 100644 --- a/controllers/common_method/utils.ts +++ b/controllers/common_method/utils.ts @@ -8,5 +8,5 @@ import { redisClient } from "../../redis/connect.js"; export const redisGetAndParse = async (key: string) => { const get = await redisClient.get(key); if (get) return JSON.parse(get); - throw new Error(`${key}: 캐싱된 데이터 없음`); + throw new Error(`${key.split(':')[1]}: 캐싱된 데이터 없음`); } \ No newline at end of file diff --git a/controllers/linktree/index.ts b/controllers/linktree/index.ts new file mode 100644 index 0000000..59b9469 --- /dev/null +++ b/controllers/linktree/index.ts @@ -0,0 +1,5 @@ +import { getLinktrees } from "./linktree.js"; + +export { + getLinktrees +} \ No newline at end of file diff --git a/controllers/linktree/linktree.ts b/controllers/linktree/linktree.ts new file mode 100644 index 0000000..71e941e --- /dev/null +++ b/controllers/linktree/linktree.ts @@ -0,0 +1,19 @@ +import { NextFunction, Request, Response } from "express"; +import { redisGetAndParse } from "../common_method/utils.js"; + +// GET /events +export const getLinktrees = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + const major = req.params.major_advisor; + if (!major) throw new Error('올바르지 않은 학과') + const linktrees = await redisGetAndParse(`linktrees:${major}`); + return res.status(200).json(linktrees); + } catch (error) { + console.error(error); + res.status(500).json({ error: error.message }); + } +} \ No newline at end of file diff --git a/index.ts b/index.ts index 01baec4..e591294 100644 --- a/index.ts +++ b/index.ts @@ -9,11 +9,8 @@ import * as url from "url"; import { adminOptions, authProvider } from "./adminPage/index.js"; import { sequelize } from "./models/index.js"; import { initializeRedis } from "./redis/initialize.js"; -import alarmRouter from "./routes/alarm.js"; -import eventRouter from "./routes/event.js"; -import noticeRouter from "./routes/notice.js"; -import userRouter from "./routes/user.js"; import { isRunOnDist, mode } from "./adminPage/components/index.js"; +import { alarmRouter, eventRouter, linktreeRouter, noticeRouter, userRouter } from "./routes/index.js"; const port = 7070; const corsOptions = { @@ -66,6 +63,7 @@ const start = async () => { app.use("/api", userRouter); app.use("/api", eventRouter); app.use("/api", alarmRouter); + app.use("/api", linktreeRouter); await sequelize .authenticate() .then(async () => { diff --git a/models/admin.ts b/models/admin.ts index cc0f48d..a0656f3 100644 --- a/models/admin.ts +++ b/models/admin.ts @@ -39,7 +39,7 @@ Admin.init( { sequelize, // assuming you have a Sequelize instance named 'sequelize' modelName: 'Admin', - tableName: 'admin', + tableName: 'admins', timestamps: false, } ); diff --git a/models/linktree.ts b/models/linktree.ts new file mode 100644 index 0000000..fcfa583 --- /dev/null +++ b/models/linktree.ts @@ -0,0 +1,53 @@ +import { DataTypes, Model } from 'sequelize'; +import { sequelize } from './sequelize.js'; + +interface LinktreeAttributes { + id: number; + src: string; + text: string; + major: '컴퓨터' | '소프트'; + order: number; +} + +class Linktree extends Model implements LinktreeAttributes { + public id!: number; + public src!: string; + public text!: string; + public major!: '컴퓨터' | '소프트'; + public order!: number; +} + +Linktree.init( + { + id: { + type: DataTypes.INTEGER.UNSIGNED, + primaryKey: true, + autoIncrement: true, + allowNull: false, + }, + src: { + type: DataTypes.STRING(200), + allowNull: false, + }, + text: { + type: DataTypes.STRING(100), + allowNull: false, + }, + major: { + type: DataTypes.ENUM('컴퓨터', '소프트'), + allowNull: false, + }, + order: { + type: DataTypes.INTEGER({unsigned: true}), + allowNull: false + } + }, + { + sequelize, + modelName: 'Linktree', + tableName: 'linktrees', + timestamps: false, + } +); + +export default Linktree; diff --git a/redis/caching.ts b/redis/caching.ts index eb001e7..2ab6ce7 100644 --- a/redis/caching.ts +++ b/redis/caching.ts @@ -1,9 +1,29 @@ import Event from "../models/events.js"; +import Linktree from "../models/linktree.js"; import Notice from "../models/notice.js"; import { IEvent, INotice } from "../models/types.js"; import { redisClient } from "./connect.js"; import { getNextDay, setNoticeSchedule } from "./schedule.js"; +export const initAllLinktrees = async () => { + const redisKey = "linktrees"; + const eachLinks = await redisClient.keys(`linktrees:*`); + const [computer, soft] = (await Linktree.findAll()).reduce( + (acc, val: Linktree) => { + acc[+(val.major == "소프트")].push(val); + return acc; + }, + [[], []] + ); + if (eachLinks.length) { + await redisClient.del(eachLinks); + } + await redisClient.set(`${redisKey}:컴퓨터`, JSON.stringify(computer)); + await redisClient.set(`${redisKey}:소프트`, JSON.stringify(soft)); + console.log("컴터 linktree 리스트:", computer.map(c => c.dataValues)); + console.log("소프트 linktree 리스트:", soft.map(s => s.dataValues)); +}; + /** 행사 글 전부 캐싱 ** 전체 행사글 redisKey: allEvents ** 개별 행사글 redisKey: event:id @@ -53,17 +73,23 @@ export const initAllOngoingNotices = async () => { if (noticeNextDay > currentDate.getTime()) { setNoticeSchedule(notice); } else { - notice.update({ ...notice, priority: "일반" }) + notice.update({ ...notice, priority: "일반" }); } // 개별 공지 캐싱 await redisClient.set(redisKey, JSON.stringify(notice)); } - console.log('진행중인 긴급공지:', urgent.map(n => n.id)) + console.log( + "진행중인 긴급공지:", + urgent.map((n) => n.id) + ); for (const notice of general as INotice[]) { const redisKey = `notice:${notice.id}`; await redisClient.set(redisKey, JSON.stringify(notice)); } - console.log('진행중인 일반공지:', general.map(n => n.id)) + console.log( + "진행중인 일반공지:", + general.map((n) => n.id) + ); await cachingAllNotices(urgent, general); }; @@ -71,20 +97,25 @@ export const cachingAllNotices = async (urgent?, general?) => { if (!urgent && !general) [urgent, general] = await getAllNotices(); await redisClient.set(`alerts:urgent`, JSON.stringify(urgent)); await redisClient.set(`alerts:general`, JSON.stringify(general)); -} +}; /** * @returns [urgent, general] */ const getAllNotices = async () => { // 전체 긴급 공지 목록 캐싱 - const [urgent, general] = (await Notice.findAll({ - where: { - expired: false, // 활성화된 공지만 가져오기 - } - })).reduce((acc, val: INotice) => { - acc[+(val.priority == '일반')].push(val); - return acc; - }, [[], []]); + const [urgent, general] = ( + await Notice.findAll({ + where: { + expired: false, // 활성화된 공지만 가져오기 + }, + }) + ).reduce( + (acc, val: INotice) => { + acc[+(val.priority == "일반")].push(val); + return acc; + }, + [[], []] + ); return [urgent, general] as [INotice[], INotice[]]; -} \ No newline at end of file +}; diff --git a/redis/initialize.ts b/redis/initialize.ts index 9ad8d22..668eb84 100644 --- a/redis/initialize.ts +++ b/redis/initialize.ts @@ -1,8 +1,9 @@ -import { initAllOngoingEvents, initAllOngoingNotices } from "./caching.js"; +import { initAllLinktrees, initAllOngoingEvents, initAllOngoingNotices } from "./caching.js"; import { connectRedis } from "./connect.js"; export const initializeRedis = async () => { await connectRedis(); await initAllOngoingEvents(); await initAllOngoingNotices(); + await initAllLinktrees(); } diff --git a/routes/index.ts b/routes/index.ts index e69de29..9bca626 100644 --- a/routes/index.ts +++ b/routes/index.ts @@ -0,0 +1,13 @@ +import alarmRouter from "./alarm.js"; +import eventRouter from "./event.js"; +import linktreeRouter from "./linktree.js"; +import noticeRouter from "./notice.js"; +import userRouter from "./user.js"; + +export { + noticeRouter, + userRouter, + eventRouter, + alarmRouter, + linktreeRouter +} \ No newline at end of file diff --git a/routes/linktree.ts b/routes/linktree.ts new file mode 100644 index 0000000..183a7bd --- /dev/null +++ b/routes/linktree.ts @@ -0,0 +1,9 @@ +import { Router } from "express"; +import { getLinktrees } from "../controllers/linktree/index.js"; + +const linktreeRouter = Router(); + +// ex) /api/linktree/컴퓨터 +linktreeRouter.get('/linktree/:major_advisor', getLinktrees); + +export default linktreeRouter; \ No newline at end of file