Skip to content

Commit cfa8b4a

Browse files
authored
Merge pull request #93 from wafflestudio/feat/pretty-admin
어드민페이지 꾸미기
2 parents 091c8ba + 4014a07 commit cfa8b4a

2 files changed

Lines changed: 656 additions & 149 deletions

File tree

src/pages/AdminEvents.tsx

Lines changed: 194 additions & 149 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
type AdminEventPatchRequest,
1111
} from "@/api/adminEvent";
1212
import { useState } from "react";
13+
import styles from "@/styles/AdminEvents.module.css";
1314

1415
const EMPTY_FORM = {
1516
title: "",
@@ -351,157 +352,201 @@ export default function AdminEventsPage() {
351352
["location", "장소", "text"],
352353
["applyLink", "신청 링크", "text"],
353354

354-
["tags", "태그, 쉼표 구분", "text"],
355+
["tags", "태그 (쉼표 구분)", "text"],
355356
];
356357

357358
return (
358-
<main
359-
style={{
360-
width: "100vw",
361-
height: "100dvh",
362-
overflowY: "auto",
363-
boxSizing: "border-box",
364-
maxWidth: 1080,
365-
margin: "0 auto",
366-
padding: 24,
367-
}}
368-
>
369-
<h1>행샤 어드민</h1>
370-
371-
<section style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
372-
<input
373-
value={eventId}
374-
onChange={(e) => setEventId(e.currentTarget.value)}
375-
placeholder="행사 ID"
376-
/>
377-
378-
<button type="button" onClick={handleLoad} disabled={isLoading}>
379-
불러오기
380-
</button>
381-
382-
<button type="button" onClick={handlePatch} disabled={isLoading}>
383-
수정
384-
</button>
385-
386-
<button type="button" onClick={handleDelete} disabled={isLoading}>
387-
단건 삭제
388-
</button>
389-
390-
<button type="button" onClick={handleDeleteAll} disabled={isLoading}>
391-
전체 삭제
392-
</button>
393-
394-
<button type="button" onClick={handleCreate} disabled={isLoading}>
395-
신규 생성
396-
</button>
397-
</section>
398-
399-
<section style={{ marginTop: 20 }}>
400-
<h2>JSON 파일 Sync</h2>
401-
402-
<input
403-
type="file"
404-
accept=".json,application/json"
405-
onChange={(e) => {
406-
setSelectedFile(e.currentTarget.files?.[0] ?? null);
407-
}}
408-
/>
409-
410-
<button
411-
type="button"
412-
onClick={handleSyncFile}
413-
disabled={isLoading}
414-
style={{ marginLeft: 8 }}
415-
>
416-
sync-file 업로드
417-
</button>
418-
</section>
419-
420-
<section style={{ marginTop: 20 }}>
421-
<h2>adminOverriddenFields lock/unlock</h2>
422-
423-
<p style={{ margin: "8px 0" }}>
424-
마지막 응답 기준 lock: {overrideFields.length > 0 ? overrideFields.join(", ") : "없음"}
425-
</p>
426-
427-
<div
428-
style={{
429-
display: "grid",
430-
gridTemplateColumns: "repeat(4, minmax(0, 1fr))",
431-
gap: 8,
432-
}}
433-
>
434-
{OVERRIDABLE_FIELDS.map(({ key, label }) => (
435-
<label key={key} style={{ display: "flex", gap: 6 }}>
436-
<input
437-
type="checkbox"
438-
checked={selectedOverrideFields.includes(key)}
439-
onChange={() => toggleOverrideField(key)}
440-
/>
441-
{label} ({key})
442-
</label>
443-
))}
359+
<div className={styles.page}>
360+
<div className={styles.inner}>
361+
<h1 className={styles.pageTitle}>행샤 어드민</h1>
362+
363+
<div className={styles.layout}>
364+
{/* ── 왼쪽 패널 ── */}
365+
<div className={styles.leftPanel}>
366+
{/* 행사 관리 */}
367+
<div className={styles.card}>
368+
<p className={styles.cardTitle}>행사 관리</p>
369+
<div className={styles.idBar}>
370+
<input
371+
className={styles.idInput}
372+
value={eventId}
373+
onChange={(e) => setEventId(e.currentTarget.value)}
374+
placeholder="행사 ID"
375+
/>
376+
377+
<button
378+
type="button"
379+
className={`${styles.btn} ${styles.btnPrimary}`}
380+
onClick={handleLoad}
381+
disabled={isLoading}
382+
>
383+
불러오기
384+
</button>
385+
386+
<button
387+
type="button"
388+
className={`${styles.btn} ${styles.btnPrimary}`}
389+
onClick={handlePatch}
390+
disabled={isLoading}
391+
>
392+
수정
393+
</button>
394+
395+
<button
396+
type="button"
397+
className={`${styles.btn} ${styles.btnPrimary}`}
398+
onClick={handleCreate}
399+
disabled={isLoading}
400+
>
401+
신규 생성
402+
</button>
403+
404+
<div className={styles.divider} />
405+
406+
<button
407+
type="button"
408+
className={`${styles.btn} ${styles.btnDanger}`}
409+
onClick={handleDelete}
410+
disabled={isLoading}
411+
>
412+
단건 삭제
413+
</button>
414+
415+
<button
416+
type="button"
417+
className={`${styles.btn} ${styles.btnDanger}`}
418+
onClick={handleDeleteAll}
419+
disabled={isLoading}
420+
>
421+
전체 삭제
422+
</button>
423+
</div>
424+
</div>
425+
426+
{/* 피드백 메시지 */}
427+
{message && <p className={styles.message}>{message}</p>}
428+
429+
{/* JSON 파일 Sync */}
430+
<div className={styles.card}>
431+
<p className={styles.cardTitle}>JSON 파일 Sync</p>
432+
<div className={styles.fileRow}>
433+
<label className={styles.fileLabel}>
434+
파일 선택
435+
<input
436+
type="file"
437+
accept=".json,application/json"
438+
className={styles.fileInput}
439+
onChange={(e) => {
440+
setSelectedFile(e.currentTarget.files?.[0] ?? null);
441+
}}
442+
/>
443+
</label>
444+
445+
<span className={styles.fileName}>
446+
{selectedFile ? selectedFile.name : "선택된 파일 없음"}
447+
</span>
448+
449+
<button
450+
type="button"
451+
className={`${styles.btn} ${styles.btnSecondary}`}
452+
onClick={handleSyncFile}
453+
disabled={isLoading}
454+
>
455+
업로드
456+
</button>
457+
</div>
458+
</div>
459+
460+
{/* adminOverriddenFields lock/unlock */}
461+
<div className={`${styles.card} ${styles.cardGrow}`}>
462+
<p className={styles.cardTitle}>Override Fields</p>
463+
464+
<div className={styles.overrideMeta}>
465+
<span className={styles.overrideMetaLabel}>현재 lock</span>
466+
{overrideFields.length > 0 ? (
467+
overrideFields.map((f) => (
468+
<span key={f} className={styles.lockedBadge}>
469+
{f}
470+
</span>
471+
))
472+
) : (
473+
<span className={styles.overrideMetaLabel}>없음</span>
474+
)}
475+
</div>
476+
477+
<div className={styles.checkboxGrid}>
478+
{OVERRIDABLE_FIELDS.map(({ key, label }) => {
479+
const checked = selectedOverrideFields.includes(key);
480+
return (
481+
<label
482+
key={key}
483+
className={`${styles.checkboxLabel} ${checked ? styles.checkboxLabelChecked : ""}`}
484+
>
485+
<input
486+
type="checkbox"
487+
className={styles.checkboxNative}
488+
checked={checked}
489+
onChange={() => toggleOverrideField(key)}
490+
/>
491+
{label}
492+
<span className={styles.checkboxKey}>({key})</span>
493+
</label>
494+
);
495+
})}
496+
</div>
497+
498+
<div className={styles.overrideActions}>
499+
<button
500+
type="button"
501+
className={`${styles.btn} ${styles.btnLock}`}
502+
onClick={() => handleUpdateOverrides("lock")}
503+
disabled={isLoading}
504+
>
505+
선택 필드 lock
506+
</button>
507+
508+
<button
509+
type="button"
510+
className={`${styles.btn} ${styles.btnSecondary}`}
511+
onClick={() => handleUpdateOverrides("unlock")}
512+
disabled={isLoading}
513+
>
514+
선택 필드 unlock
515+
</button>
516+
</div>
517+
</div>
518+
</div>
519+
520+
{/* ── 오른쪽 패널 ── */}
521+
<div className={styles.rightPanel}>
522+
<div className={styles.formCard}>
523+
<p className={styles.cardTitle}>행사 데이터</p>
524+
<div className={styles.formGrid}>
525+
{fields.map(([key, label, type]) => (
526+
<div key={key} className={styles.formField}>
527+
<label className={styles.formLabel}>{label}</label>
528+
<input
529+
type={type}
530+
className={styles.formInput}
531+
value={form[key]}
532+
onChange={(e) => updateForm(key, e.currentTarget.value)}
533+
/>
534+
</div>
535+
))}
536+
537+
<div className={`${styles.formField} ${styles.formFieldFull}`}>
538+
<label className={styles.formLabel}>상세 HTML</label>
539+
<textarea
540+
className={styles.formTextarea}
541+
value={form.mainContentHtml}
542+
onChange={(e) => updateForm("mainContentHtml", e.currentTarget.value)}
543+
/>
544+
</div>
545+
</div>
546+
</div>
547+
</div>
444548
</div>
445-
446-
<div style={{ display: "flex", gap: 8, marginTop: 12 }}>
447-
<button
448-
type="button"
449-
onClick={() => handleUpdateOverrides("lock")}
450-
disabled={isLoading}
451-
>
452-
선택 필드 lock
453-
</button>
454-
455-
<button
456-
type="button"
457-
onClick={() => handleUpdateOverrides("unlock")}
458-
disabled={isLoading}
459-
>
460-
선택 필드 unlock
461-
</button>
462-
</div>
463-
</section>
464-
465-
{message && <p style={{ marginTop: 16 }}>{message}</p>}
466-
467-
<section
468-
style={{
469-
display: "grid",
470-
gridTemplateColumns: "repeat(2, minmax(0, 1fr))",
471-
gap: 12,
472-
marginTop: 20,
473-
}}
474-
>
475-
{fields.map(([key, label, type]) => (
476-
<label
477-
key={key}
478-
style={{ display: "flex", flexDirection: "column", gap: 4 }}
479-
>
480-
{label}
481-
<input
482-
type={type}
483-
value={form[key]}
484-
onChange={(e) => updateForm(key, e.currentTarget.value)}
485-
/>
486-
</label>
487-
))}
488-
489-
<label
490-
style={{
491-
display: "flex",
492-
flexDirection: "column",
493-
gap: 4,
494-
gridColumn: "1 / -1",
495-
}}
496-
>
497-
상세 HTML
498-
<textarea
499-
value={form.mainContentHtml}
500-
onChange={(e) => updateForm("mainContentHtml", e.currentTarget.value)}
501-
rows={12}
502-
/>
503-
</label>
504-
</section>
505-
</main>
549+
</div>
550+
</div>
506551
);
507-
}
552+
}

0 commit comments

Comments
 (0)