diff --git a/frontend/src/App.js b/frontend/src/App.js index 7c37e68..dc6becf 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -8,6 +8,7 @@ import QnADetailPage from './pages/qna/QnADetailPage'; import CurriculumPage from './pages/curriculum/CurriculumPage'; import PiroCheckMain from './pages/pirocheck/PIroCheckMain'; import Attendance from './pages/pirocheck/attendance/Attendance' +import Assignment from './pages/pirocheck/assignment/Assignment'; function App() { return ( @@ -29,8 +30,9 @@ function App() { {/* 다크 헤더 페이지 */} }> - } /> - } /> + }/> + }/> + }/> diff --git a/frontend/src/assets/images/icon_delete.svg b/frontend/src/assets/images/icon_delete.svg new file mode 100644 index 0000000..da16964 --- /dev/null +++ b/frontend/src/assets/images/icon_delete.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/assets/images/icon_edit.svg b/frontend/src/assets/images/icon_edit.svg new file mode 100644 index 0000000..a0788a2 --- /dev/null +++ b/frontend/src/assets/images/icon_edit.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/assets/images/icon_status_o.svg b/frontend/src/assets/images/icon_status_o.svg new file mode 100644 index 0000000..87e1cf5 --- /dev/null +++ b/frontend/src/assets/images/icon_status_o.svg @@ -0,0 +1,16 @@ + + + + + + + + \ No newline at end of file diff --git a/frontend/src/assets/images/icon_status_triangle.svg b/frontend/src/assets/images/icon_status_triangle.svg new file mode 100644 index 0000000..4764cf3 --- /dev/null +++ b/frontend/src/assets/images/icon_status_triangle.svg @@ -0,0 +1,16 @@ + + + + + + + + \ No newline at end of file diff --git a/frontend/src/assets/images/icon_status_x.svg b/frontend/src/assets/images/icon_status_x.svg new file mode 100644 index 0000000..8152f92 --- /dev/null +++ b/frontend/src/assets/images/icon_status_x.svg @@ -0,0 +1,16 @@ + + + + + + + + \ No newline at end of file diff --git a/frontend/src/pages/pirocheck/assignment/Assignment.js b/frontend/src/pages/pirocheck/assignment/Assignment.js new file mode 100644 index 0000000..9f1644a --- /dev/null +++ b/frontend/src/pages/pirocheck/assignment/Assignment.js @@ -0,0 +1,223 @@ +import { useState, useEffect } from 'react'; +import styles from './Assignment.module.css'; +import LogoImg from '../../../assets/images/logo.png'; +import EditIcon from '../../../assets/images/icon_edit.svg'; +import DeleteIcon from '../../../assets/images/icon_delete.svg'; +import StatusO from '../../../assets/images/icon_status_o.svg'; +import StatusT from '../../../assets/images/icon_status_triangle.svg'; +import StatusX from '../../../assets/images/icon_status_x.svg'; + +// 요일 변환 +const dayMap = { + TUESDAY: 'TUE', + THURSDAY: 'THU', + SATURDAY: 'SAT', +}; + +// 임시 데이터 (백엔드 연동 전) +const MOCK_DATA = [ + { + week: '1', + assignments: [ + { assignmentId: 1, title: '코딩앵무 클론 코딩', week: '1', day: 'TUESDAY', sessionDate: 'HTML/CSS, Git 기초', submitted: 'SUBMITTED' }, + { assignmentId: 2, title: '피로그래밍 페이지 클론 코딩', week: '1', day: 'TUESDAY', sessionDate: 'HTML/CSS, Git 기초', submitted: 'NOT_SUBMITTED' }, + { assignmentId: 3, title: '코딩앵무 클론 코딩', week: '1', day: 'THURSDAY', sessionDate: 'JS', submitted: 'SUBMITTED' }, + { assignmentId: 4, title: '숫자야구 게임', week: '1', day: 'THURSDAY', sessionDate: 'JS', submitted: 'LATE' }, + { assignmentId: 5, title: '파이썬 코딩도장', week: '1', day: 'THURSDAY', sessionDate: 'JS', submitted: 'NOT_SUBMITTED' }, + { assignmentId: 6, title: '아르사 팀플', week: '1', day: 'SATURDAY', sessionDate: 'DB 개론', submitted: 'SUBMITTED' }, + ], + }, + { week: '2', assignments: [] }, + { week: '3', assignments: [] }, + { week: '4', assignments: [] }, + { week: '5', assignments: [] }, +]; + +const IS_MOCK = true; // 백엔드 연동 시 false로 변경 + +// 제출 상태 아이콘 (부원용) +function StatusIcon({ status }) { + if (status === 'SUBMITTED') return 제출; + if (status === 'LATE') return 지각제출; + return 미제출; +} + +// 세션별 과제 묶기 +function groupByDay(assignments) { + const order = ['TUESDAY', 'THURSDAY', 'SATURDAY']; + const grouped = {}; + assignments.forEach(a => { + if (!grouped[a.day]) grouped[a.day] = { day: a.day, sessionDate: a.sessionDate, items: [] }; + grouped[a.day].items.push(a); + }); + return order.filter(d => grouped[d]).map(d => grouped[d]); +} + +// ── 과제 등록/수정 모달 ─────────────────────────────── +function AssignmentModal({ item, onClose, onSave }) { + const isEdit = !!item; + const [form, setForm] = useState({ + week: item?.week || '1', + day: item?.day || 'TUESDAY', + title: item?.title || '', + }); + const weeks = ['1', '2', '3', '4', '5']; + const days = ['TUESDAY', 'THURSDAY', 'SATURDAY']; + + const handleSave = async () => { + const url = isEdit + ? `/api/assignments/modify/${item.assignmentId}` + : '/api/assignments/create'; + const method = isEdit ? 'PATCH' : 'POST'; + await fetch(url, { + method, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ title: form.title, week: form.week, day: form.day }), + }); + onSave(); + onClose(); + }; + + return ( +
+
+ logo +
ASSIGNMENT
+
+ + + 과제 +
+ setForm({ ...form, title: e.target.value })} + /> + +
+
+ ); +} + +// ── 주차 블록 (공통) ────────────────────────────────── +function WeekBlock({ weekData, role, onEdit, onDelete }) { + const [isOpen, setIsOpen] = useState(false); + const grouped = groupByDay(weekData.assignments || []); + + return ( +
+
setIsOpen(prev => !prev)}> +
+ logo + WEEK {weekData.week} +
+ {isOpen ? '▲' : '▼'} +
+ + {isOpen && ( +
+ {grouped.length === 0 && ( +
등록된 과제가 없습니다.
+ )} + {grouped.map((session, j) => ( +
+
+
+ {dayMap[session.day]} + {session.sessionDate && {session.sessionDate}} +
+ {role === 'ADMIN' && ( +
+ + +
+ )} +
+ {session.items.map((item, k) => ( +
+ {item.title} + {role === 'MEMBER' && } +
+ ))} + {j < grouped.length - 1 &&
} +
+ ))} +
+ )} +
+ ); +} + +// ── 메인 컴포넌트 ───────────────────────────────────── +function Assignment() { + const role = localStorage.getItem('role') || 'MEMBER'; + const [weeks, setWeeks] = useState([]); + const [modalItem, setModalItem] = useState(undefined); // undefined=닫힘, null=생성, object=수정 + + const fetchAll = async () => { + if (IS_MOCK) { + setWeeks(MOCK_DATA); + return; + } + const results = await Promise.all( + ['1', '2', '3', '4', '5'].map(w => + fetch(`/api/assignments/me/${w}`) + .then(r => r.json()) + .catch(() => ({ week: w, assignments: [] })) + ) + ); + setWeeks(results); + }; + + useEffect(() => { fetchAll(); }, []); + + const handleDelete = async (assignmentId) => { + if (!window.confirm('삭제하시겠습니까?')) return; + if (!IS_MOCK) { + await fetch(`/api/assignments/${assignmentId}`, { method: 'DELETE' }); + } + fetchAll(); + }; + + return ( +
+ {IS_MOCK && ( +
⚠️ 현재 임시 데이터로 표시 중입니다. 백엔드 연동 후 IS_MOCK을 false로 변경하세요.
+ )} +
ASSIGNMENT CHECK
+ + {weeks.map((w, i) => ( + setModalItem(item)} + onDelete={handleDelete} + /> + ))} + + {role === 'ADMIN' && ( + + )} + + {modalItem !== undefined && ( + setModalItem(undefined)} + onSave={fetchAll} + /> + )} +
+ ); +} + +export default Assignment; \ No newline at end of file diff --git a/frontend/src/pages/pirocheck/assignment/Assignment.module.css b/frontend/src/pages/pirocheck/assignment/Assignment.module.css new file mode 100644 index 0000000..8e38911 --- /dev/null +++ b/frontend/src/pages/pirocheck/assignment/Assignment.module.css @@ -0,0 +1,287 @@ +.container { + display: flex; + flex-direction: column; + align-items: center; + padding: 60px 20px; + min-height: calc(100vh - 100px); + background: var(--black); +} + +.mockBanner { + background: #5a3e00; + color: #ffd166; + font-family: var(--font-main); + font-size: 0.85rem; + padding: 10px 20px; + border-radius: 8px; + margin-bottom: 20px; + text-align: center; + width: 600px; + box-sizing: border-box; +} + +.title { + font-family: var(--font-title); + font-size: 3rem; + font-weight: 800; + color: var(--main); + margin-bottom: 40px; + letter-spacing: 0; +} + +/* 주차 블록 */ +.weekBlock { + width: 600px; + background: #3a3a3a; + border-radius: 16px; + margin-bottom: 20px; + overflow: hidden; +} + +.weekHeader { + display: flex; + align-items: center; + justify-content: space-between; + padding: 20px 24px; + cursor: pointer; + color: var(--white); +} + +.weekLeft { + display: flex; + align-items: center; + gap: 12px; +} + +.logoIcon { + width: 28px; + height: 28px; + object-fit: contain; +} + +.weekLabel { + font-family: var(--font-main); + font-size: 1.1rem; + font-weight: 600; + color: var(--white); +} + +.arrow { + color: var(--white); + font-size: 0.9rem; +} + +.weekBody { + padding: 0 24px 20px; +} + +.empty { + color: #aaa; + font-family: var(--font-main); + font-size: 0.9rem; + text-align: center; + padding: 16px 0; +} + +/* 세션 */ +.session { + margin-top: 12px; +} + +.sessionHeader { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 8px; +} + +.sessionLeft { + display: flex; + align-items: center; + gap: 10px; +} + +.dayLabel { + color: var(--dark); + font-family: var(--font-title); + font-size: 1.2rem; + font-weight: 700; +} + +.sessionTitle { + color: var(--light); + font-family: var(--font-main); + font-size: 1rem; +} + +.sessionActions { + display: flex; + gap: 8px; +} + +.iconBtn { + background: none; + border: none; + cursor: pointer; + padding: 0; + display: flex; + align-items: center; +} + +.actionIcon { + width: 20px; + height: 20px; + opacity: 0.7; +} + +.actionIcon:hover { opacity: 1; } + +/* 과제 항목 */ +.assignmentRow { + display: flex; + align-items: center; + justify-content: space-between; + padding: 5px 0; +} + +.assignmentTitle { + color: var(--white); + font-family: var(--font-main); + font-size: 0.95rem; +} + +.statusIcon { + width: 20px; + height: 20px; +} + +.divider { + border: none; + border-top: 1px solid #555; + margin: 12px 0; +} + +/* + 추가 버튼 */ +.addBtn { + position: fixed; + bottom: 40px; + right: 40px; + width: 56px; + height: 56px; + border-radius: 50%; + background: #3a3a3a; + color: var(--main); + font-size: 2rem; + border: none; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 4px 12px rgba(0,0,0,0.4); + transition: background 0.2s; +} + +.addBtn:hover { background: #4a4a4a; } + +/* 모달 */ +.modalOverlay { + position: fixed; + inset: 0; + background: var(--black); + display: flex; + align-items: center; + justify-content: center; + z-index: 200; +} + +.modal { + background: #3a3a3a; + border-radius: 20px; + padding: 40px 60px; + width: 420px; + display: flex; + flex-direction: column; + align-items: center; + gap: 16px; +} + +.modalLogo { + width: 200px; + height: 200px; + object-fit: contain; +} + +.modalTitle { + font-family: var(--font-title); + font-size: 3rem; + font-weight: 800; + color: var(--main); + letter-spacing: 0; +} + +.modalRow { + display: flex; + align-items: center; + gap: 12px; + width: 85%; + margin-top: 10px; +} + +.select { + padding: 10px 36px 10px 20px; + background-color: var(--pale); + + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + + background-image: url("data:image/svg+xml;utf8,"); + background-repeat: no-repeat; + background-position: right 16px center; + + border: none; + border-radius: 8px; + font-family: var(--font-main); + font-size: 1rem; + cursor: pointer; + flex: 1; +} + +.select::-ms-expand { + display: none; +} +.modalInput { + width: 85%; + padding: 12px 20px; + background: var(--pale); + border: none; + border-radius: 8px; + font-family: var(--font-main); + font-size: 1rem; + box-sizing: border-box; +} + +.modalLabel { + color: var(--white); + font-family: var(--font-main); + font-size: 1.2rem; +} + +.saveBtn { + margin-top: 15px; + padding: 10px 45px; + background: transparent; + border: 2px solid var(--main); + border-radius: 10px; + color: var(--main); + font-family: var(--font-main); + font-size: 1rem; + font-weight: 650; + cursor: pointer; + transition: all 0.2s; +} + +.saveBtn:hover { + background: var(--main); + color: var(--black); +} \ No newline at end of file