@@ -13,7 +13,7 @@ export default function ManitoAdminPage() {
1313 // 세션 관리용
1414 const [ sessions , setSessions ] = useState ( [ ] ) ; // [{id, code, name, createdAt,...}]
1515 const [ newSessionCode , setNewSessionCode ] = useState ( '' ) ;
16- const [ newSessionName , setNewSessionName ] = useState ( '' ) ;
16+ const [ newSessionTitle , setNewSessionTitle ] = useState ( '' ) ;
1717 const [ loadingSessions , setLoadingSessions ] = useState ( false ) ;
1818
1919 // 파일
@@ -58,28 +58,28 @@ export default function ManitoAdminPage() {
5858 const handleCreateSession = async ( ) => {
5959 resetMessages ( ) ;
6060 const code = newSessionCode . trim ( ) ;
61- const name = newSessionName . trim ( ) ;
61+ const title = newSessionTitle . trim ( ) ;
6262
6363 if ( ! code ) {
6464 setError ( '세션 코드를 입력해 주세요.' ) ;
6565 return ;
6666 }
67- if ( ! name ) {
67+ if ( ! title ) {
6868 setError ( '세션 이름을 입력해 주세요.' ) ;
6969 return ;
7070 }
7171
7272 try {
7373 setLoadingSessions ( true ) ;
74- const res = await apiClient . post ( '/admin/manito/sessions' , { code, name} ) ;
74+ const res = await apiClient . post ( '/admin/manito/sessions' , { code, name : title } ) ;
7575 const created = res . data ?. data ;
7676
7777 // 세션 목록 갱신
7878 await fetchSessions ( ) ;
7979 // 공통 sessionCode 에도 세팅
8080 setSessionCode ( code ) ;
8181 setNewSessionCode ( '' ) ;
82- setNewSessionName ( '' ) ;
82+ setNewSessionTitle ( '' ) ;
8383 setMessage ( `세션이 생성되었습니다. (code: ${ code } )` ) ;
8484 } catch ( e ) {
8585 console . error ( e ) ;
@@ -188,184 +188,184 @@ export default function ManitoAdminPage() {
188188 } ;
189189
190190 return ( < div className = "dark flex flex-col max-w-3xl mx-auto min-h-[100svh] py-16 px-6" >
191- < h1 className = "font-bold mb-6 text-3xl text-white" > 마니또 관리(Admin)</ h1 >
192-
193- { /* 공통 설정 + 세션 등록/선택 */ }
194- < Card className = "mb-6 bg-default-100 dark:bg-zinc-900 border border-zinc-800" >
195- < CardHeader className = "flex flex-col items-start gap-1" >
196- < h2 className = "text-xl font-semibold text-white" > 공통 설정 · 세션 관리</ h2 >
197- < p className = "text-xs text-zinc-400" >
198- 세션 단위로 참가자/매칭/암호문을 관리합니다.
199- < br />
200- 먼저 세션을 생성한 뒤, 해당 세션을 선택하고 아래 단계를 진행하세요.
201- </ p >
202- </ CardHeader >
191+ < h1 className = "font-bold mb-6 text-3xl text-white" > 마니또 관리(Admin)</ h1 >
192+
193+ { /* 공통 설정 + 세션 등록/선택 */ }
194+ < Card className = "mb-6 bg-default-100 dark:bg-zinc-900 border border-zinc-800" >
195+ < CardHeader className = "flex flex-col items-start gap-1" >
196+ < h2 className = "text-xl font-semibold text-white" > 공통 설정 · 세션 관리</ h2 >
197+ < p className = "text-xs text-zinc-400" >
198+ 세션 단위로 참가자/매칭/암호문을 관리합니다.
199+ < br />
200+ 먼저 세션을 생성한 뒤, 해당 세션을 선택하고 아래 단계를 진행하세요.
201+ </ p >
202+ </ CardHeader >
203+ < Divider className = "border-zinc-800" />
204+ < CardBody className = "gap-4 text-white" >
205+ { /* 현재 사용 세션 코드 (직접 입력/수정 가능) */ }
206+ < Input
207+ label = "현재 사용 중인 세션 코드"
208+ placeholder = "예: WINTER_2025"
209+ value = { sessionCode }
210+ onChange = { ( e ) => setSessionCode ( e . target . value ) }
211+ variant = "bordered"
212+ classNames = { {
213+ label : 'text-zinc-300' ,
214+ input : 'text-white' ,
215+ inputWrapper : 'bg-zinc-900 border-zinc-700 group-data-[focus=true]:border-zinc-400' ,
216+ } }
217+ />
218+
203219 < Divider className = "border-zinc-800" />
204- < CardBody className = "gap-4 text-white" >
205- { /* 현재 사용 세션 코드 (직접 입력/수정 가능) */ }
206- < Input
207- label = "현재 사용 중인 세션 코드"
208- placeholder = "예: WINTER_2025"
209- value = { sessionCode }
210- onChange = { ( e ) => setSessionCode ( e . target . value ) }
211- variant = "bordered"
212- classNames = { {
213- label : 'text-zinc-300' ,
214- input : 'text-white' ,
215- inputWrapper : 'bg-zinc-900 border-zinc-700 group-data-[focus=true]:border-zinc-400' ,
216- } }
217- />
218-
219- < Divider className = "border-zinc-800" />
220-
221- { /* 새 세션 생성 */ }
222- < div className = "space-y-2" >
223- < p className = "text-sm text-zinc-300 font-semibold" > 새 세션 등록</ p >
224- < div className = "grid grid-cols-1 md:grid-cols-2 gap-3" >
225- < Input
226- label = "세션 코드"
227- placeholder = "예: WINTER_2025"
228- value = { newSessionCode }
229- onChange = { ( e ) => setNewSessionCode ( e . target . value ) }
230- variant = "bordered"
231- classNames = { {
232- label : 'text-zinc-300' ,
233- input : 'text-white' ,
234- inputWrapper : 'bg-zinc-900 border-zinc-700 group-data-[focus=true]:border-zinc-400' ,
235- } }
236- />
237- < Input
238- label = "세션 이름"
239- placeholder = "예: 2025 겨울 마니또"
240- value = { newSessionName }
241- onChange = { ( e ) => setNewSessionName ( e . target . value ) }
242- variant = "bordered"
243- classNames = { {
244- label : 'text-zinc-300' ,
245- input : 'text-white' ,
246- inputWrapper : 'bg-zinc-900 border-zinc-700 group-data-[focus=true]:border-zinc-400' ,
247- } }
248- />
249- </ div >
250- < Button
251- color = "primary"
252- variant = "flat"
253- size = "sm"
254- onPress = { handleCreateSession }
255- isLoading = { loadingSessions }
256- className = "mt-1"
257- >
258- 새 세션 생성
259- </ Button >
260- </ div >
261220
262- { /* 세션 목록 */ }
263- < Divider className = "border-zinc-800 my-4" />
264- < div className = "space-y-2" >
265- < p className = "text-sm text-zinc-300 font-semibold" > 세션 목록</ p >
266- < div className = "max-h-40 overflow-auto space-y-1 text-sm" >
267- { loadingSessions && ( < p className = "text-xs text-zinc-400" > 세션 목록을 불러오는 중...</ p > ) }
268- { ! loadingSessions && sessions . length === 0 && ( < p className = "text-xs text-zinc-500" >
269- 등록된 세션이 없습니다. 위에서 새 세션을 생성해 주세요.
270- </ p > ) }
271- { sessions . map ( ( s ) => ( < div
272- key = { s . id ?? s . code }
273- className = "flex items-center justify-between py-1 border-b border-zinc-800/40"
274- >
275- < div className = "flex flex-col" >
276- < span className = "font-medium text-zinc-100" >
277- { s . name || '(이름 없음)' }
278- </ span >
279- < span className = "text-xs text-zinc-400" >
280- code: { s . code }
281- </ span >
282- </ div >
283- < Button
284- size = "sm"
285- variant = { sessionCode === s . code ? 'solid' : 'flat' }
286- color = "secondary"
287- onPress = { ( ) => setSessionCode ( s . code ) }
288- >
289- 사용
290- </ Button >
291- </ div > ) ) }
292- </ div >
221+ { /* 새 세션 생성 */ }
222+ < div className = "space-y-2" >
223+ < p className = "text-sm text-zinc-300 font-semibold" > 새 세션 등록</ p >
224+ < div className = "grid grid-cols-1 md:grid-cols-2 gap-3" >
225+ < Input
226+ label = "세션 코드"
227+ placeholder = "예: WINTER_2025"
228+ value = { newSessionCode }
229+ onChange = { ( e ) => setNewSessionCode ( e . target . value ) }
230+ variant = "bordered"
231+ classNames = { {
232+ label : 'text-zinc-300' ,
233+ input : 'text-white' ,
234+ inputWrapper : 'bg-zinc-900 border-zinc-700 group-data-[focus=true]:border-zinc-400' ,
235+ } }
236+ />
237+ < Input
238+ label = "세션 이름"
239+ placeholder = "예: 2025 겨울 마니또"
240+ value = { newSessionTitle }
241+ onChange = { ( e ) => setNewSessionTitle ( e . target . value ) }
242+ variant = "bordered"
243+ classNames = { {
244+ label : 'text-zinc-300' ,
245+ input : 'text-white' ,
246+ inputWrapper : 'bg-zinc-900 border-zinc-700 group-data-[focus=true]:border-zinc-400' ,
247+ } }
248+ />
293249 </ div >
294- </ CardBody >
295- </ Card >
296-
297- { /* 1단계: 참가자 CSV 업로드 */ }
298- < Card className = "mb-6 bg-default-100 dark:bg-zinc-900 border border-zinc-800" >
299- < CardHeader className = "flex flex-col items-start gap-1" >
300- < h2 className = "text-lg font-semibold text-white" >
301- 1단계 · 참가자 CSV 업로드 & 매칭 생성
302- </ h2 >
303- < p className = "text-xs text-zinc-400" >
304- CSV 헤더: < code > studentId,name,pin</ code > · 업로드 후, 서버에서 매칭을 생성하고
305- < br />
306- < code > giverStudentId,giverName,receiverStudentId,receiverName</ code > CSV를
307- 바로 다운로드합니다.
308- </ p >
309- </ CardHeader >
310- < Divider className = "border-zinc-800" />
311- < CardBody className = "gap-4 text-white" >
312- < input
313- type = "file"
314- accept = ".csv"
315- onChange = { handleParticipantsFileChange }
316- className = "text-sm text-zinc-300"
317- />
318250 < Button
319251 color = "primary"
320252 variant = "flat"
321- onPress = { handleUploadParticipants }
322- isLoading = { loadingParticipants }
323- isDisabled = { ! sessionCode . trim ( ) || ! participantsFile || loadingParticipants }
324- className = "mt-2 "
253+ size = "sm"
254+ onPress = { handleCreateSession }
255+ isLoading = { loadingSessions }
256+ className = "mt-1 "
325257 >
326- 참가자 CSV 업로드 & 매칭 CSV 다운로드
258+ 새 세션 생성
327259 </ Button >
328- </ CardBody >
329- </ Card >
330-
331- { /* 2단계: 암호문 CSV 업로드 */ }
332- < Card className = "mb-6 bg-default-100 dark:bg-zinc-900 border border-zinc-800" >
333- < CardHeader className = "flex flex-col items-start gap-1" >
334- < h2 className = "text-lg font-semibold text-white" >
335- 2단계 · 암호문(encryptedManitto) CSV 업로드
336- </ h2 >
337- < p className = "text-xs text-zinc-400" >
338- 클라이언트에서 매칭 CSV를 기반으로 암호화한 결과를 업로드합니다.
339- < br />
340- CSV 헤더 예시: < code > studentId,encryptedManitto</ code >
341- </ p >
342- </ CardHeader >
343- < Divider className = "border-zinc-800" />
344- < CardBody className = "gap-4 text-white" >
345- < input
346- type = "file"
347- accept = ".csv"
348- onChange = { handleEncryptedFileChange }
349- className = "text-sm text-zinc-300"
350- />
351- < Button
352- color = "secondary"
353- variant = "flat"
354- onPress = { handleUploadEncrypted }
355- isLoading = { loadingEncrypted }
356- isDisabled = { ! sessionCode . trim ( ) || ! encryptedFile || loadingEncrypted }
357- className = "mt-2"
358- >
359- 암호문 CSV 업로드
360- </ Button >
361- </ CardBody >
362- </ Card >
363-
364- { ( message || error ) && ( < Card className = "bg-default-100 dark:bg-zinc-900 border border-zinc-800" >
365- < CardBody className = "text-sm" >
366- { message && ( < p className = "text-emerald-400 whitespace-pre-line" > { message } </ p > ) }
367- { error && < p className = "text-red-400 whitespace-pre-line" > { error } </ p > }
368- </ CardBody >
369- </ Card > ) }
370- </ div > ) ;
260+ </ div >
261+
262+ { /* 세션 목록 */ }
263+ < Divider className = "border-zinc-800 my-4" />
264+ < div className = "space-y-2" >
265+ < p className = "text-sm text-zinc-300 font-semibold" > 세션 목록</ p >
266+ < div className = "max-h-40 overflow-auto space-y-1 text-sm" >
267+ { loadingSessions && ( < p className = "text-xs text-zinc-400" > 세션 목록을 불러오는 중...</ p > ) }
268+ { ! loadingSessions && sessions . length === 0 && ( < p className = "text-xs text-zinc-500" >
269+ 등록된 세션이 없습니다. 위에서 새 세션을 생성해 주세요.
270+ </ p > ) }
271+ { sessions . map ( ( s ) => ( < div
272+ key = { s . id ?? s . code }
273+ className = "flex items-center justify-between py-1 border-b border-zinc-800/40"
274+ >
275+ < div className = "flex flex-col" >
276+ < span className = "font-medium text-zinc-100" >
277+ { s . name || '(이름 없음)' }
278+ </ span >
279+ < span className = "text-xs text-zinc-400" >
280+ code: { s . code }
281+ </ span >
282+ </ div >
283+ < Button
284+ size = "sm"
285+ variant = { sessionCode === s . code ? 'solid' : 'flat' }
286+ color = "secondary"
287+ onPress = { ( ) => setSessionCode ( s . code ) }
288+ >
289+ 사용
290+ </ Button >
291+ </ div > ) ) }
292+ </ div >
293+ </ div >
294+ </ CardBody >
295+ </ Card >
296+
297+ { /* 1단계: 참가자 CSV 업로드 */ }
298+ < Card className = "mb-6 bg-default-100 dark:bg-zinc-900 border border-zinc-800" >
299+ < CardHeader className = "flex flex-col items-start gap-1" >
300+ < h2 className = "text-lg font-semibold text-white" >
301+ 1단계 · 참가자 CSV 업로드 & 매칭 생성
302+ </ h2 >
303+ < p className = "text-xs text-zinc-400" >
304+ CSV 헤더: < code > studentId,name,pin</ code > · 업로드 후, 서버에서 매칭을 생성하고
305+ < br />
306+ < code > giverStudentId,giverName,receiverStudentId,receiverName</ code > CSV를
307+ 바로 다운로드합니다.
308+ </ p >
309+ </ CardHeader >
310+ < Divider className = "border-zinc-800" />
311+ < CardBody className = "gap-4 text-white" >
312+ < input
313+ type = "file"
314+ accept = ".csv"
315+ onChange = { handleParticipantsFileChange }
316+ className = "text-sm text-zinc-300"
317+ />
318+ < Button
319+ color = "primary"
320+ variant = "flat"
321+ onPress = { handleUploadParticipants }
322+ isLoading = { loadingParticipants }
323+ isDisabled = { ! sessionCode . trim ( ) || ! participantsFile || loadingParticipants }
324+ className = "mt-2"
325+ >
326+ 참가자 CSV 업로드 & 매칭 CSV 다운로드
327+ </ Button >
328+ </ CardBody >
329+ </ Card >
330+
331+ { /* 2단계: 암호문 CSV 업로드 */ }
332+ < Card className = "mb-6 bg-default-100 dark:bg-zinc-900 border border-zinc-800" >
333+ < CardHeader className = "flex flex-col items-start gap-1" >
334+ < h2 className = "text-lg font-semibold text-white" >
335+ 2단계 · 암호문(encryptedManitto) CSV 업로드
336+ </ h2 >
337+ < p className = "text-xs text-zinc-400" >
338+ 클라이언트에서 매칭 CSV를 기반으로 암호화한 결과를 업로드합니다.
339+ < br />
340+ CSV 헤더 예시: < code > studentId,encryptedManitto</ code >
341+ </ p >
342+ </ CardHeader >
343+ < Divider className = "border-zinc-800" />
344+ < CardBody className = "gap-4 text-white" >
345+ < input
346+ type = "file"
347+ accept = ".csv"
348+ onChange = { handleEncryptedFileChange }
349+ className = "text-sm text-zinc-300"
350+ />
351+ < Button
352+ color = "secondary"
353+ variant = "flat"
354+ onPress = { handleUploadEncrypted }
355+ isLoading = { loadingEncrypted }
356+ isDisabled = { ! sessionCode . trim ( ) || ! encryptedFile || loadingEncrypted }
357+ className = "mt-2"
358+ >
359+ 암호문 CSV 업로드
360+ </ Button >
361+ </ CardBody >
362+ </ Card >
363+
364+ { ( message || error ) && ( < Card className = "bg-default-100 dark:bg-zinc-900 border border-zinc-800" >
365+ < CardBody className = "text-sm" >
366+ { message && ( < p className = "text-emerald-400 whitespace-pre-line" > { message } </ p > ) }
367+ { error && < p className = "text-red-400 whitespace-pre-line" > { error } </ p > }
368+ </ CardBody >
369+ </ Card > ) }
370+ </ div > ) ;
371371}
0 commit comments