import { apiRequest } from './api.js'; import { mountAppBar } from './app-bar.js'; import { bindLogout, loadSession } from './auth.js'; import { bindScrollToTop } from './scroll-to-top.js'; const appBar = mountAppBar(); const detail = document.querySelector('#recipe-detail'); const toast = document.querySelector('#toast'); const scrollToTop = document.querySelector('#scroll-to-top'); let currentRecipe = null; let currentUser = null; let toastTimer; bindScrollToTop(scrollToTop); function escapeHtml(value) { return String(value ?? '') .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll('"', '"') .replaceAll("'", '''); } function youtubeTimestampUrl(sourceUrl, seconds) { try { const url = new URL(sourceUrl); url.searchParams.set('t', `${Math.round(seconds)}s`); return url.href; } catch { return sourceUrl; } } function showToast(message) { window.clearTimeout(toastTimer); toast.textContent = message; toast.hidden = false; toastTimer = window.setTimeout(() => { toast.hidden = true; }, 3200); } function ingredientGroupTemplate(group, groupIndex) { return `
${group.items.map((item, itemIndex) => `
`).join('')}
`; } function readRecipeForm(form) { const ingredientGroups = [...form.querySelectorAll('[data-ingredient-group]')].map((group) => ({ name: group.querySelector('[data-group-name]').value.trim(), items: [...group.querySelectorAll('[data-ingredient-item]')].map((item) => ({ name: item.querySelector('[data-item-name]').value.trim(), amount: item.querySelector('[data-item-amount]').value.trim() || null, })), })); const steps = [...form.querySelectorAll('[data-step]')].map((step, index) => { const timestamp = step.querySelector('[data-step-time]').value; return { order: index + 1, text: step.querySelector('[data-step-text]').value.trim(), timestampSec: timestamp === '' ? null : Number(timestamp), }; }); return { title: form.querySelector('#recipe-title').value.trim(), summary: form.querySelector('#recipe-summary').value.trim() || null, servings: form.querySelector('#recipe-servings').value.trim() || null, ingredientGroups, steps, tips: form.querySelector('#recipe-tips').value.split(/\r?\n/).map((item) => item.trim()).filter(Boolean), tags: form.querySelector('#recipe-tags').value.split(',').map((item) => item.trim()).filter(Boolean), }; } function renderRecipeEditor(recipe) { const image = recipe.imagePath ? `/media/${recipe.imagePath}` : null; detail.innerHTML = ` ← Recipe Library

EDIT RECIPE

레시피 수정

${image ? `` : 'NO IMAGE'} ${escapeHtml(recipe.source.platform)}

재료

${recipe.ingredientGroups.map(ingredientGroupTemplate).join('')}

조리 순서

${recipe.steps.map((step, index) => `
${index + 1}
`).join('')}
`; const form = document.querySelector('#recipe-edit-form'); document.querySelector('#cancel-edit').addEventListener('click', () => renderRecipe(currentRecipe)); form.addEventListener('click', (event) => { const button = event.target.closest('[data-action]'); if (!button) return; const draft = { ...recipe, ...readRecipeForm(form) }; const groupIndex = Number(button.dataset.group); const itemIndex = Number(button.dataset.item); const stepIndex = Number(button.dataset.stepIndex); if (button.dataset.action === 'add-group') draft.ingredientGroups.push({ name: '재료', items: [] }); if (button.dataset.action === 'remove-group') draft.ingredientGroups.splice(groupIndex, 1); if (button.dataset.action === 'add-item') draft.ingredientGroups[groupIndex].items.push({ name: '', amount: null }); if (button.dataset.action === 'remove-item') draft.ingredientGroups[groupIndex].items.splice(itemIndex, 1); if (button.dataset.action === 'add-step') draft.steps.push({ order: draft.steps.length + 1, text: '', timestampSec: null }); if (button.dataset.action === 'remove-step') draft.steps.splice(stepIndex, 1); draft.steps.forEach((step, index) => { step.order = index + 1; }); renderRecipeEditor(draft); }); form.addEventListener('submit', async (event) => { event.preventDefault(); const button = form.querySelector('[type="submit"]'); button.disabled = true; try { const { recipe: updatedRecipe } = await apiRequest(`/api/recipes/${encodeURIComponent(recipe._id)}`, { method: 'PATCH', body: JSON.stringify(readRecipeForm(form)), }); currentRecipe = updatedRecipe; renderRecipe(currentRecipe); showToast('레시피를 수정했습니다.'); } catch (error) { showToast(error.message); button.disabled = false; } }); } function renderRecipe(recipe) { document.title = `${recipe.title} · Our Recipe Atlas`; const image = recipe.imagePath ? `/media/${recipe.imagePath}` : null; const canEdit = recipe.ownerGoogleSub === currentUser.googleSub; detail.innerHTML = ` ← Recipe Library
${image ? `` : 'NO IMAGE'}
${escapeHtml(recipe.source.platform)}

${escapeHtml(recipe.title)}

${recipe.summary ? `

${escapeHtml(recipe.summary)}

` : ''} ${recipe.servings ? `

분량 · ${escapeHtml(recipe.servings)}

` : ''}
${canEdit ? '' : ''} 원본 보기 ↗

INGREDIENTS

재료

${recipe.ingredientGroups.map((group) => `

${escapeHtml(group.name)}

    ${group.items.map((item) => `
  • ${escapeHtml(item.name)}${escapeHtml(item.amount || '')}
  • `).join('')}
`).join('')}

METHOD

조리 순서

    ${recipe.steps.map((step) => { const body = `${escapeHtml(step.text)}`; return `
  1. ${String(step.order).padStart(2, '0')}${step.timestampSec != null && recipe.source.platform === 'youtube' ? `${body}${Math.floor(step.timestampSec / 60)}:${String(Math.round(step.timestampSec) % 60).padStart(2, '0')} ↗` : body}
  2. `; }).join('')}
${recipe.tips.length ? `

Tips

    ${recipe.tips.map((tip) => `
  • ${escapeHtml(tip)}
  • `).join('')}
` : ''}
`; if (!canEdit) return; document.querySelector('#edit-recipe').addEventListener('click', () => renderRecipeEditor(recipe)); document.querySelector('#delete-recipe').addEventListener('click', async () => { if (!window.confirm(`'${recipe.title}' 레시피를 삭제할까요?`)) return; try { await apiRequest(`/api/recipes/${encodeURIComponent(recipe._id)}`, { method: 'DELETE' }); window.location.assign('/'); } catch (error) { showToast(error.message); } }); } bindLogout(); currentUser = await loadSession(); appBar.setUser(currentUser); try { const recipeId = decodeURIComponent(window.location.pathname.split('/').filter(Boolean).pop()); const { recipe } = await apiRequest(`/api/recipes/${encodeURIComponent(recipeId)}`); currentRecipe = recipe; renderRecipe(currentRecipe); } catch (error) { detail.innerHTML = `

${escapeHtml(error.message)}

목록으로 돌아가기`; }