diff --git a/TODO.md b/TODO.md index 181417f..e3cb46b 100644 --- a/TODO.md +++ b/TODO.md @@ -266,6 +266,20 @@ - backend debug broadcasts ## Recent Change Log +- Date: `2026-03-24` +- What changed: + - Added a frontend-only `Video / Image` media-type toggle to Zone A. + - Kept the existing backend-connected video search flow as the default mode. + - Added an image-search prototype panel with sample prompt chips and test images. + - Added mock image-result cards so the image-search layout can be reviewed before backend image search exists. +- Why it changed: + - Image search is planned next, and the user wanted the Zone A UI shape in place first so the workflow can be tested before backend integration. +- How it was verified: + - `node --check frontend/app.js` +- What is still risky or incomplete: + - This is UI-only; image mode does not call a backend API yet. + - The test images currently use placeholder assets and do not represent final data contracts or modal behavior. + - Date: `2026-03-24` - What changed: - Added an operating rule that future plans recorded in this repo should be written in Korean by default. diff --git a/frontend/app.js b/frontend/app.js index 7525620..67166c5 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -5,7 +5,13 @@ const searchQuery = document.getElementById("searchQuery"); const searchResults = document.getElementById("searchResults"); const searchWarning = document.getElementById("searchWarning"); const queryVariants = document.getElementById("queryVariants"); +const searchModeTitle = document.getElementById("searchModeTitle"); +const searchModeHint = document.getElementById("searchModeHint"); +const searchSubmitButton = document.getElementById("searchSubmitButton"); +const mediaTypeToggles = Array.from(document.querySelectorAll("[data-media-type-toggle]")); const platformToggles = Array.from(document.querySelectorAll("[data-platform-toggle]")); +const imageSearchSandbox = document.getElementById("imageSearchSandbox"); +const imagePromptChips = Array.from(document.querySelectorAll("[data-image-prompt]")); const dropzone = document.getElementById("dropzone"); const fileInput = document.getElementById("fileInput"); const uploadResult = document.getElementById("uploadResult"); @@ -13,6 +19,7 @@ const downloadForm = document.getElementById("downloadForm"); const downloadUrl = document.getElementById("downloadUrl"); const downloadResult = document.getElementById("downloadResult"); const cardTemplate = document.getElementById("searchCardTemplate"); +const imageCardTemplate = document.getElementById("imageCardTemplate"); const previewModal = document.getElementById("previewModal"); const previewMediaFrame = document.getElementById("previewMediaFrame"); const previewTitle = document.getElementById("previewTitle"); @@ -89,7 +96,46 @@ const summaryTranslationInflight = new Map(); const resultPreviewCache = new Map(); const resultPreviewInflight = new Map(); let cardSummaryObserver = null; +let activeMediaType = "video"; const PREVIEW_PLACEHOLDER = "https://placehold.co/1280x720/0a0a0a/ffffff?text=Preview"; +const MOCK_IMAGE_RESULTS = [ + { + title: "Neon Crosswalk Portrait", + tag: "Test Image", + caption: "도심 야간 조명과 보케를 강조한 테스트용 이미지 카드입니다. 향후 이미지 검색 결과 카드 레이아웃 검증에 사용할 수 있습니다.", + imageUrl: "https://placehold.co/1200x900/0f172a/f8fafc?text=Neon+Crosswalk", + }, + { + title: "Editorial Summer Street", + tag: "Prototype", + caption: "에디토리얼 라이프스타일 톤을 가정한 샘플 썸네일입니다. 카드 비율과 타이포 계층을 보기에 적당합니다.", + imageUrl: "https://placehold.co/1200x900/f59e0b/111827?text=Summer+Street", + }, + { + title: "Minimal Product Tabletop", + tag: "Mock Result", + caption: "제품 컷 기반 이미지 검색을 상정한 목업 결과입니다. 이미지 중심 레이아웃에서 텍스트 양을 테스트하기 위한 예시입니다.", + imageUrl: "https://placehold.co/1200x900/e5e7eb/111827?text=Product+Tabletop", + }, + { + title: "Vintage Fashion Frame", + tag: "Image Search", + caption: "패션 무드보드나 포스터 레퍼런스 탐색 화면에 어울리도록 구성한 테스트용 결과입니다.", + imageUrl: "https://placehold.co/1200x900/7c3aed/f5f3ff?text=Vintage+Fashion", + }, + { + title: "Botanical Studio Light", + tag: "Preview", + caption: "정적인 피사체 중심 이미지 검색 UI에서 호버 없이도 충분히 읽히는 카드 밀도를 보기 위한 샘플입니다.", + imageUrl: "https://placehold.co/1200x900/14532d/d1fae5?text=Botanical+Studio", + }, + { + title: "Magazine Cover Layout", + tag: "Test Asset", + caption: "향후 이미지 상세 모달이나 컬렉션 기능을 붙일 때 사용할 수 있는 테스트용 카드 자리입니다.", + imageUrl: "https://placehold.co/1200x900/111827/fde68a?text=Magazine+Cover", + }, +]; function proxiedPreviewURL(src) { if (!src) { @@ -281,6 +327,59 @@ function renderQueryVariants(queries = []) { queryVariants.classList.add("hidden"); } +function syncMediaTypeButtons() { + for (const button of mediaTypeToggles) { + const type = button.dataset.mediaTypeToggle; + const active = type === activeMediaType; + button.classList.toggle("bg-white", active); + button.classList.toggle("text-black", active); + button.classList.toggle("text-zinc-300", !active); + } +} + +function renderMockImageResults(queryText = "") { + const queryLabel = String(queryText || "").trim() || "test image"; + searchResults.innerHTML = ""; + searchResults.classList.remove("xl:grid-cols-3"); + searchResults.classList.add("xl:grid-cols-4"); + for (const item of MOCK_IMAGE_RESULTS) { + const node = imageCardTemplate.content.firstElementChild.cloneNode(true); + const image = node.querySelector("img"); + image.src = item.imageUrl; + image.alt = item.title; + node.querySelector(".image-card-tag").textContent = `${item.tag} / ${queryLabel}`; + node.querySelector(".image-card-title").textContent = item.title; + node.querySelector(".image-card-caption").textContent = item.caption; + searchResults.appendChild(node); + } +} + +function applyMediaTypeUI() { + const isImageMode = activeMediaType === "image"; + syncMediaTypeButtons(); + setHidden(imageSearchSandbox, !isImageMode, "block"); + setHidden(queryVariants, true, ""); + showWarning(""); + searchModeTitle.textContent = isImageMode ? "AI Image Discovery" : "AI Smart Discovery"; + searchModeHint.textContent = isImageMode + ? "이미지 검색 프로토타입 모드입니다. 현재는 UI 전용 테스트 결과를 표시합니다." + : "비디오 검색 모드입니다. 실제 검색 API와 연결되어 있습니다."; + searchQuery.placeholder = isImageMode ? "검색할 이미지를 설명하세요" : "한글 검색어를 입력하세요"; + searchSubmitButton.textContent = isImageMode ? "Image Search Test" : "AI Search"; + for (const button of platformToggles) { + button.classList.toggle("hidden", isImageMode); + } + if (isImageMode) { + setStatus("image prototype mode", 0); + renderMockImageResults(searchQuery.value); + } else { + searchResults.classList.add("xl:grid-cols-3"); + searchResults.classList.remove("xl:grid-cols-4"); + searchResults.innerHTML = ""; + setStatus(`active platforms: ${Array.from(activePlatforms).join(", ")}`, 0); + } +} + function syncPlatformButtons() { for (const button of platformToggles) { const platform = button.dataset.platformToggle; @@ -796,6 +895,12 @@ function closeResultViewer() { searchForm.addEventListener("submit", async (event) => { event.preventDefault(); + if (activeMediaType === "image") { + renderMockImageResults(searchQuery.value); + logEvent("image-search:prototype", { query: searchQuery.value, results: MOCK_IMAGE_RESULTS.length }); + setStatus("image prototype results ready", 100); + return; + } setStatus("preparing search", 5); showWarning(""); try { @@ -816,6 +921,24 @@ searchForm.addEventListener("submit", async (event) => { } }); +for (const button of mediaTypeToggles) { + button.addEventListener("click", () => { + activeMediaType = button.dataset.mediaTypeToggle || "video"; + applyMediaTypeUI(); + logEvent("media-type:update", { active: activeMediaType }); + }); +} + +for (const chip of imagePromptChips) { + chip.addEventListener("click", () => { + searchQuery.value = chip.dataset.imagePrompt || ""; + if (activeMediaType === "image") { + renderMockImageResults(searchQuery.value); + setStatus("image prompt applied", 100); + } + }); +} + async function uploadFile(file) { const formData = new FormData(); formData.append("file", file); @@ -1075,5 +1198,5 @@ window.addEventListener("unhandledrejection", (event) => { connectWS(); syncPlatformButtons(); -setStatus(`active platforms: ${Array.from(activePlatforms).join(", ")}`, 0); +applyMediaTypeUI(); logEvent("app:ready", { activePlatforms: Array.from(activePlatforms) }); diff --git a/frontend/index.html b/frontend/index.html index 3bc5fae..89a6fe4 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -33,9 +33,16 @@

Zone A

-

AI Smart Discovery

+

AI Smart Discovery

+
+
+ + +
+

비디오 검색 모드입니다. 실제 검색 API와 연결되어 있습니다.

+
@@ -43,8 +50,34 @@
- +
+
@@ -224,6 +257,21 @@ - + + + diff --git a/frontend/style.css b/frontend/style.css index 1ebcad3..5d49798 100644 --- a/frontend/style.css +++ b/frontend/style.css @@ -26,6 +26,26 @@ body { -webkit-line-clamp: 3; } +.line-clamp-4 { + display: -webkit-box; + -webkit-box-orient: vertical; + overflow: hidden; + -webkit-line-clamp: 4; +} + +.media-type-toggle { + min-width: 6rem; +} + +.image-prompt-chip { + background: rgba(255, 255, 255, 0.03); +} + +.image-prompt-chip:hover { + border-color: rgba(255, 255, 255, 0.28); + background: rgba(255, 255, 255, 0.08); +} + .dual-slider__thumb { touch-action: none; cursor: ew-resize;