diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 234cd69..69e3cdc 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -23,7 +23,7 @@ jobs: - name: Send Discord notification env: - DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} + DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK }} run: | curl -H "Content-Type: application/json" \ -X POST \ diff --git a/.gitignore b/.gitignore index bf54333..53d4561 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,3 @@ .env *.pem .idea/ - diff --git a/frontend/index.html b/frontend/index.html index 0c589ec..79c4701 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,4 +1,4 @@ - + diff --git a/frontend/package.json b/frontend/package.json index 261de3d..aac2c62 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,6 +10,7 @@ "preview": "vite preview" }, "dependencies": { + "axios": "^1.9.0", "lucide-react": "^0.507.0", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/frontend/public/assets/fonts/Akira Expanded Demo.otf b/frontend/public/assets/fonts/Akira Expanded Demo.otf new file mode 100644 index 0000000..c75fdb3 Binary files /dev/null and b/frontend/public/assets/fonts/Akira Expanded Demo.otf differ diff --git a/frontend/src/assets/img/arrowicon.png b/frontend/public/assets/img/arrowicon.png similarity index 100% rename from frontend/src/assets/img/arrowicon.png rename to frontend/public/assets/img/arrowicon.png diff --git a/frontend/src/assets/img/arrowicon.svg b/frontend/public/assets/img/arrowicon.svg similarity index 100% rename from frontend/src/assets/img/arrowicon.svg rename to frontend/public/assets/img/arrowicon.svg diff --git a/frontend/public/assets/img/boom-filled-green.png b/frontend/public/assets/img/boom-filled-green.png new file mode 100644 index 0000000..6416093 Binary files /dev/null and b/frontend/public/assets/img/boom-filled-green.png differ diff --git a/frontend/public/assets/img/boom-filled-red.png b/frontend/public/assets/img/boom-filled-red.png new file mode 100644 index 0000000..ef5bb3b Binary files /dev/null and b/frontend/public/assets/img/boom-filled-red.png differ diff --git a/frontend/public/assets/img/full_coin_green.png b/frontend/public/assets/img/full_coin_green.png new file mode 100644 index 0000000..7471e14 Binary files /dev/null and b/frontend/public/assets/img/full_coin_green.png differ diff --git a/frontend/public/assets/img/logo.svg b/frontend/public/assets/img/logo.svg new file mode 100644 index 0000000..62a3683 --- /dev/null +++ b/frontend/public/assets/img/logo.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/frontend/src/assets/img/moneyicon.png b/frontend/public/assets/img/moneyicon.png similarity index 100% rename from frontend/src/assets/img/moneyicon.png rename to frontend/public/assets/img/moneyicon.png diff --git a/frontend/src/assets/img/moneyicon.svg b/frontend/public/assets/img/moneyicon.svg similarity index 100% rename from frontend/src/assets/img/moneyicon.svg rename to frontend/public/assets/img/moneyicon.svg diff --git a/frontend/public/assets/img/one_coin_yellow.png b/frontend/public/assets/img/one_coin_yellow.png new file mode 100644 index 0000000..ea0b67a Binary files /dev/null and b/frontend/public/assets/img/one_coin_yellow.png differ diff --git a/frontend/public/assets/img/tabler--boom.png b/frontend/public/assets/img/tabler--boom.png new file mode 100644 index 0000000..23126cf Binary files /dev/null and b/frontend/public/assets/img/tabler--boom.png differ diff --git a/frontend/public/assets/img/tabler--boom.svg b/frontend/public/assets/img/tabler--boom.svg new file mode 100644 index 0000000..8c6b2eb --- /dev/null +++ b/frontend/public/assets/img/tabler--boom.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/assets/img/three_out_red.png b/frontend/public/assets/img/three_out_red.png new file mode 100644 index 0000000..d2729d1 Binary files /dev/null and b/frontend/public/assets/img/three_out_red.png differ diff --git a/frontend/public/assets/img/two_coin_yellow.png b/frontend/public/assets/img/two_coin_yellow.png new file mode 100644 index 0000000..80650f2 Binary files /dev/null and b/frontend/public/assets/img/two_coin_yellow.png differ diff --git a/frontend/src/Admin.jsx b/frontend/src/Admin.jsx new file mode 100644 index 0000000..1eb01d0 --- /dev/null +++ b/frontend/src/Admin.jsx @@ -0,0 +1,9 @@ +const Admin = () => { + return ( +
+

Admin Page

+

This is the admin page.

+
+ ); +}; +export default Admin; diff --git a/frontend/src/App.css b/frontend/src/App.css deleted file mode 100644 index b9d355d..0000000 --- a/frontend/src/App.css +++ /dev/null @@ -1,42 +0,0 @@ -#root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} - -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } -} - -.card { - padding: 2em; -} - -.read-the-docs { - color: #888; -} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 993fb09..db217ff 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -7,6 +7,7 @@ import Deposit from "./Deposit"; import Intro from "./Intro"; import Admin from "./Admin"; import Attendance from "./Attendance"; + function App() { return ( @@ -23,4 +24,4 @@ function App() { ); } -export default App; \ No newline at end of file +export default App; diff --git a/frontend/src/Assignment.jsx b/frontend/src/Assignment.jsx new file mode 100644 index 0000000..cff140f --- /dev/null +++ b/frontend/src/Assignment.jsx @@ -0,0 +1,66 @@ +import React, { useEffect, useState } from "react"; +import WeeklyListBlock from "./components/WeeklyListBlock"; +import Header from "./components/Header"; +import AssignmentInfoBlock from "./components/AssignmentInfoBlock"; +import styles from "./Assignment.module.css"; +import { mapStatus } from "./utils/AssignmentStatus.js"; + +const Assignment = () => { + const [weeks, setWeeks] = useState([]); + const [highlightCard, setHighlightCard] = useState(null); + + useEffect(() => { + const user = JSON.parse(localStorage.getItem("user")); + const userId = user?.id; + + if (!userId) return; + + fetchAssignmentsByUser(userId) + .then((weekData) => { + const formatted = weekData.map((weekItem) => ({ + label: `${weekItem.week}주차 ${weekItem.title}`, + details: weekItem.days.map((dayItem) => ({ + day: dayItem.day, + subject: weekItem.title, + tasks: dayItem.details.map((task) => ({ + label: task.assignmentName, + status: mapStatus(task.status), + })), + })), + })); + + setWeeks(formatted); + + // 형광 카드용 하이라이트 카드 추출 (가장 최근 주차 + 첫 요일)=>운영진용 페이지 만든 후 수정필요 + // 운영진이 가장 최근 공개한 과제로. + if (formatted.length > 0 && formatted[0].details.length > 0) { + const first = formatted[0]; + const firstDay = first.details[0]; + + setHighlightCard({ + weekLabel: first.label, + day: firstDay.day, + tasks: firstDay.tasks, + }); + } + }) + .catch(() => { + alert("과제 정보를 불러오지 못했습니다."); + }); + }, []); + + return ( +
+
+ {highlightCard && ( +
+ +
+ )} + +
+ ); +}; + +export default Assignment; + diff --git a/frontend/src/Assignment.module.css b/frontend/src/Assignment.module.css new file mode 100644 index 0000000..ef95f36 --- /dev/null +++ b/frontend/src/Assignment.module.css @@ -0,0 +1,13 @@ +.info { + margin-top: 28px; + margin-bottom: 54px; +} + +.assignment_page { + background-color: "black"; + min-height: "100vh"; + color: "white"; + display: flex; + flex-direction: column; + align-items: center; +} diff --git a/frontend/src/Attendance.jsx b/frontend/src/Attendance.jsx new file mode 100644 index 0000000..79650f7 --- /dev/null +++ b/frontend/src/Attendance.jsx @@ -0,0 +1,62 @@ +import { useState } from "react"; +import Header from "./components/Header"; +import InputBlock from "./components/InputBlock"; +import AttendanceWeekInfo from "./components/AttendanceWeekInfo"; +import styles from "./Attendance.module.css"; + +const Attendance = () => { + const [attendanceCode, setAttendanceCode] = useState([""]); + const handleChange = (index, value) => { + // 숫자만 입력 허용 + if (/^\d*$/.test(value)) { + const userCodes = [...attendanceCode]; + userCodes[index] = value; + setAttendanceCode(userCodes); + } + }; + const handleSubmit = () => { + console.log("제출된 출석 코드: ", attendanceCode[0]); + // 서버 요청 등 추가 작업 + }; + + return ( +
+
+ + {attendanceCode[0].length === 4 && ( + + )} +
+
+ +
+
+ +
+
+ +
+
+
+ + + + + +
+
+ ); +}; + +export default Attendance; diff --git a/frontend/src/Attendance.module.css b/frontend/src/Attendance.module.css new file mode 100644 index 0000000..a6f3852 --- /dev/null +++ b/frontend/src/Attendance.module.css @@ -0,0 +1,35 @@ +.attendance_page { + background-color: "black"; + height: "100vh"; + display: flex; + flex-direction: column; + align-items: center; + position: relative; +} +.submitBtn { + background-color: #ffffff; + opacity: 42%; + border-radius: 10px; + position: absolute; + top: 109px; + right: 63px; + padding: 7px; +} +.attend_img_container { + display: flex; + gap: 22px; + width: 100%; + padding-block: 40px; + justify-content: center; +} +.boom_icon { + width: 70px; + height: 65px; +} +.boom_icon > img { + width: 100%; + height: 100%; +} +.attend_week_container { + margin-top: 20px; +} diff --git a/frontend/src/Deposit.jsx b/frontend/src/Deposit.jsx new file mode 100644 index 0000000..ce2198c --- /dev/null +++ b/frontend/src/Deposit.jsx @@ -0,0 +1,46 @@ +import Header from "./components/Header"; +import styles from "./Deposit.module.css"; +import axios from "axios"; +import { useEffect, useState } from "react"; + +const Deposit = () => { + const [deposit, setDeposit] = useState(null); + useEffect(() => { + const user = JSON.parse(localStorage.getItem("user")); + const userId = user?.id; + + if (!userId) return; + + axios + .get(`/api/deposit/${userId}`) + .then((res) => setDeposit(res.data)) + .catch((err) => { + alert("보증금 정보를 불러오지 못했습니다."); + }); + }, []); + + if (!deposit) return
loagin...
; + + return ( +
+
+
+ 잔여 보증금 + {deposit.amount}원 +
+
+ 과제 차감 + {deposit.descentAssignment}원 +
+
+ 출석 차감 + {deposit.descentAttendance}원 +
+
+ 보증금 방어권 + {deposit.ascentDefence}원 +
+
+ ); +}; +export default Deposit; diff --git a/frontend/src/Deposit.module.css b/frontend/src/Deposit.module.css new file mode 100644 index 0000000..3f83c5d --- /dev/null +++ b/frontend/src/Deposit.module.css @@ -0,0 +1,41 @@ +.deposit_container { + width: 390px; + display: flex; + flex-direction: column; + align-items: center; +} +.deposit { + background-color: var(--main-green); + width: 309px; + padding-top: 26px; + padding-bottom: 30px; + display: flex; + flex-direction: column; + align-items: center; + gap: 17px; + border-radius: 9px; + margin-top: 80px; + margin-bottom: 87px; +} +.deposit > span:nth-child(1) { + font-weight: bold; + font-size: 16px; +} +.deposit > span:nth-child(2) { + font-weight: bold; + font-size: 20px; +} +.deposit_detail { + background-color: #575757; + color: white; + border: 1px solid rgba(217, 217, 217, 0.4); + border-radius: 8px; + padding: 12px 20px; + font-size: 15px; + display: flex; + justify-content: space-between; + align-items: center; + width: 309px; + height: 47px; + margin-bottom: 15px; +} diff --git a/frontend/src/Home.jsx b/frontend/src/Home.jsx new file mode 100644 index 0000000..f1fe4fa --- /dev/null +++ b/frontend/src/Home.jsx @@ -0,0 +1,31 @@ +import React from "react"; +import { useNavigate } from "react-router-dom"; +import styles from "./Home.module.css"; + +const Home = () => { + const navigate = useNavigate(); + return ( +
+
+

PIROCHECK

+ + + 로고 +
+
+ ); +}; + +export default Home; diff --git a/frontend/src/Home.module.css b/frontend/src/Home.module.css new file mode 100644 index 0000000..9cdf1de --- /dev/null +++ b/frontend/src/Home.module.css @@ -0,0 +1,48 @@ +.home { + height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; + overflow: hidden; + font-family: 'Akira Expanded'; +} +.pirocheck { + margin-top: 43px; + font-size: 1.25rem; + margin-bottom: 103px; +} +.home_container { + background-color: var(--background-black); + color: var(--main-green); + font-family: "Cafe24Moyamoya-Regular-v1.0", sans-serif; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); +} +.button { + width: 309px; + height: 81px; + border-radius: 8px; + background-color: var(--border-gray); + color: var(--text-white); + font-family: 'Akira Expanded'; + font-weight: 400; + font-size: 16px; + border: 1px var(--background-black) solid; + padding: 15px; + font-weight: bold; + margin-bottom: 26px; + z-index: 1; +} +.button p { + text-align: start; +} +.button:hover { + background-color: var(--card-toggle-green); + color: var(--text-white); +} +.home img { + position: relative; + opacity: 0.8; + top: -92px; + z-index: 0; +} diff --git a/frontend/src/Intro.jsx b/frontend/src/Intro.jsx new file mode 100644 index 0000000..0419c6a --- /dev/null +++ b/frontend/src/Intro.jsx @@ -0,0 +1,25 @@ +import React, { useEffect } from "react"; +import { useNavigate } from "react-router-dom"; +import styles from "./Intro.module.css"; + +const Intro = () => { + const navigate = useNavigate(); + + useEffect(() => { + const timer = setTimeout(() => { + navigate("/login"); + }, 2000); + + return () => clearTimeout(timer); + }, [navigate]); + + return ( +
+
+

PIROCHECK

+
+
+ ); +}; + +export default Intro; diff --git a/frontend/src/Intro.module.css b/frontend/src/Intro.module.css new file mode 100644 index 0000000..9fc44ae --- /dev/null +++ b/frontend/src/Intro.module.css @@ -0,0 +1,34 @@ +.intro { + width: 390px; + position: relative; + height: 100vh; + display: flex; + justify-content: center; + align-items: center; + z-index: 0; + overflow: hidden; +} + +.intro::before { + content: ""; + position: absolute; + inset: 0; + background-image: url("./assets/img/logo.svg"); + background-repeat: no-repeat; + background-size: contain; + background-position: center; + opacity: 0.5; + z-index: -1; +} +.pirocheck { + font-size: 1.25rem; + transform: scaleX(1.5); +} +.intro_container { + background-color: var(--background-black); + color: var(--main-green); + font-family: "Cafe24Moyamoya-Regular-v1.0", sans-serif; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + display: flex; + justify-content: center; +} diff --git a/frontend/src/Login.jsx b/frontend/src/Login.jsx new file mode 100644 index 0000000..e74413a --- /dev/null +++ b/frontend/src/Login.jsx @@ -0,0 +1,90 @@ +import React from "react"; +import { useState } from "react"; +import InputBlock from "./components/InputBlock"; +import { useNavigate } from "react-router-dom"; +import styles from "./Login.module.css"; +import { loginUser } from "./api/user"; + +const Login = () => { + const [name, setName] = useState(""); + const [password, setPassword] = useState(""); + const [responseMessage, setResponseMessage] = useState(""); + + const navigate = useNavigate(); + + const handleChange = (index, value) => { + if (index === 0) setName(value); + else if (index === 1) setPassword(value); + }; + + const handleLogin = async () => { + try { + const res = await loginUser({ name, password }); + + if (!res.ok) { + const data = await res.json(); + + if (res.status === 401) { + setResponseMessage( + data?.message || "이름 또는 비밀번호를 다시 확인해주세요." + ); + } else { + setResponseMessage( + data?.message || + "알 수 없는 오류가 발생했습니다. 다시 시도해주세요." + ); + } + + return; + } + + const data = await res.json(); // { id, name, role } + + localStorage.setItem("user", JSON.stringify(data)); + + if (data.role === "ADMIN") { + navigate("/admin"); + } else if (data.role === "MEMBER") { + navigate("/home"); + } else { + setResponseMessage("알 수 없는 사용자 유형입니다."); + } + } catch (error) { + console.error(error); + setResponseMessage("서버 연결에 실패했습니다. 다시 시도해주세요."); + } + }; + + return ( +
+
+

PIROCHECK

+ + +
+ {responseMessage} +
+ +
+
+ ); +}; +export default Login; diff --git a/frontend/src/Login.module.css b/frontend/src/Login.module.css new file mode 100644 index 0000000..170420b --- /dev/null +++ b/frontend/src/Login.module.css @@ -0,0 +1,43 @@ +.login { + height: 100vh; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} +.pirocheck { + font-size: 1.25rem; + transform: scaleX(1.1); + margin-bottom: 65px; +} +.login_container { + background-color: var(--background-black); + color: var(--main-green); + font-family: 'Akira Expanded'; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); +} +.button { + background-color: var(--main-green); + color: white; + width: 309px; + height: 47px; + border-radius: 14px; + margin-top: 15px; + font-size: 16px; +} +button:disabled { + opacity: 0.6; + cursor: default; +} +.errorWrapper { + width: 309px; + display: flex; + justify-content: flex-start; + margin-left: 8px; +} +.errormessage { + font-family: "Noto Sans", sans-serif; + color: #ff5858; + font-size: 10px; + margin-top: 10px; +} diff --git a/frontend/src/api/assignment.js b/frontend/src/api/assignment.js new file mode 100644 index 0000000..4102105 --- /dev/null +++ b/frontend/src/api/assignment.js @@ -0,0 +1,5 @@ +import axios from "axios"; + +export const fetchAssignmentsByUser = async (userId) => { + const res = await axios.get(`/api/assignment/grouped/${userId}`); return res.data; +}; diff --git a/frontend/src/api/user.js b/frontend/src/api/user.js new file mode 100644 index 0000000..3964bac --- /dev/null +++ b/frontend/src/api/user.js @@ -0,0 +1,14 @@ +export const loginUser = async ({ name, password }) => { + const res = await fetch("/api/login", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + credentials: "include", + body: JSON.stringify({ name, password }), + }); + + return res; +}; + +export default loginUser; diff --git a/frontend/src/components/AssignmentInfoBlock.jsx b/frontend/src/components/AssignmentInfoBlock.jsx new file mode 100644 index 0000000..4fe77b1 --- /dev/null +++ b/frontend/src/components/AssignmentInfoBlock.jsx @@ -0,0 +1,16 @@ +import React from "react"; +import "./componentsCss/AssignmentInfoBlock.css" + +const AssignmentInfoBlock = ({ weekLabel, day, tasks }) => { + return ( +
+

{weekLabel} ({day})

+ +
+ ); +}; +export default AssignmentInfoBlock; \ No newline at end of file diff --git a/frontend/src/components/AttendanceWeekInfo.jsx b/frontend/src/components/AttendanceWeekInfo.jsx new file mode 100644 index 0000000..2bae09c --- /dev/null +++ b/frontend/src/components/AttendanceWeekInfo.jsx @@ -0,0 +1,21 @@ +import React from "react"; +import "./componentsCss/AttendanceWeekInfo.css"; + +const AttendanceWeekInfo = ({ week }) => { + return ( +
+

{week}주차

+
+ +
+
+ +
+
+ +
+
+ ); +}; + +export default AttendanceWeekInfo; diff --git a/frontend/src/components/Header.jsx b/frontend/src/components/Header.jsx index 3b9d2a0..a4f62b1 100644 --- a/frontend/src/components/Header.jsx +++ b/frontend/src/components/Header.jsx @@ -1,35 +1,51 @@ -import React from 'react'; -import { useNavigate, useLocation } from 'react-router-dom'; -import { ArrowLeft, Wallet } from 'lucide-react'; -import './componentsCss/Header.css'; -import arrowIcon from '../assets/img/arrowicon.svg'; -import moneyIcon from '../assets/img/moneyicon.svg'; +import React from "react"; +import { useNavigate, useLocation } from "react-router-dom"; +import "./componentsCss/Header.css"; const Header = () => { const navigate = useNavigate(); const location = useLocation(); const path = location.pathname; let title = "ATTENDANCE\nCHECK"; - if (path.includes('assignment')) title = "ASSIGNMENT\nCHECK"; - else if (path.includes('deposit')) title = "DEPOSIT"; + if (path.includes("assignment")) title = "ASSIGNMENT\nCHECK"; + else if (path.includes("deposit")) title = "DEPOSIT"; + else if (path.includes("attendance")) title = "ATTENDANCE\nCHECK"; - const showRightButton = !path.includes('deposit'); + const showRightButton = !path.includes("deposit"); return (
-

{title}

{showRightButton ? ( - ) : ( -
// 오른쪽 공백 유지 +
)}
); }; -export default Header; \ No newline at end of file +export default Header; diff --git a/frontend/src/components/InputBlock.jsx b/frontend/src/components/InputBlock.jsx index 6aaf0bb..65ae0ab 100644 --- a/frontend/src/components/InputBlock.jsx +++ b/frontend/src/components/InputBlock.jsx @@ -1,16 +1,19 @@ import React from "react"; -import "./componentsCss/InfoBlock.css"; +import "./componentsCss/InputBlock.css"; -const InputBlock = () => { +const InputBlock = ({ inputs, onChange, values }) => { return ( -
- - - +
+ {inputs.map((input, index) => ( + onChange && onChange(index, e.target.value)} + /> + ))}
); }; diff --git a/frontend/src/components/WeeklyListBlock.jsx b/frontend/src/components/WeeklyListBlock.jsx index 8f8a7ee..52a7b92 100644 --- a/frontend/src/components/WeeklyListBlock.jsx +++ b/frontend/src/components/WeeklyListBlock.jsx @@ -12,7 +12,7 @@ const WeeklyListBlock = ({ weeks }) => { return (
{weeks.map((week, index) => ( -
+