+ );
+}
+
+// ── 부원용 세션 카드 ──────────────────────────────────
+function MemberSessionCard({ day }) {
+ const [isOpen, setIsOpen] = useState(false);
+ const amSession = day.sessions?.find(s => s.dayPart === 'AM');
+ const pmSession = day.sessions?.find(s => s.dayPart === 'PM');
+ const weekDay = DAY_LABEL[day.dayOfWeek] || '';
+
+ return (
+
- )}
+ );
+}
+
+// ── 운영진용 세션 카드 ────────────────────────────────
+function AdminSessionCard({ day, onEdit, onDelete }) {
+ const [isOpen, setIsOpen] = useState(true);
+ const amSession = day.sessions?.find(s => s.dayPart === 'AM');
+ const pmSession = day.sessions?.find(s => s.dayPart === 'PM');
+ const weekDay = DAY_LABEL[day.dayOfWeek] || '';
+
+ return (
+
- );
+ );
}
-function WeekSection({ week, dateGroup }) {
- return (
-
-
-
-

+// ── 운영진 세션 생성/수정 폼 ──────────────────────────
+function SessionForm({ day, week, onClose, onSave }) {
+ const isEdit = !!day;
+ const [form, setForm] = useState({
+ week: day?.week || week || 1,
+ sessionDate: day?.sessionDate || '',
+ generation: day?.generation || 25,
+ amTitle: day?.sessions?.find(s => s.dayPart === 'AM')?.title || '',
+ amHost: day?.sessions?.find(s => s.dayPart === 'AM')?.hostName || '',
+ amMaterialUrl: day?.sessions?.find(s => s.dayPart === 'AM')?.sessionMaterialUrl || '',
+ amMaterialName: day?.sessions?.find(s => s.dayPart === 'AM')?.sessionMaterialName || '',
+ amRecordingUrl: day?.sessions?.find(s => s.dayPart === 'AM')?.recordingUrl || '',
+ amRecordingPw: day?.sessions?.find(s => s.dayPart === 'AM')?.recordingPassword || '',
+ amStatus: day?.sessions?.find(s => s.dayPart === 'AM')?.status || 'BEFORE',
+ pmTitle: day?.sessions?.find(s => s.dayPart === 'PM')?.title || '',
+ pmHost: day?.sessions?.find(s => s.dayPart === 'PM')?.hostName || '',
+ pmMaterialUrl: day?.sessions?.find(s => s.dayPart === 'PM')?.sessionMaterialUrl || '',
+ pmMaterialName: day?.sessions?.find(s => s.dayPart === 'PM')?.sessionMaterialName || '',
+ pmRecordingUrl: day?.sessions?.find(s => s.dayPart === 'PM')?.recordingUrl || '',
+ pmRecordingPw: day?.sessions?.find(s => s.dayPart === 'PM')?.recordingPassword || '',
+ pmStatus: day?.sessions?.find(s => s.dayPart === 'PM')?.status || 'BEFORE',
+ assignmentUrl: day?.assignmentUrl || '',
+ assignmentName: day?.assignmentName || '',
+ });
+
+ // sessionDate 변경 시 요일 자동 계산
+ const getWeekDay = (dateStr) => {
+ if (!dateStr) return '';
+ const [year, month, day] = dateStr.split('-').map(Number);
+ const date = new Date(year, month - 1, day);
+ const map = { 2: '화요일', 4: '목요일', 6: '토요일' };
+ return map[date.getDay()] || '';
+ };
+
+ const handleSave = async () => {
+ const body = {
+ generation: Number(form.generation),
+ week: Number(form.week),
+ sessionDate: form.sessionDate,
+ sessions: [
+ {
+ dayPart: 'AM',
+ title: form.amTitle,
+ hostName: form.amHost,
+ sessionMaterialUrl: form.amMaterialUrl,
+ sessionMaterialName: form.amMaterialName,
+ recordingUrl: form.amRecordingUrl,
+ recordingPassword: form.amRecordingPw,
+ status: form.amStatus,
+ },
+ {
+ dayPart: 'PM',
+ title: form.pmTitle,
+ hostName: form.pmHost,
+ sessionMaterialUrl: form.pmMaterialUrl,
+ sessionMaterialName: form.pmMaterialName,
+ recordingUrl: form.pmRecordingUrl,
+ recordingPassword: form.pmRecordingPw,
+ assignmentUrl: form.assignmentUrl,
+ assignmentName: form.assignmentName,
+ status: form.pmStatus,
+ },
+ ],
+ };
+
+ if (isEdit) {
+ await authFetch(`/api/curriculums/${day.sessionDate}`, {
+ method: 'PATCH',
+ body: JSON.stringify({
+ generation: body.generation,
+ week: body.week,
+ newSessionDate: form.sessionDate,
+ sessions: body.sessions,
+ }),
+ });
+ } else {
+ await authFetch('/api/curriculums', {
+ method: 'POST',
+ body: JSON.stringify(body),
+ });
+ }
+ onSave();
+ onClose();
+ };
+
+ const weeks = [1, 2, 3, 4, 5];
+
+ return (
+
+
+
+
+
+
+
+
+
+ {/* 오전 세션 */}
+
+

+
오전 세션
+
+ {STATUS_OPTIONS.map(s => (
+
+ ))}
+
+
+
+
+ {/* 오후 세션 */}
+
+

+
오후 세션
+
+ {STATUS_OPTIONS.map(s => (
+
+ ))}
+
+
+
+
+ {/* 과제 */}
+
+
과제
+
setForm({ ...form, assignmentName: e.target.value })} />
+
setForm({ ...form, assignmentUrl: e.target.value })} />
+
+
+
+
+
-
WEEK {week}
-
-
- {Object.entries(dateGroup).map(([date, sessions]) => (
-
- ))}
-
-
- );
+ );
}
+// ── 메인 컴포넌트 ─────────────────────────────────────
function CurriculumPage() {
- const [sessions, setSessions] = useState([]);
- const role = localStorage.getItem('role');
-
- useEffect(() => {
- setSessions([
- {
- id: 1,
- week: 1,
- sessionDate: '2026-06-23',
- dayPart: 'AM',
- title: 'HTML/CSS',
- hostName: '24기 김서윤',
- status: 'AFTER_SESSION',
- description: '코딩앵무 클론 코딩, 피로그래밍 페이지 클론 코딩',
- sessionMaterialUrl: '#',
- sessionMaterialName: 'HTML/CSS',
- recordingUrl: '#',
- recordingPassword: '%8.D^G&z',
- },
- {
- id: 2,
- week: 1,
- sessionDate: '2026-06-23',
- dayPart: 'PM',
- title: 'Git 기초',
- hostName: '24기 한혜담',
- status: 'AFTER_SESSION',
- description: '코딩앵무 클론 코딩, 피로그래밍 페이지 클론 코딩',
- sessionMaterialUrl: '#',
- sessionMaterialName: 'Git 기초',
- recordingUrl: '#',
- recordingPassword: '%8.D^G&z',
- },
- {
- id: 3,
- week: 1,
- sessionDate: '2026-06-25',
- dayPart: 'AM',
- title: '',
- hostName: '',
- status: 'BEFORE_SESSION',
- description: '',
- sessionMaterialUrl: '',
- sessionMaterialName: '',
- recordingUrl: '',
- recordingPassword: '',
- },
- {
- id: 4,
- week: 1,
- sessionDate: '2026-06-27',
- dayPart: 'AM',
- title: '',
- hostName: '',
- status: 'BEFORE_SESSION',
- description: '',
- sessionMaterialUrl: '',
- sessionMaterialName: '',
- recordingUrl: '',
- recordingPassword: '',
- },
- ]);
- }, []);
-
- const grouped = sessions.reduce((acc, session) => {
- const week = session.week;
- const date = session.sessionDate;
- if (!acc[week]) acc[week] = {};
- if (!acc[week][date]) acc[week][date] = [];
- acc[week][date].push(session);
- return acc;
- }, {});
-
- return (
-
- {Object.entries(grouped).map(([week, dateGroup]) => (
-
- ))}
-
- );
+ const [days, setDays] = useState([]);
+ const [showForm, setShowForm] = useState(false);
+ const [editDay, setEditDay] = useState(null);
+ const [createWeek, setCreateWeek] = useState(null);
+
+ const fetchDays = async () => {
+ try {
+ const res = await authFetch('/api/curriculums');
+ const data = await res.json();
+ setDays(Array.isArray(data) ? data : []);
+ } catch (e) {}
+ };
+
+ useEffect(() => { fetchDays(); }, []);
+
+ const handleDelete = async (sessionDate) => {
+ if (!window.confirm('삭제하시겠습니까?')) return;
+ await authFetch(`/api/curriculums/${sessionDate}`, { method: 'DELETE' });
+ fetchDays();
+ };
+
+ // 주차별로 그룹화
+ const grouped = days.reduce((acc, day) => {
+ const week = day.week;
+ if (!acc[week]) acc[week] = [];
+ acc[week].push(day);
+ return acc;
+ }, {});
+
+ return (
+
+ {role === 'ADMIN' && (
+
+
+
+ )}
+ {Object.entries(grouped).map(([week, weekDays]) => (
+
+
+
+

+
WEEK {week}
+
+
+
+
+ {weekDays.map((day, i) => (
+ role === 'ADMIN'
+ ?
{ setEditDay(d); setCreateWeek(null); setShowForm(true); }}
+ onDelete={handleDelete} />
+ :
+ ))}
+
+
+ ))}
+
+ {showForm && (
+
+
{ setShowForm(false); setEditDay(null); setCreateWeek(null); }}
+ onSave={fetchDays}
+ />
+ )}
+
+ );
}
export default CurriculumPage;
\ No newline at end of file
diff --git a/frontend/src/pages/curriculum/CurriculumPage.module.css b/frontend/src/pages/curriculum/CurriculumPage.module.css
index 69b1b67..5adccb5 100644
--- a/frontend/src/pages/curriculum/CurriculumPage.module.css
+++ b/frontend/src/pages/curriculum/CurriculumPage.module.css
@@ -1,201 +1,455 @@
-.page {
- padding: 40px 60px;
- background-color: var(--gray50);
- min-height: 100vh;
+.container {
+ padding: 40px 60px;
+ background: var(--gray20);
+ min-height: calc(100vh - 100px);
}
+/* 주차 섹션 */
.weekSection {
- margin-bottom: 48px;
+ margin-bottom: 48px;
}
.weekHeader {
- display: flex;
- align-items: center;
- gap: 12px;
- margin-bottom: 24px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 24px;
}
-.weekLogo {
- width: 36px;
- height: 36px;
+.weekLeft {
+ display: flex;
+ align-items: center;
+ gap: 12px;
}
-.weekLogo img {
- width: 36px;
- height: 36px;
+.logoIcon {
+ width: 40px;
+ height: 40px;
+ object-fit: contain;
}
.weekTitle {
- font-family: var(--font-main);
- font-size: 30px;
- font-weight: 700;
- color: var(--black);
- margin: 0;
+ font-family: var(--font-main);
+ font-size: 1.6rem;
+ font-weight: 700;
+ color: var(--black);
+}
+
+.topBar {
+ display: flex;
+ justify-content: flex-end;
+ margin-bottom: 24px;
+}
+
+.createBtn {
+ padding: 8px 30px;
+ background: transparent;
+ border: 1.5px solid var(--dark);
+ border-radius: 10px;
+ color: var(--dark);
+ font-family: var(--font-main);
+ font-size: 0.95rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.2s;
}
-.cardGrid {
- display: grid;
- grid-template-columns: repeat(3, 1fr);
- gap: 24px;
- align-items: start;
+.createBtn:hover { background: var(--dark); color: var(--white); }
+
+/* 카드 행 */
+.cardsRow {
+ display: flex;
+ gap: 20px;
+ flex-wrap: wrap;
+ align-items: flex-start;
}
-.card {
- background-color: var(--white);
- border-radius: 16px;
- padding: 30px;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
+/* 세션 카드 */
+.sessionCard {
+ background: var(--white);
+ border: 1px solid #eee;
+ border-radius: 20px;
+ padding: 30px;
+ max-width: 335px;
+ flex: 1;
+ box-shadow: 0 1px 4px rgba(0,0,0,0.06);
}
.cardHeader {
- display: flex;
- justify-content: space-between;
- align-items: flex-start;
- cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ cursor: pointer;
+ margin-bottom: 0;
}
-.cardTitleWrap {
- display: flex;
- align-items: baseline;
- gap: 8px;
+.cardHeaderLeft {
+ display: flex;
+ align-items: center;
+ gap: 10px;
}
.cardTitle {
- font-family: var(--font-main);
- font-size: 20px;
- font-weight: 550;
- color: var(--black);
+ font-family: var(--font-main);
+ font-size: 1.3rem;
+ font-weight: 650;
+ color: var(--black);
}
.cardDate {
- font-size: 12px;
- color: var(--gray200);
+ font-family: var(--font-main);
+ font-size: 0.8rem;
+ color: #aaa;
+ margin-left: 10px;
}
-.toggle {
- color: var(--dark);
- font-size: 20px;
+.cardToggle {
+ color: var(--dark);
+ font-size: 0.8rem;
}
.cardBody {
- margin-top: 8px;
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
}
-.section {
- display: flex;
- flex-direction: column;
- gap: 8px;
- margin-bottom: 16px;
+.divider {
+ border: none;
+ border-top: 1px solid var(--gray200);
+ margin: 20px 0 20px 0;
}
-.sectionRow {
- display: flex;
- justify-content: space-between;
- align-items: center;
- width: 100%;
+/* 세션 정보 */
+.sessionInfo {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
}
-.sectionTitle {
- font-size: 18px;
- font-weight: 500;
- color: var(--black);
+.sessionInfoRow {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex-wrap: wrap;
+}
+
+.sessionRow {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex-wrap: wrap;
+}
+
+.sessionTitleRow {
margin: 5px 0;
- display: flex;
- align-items: center;
- gap: 6px;
}
-.host {
- font-size: 14px;
- color: var(--gray600);
- font-weight: 500;
+.sessionIcon {
+ width: 18px;
+ height: 18px;
+ object-fit: contain;
+ filter: brightness(0) saturate(100%) invert(44%) sepia(98%) saturate(500%) hue-rotate(90deg) brightness(95%) contrast(110%);
}
-.file_row {
- display: flex;
- justify-content: space-between;
- width: 100%;
- text-decoration: none;
+.sessionTitle {
+ font-family: var(--font-main);
+ font-size: 1.1rem;
+ font-weight: 550;
+ color: var(--black);
+ padding: 5px 0;
}
-.file_row:hover .file,
-.file_row:hover .file_name {
- color: var(--main);
+.sessionHost {
+ font-family: var(--font-main);
+ font-size: 0.9rem;
+ color: var(--gray600);
+ margin-left: auto;
}
-.video_row {
+.sessionDetailRow {
display: flex;
justify-content: space-between;
- width: 100%;
- text-decoration: none;
+ padding: 3px 0;
}
-.video_row:hover .video,
-.video_row:hover .video_pw {
- color: var(--main);
- transition: all 0.1s ease-in-out;
+.sessionLink {
+ font-family: var(--font-main);
+ font-size: 0.9rem;
+ color: var(--black);
+ text-decoration: none;
}
-.file {
- font-size: 16px;
- color: var(--black);
- font-weight: 500;
- margin-left: 27px;
+.sessionLink:hover {
+ color: var(--dark);
+ transition: all ease-in-out 0.2s;
+ cursor: pointer;
}
-.file_name {
- font-size: 14px;
- color: var(--gray600);
+.sessionLinkName {
+ font-family: var(--font-main);
+ font-size: 0.9rem;
+ color: var(--black);
}
-.video {
- font-size: 16px;
- color: var(--black);
- margin-left: 27px;
- font-weight: 500;
+.sessionRecording {
+ font-family: var(--font-main);
+ font-size: 0.9rem;
+ color: var(--black);
}
-.video_pw {
- font-size: 14px;
- color: var(--gray600);
+.sessionPw {
+ font-family: var(--font-main);
+ font-size: 0.9rem;
+ color: var(--black);
}
-.divider {
- height: 1.2px;
- background-color: var(--gray200);
- margin: 20px 0;
+/* 과제 */
+.assignmentRow {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ padding-top: 4px;
+ margin-left: 22px;
}
.assignmentLabel {
- font-size: 20px;
- font-weight: 700;
- color: var(--dark);
- margin-left: 25px;
+ font-family: var(--font-main);
+ font-size: 1.2rem;
+ font-weight: 700;
+ color: var(--dark);
+}
+
+.assignmentSection {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ margin-top: 8px;
+}
+
+/* 운영진 버튼 */
+.adminBtns {
+ display: flex;
+ margin: 10px auto;
+ gap: 8px;
+}
+
+.editBtn {
+ padding: 6px 20px;
+ background: transparent;
+ border: 1.5px solid var(--dark);
+ border-radius: 10px;
+ color: var(--dark);
+ font-family: var(--font-main);
+ font-size: 0.85rem;
+ cursor: pointer;
+}
+
+.editBtn:hover {
+ background: var(--dark);
+ color: var(--white);
+ transition: all ease-in-out 0.2s;
+}
+
+.deleteBtn {
+ padding: 6px 20px;
+ background: transparent;
+ border: 1.5px solid var(--dark);
+ border-radius: 10px;
+ color: var(--dark);
+ font-family: var(--font-main);
+ font-size: 0.85rem;
+ cursor: pointer;
+}
+
+.deleteBtn:hover {
+ background: var(--dark);
+ color: var(--white);
+ transition: all ease-in-out 0.2s;
+}
+
+/* 세션 생성/수정 폼 */
+.formOverlay {
+ position: fixed;
+ inset: 0;
+ background: var(--gray20);
+ display: flex;
+ align-items: flex-start;
+ justify-content: center;
+ overflow-y: auto;
+ padding: 40px 20px;
+ z-index: 200;
+}
+
+.formCard {
+ background: var(--white);
+ border-radius: 16px;
+ padding: 40px;
+ width: 560px;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
+
+.formTitle {
+ font-family: var(--font-main);
+ font-size: 1.3rem;
+ font-weight: 700;
+ color: var(--dark);
+ margin-bottom: 8px;
+}
+
+.formSection {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.formSectionTitle {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-top: 8px;
+}
+
+.amLabel {
+ font-family: var(--font-main);
+ font-size: 1rem;
+ font-weight: 700;
+ color: var(--dark);
+}
+
+.pmLabel {
+ font-family: var(--font-main);
+ font-size: 1rem;
+ font-weight: 700;
+ color: var(--dark);
+}
+
+.statusBtns {
+ display: flex;
+ gap: 6px;
+ margin-left: auto;
+}
+
+.statusBtn {
+ padding: 4px 12px;
+ background: transparent;
+ border: 1.5px solid var(--dark);
+ border-radius: 7px;
+ color: var(--dark);
+ font-family: var(--font-main);
+ font-size: 0.8rem;
+ cursor: pointer;
+}
+
+.statusBtn:hover {
+ background: var(--dark);
+ color: var(--white);
+ transition: all ease-in-out 0.2s;
+}
+
+.statusActive {
+ background: var(--dark);
+ color: var(--white);
+}
+
+.formGrid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 12px;
+}
+
+.formLabel {
+ font-family: var(--font-main);
+ font-size: 0.85rem;
+ color: #666;
+ margin-bottom: 4px;
+ display: block;
+}
+
+.formInput {
+ width: 100%;
+ padding: 8px 12px;
+ border: 1px solid #ddd;
+ border-radius: 8px;
+ font-family: var(--font-main);
+ font-size: 0.9rem;
+ outline: none;
+ box-sizing: border-box;
+}
+
+.formInput:focus { border-color: var(--dark); }
+
+.saveFormBtn {
+ width: 40%;
+ margin: 30px auto 0;
+ padding: 10px 0;
+ background: transparent;
+ border: 1.5px solid var(--dark);
+ border-radius: 10px;
+ color: var(--dark);
+ font-family: var(--font-main);
+ font-size: 1rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.2s;
+}
+
+.saveFormBtn:hover { background: var(--dark); color: var(--white); }
+
+.cancelBtn {
+ padding: 10px 0;
+ background: transparent;
+ border: none;
+ color: #aaa;
+ font-family: var(--font-main);
+ font-size: 0.9rem;
+ cursor: pointer;
+ text-align: center;
+}
+
+/* 추가 스타일 */
+.toggleIcon {
+ width: 14px;
+ height: 14px;
+ transition: transform 0.3s ease;
+ filter: brightness(0) saturate(100%) invert(44%) sepia(60%) saturate(1693%) hue-rotate(89deg) brightness(107%) contrast(95%);
+}
+
+.toggleOpen {
+ transform: rotate(180deg);
+}
+
+.sessionTitleRow {
+ display: flex;
+ align-items: center;
+ gap: 8px;
}
-.assignmentText {
- font-size: 14px;
- color: var(--gray600);
- margin: 0;
- margin-left: 25px;
+.sessionDetailRow {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding-left: 26px;
}
-.placeholder {
- font-size: 13px;
- color: var(--gray200);
- text-align: center;
- padding: 12px 0;
+.sessionDetailLabel {
+ font-family: var(--font-main);
+ font-size: 0.9rem;
+ color: var(--black);
+ min-width: 50px;
}
-.amIcon {
- width: 20px;
- height: 20px;
- vertical-align: middle;
+.sessionDetailVal {
+ font-family: var(--font-main);
+ font-size: 0.9rem;
+ color: var(--black);
}
-.pmIcon {
- width: 15px;
- height: 20px;
- vertical-align: middle;
- margin-right: 5px;
+.formRow2 {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 12px;
}
\ No newline at end of file