From 80467a5fb0f71cb79f3eae028a5d3ea2a474aaff Mon Sep 17 00:00:00 2001 From: Seokyun Ha Date: Mon, 11 Dec 2023 16:33:38 +0900 Subject: [PATCH] Feat: Notice (#62) --- .../affiliate}/affiliate.cards.jsx | 1 - .../{ => benefit/discount}/discount.cards.jsx | 1 - components/board/board.menubar.jsx | 5 + components/board/notice/notice.table.jsx | 63 +++++++ .../whitebook.create.modal.jsx | 0 .../board/{ => whitebook}/whitebook.table.jsx | 0 .../whitebook.update.modal.jsx | 2 +- package-lock.json | 95 +++++++++- package.json | 1 + pages/board/notice/create.jsx | 130 ++++++++++++++ pages/board/notice/index.jsx | 42 +++++ pages/board/notice/update/[id].jsx | 165 ++++++++++++++++++ pages/board/whitebook.jsx | 4 +- 13 files changed, 498 insertions(+), 11 deletions(-) rename components/board/{ => benefit/affiliate}/affiliate.cards.jsx (98%) rename components/board/{ => benefit/discount}/discount.cards.jsx (98%) create mode 100644 components/board/notice/notice.table.jsx rename components/board/{ => whitebook}/whitebook.create.modal.jsx (100%) rename components/board/{ => whitebook}/whitebook.table.jsx (100%) rename components/board/{ => whitebook}/whitebook.update.modal.jsx (97%) create mode 100644 pages/board/notice/create.jsx create mode 100644 pages/board/notice/index.jsx create mode 100644 pages/board/notice/update/[id].jsx diff --git a/components/board/affiliate.cards.jsx b/components/board/benefit/affiliate/affiliate.cards.jsx similarity index 98% rename from components/board/affiliate.cards.jsx rename to components/board/benefit/affiliate/affiliate.cards.jsx index 4f74615..f0b4567 100644 --- a/components/board/affiliate.cards.jsx +++ b/components/board/benefit/affiliate/affiliate.cards.jsx @@ -1,5 +1,4 @@ import React, { useState } from 'react' -import moment from 'moment' import { Card, Modal, Grid, Header } from 'semantic-ui-react' const AffiliateCards = ({affiliates}) => { diff --git a/components/board/discount.cards.jsx b/components/board/benefit/discount/discount.cards.jsx similarity index 98% rename from components/board/discount.cards.jsx rename to components/board/benefit/discount/discount.cards.jsx index f200123..fa843c9 100644 --- a/components/board/discount.cards.jsx +++ b/components/board/benefit/discount/discount.cards.jsx @@ -1,5 +1,4 @@ import React, { useState } from 'react' -import moment from 'moment' import { Card, Modal, Grid, Header, Divider, Icon } from 'semantic-ui-react' const DiscountOfferCards = ({discountOffers}) => { diff --git a/components/board/board.menubar.jsx b/components/board/board.menubar.jsx index 41001ee..12975cf 100644 --- a/components/board/board.menubar.jsx +++ b/components/board/board.menubar.jsx @@ -11,6 +11,11 @@ export default class BoardMenubar extends Component { POPO 설정값 + + + 공지사항 + + RC 사생 명단 업로드 diff --git a/components/board/notice/notice.table.jsx b/components/board/notice/notice.table.jsx new file mode 100644 index 0000000..50c185c --- /dev/null +++ b/components/board/notice/notice.table.jsx @@ -0,0 +1,63 @@ +import Link from 'next/link' +import moment from 'moment' +import { Table } from 'semantic-ui-react' + +const NoticeTable + = ({notices}) => { + return ( + + + + id. + 제목 + 내용 + {/* 이미지 */} + 게시 일자 + 클릭수 + + + + { + notices.map((notice, idx) => { + const isActive = moment().isBetween(moment(notice.start_datetime), moment(notice.end_datetime)); + const duration = moment(notice.end_datetime).diff(moment(notice.start_datetime), 'hours'); + return ( + + + {notice.id} + + { + notice.link ? ( + + {notice.title} + + ) : notice.title + } + + + {notice.content} + + {/* + + */} + + {moment(notice.start_datetime).format('YYYY-MM-DD HH:mm')} ~ {moment(notice.end_datetime).format('YYYY-MM-DD HH:mm')}
+ ({Number(duration/24).toFixed(0)}일 {duration%24}시간) +
+ + {notice.click_count} + +
+ + ) + }) + } +
+
+ ) +} + +export default NoticeTable diff --git a/components/board/whitebook.create.modal.jsx b/components/board/whitebook/whitebook.create.modal.jsx similarity index 100% rename from components/board/whitebook.create.modal.jsx rename to components/board/whitebook/whitebook.create.modal.jsx diff --git a/components/board/whitebook.table.jsx b/components/board/whitebook/whitebook.table.jsx similarity index 100% rename from components/board/whitebook.table.jsx rename to components/board/whitebook/whitebook.table.jsx diff --git a/components/board/whitebook.update.modal.jsx b/components/board/whitebook/whitebook.update.modal.jsx similarity index 97% rename from components/board/whitebook.update.modal.jsx rename to components/board/whitebook/whitebook.update.modal.jsx index 49e9527..452a2cd 100644 --- a/components/board/whitebook.update.modal.jsx +++ b/components/board/whitebook/whitebook.update.modal.jsx @@ -1,6 +1,6 @@ import { Button, Form, Icon, Modal } from 'semantic-ui-react' import { useState } from 'react' -import DeleteConfirmModal from '../common/delete.confirm.modal' +import DeleteConfirmModal from '../../common/delete.confirm.modal' import { PoPoAxios } from '@/utils/axios.instance'; const WhitebookUpdateModal = ({ trigger, whitebook }) => { diff --git a/package-lock.json b/package-lock.json index 369412f..8544ec4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "moment": "^2.29.4", "next": "^12.3.4", "react": "^17.0.2", + "react-datepicker": "^4.24.0", "react-dom": "17.0.2", "react-responsive": "^9.0.2", "semantic-ui-css": "^2.5.0", @@ -595,9 +596,9 @@ } }, "node_modules/@popperjs/core": { - "version": "2.11.6", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz", - "integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==", + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", "funding": { "type": "opencollective", "url": "https://opencollective.com/popperjs" @@ -1144,6 +1145,11 @@ } ] }, + "node_modules/classnames": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz", + "integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==" + }, "node_modules/clsx": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz", @@ -1298,6 +1304,21 @@ "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", "dev": true }, + "node_modules/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -3702,6 +3723,23 @@ "node": ">=0.10.0" } }, + "node_modules/react-datepicker": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-4.24.0.tgz", + "integrity": "sha512-2QUC2pP+x4v3Jp06gnFllxKsJR0yoT/K6y86ItxEsveTXUpsx+NBkChWXjU0JsGx/PL8EQnsxN0wHl4zdA1m/g==", + "dependencies": { + "@popperjs/core": "^2.11.8", + "classnames": "^2.2.6", + "date-fns": "^2.30.0", + "prop-types": "^15.7.2", + "react-onclickoutside": "^6.13.0", + "react-popper": "^2.3.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17 || ^18", + "react-dom": "^16.9.0 || ^17 || ^18" + } + }, "node_modules/react-dom": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", @@ -3730,6 +3768,19 @@ "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" }, + "node_modules/react-onclickoutside": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.13.0.tgz", + "integrity": "sha512-ty8So6tcUpIb+ZE+1HAhbLROvAIJYyJe/1vRrrcmW+jLsaM+/powDRqxzo6hSh9CuRZGSL1Q8mvcF5WRD93a0A==", + "funding": { + "type": "individual", + "url": "https://github.com/Pomax/react-onclickoutside/blob/master/FUNDING.md" + }, + "peerDependencies": { + "react": "^15.5.x || ^16.x || ^17.x || ^18.x", + "react-dom": "^15.5.x || ^16.x || ^17.x || ^18.x" + } + }, "node_modules/react-popper": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.3.0.tgz", @@ -4967,9 +5018,9 @@ } }, "@popperjs/core": { - "version": "2.11.6", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz", - "integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==" + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==" }, "@react-spring/animated": { "version": "9.4.5", @@ -5357,6 +5408,11 @@ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001521.tgz", "integrity": "sha512-fnx1grfpEOvDGH+V17eccmNjucGUnCbP6KL+l5KqBIerp26WK/+RQ7CIDE37KGJjaPyqWXXlFUyKiWmvdNNKmQ==" }, + "classnames": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz", + "integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==" + }, "clsx": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz", @@ -5501,6 +5557,14 @@ "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", "dev": true }, + "date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "requires": { + "@babel/runtime": "^7.21.0" + } + }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -7249,6 +7313,19 @@ "object-assign": "^4.1.1" } }, + "react-datepicker": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-4.24.0.tgz", + "integrity": "sha512-2QUC2pP+x4v3Jp06gnFllxKsJR0yoT/K6y86ItxEsveTXUpsx+NBkChWXjU0JsGx/PL8EQnsxN0wHl4zdA1m/g==", + "requires": { + "@popperjs/core": "^2.11.8", + "classnames": "^2.2.6", + "date-fns": "^2.30.0", + "prop-types": "^15.7.2", + "react-onclickoutside": "^6.13.0", + "react-popper": "^2.3.0" + } + }, "react-dom": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", @@ -7274,6 +7351,12 @@ "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" }, + "react-onclickoutside": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.13.0.tgz", + "integrity": "sha512-ty8So6tcUpIb+ZE+1HAhbLROvAIJYyJe/1vRrrcmW+jLsaM+/powDRqxzo6hSh9CuRZGSL1Q8mvcF5WRD93a0A==", + "requires": {} + }, "react-popper": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.3.0.tgz", diff --git a/package.json b/package.json index 3b890f7..5028182 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "moment": "^2.29.4", "next": "^12.3.4", "react": "^17.0.2", + "react-datepicker": "^4.24.0", "react-dom": "17.0.2", "react-responsive": "^9.0.2", "semantic-ui-css": "^2.5.0", diff --git a/pages/board/notice/create.jsx b/pages/board/notice/create.jsx new file mode 100644 index 0000000..20ddb6c --- /dev/null +++ b/pages/board/notice/create.jsx @@ -0,0 +1,130 @@ +import { useState } from 'react' +import { useRouter } from "next/router"; +import moment from 'moment' +import { Form, Message } from "semantic-ui-react"; +import ReactDatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css' + +import { PoPoAxios } from "@/utils/axios.instance"; +import BoardLayout from '@/components/board/board.layout'; + +const NoticeCreatePage = () => { + const router = useRouter(); + + const [title, setTitle] = useState('') + const [content, setContent] = useState() + const [link, setLink] = useState() + const [start_datetime, setStartDatetime] = useState() + const [end_datetime, setEndDatetime] = useState() + + const duration = moment(end_datetime).diff(moment(start_datetime), 'hours'); + + const handleSubmit = async () => { + const body = { + 'title': title, + 'content': content, + 'link': link, + 'start_datetime': start_datetime, + 'end_datetime': end_datetime, + } + + if (!start_datetime || !end_datetime) { + alert('시작 일자와 종료 일자를 입력해주세요.') + return; + } + + if (start_datetime > end_datetime) { + alert('시작 일자가 종료 일자보다 늦을 수 없습니다.') + return; + } + + PoPoAxios.post('/notice', + body, + { withCredentials: true }, + ).then(() => { + alert('공지사항이 등록 되었습니다!') + router.push('/board/notice'); + }).catch(err => { + const errMsg = err.response.data.message; + alert(`공지사항 등록에 실패했습니다.\n${errMsg}`); + }) + } + + return ( + +

공지사항 생성

+ +
+ setTitle(e.target.value)} + /> + + + 공지사항 이미지 업로드는 공지사항 생성 후, 등록 할 수 있습니다. + + + setContent(e.target.value)} + /> + + setLink(e.target.value)} + /> +

+ 링크가 존재하는 공지사항일 경우 링크를 입력해주세요. +

+ +
+
+ + setStartDatetime(moment(date).format('YYYY-MM-DD HH:mm:ss'))} + onKeyDown={e => e.preventDefault()} + dateFormat="yyyy-MM-dd HH:mm" + timeIntervals={60} + minDate={new Date()} + showTimeSelect + /> +
+
+ + setEndDatetime(moment(date).format('YYYY-MM-DD HH:mm:ss'))} + onKeyDown={e => e.preventDefault()} + dateFormat="yyyy-MM-dd HH:mm" + timeIntervals={60} + minDate={moment(start_datetime).toDate()} + showTimeSelect + /> +
+
+ + { + (!start_datetime || !end_datetime) ? ( + "게시 시작 날짜와 종료 날짜를 입력해주세요." + ) : ( + start_datetime > end_datetime ? ( + "시작 날짜가 종료 날짜보다 늦을 수 없습니다." + ) : ( + `게시 기간: ${start_datetime} ~ ${end_datetime} (${Number(duration/24).toFixed(0)}일 ${duration%24}시간)` + ) + ) + } + + + + 생성 + + +
+ ) +} + +export default NoticeCreatePage; diff --git a/pages/board/notice/index.jsx b/pages/board/notice/index.jsx new file mode 100644 index 0000000..a5acf56 --- /dev/null +++ b/pages/board/notice/index.jsx @@ -0,0 +1,42 @@ +import Link from 'next/link' +import { Button, Message } from 'semantic-ui-react' + +import BoardLayout from '@/components/board/board.layout' +import { PoPoAxios } from '@/utils/axios.instance'; +import NoticeTable from '@/components/board/notice/notice.table' + +const AnnouncementPage = ({ noticeList }) => { + return ( + +

공지사항

+
+ + + +
+ + + 공지사항은 빠른 게시 시작 일자로 정렬되어 표시됩니다! + + + + 기능 개발이 계속 이뤄지고 있습니다. + + +
+ +
+
+ ) +} + +export default AnnouncementPage; + +export async function getServerSideProps() { + const res1 = await PoPoAxios.get('notice'); + const noticeList = res1.data; + + return { props: { noticeList } }; +} diff --git a/pages/board/notice/update/[id].jsx b/pages/board/notice/update/[id].jsx new file mode 100644 index 0000000..a274397 --- /dev/null +++ b/pages/board/notice/update/[id].jsx @@ -0,0 +1,165 @@ +import { useState } from 'react' +import { useRouter } from "next/router"; +import moment from 'moment' +import { Button, Form, Icon, Message } from "semantic-ui-react"; +import ReactDatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css' + +import { PoPoAxios } from "@/utils/axios.instance"; +import BoardLayout from '@/components/board/board.layout'; +import DeleteConfirmModal from "@/components/common/delete.confirm.modal"; +// import ImageUploadForm from '@/components/common/image-upload.form'; + +const NoticeUpdatePage = ({ noticeInfo }) => { + const router = useRouter(); + + const [deleteModalOpen, setDeleteModalOpen] = useState(false) + + const id = noticeInfo.id; + const [title, setTitle] = useState(noticeInfo.title) + const [content, setContent] = useState(noticeInfo.content) + const [link, setLink] = useState(noticeInfo.link) + const [start_datetime, setStartDatetime] = useState(noticeInfo.start_datetime) + const [end_datetime, setEndDatetime] = useState(noticeInfo.end_datetime) + + const duration = moment(end_datetime).diff(moment(start_datetime), 'hours'); + + const handleSubmit = async () => { + const body = { + 'title': title, + 'content': content, + 'link': link, + 'start_datetime': start_datetime, + 'end_datetime': end_datetime, + } + + if (start_datetime > end_datetime) { + alert('시작 일자가 종료 일자보다 늦을 수 없습니다.') + return; + } + + PoPoAxios.put(`/notice/${id}`, + body, + { withCredentials: true }, + ).then(() => { + alert('공지사항이 업데이트 되었습니다!') + router.push('/board/notice'); + }).catch(err => { + const errMsg = err.response.data.message; + alert(`공지사항 업데이트에 실패했습니다.\n${errMsg}`); + }) + } + + return ( + +

공지사항 수정

+ +
+ setTitle(e.target.value)} + /> + + + 공지사항 이미지 업로드는 공지사항 생성 후, 수정 페이지에서 가능합니다. + + + setContent(e.target.value)} + /> + + setLink(e.target.value)} + /> +

+ 링크가 존재하는 공지사항일 경우 링크를 입력해주세요. +

+ + {/* + + 권장 이미지 사이즈(가로 x 세로): 540 x 200 + */} + +
+
+ + setStartDatetime(moment(date).format('YYYY-MM-DD HH:mm:ss'))} + onKeyDown={e => e.preventDefault()} + dateFormat="yyyy-MM-dd HH:mm" + timeIntervals={60} + minDate={new Date()} + showTimeSelect + /> +
+
+ + setEndDatetime(moment(date).format('YYYY-MM-DD HH:mm:ss'))} + onKeyDown={e => e.preventDefault()} + dateFormat="yyyy-MM-dd HH:mm" + timeIntervals={60} + minDate={moment(start_datetime).toDate()} + showTimeSelect + /> +
+
+ + { + (!start_datetime || !end_datetime) ? ( + "게시 시작 날짜와 종료 날짜를 입력해주세요." + ) : ( + start_datetime > end_datetime ? ( + "시작 날짜가 종료 날짜보다 늦을 수 없습니다." + ) : ( + `게시 기간: ${start_datetime} ~ ${end_datetime} (${Number(duration/24).toFixed(0)}일 ${duration%24}시간)` + ) + ) + } + + + + + + 수정 + + setDeleteModalOpen(true)}> + 삭제 + )} + /> + + + +
+ ) +} + +export default NoticeUpdatePage; + +export async function getServerSideProps(ctx) { + const { id } = ctx['params']; + const res = await PoPoAxios.get(`notice/${id}`); + const noticeInfo = res.data; + + return { props: { noticeInfo } } +} diff --git a/pages/board/whitebook.jsx b/pages/board/whitebook.jsx index d40ea36..122ded4 100644 --- a/pages/board/whitebook.jsx +++ b/pages/board/whitebook.jsx @@ -2,8 +2,8 @@ import { useState, useEffect } from 'react' import { Button } from 'semantic-ui-react' import BoardLayout from '@/components/board/board.layout' -import WhitebookCreateModal from '@/components/board/whitebook.create.modal' -import WhitebookTable from '@/components/board/whitebook.table' +import WhitebookCreateModal from '@/components/board/whitebook/whitebook.create.modal' +import WhitebookTable from '@/components/board/whitebook/whitebook.table' import { PoPoAxios } from '@/utils/axios.instance'; const WhitebookPage = () => {