feat: 첨부파일 다중 업로드, 관리자 전용 업로드, 파일유형별 다운로드 아이콘
- 업로드시 제목 입력 + 첨부파일 최대 3개 등록 가능 (posts/attachments 구조 도입) - 개별 첨부파일 다운로드 API 및 UI 추가 (일반/관리자 페이지 모두) - 파일 업로드는 관리자만 가능하도록 서버 인증 적용, 일반 페이지 업로드 버튼 제거 - 다운로드 아이콘을 파일 유형(문서/이미지/동영상/압축/코드)별로 색상·아이콘 구분 - 관리자 페이지 강조 버튼 hover 시 배경색 사라지던 CSS 버그 수정 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+59
-177
@@ -143,7 +143,7 @@
|
||||
|
||||
/* LIST */
|
||||
.list{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-lg);overflow:hidden}
|
||||
.list-head,.list-row{display:grid;grid-template-columns:32px 1fr 100px 130px 130px 70px 32px;gap:14px;align-items:center;padding:10px 16px}
|
||||
.list-head,.list-row{display:grid;grid-template-columns:32px 1fr 100px 130px 130px 118px 32px;gap:14px;align-items:center;padding:10px 16px}
|
||||
.list-head{font-size:11px;font-weight:600;letter-spacing:.05em;text-transform:uppercase;color:var(--faint);border-bottom:1px solid var(--border);background:var(--surface-2)}
|
||||
.list-row{border-bottom:1px solid var(--border);font-size:13.5px;color:var(--ink-2);cursor:default;transition:background .12s ease}
|
||||
.list-row:last-child{border-bottom:0}
|
||||
@@ -160,18 +160,20 @@
|
||||
.list-name .ftxt .nm{font-weight:500;color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.list-name .ftxt .pth{font-size:11px;color:var(--faint);font-family:var(--font-mono);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.list-meta{font-family:var(--font-mono);font-size:12px;color:var(--muted);font-variant-numeric:tabular-nums}
|
||||
.list-dl{display:flex;align-items:center;gap:4px}
|
||||
.dlbtn{width:22px;height:22px;border:1px solid var(--border-strong);border-radius:5px;color:var(--muted);background:var(--surface);display:inline-flex;align-items:center;justify-content:center;flex-shrink:0}
|
||||
.dlbtn:hover{filter:brightness(0.96)}
|
||||
.dlbtn.dlbtn-doc{background:#fef4e6;color:#b56b18;border-color:transparent}
|
||||
.dlbtn.dlbtn-img{background:#e8f4ee;color:#1f7a52;border-color:transparent}
|
||||
.dlbtn.dlbtn-vid{background:#1a1a1a;color:#fff;border-color:transparent}
|
||||
.dlbtn.dlbtn-zip{background:#f3eed9;color:#7a6418;border-color:transparent}
|
||||
.dlbtn.dlbtn-code{background:#0f1115;color:#a3b8ff;border-color:transparent}
|
||||
.check{width:16px;height:16px;border-radius:4px;border:1.5px solid var(--border-strong);display:grid;place-items:center;cursor:default;color:transparent;background:var(--surface);transition:all .12s ease}
|
||||
.check:hover{border-color:var(--ink-2)}
|
||||
.check.on{background:var(--accent);border-color:var(--accent);color:#fff}
|
||||
.iconbtn{appearance:none;border:0;background:transparent;color:var(--faint);width:28px;height:28px;border-radius:5px;cursor:default;display:grid;place-items:center}
|
||||
.iconbtn:hover{background:rgba(20,20,20,.06);color:var(--ink)}
|
||||
|
||||
/* Drop zone */
|
||||
.dropzone{position:absolute;inset:0;background:rgba(59,91,219,.06);backdrop-filter:blur(2px);border:2px dashed var(--accent);border-radius:18px;margin:12px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:14px;pointer-events:none;z-index:200;opacity:0;transition:opacity .15s ease}
|
||||
.dropzone.on{opacity:1}
|
||||
.dropzone .t{font-size:18px;font-weight:600;color:var(--accent)}
|
||||
.dropzone .s{font-size:13px;color:var(--ink-2);font-family:var(--font-mono)}
|
||||
|
||||
/* Selection bar */
|
||||
.selbar{position:fixed;left:50%;bottom:24px;transform:translateX(-50%);background:var(--ink);color:#fff;padding:8px 8px 8px 18px;border-radius:99px;display:flex;align-items:center;gap:14px;box-shadow:var(--shadow-lg);z-index:100;font-size:13.5px}
|
||||
.selbar .selcount{font-weight:500}
|
||||
@@ -188,19 +190,6 @@
|
||||
.modal-h{display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-bottom:1px solid var(--border)}
|
||||
.modal-h h3{margin:0;font-size:15px;font-weight:600}
|
||||
.modal-b{padding:20px;overflow-y:auto}
|
||||
.updz{border:2px dashed var(--border-strong);border-radius:12px;padding:36px 20px;text-align:center;color:var(--muted);transition:all .15s ease;background:var(--surface-2)}
|
||||
.updz.on{border-color:var(--accent);background:var(--accent-soft);color:var(--accent)}
|
||||
.updz .t{font-size:15px;font-weight:600;color:var(--ink);margin-bottom:4px}
|
||||
.updz .s{font-size:12.5px;color:var(--muted);font-family:var(--font-mono)}
|
||||
.updz button{margin-top:14px}
|
||||
.uplist{margin-top:14px;display:flex;flex-direction:column;gap:8px}
|
||||
.uprow{display:flex;align-items:center;gap:12px;padding:10px 12px;background:var(--surface-2);border-radius:8px}
|
||||
.uprow .ficon{width:32px;height:32px;border-radius:6px;background:var(--surface);display:grid;place-items:center;font-family:var(--font-mono);font-size:9px;font-weight:600;color:var(--muted);flex-shrink:0}
|
||||
.uprow .info{flex:1;min-width:0}
|
||||
.uprow .nm{font-size:13px;font-weight:500;color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.uprow .progbar{height:3px;background:rgba(20,20,20,.08);border-radius:99px;margin-top:5px;overflow:hidden}
|
||||
.uprow .progfill{height:100%;background:var(--accent);transition:width .3s ease}
|
||||
.uprow .pct{font-family:var(--font-mono);font-size:11px;color:var(--muted);min-width:36px;text-align:right}
|
||||
|
||||
/* Preview modal */
|
||||
.preview-modal{width:min(960px,94vw);max-height:90vh}
|
||||
@@ -217,6 +206,8 @@
|
||||
.preview-info .infogrp{display:flex;flex-direction:column;gap:7px}
|
||||
.preview-info .infogrp .lbl{font-size:11px;font-weight:600;letter-spacing:.05em;text-transform:uppercase;color:var(--faint)}
|
||||
.preview-info .row{display:flex;justify-content:space-between;font-size:13px;color:var(--ink-2);font-family:var(--font-mono)}
|
||||
.preview-info .infogrp .row.att{padding:6px 8px;margin:0 -8px;border-radius:6px;gap:10px}
|
||||
.preview-info .infogrp .row.att:hover{background:var(--surface-2);color:var(--accent)}
|
||||
.preview-info .row span:last-child{color:var(--ink);font-variant-numeric:tabular-nums}
|
||||
.preview-info .actions{display:flex;flex-direction:column;gap:6px;margin-top:auto}
|
||||
|
||||
@@ -651,6 +642,8 @@ const FIcon = ({ type, ext, size = 28 }) => (
|
||||
<div className={`ficon ${type}`} style={{ width: size, height: size, borderRadius: size * 0.18 }}>{ext}</div>
|
||||
);
|
||||
|
||||
const TYPE_ICON = { doc: 'file', img: 'image', vid: 'video', zip: 'folder', code: 'code' };
|
||||
|
||||
const Thumb = ({ file }) => {
|
||||
const k = file.thumb;
|
||||
if (k === "doc") return <div className="thumb tn-doc"><span className="ext">{file.ext}</span><div className="doc-page"><i/><i/><i/><i/><i/><i/></div></div>;
|
||||
@@ -759,7 +752,7 @@ const Sidebar = ({ activeFolder, setActiveFolder, activeCat, setActiveCat, categ
|
||||
);
|
||||
};
|
||||
|
||||
const Header = ({ folder, category, onUpload, accent, categories, currentUser, onLogout, onMenuToggle }) => {
|
||||
const Header = ({ folder, category, categories, currentUser, onLogout, onMenuToggle }) => {
|
||||
const folderObj = FOLDERS_DEF.find(f => f.id === folder);
|
||||
const catObj = categories.find(c => c.id === category);
|
||||
return (
|
||||
@@ -785,9 +778,6 @@ const Header = ({ folder, category, onUpload, accent, categories, currentUser, o
|
||||
관리자
|
||||
</span>
|
||||
<button className="btn btn-ghost" style={{ fontSize:12.5 }} onClick={onLogout}>로그아웃</button>
|
||||
<button className="btn btn-accent" onClick={onUpload} style={{ background:accent, borderColor:accent }}>
|
||||
<Icon name="upload" size={14} />업로드
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -878,7 +868,7 @@ const GridView = ({ files, selected, toggleSel, toggleStar, onOpen, isLoggedIn }
|
||||
</div>
|
||||
);
|
||||
|
||||
const ListView = ({ files, selected, toggleSel, toggleStar, onOpen, categories, isLoggedIn, currentUser, onDownload, onCopyLink, onDelete }) => {
|
||||
const ListView = ({ files, selected, toggleSel, toggleStar, onOpen, categories, isLoggedIn, currentUser, onDownload, onDownloadAttachment, onCopyLink, onDelete }) => {
|
||||
const [menuFile, setMenuFile] = useState(null);
|
||||
const [menuPos, setMenuPos] = useState({ x: 0, y: 0 });
|
||||
const menuRef = useRef(null);
|
||||
@@ -902,8 +892,8 @@ const ListView = ({ files, selected, toggleSel, toggleStar, onOpen, categories,
|
||||
};
|
||||
|
||||
const cols = isLoggedIn
|
||||
? "32px 1fr 100px 130px 130px 70px 52px"
|
||||
: "1fr 100px 130px 130px 70px 52px";
|
||||
? "32px 1fr 100px 130px 130px 118px 52px"
|
||||
: "1fr 100px 130px 130px 118px 52px";
|
||||
|
||||
return (
|
||||
<div className="list">
|
||||
@@ -931,7 +921,15 @@ const ListView = ({ files, selected, toggleSel, toggleStar, onOpen, categories,
|
||||
<div className="list-meta">{f.size_label}</div>
|
||||
<div className="list-meta">{f.modified_at}</div>
|
||||
<div className="list-meta" style={{ fontFamily:"var(--font-sans)", fontSize:13 }}>{f.author}</div>
|
||||
<div className="list-meta">↓ {f.downloads}</div>
|
||||
<div className="list-dl" onClick={e => e.stopPropagation()}>
|
||||
{(f.attachments || []).map(a => (
|
||||
<button key={a.id} className={`dlbtn dlbtn-${a.type}`} title={`${a.name} 다운로드`}
|
||||
onClick={() => onDownloadAttachment(a)}>
|
||||
<Icon name={TYPE_ICON[a.type] || 'file'} size={11} />
|
||||
</button>
|
||||
))}
|
||||
{(!f.attachments || f.attachments.length === 0) && <span className="pth">↓ {f.downloads}</span>}
|
||||
</div>
|
||||
<div style={{display:"flex",gap:2,justifyContent:"flex-end"}}>
|
||||
<button className="iconbtn" onClick={e => { e.stopPropagation(); toggleStar(f.id); }}
|
||||
style={{ color: f.starred ? "#e8a317" : "var(--faint)" }}>
|
||||
@@ -966,109 +964,7 @@ const ListView = ({ files, selected, toggleSel, toggleStar, onOpen, categories,
|
||||
);
|
||||
};
|
||||
|
||||
const UploadModal = ({ onClose, onUploaded, accent, categories, users }) => {
|
||||
const [dragOver, setDragOver] = useState(false);
|
||||
const [uploads, setUploads] = useState([]);
|
||||
const [meta, setMeta] = useState({ author: '', category: '', visibility: 'team' });
|
||||
const inputRef = useRef(null);
|
||||
|
||||
const uploadFile = async file => {
|
||||
const id = `up-${Date.now()}-${Math.random()}`;
|
||||
const ext = (file.name.split('.').pop() || 'FILE').toUpperCase();
|
||||
const type = file.type.startsWith('image/') ? 'img' : file.type.startsWith('video/') ? 'vid' : 'doc';
|
||||
setUploads(u => [...u, { id, name: file.name, ext, type, progress: 10 }]);
|
||||
const fd = new FormData();
|
||||
fd.append('file', file);
|
||||
if (meta.author) fd.append('author', meta.author);
|
||||
if (meta.category) fd.append('category', meta.category);
|
||||
fd.append('visibility', meta.visibility);
|
||||
try {
|
||||
setUploads(u => u.map(x => x.id === id ? { ...x, progress: 50 } : x));
|
||||
const saved = await apiFetch('/api/files/upload', { method: 'POST', body: fd });
|
||||
setUploads(u => u.map(x => x.id === id ? { ...x, progress: 100 } : x));
|
||||
onUploaded(saved);
|
||||
} catch {
|
||||
setUploads(u => u.map(x => x.id === id ? { ...x, progress: -1 } : x));
|
||||
}
|
||||
};
|
||||
|
||||
const addFiles = fileList => Array.from(fileList || []).forEach(uploadFile);
|
||||
const sel = { border:"1px solid var(--border-strong)", borderRadius:6, padding:"6px 8px", fontSize:13, background:"var(--surface)", color:"var(--ink)", outline:"none", width:"100%" };
|
||||
|
||||
return (
|
||||
<div className="modal-bg" onClick={onClose}>
|
||||
<div className="modal" onClick={e => e.stopPropagation()}>
|
||||
<div className="modal-h">
|
||||
<h3>파일 업로드</h3>
|
||||
<button className="iconbtn" onClick={onClose}><Icon name="x" size={16} /></button>
|
||||
</div>
|
||||
<div className="modal-b">
|
||||
<div style={{ display:"grid", gridTemplateColumns:"1fr 1fr 1fr", gap:10, marginBottom:12 }}>
|
||||
<div>
|
||||
<div style={{ fontSize:11, fontWeight:600, color:"var(--muted)", marginBottom:4 }}>업로더</div>
|
||||
<select style={sel} value={meta.author} onChange={e => setMeta(m => ({...m, author: e.target.value}))}>
|
||||
<option value="">자동</option>
|
||||
{users.map(u => <option key={u.id} value={u.name}>{u.name}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize:11, fontWeight:600, color:"var(--muted)", marginBottom:4 }}>카테고리</div>
|
||||
<select style={sel} value={meta.category} onChange={e => setMeta(m => ({...m, category: e.target.value}))}>
|
||||
<option value="">자동</option>
|
||||
{categories.map(c => <option key={c.id} value={c.id}>{c.label}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize:11, fontWeight:600, color:"var(--muted)", marginBottom:4 }}>공개 범위</div>
|
||||
<select style={sel} value={meta.visibility} onChange={e => setMeta(m => ({...m, visibility: e.target.value}))}>
|
||||
<option value="team">팀 공유</option>
|
||||
<option value="public">공개</option>
|
||||
<option value="private">비공개</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`updz ${dragOver ? "on" : ""}`}
|
||||
onDragOver={e => { e.preventDefault(); setDragOver(true); }}
|
||||
onDragLeave={() => setDragOver(false)}
|
||||
onDrop={e => { e.preventDefault(); setDragOver(false); addFiles(e.dataTransfer.files); }}>
|
||||
<Icon name="cloud" size={38} stroke={1.5} style={{color: dragOver ? accent : "var(--muted)"}}/>
|
||||
<div className="t">파일을 여기로 드래그하세요</div>
|
||||
<div className="s">또는 클릭하여 선택 · 최대 2GB · PDF, DOC, PPT, 이미지, 영상, ZIP</div>
|
||||
<input ref={inputRef} type="file" multiple style={{display:"none"}} onChange={e => addFiles(e.target.files)} />
|
||||
<button className="btn btn-primary" onClick={() => inputRef.current?.click()}>
|
||||
<Icon name="upload" size={13} />파일 선택
|
||||
</button>
|
||||
</div>
|
||||
{uploads.length > 0 && (
|
||||
<div className="uplist">
|
||||
{uploads.map(u => (
|
||||
<div key={u.id} className="uprow">
|
||||
<FIcon type={u.type} ext={u.ext} size={32} />
|
||||
<div className="info">
|
||||
<div className="nm">{u.name}</div>
|
||||
<div className="progbar">
|
||||
<div className="progfill" style={{ width:`${Math.max(0, u.progress)}%`, background: u.progress < 0 ? 'var(--warn)' : accent }}></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pct">{u.progress < 0 ? '오류' : u.progress >= 100 ? '✓' : `${Math.round(u.progress)}%`}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{uploads.length > 0 && uploads.every(u => u.progress === 100 || u.progress < 0) && (
|
||||
<div style={{ display:'flex', justifyContent:'flex-end', marginTop:16 }}>
|
||||
<button className="btn btn-accent" onClick={onClose} style={{ background:accent, borderColor:accent }}>
|
||||
<Icon name="check" size={14} />확인
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const PreviewModal = ({ file, onClose, onDownload, onStar, onCopyLink, accent, categories }) => {
|
||||
const PreviewModal = ({ file, onClose, onDownload, onDownloadAttachment, onStar, onCopyLink, accent, categories }) => {
|
||||
if (!file) return null;
|
||||
return (
|
||||
<div className="modal-bg" onClick={onClose}>
|
||||
@@ -1122,6 +1018,22 @@ const PreviewModal = ({ file, onClose, onDownload, onStar, onCopyLink, accent, c
|
||||
<div className="row"><span>수정</span><span>{file.modified_at}</span></div>
|
||||
<div className="row"><span>분류</span><span>{categories.find(c=>c.id===file.category)?.label || "—"}</span></div>
|
||||
</div>
|
||||
{file.attachments && file.attachments.length > 0 && (
|
||||
<div className="infogrp">
|
||||
<span className="lbl">첨부파일 ({file.attachments.length})</span>
|
||||
{file.attachments.map(a => (
|
||||
<div key={a.id} className="row att" onClick={() => onDownloadAttachment(a)}>
|
||||
<span style={{ display:"flex", alignItems:"center", gap:8, minWidth:0 }}>
|
||||
<span className={`dlbtn dlbtn-${a.type}`} style={{ flexShrink:0 }}><Icon name={TYPE_ICON[a.type] || 'file'} size={11} /></span>
|
||||
<span style={{ overflow:"hidden", textOverflow:"ellipsis", whiteSpace:"nowrap" }}>{a.name}</span>
|
||||
</span>
|
||||
<span style={{ display:"flex", alignItems:"center", gap:6, flexShrink:0 }}>
|
||||
{a.size_label}<Icon name="download" size={12} />
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="actions">
|
||||
<button className="btn btn-accent" onClick={() => onDownload(file)} style={{ background:accent, borderColor:accent, justifyContent:"center" }}>
|
||||
<Icon name="download" size={14} />다운로드
|
||||
@@ -1269,16 +1181,11 @@ function App() {
|
||||
|
||||
const [selected, setSelected] = useState(new Set());
|
||||
const [previewing, setPreviewing] = useState(null);
|
||||
const [uploadOpen, setUploadOpen] = useState(false);
|
||||
const [globalDrag, setGlobalDrag] = useState(false);
|
||||
const [toast, setToast] = useState({ on: false, msg: "" });
|
||||
const [currentUser, setCurrentUser] = useState(() => {
|
||||
try { return JSON.parse(sessionStorage.getItem('jaryo_user') || 'null'); } catch { return null; }
|
||||
});
|
||||
|
||||
const dragCounter = useRef(0);
|
||||
const currentUserRef = useRef(null);
|
||||
|
||||
// 액센트 컬러 적용
|
||||
useEffect(() => {
|
||||
document.documentElement.style.setProperty("--accent", t.accent);
|
||||
@@ -1289,8 +1196,6 @@ function App() {
|
||||
document.documentElement.style.setProperty("--radius-lg", `${t.cornerRadius + 4}px`);
|
||||
}, [t.accent, t.cornerRadius]);
|
||||
|
||||
useEffect(() => { currentUserRef.current = currentUser; }, [currentUser]);
|
||||
|
||||
const fetchStats = useCallback(() => {
|
||||
apiFetch('/api/stats').then(setStats).catch(() => {});
|
||||
}, []);
|
||||
@@ -1343,28 +1248,6 @@ function App() {
|
||||
|
||||
const showToast = msg => { setToast({ on: true, msg }); setTimeout(() => setToast(s => ({...s, on: false})), 2200); };
|
||||
|
||||
// 전역 드래그
|
||||
useEffect(() => {
|
||||
const onDragEnter = e => { if (e.dataTransfer?.types?.includes("Files")) { dragCounter.current++; setGlobalDrag(true); } };
|
||||
const onDragLeave = () => { dragCounter.current = Math.max(0, dragCounter.current - 1); if (!dragCounter.current) setGlobalDrag(false); };
|
||||
const onDragOver = e => e.preventDefault();
|
||||
const onDrop = e => {
|
||||
e.preventDefault(); dragCounter.current = 0; setGlobalDrag(false);
|
||||
if (currentUserRef.current) setUploadOpen(true);
|
||||
else setToast({ on: true, msg: '업로드는 관리자만 가능합니다' });
|
||||
};
|
||||
window.addEventListener("dragenter", onDragEnter);
|
||||
window.addEventListener("dragleave", onDragLeave);
|
||||
window.addEventListener("dragover", onDragOver);
|
||||
window.addEventListener("drop", onDrop);
|
||||
return () => {
|
||||
window.removeEventListener("dragenter", onDragEnter);
|
||||
window.removeEventListener("dragleave", onDragLeave);
|
||||
window.removeEventListener("dragover", onDragOver);
|
||||
window.removeEventListener("drop", onDrop);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const toggleSel = id => {
|
||||
if (!currentUser) return;
|
||||
setSelected(prev => { const next = new Set(prev); next.has(id) ? next.delete(id) : next.add(id); return next; });
|
||||
@@ -1409,6 +1292,18 @@ function App() {
|
||||
} catch { showToast("오류가 발생했습니다"); }
|
||||
};
|
||||
|
||||
const onDownloadAttachment = a => {
|
||||
const el = document.createElement('a');
|
||||
el.href = `/api/attachments/${a.id}/download`;
|
||||
el.style.display = 'none';
|
||||
document.body.appendChild(el);
|
||||
el.click();
|
||||
document.body.removeChild(el);
|
||||
setFiles(prev => prev.map(x => x.id === a.file_id ? { ...x, downloads: x.downloads + 1 } : x));
|
||||
if (previewing?.id === a.file_id) setPreviewing(p => ({ ...p, downloads: (p.downloads || 0) + 1 }));
|
||||
showToast(`${a.name} 다운로드를 시작합니다`);
|
||||
};
|
||||
|
||||
const onCopyLink = async f => {
|
||||
const url = `${window.location.origin}/api/files/${f.id}/download`;
|
||||
try {
|
||||
@@ -1460,12 +1355,6 @@ function App() {
|
||||
|
||||
const onBulkDelete = () => deleteFiles([...selected]);
|
||||
|
||||
const onUploaded = newFile => {
|
||||
setFiles(prev => [newFile, ...prev]);
|
||||
fetchStats();
|
||||
showToast(`${newFile.name} 업로드 완료`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="app" style={{ fontSize: t.density === "compact" ? 13 : t.density === "comfy" ? 15 : 14 }}>
|
||||
{sidebarOpen && <div className="sb-overlay on" onClick={() => setSidebarOpen(false)} />}
|
||||
@@ -1481,7 +1370,7 @@ function App() {
|
||||
<div className="main">
|
||||
<Header
|
||||
folder={activeFolder} category={activeCat}
|
||||
onUpload={() => setUploadOpen(true)} accent={t.accent} categories={categories}
|
||||
categories={categories}
|
||||
currentUser={currentUser} onLogout={handleLogout}
|
||||
onMenuToggle={() => setSidebarOpen(o => !o)}
|
||||
/>
|
||||
@@ -1510,21 +1399,15 @@ function App() {
|
||||
<div className="empty">
|
||||
<Icon name="folder-open" size={42} stroke={1.4} />
|
||||
<div className="t">표시할 자료가 없습니다</div>
|
||||
<div className="s">필터를 조정하거나 새 파일을 업로드해보세요</div>
|
||||
<div className="s">필터를 조정해보세요</div>
|
||||
</div>
|
||||
) : view === "grid" ? (
|
||||
<GridView files={files} selected={selected} toggleSel={toggleSel} toggleStar={toggleStar} onOpen={setPreviewing} isLoggedIn={!!currentUser} />
|
||||
) : (
|
||||
<ListView files={files} selected={selected} toggleSel={toggleSel} toggleStar={toggleStar} onOpen={setPreviewing} categories={categories}
|
||||
isLoggedIn={!!currentUser} currentUser={currentUser}
|
||||
onDownload={onDownload} onCopyLink={onCopyLink} onDelete={deleteFiles} />
|
||||
onDownload={onDownload} onDownloadAttachment={onDownloadAttachment} onCopyLink={onCopyLink} onDelete={deleteFiles} />
|
||||
)}
|
||||
|
||||
<div className={`dropzone ${globalDrag ? "on" : ""}`}>
|
||||
<Icon name="cloud" size={48} stroke={1.4} />
|
||||
<div className="t">파일을 드롭하여 업로드</div>
|
||||
<div className="s">최대 2GB · 다중 파일 지원</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1534,13 +1417,12 @@ function App() {
|
||||
isAdmin={currentUser?.isAdmin} />
|
||||
)}
|
||||
|
||||
{uploadOpen && <UploadModal onClose={() => setUploadOpen(false)} onUploaded={onUploaded} accent={t.accent} categories={categories} users={[]} />}
|
||||
|
||||
{previewing && (
|
||||
<PreviewModal
|
||||
file={previewing}
|
||||
onClose={() => setPreviewing(null)}
|
||||
onDownload={f => { onDownload(f); setPreviewing(null); }}
|
||||
onDownloadAttachment={onDownloadAttachment}
|
||||
onStar={id => toggleStar(id)}
|
||||
onCopyLink={onCopyLink}
|
||||
accent={t.accent}
|
||||
|
||||
Reference in New Issue
Block a user