@@ -316,6 +316,96 @@ function SessionForm({ day, week, onClose, onSave }) {
316316 ) ;
317317}
318318
319+ // ── 명예의 전당 (과제 MVP) ────────────────────────────
320+ const MVP_WEEKS = [ 1 , 2 , 3 , 4 , 5 ] ;
321+
322+ function CrownIcon ( ) {
323+ return (
324+ < svg className = { styles . crownIcon } viewBox = "0 0 24 24" fill = "none" xmlns = "http://www.w3.org/2000/svg" aria-hidden = "true" >
325+ < path d = "M3 18.5L1.5 7L7 11L12 4L17 11L22.5 7L21 18.5H3Z" fill = "currentColor" />
326+ < rect x = "3" y = "19.5" width = "18" height = "2" rx = "1" fill = "currentColor" />
327+ </ svg >
328+ ) ;
329+ }
330+
331+ function HonorOfFame ( { isAdmin } ) {
332+ const [ mvp , setMvp ] = useState ( null ) ;
333+ const [ form , setForm ] = useState ( null ) ;
334+ const [ saving , setSaving ] = useState ( false ) ;
335+
336+ const fetchMvp = async ( ) => {
337+ try {
338+ const res = await authFetch ( '/api/curriculums/mvp' ) ;
339+ const data = await res . json ( ) ;
340+ setMvp ( data ) ;
341+ setForm ( data ) ;
342+ } catch ( e ) { }
343+ } ;
344+
345+ useEffect ( ( ) => { fetchMvp ( ) ; } , [ ] ) ;
346+
347+ if ( ! mvp || ! form ) return null ;
348+
349+ const entries = [
350+ ...MVP_WEEKS . map ( w => ( { key : `week${ w } Mvp` , label : `${ w } 주차 MVP` } ) ) ,
351+ { key : 'challengeMvp' , label : '챌린지 MVP' } ,
352+ ] ;
353+ const filledEntries = entries . filter ( e => mvp [ e . key ] && mvp [ e . key ] . trim ( ) ) ;
354+
355+ const handleSave = async ( ) => {
356+ setSaving ( true ) ;
357+ try {
358+ await authFetch ( '/api/curriculums/mvp' , {
359+ method : 'PUT' ,
360+ body : JSON . stringify ( form ) ,
361+ } ) ;
362+ await fetchMvp ( ) ;
363+ } catch ( e ) {
364+ } finally {
365+ setSaving ( false ) ;
366+ }
367+ } ;
368+
369+ return (
370+ < div className = { styles . honorSection } >
371+ < div className = { styles . honorTitleRow } >
372+ < CrownIcon />
373+ < span className = { styles . honorTitle } > 과제 MVP 명예의 전당</ span >
374+ < CrownIcon />
375+ </ div >
376+
377+ { ! isAdmin && filledEntries . length > 0 && (
378+ < div className = { styles . honorList } >
379+ { filledEntries . map ( e => (
380+ < div key = { e . key } className = { styles . honorItem } >
381+ { e . label } : < span className = { styles . honorName } > { mvp [ e . key ] } </ span >
382+ </ div >
383+ ) ) }
384+ </ div >
385+ ) }
386+
387+ { isAdmin && (
388+ < div className = { styles . honorEditList } >
389+ { entries . map ( e => (
390+ < div key = { e . key } className = { styles . honorEditRow } >
391+ < label className = { styles . honorEditLabel } > { e . label } </ label >
392+ < input
393+ className = { styles . honorEditInput }
394+ value = { form [ e . key ] || '' }
395+ placeholder = "이름을 입력하세요"
396+ onChange = { ev => setForm ( { ...form , [ e . key ] : ev . target . value } ) }
397+ />
398+ </ div >
399+ ) ) }
400+ < button className = { styles . honorSaveBtn } onClick = { handleSave } disabled = { saving } >
401+ { saving ? '저장 중...' : '저장하기' }
402+ </ button >
403+ </ div >
404+ ) }
405+ </ div >
406+ ) ;
407+ }
408+
319409// ── 메인 컴포넌트 ─────────────────────────────────────
320410function CurriculumPage ( ) {
321411 const role = localStorage . getItem ( 'role' ) || 'MEMBER' ;
@@ -365,6 +455,9 @@ function CurriculumPage() {
365455 </ button >
366456 </ div >
367457 ) }
458+
459+ < HonorOfFame isAdmin = { role === 'ADMIN' } />
460+
368461 { Object . entries ( grouped ) . map ( ( [ week , weekDays ] ) => (
369462 < div key = { week } className = { styles . weekSection } >
370463 < div className = { styles . weekHeader } >
0 commit comments