업로드 이미지 WebP 최적화 1차 적용
This commit is contained in:
@@ -32,6 +32,7 @@ const {
|
||||
adminDeleteUser,
|
||||
} = require('../db')
|
||||
const { requireAdmin } = require('../middleware/auth')
|
||||
const { createMemoryUpload, writeOptimizedImage } = require('../lib/image-storage')
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
@@ -53,21 +54,8 @@ function buildItemLabelFromFilename(file) {
|
||||
return normalized || 'item'
|
||||
}
|
||||
|
||||
const upload = multer({
|
||||
storage: multer.diskStorage({
|
||||
destination: (req, file, cb) => cb(null, path.join(__dirname, '..', '..', 'uploads', 'games')),
|
||||
filename: (req, file, cb) => cb(null, buildUploadFilename(file)),
|
||||
}),
|
||||
limits: { fileSize: 6 * 1024 * 1024 },
|
||||
})
|
||||
|
||||
const avatarUpload = multer({
|
||||
storage: multer.diskStorage({
|
||||
destination: (req, file, cb) => cb(null, path.join(__dirname, '..', '..', 'uploads', 'avatars')),
|
||||
filename: (req, file, cb) => cb(null, buildUploadFilename(file)),
|
||||
}),
|
||||
limits: { fileSize: 3 * 1024 * 1024 },
|
||||
})
|
||||
const upload = createMemoryUpload(multer, { fileSize: 8 * 1024 * 1024, maxCount: 50 })
|
||||
const avatarUpload = createMemoryUpload(multer, { fileSize: 4 * 1024 * 1024 })
|
||||
|
||||
router.post('/games', requireAdmin, async (req, res) => {
|
||||
const schema = z.object({ id: z.string().min(1), name: z.string().min(1).max(60) })
|
||||
@@ -97,7 +85,17 @@ router.post('/games/:gameId/thumbnail', requireAdmin, upload.single('thumbnail')
|
||||
if (!req.file) return res.status(400).json({ error: 'file_required' })
|
||||
const game = await findGameById(req.params.gameId)
|
||||
if (!game) return res.status(404).json({ error: 'not_found' })
|
||||
const updated = await updateGameThumbnail(req.params.gameId, `/uploads/games/${req.file.filename}`)
|
||||
|
||||
const optimized = await writeOptimizedImage({
|
||||
file: req.file,
|
||||
directory: 'games',
|
||||
width: 1280,
|
||||
height: 1280,
|
||||
fit: 'inside',
|
||||
quality: 84,
|
||||
})
|
||||
|
||||
const updated = await updateGameThumbnail(req.params.gameId, optimized.src)
|
||||
res.json({ game: updated })
|
||||
})
|
||||
|
||||
@@ -113,14 +111,23 @@ router.post('/games/:gameId/images', requireAdmin, upload.array('images', 50), a
|
||||
if (normalizedLabels.some((label) => label.length > 60)) return res.status(400).json({ error: 'bad_request' })
|
||||
|
||||
const items = await Promise.all(
|
||||
files.map((file, index) =>
|
||||
createGameItem({
|
||||
files.map(async (file, index) => {
|
||||
const optimized = await writeOptimizedImage({
|
||||
file,
|
||||
directory: 'games',
|
||||
width: 512,
|
||||
height: 512,
|
||||
fit: 'inside',
|
||||
quality: 84,
|
||||
})
|
||||
|
||||
return createGameItem({
|
||||
id: nanoid(),
|
||||
gameId: game.id,
|
||||
src: `/uploads/games/${file.filename}`,
|
||||
src: optimized.src,
|
||||
label: normalizedLabels[index] || buildItemLabelFromFilename(file),
|
||||
})
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
res.json({ item: items[0], items })
|
||||
@@ -514,8 +521,18 @@ router.post('/users/:userId/avatar', requireAdmin, avatarUpload.single('avatar')
|
||||
const user = await findUserById(req.params.userId)
|
||||
if (!user) return res.status(404).json({ error: 'not_found' })
|
||||
|
||||
const optimized = req.file
|
||||
? await writeOptimizedImage({
|
||||
file: req.file,
|
||||
directory: 'avatars',
|
||||
width: 512,
|
||||
height: 512,
|
||||
fit: 'cover',
|
||||
quality: 82,
|
||||
})
|
||||
: null
|
||||
const shouldRemoveAvatar = parsed.data.removeAvatar === '1'
|
||||
const nextAvatarSrc = shouldRemoveAvatar ? '' : req.file ? `/uploads/avatars/${req.file.filename}` : user.avatarSrc || ''
|
||||
const nextAvatarSrc = shouldRemoveAvatar ? '' : optimized?.src || user.avatarSrc || ''
|
||||
const updated = await adminUpdateUser({
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
const express = require('express')
|
||||
const path = require('path')
|
||||
const bcrypt = require('bcryptjs')
|
||||
const { z } = require('zod')
|
||||
const { nanoid } = require('nanoid')
|
||||
@@ -12,15 +11,10 @@ const {
|
||||
updateUserProfile,
|
||||
} = require('../db')
|
||||
const { requireAuth } = require('../middleware/auth')
|
||||
const { createMemoryUpload, writeOptimizedImage } = require('../lib/image-storage')
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
function buildUploadFilename(file) {
|
||||
const ext = path.extname(file.originalname || '').toLowerCase()
|
||||
const safeExt = ext && /^[.a-z0-9]+$/.test(ext) ? ext : ''
|
||||
return `${Date.now()}-${nanoid()}${safeExt}`
|
||||
}
|
||||
|
||||
const signupSchema = z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string().min(6),
|
||||
@@ -45,7 +39,6 @@ router.post('/signup', async (req, res) => {
|
||||
|
||||
req.session.userId = user.id
|
||||
req.session.isAdmin = !!user.isAdmin
|
||||
// 세션을 응답 전에 명시적으로 저장해 Set-Cookie가 확실히 내려오도록 보강
|
||||
req.session.save((err) => {
|
||||
if (err) return res.status(500).json({ error: 'session_save_failed' })
|
||||
res.json(user)
|
||||
@@ -94,13 +87,7 @@ router.get('/meta', async (req, res) => {
|
||||
res.json({ hasUsers: (await countUsers()) > 0 })
|
||||
})
|
||||
|
||||
const upload = multer({
|
||||
storage: multer.diskStorage({
|
||||
destination: (req, file, cb) => cb(null, path.join(__dirname, '..', '..', 'uploads', 'avatars')),
|
||||
filename: (req, file, cb) => cb(null, buildUploadFilename(file)),
|
||||
}),
|
||||
limits: { fileSize: 3 * 1024 * 1024 },
|
||||
})
|
||||
const upload = createMemoryUpload(multer, { fileSize: 4 * 1024 * 1024 })
|
||||
|
||||
router.post('/profile', requireAuth, upload.single('avatar'), async (req, res) => {
|
||||
const parsed = profileSchema.safeParse(req.body)
|
||||
@@ -109,12 +96,19 @@ router.post('/profile', requireAuth, upload.single('avatar'), async (req, res) =
|
||||
const user = await findUserById(req.session.userId)
|
||||
if (!user) return res.status(404).json({ error: 'not_found' })
|
||||
|
||||
const optimized = req.file
|
||||
? await writeOptimizedImage({
|
||||
file: req.file,
|
||||
directory: 'avatars',
|
||||
width: 512,
|
||||
height: 512,
|
||||
fit: 'cover',
|
||||
quality: 82,
|
||||
})
|
||||
: null
|
||||
|
||||
const shouldRemoveAvatar = parsed.data.removeAvatar === '1'
|
||||
const nextAvatarSrc = shouldRemoveAvatar
|
||||
? ''
|
||||
: req.file
|
||||
? `/uploads/avatars/${req.file.filename}`
|
||||
: user.avatarSrc || ''
|
||||
const nextAvatarSrc = shouldRemoveAvatar ? '' : optimized?.src || user.avatarSrc || ''
|
||||
const updated = await updateUserProfile({
|
||||
id: user.id,
|
||||
nickname: parsed.data.nickname,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
const express = require('express')
|
||||
const path = require('path')
|
||||
const multer = require('multer')
|
||||
const { z } = require('zod')
|
||||
const { nanoid } = require('nanoid')
|
||||
@@ -18,6 +17,7 @@ const {
|
||||
duplicateTierListForUser,
|
||||
} = require('../db')
|
||||
const { requireAuth } = require('../middleware/auth')
|
||||
const { createMemoryUpload, writeOptimizedImage } = require('../lib/image-storage')
|
||||
|
||||
const router = express.Router()
|
||||
const FREEFORM_GAME_ID = 'freeform'
|
||||
@@ -55,27 +55,8 @@ function getCustomTemplateItems(tierList) {
|
||||
})
|
||||
}
|
||||
|
||||
function buildUploadFilename(file) {
|
||||
const ext = path.extname(file.originalname || '').toLowerCase()
|
||||
const safeExt = ext && /^[.a-z0-9]+$/.test(ext) ? ext : ''
|
||||
return `${Date.now()}-${nanoid()}${safeExt}`
|
||||
}
|
||||
|
||||
const upload = multer({
|
||||
storage: multer.diskStorage({
|
||||
destination: (req, file, cb) => cb(null, path.join(__dirname, '..', '..', 'uploads', 'custom')),
|
||||
filename: (req, file, cb) => cb(null, buildUploadFilename(file)),
|
||||
}),
|
||||
limits: { fileSize: 6 * 1024 * 1024 },
|
||||
})
|
||||
|
||||
const thumbnailUpload = multer({
|
||||
storage: multer.diskStorage({
|
||||
destination: (req, file, cb) => cb(null, path.join(__dirname, '..', '..', 'uploads', 'tierlists')),
|
||||
filename: (req, file, cb) => cb(null, buildUploadFilename(file)),
|
||||
}),
|
||||
limits: { fileSize: 6 * 1024 * 1024 },
|
||||
})
|
||||
const upload = createMemoryUpload(multer, { fileSize: 6 * 1024 * 1024 })
|
||||
const thumbnailUpload = createMemoryUpload(multer, { fileSize: 8 * 1024 * 1024 })
|
||||
|
||||
const tierListUpsertSchema = z.object({
|
||||
id: z.string().optional(),
|
||||
@@ -179,10 +160,19 @@ router.post('/custom-items', requireAuth, upload.single('image'), async (req, re
|
||||
const parsed = schema.safeParse(req.body)
|
||||
if (!parsed.success) return res.status(400).json({ error: 'bad_request' })
|
||||
|
||||
const optimized = await writeOptimizedImage({
|
||||
file: req.file,
|
||||
directory: 'custom',
|
||||
width: 512,
|
||||
height: 512,
|
||||
fit: 'inside',
|
||||
quality: 84,
|
||||
})
|
||||
|
||||
const item = await createCustomItem({
|
||||
id: nanoid(),
|
||||
ownerId: req.session.userId,
|
||||
src: `/uploads/custom/${req.file.filename}`,
|
||||
src: optimized.src,
|
||||
label: parsed.data.label,
|
||||
})
|
||||
|
||||
@@ -191,7 +181,17 @@ router.post('/custom-items', requireAuth, upload.single('image'), async (req, re
|
||||
|
||||
router.post('/thumbnail', requireAuth, thumbnailUpload.single('thumbnail'), async (req, res) => {
|
||||
if (!req.file) return res.status(400).json({ error: 'file_required' })
|
||||
res.json({ thumbnailSrc: `/uploads/tierlists/${req.file.filename}` })
|
||||
|
||||
const optimized = await writeOptimizedImage({
|
||||
file: req.file,
|
||||
directory: 'tierlists',
|
||||
width: 1280,
|
||||
height: 1280,
|
||||
fit: 'inside',
|
||||
quality: 84,
|
||||
})
|
||||
|
||||
res.json({ thumbnailSrc: optimized.src })
|
||||
})
|
||||
|
||||
router.post('/:id/template-request', requireAuth, async (req, res) => {
|
||||
|
||||
Reference in New Issue
Block a user