From 65e9c384d59eabfffa48cf1be30af1e6b585b319 Mon Sep 17 00:00:00 2001 From: mokjakA Date: Mon, 27 Oct 2025 23:51:37 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat(search):=20show=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EC=83=81=EB=8B=A8=20searchBar=20UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/SearchBar.module.scss | 42 ++++++++++++++++++++++++++++ app/components/SearchBar.tsx | 20 +++++++++++++ app/features/profile/pages/show.tsx | 2 ++ 3 files changed, 64 insertions(+) create mode 100644 app/components/SearchBar.module.scss create mode 100644 app/components/SearchBar.tsx diff --git a/app/components/SearchBar.module.scss b/app/components/SearchBar.module.scss new file mode 100644 index 0000000..3db5b16 --- /dev/null +++ b/app/components/SearchBar.module.scss @@ -0,0 +1,42 @@ +@use 'styles/layout' as *; + +.root { + position: sticky; + top: 0; + z-index: 20; + padding: 12px 8px; + background: #fff; + display: flex; + align-items: center; + justify-content: center; + gap: 15px; +} + +.logo { + font-size: 35px; + text-decoration: none; + color: black; + + @include mobile { + font-size: 25px; + } +} + +.input { + flex: 1 1 auto; + max-width: 800px; + padding: 10px 12px; + border-radius: 8px; + border: 1px solid rgba(0, 0, 0, 0.12); + box-sizing: border-box; + background: rgba(0, 0, 0, 0.04); + height: 40px; + + @include mobile { + height: 36px; + } +} + +.input::placeholder { + color: rgba(0, 0, 0, 0.45); +} diff --git a/app/components/SearchBar.tsx b/app/components/SearchBar.tsx new file mode 100644 index 0000000..65b2bda --- /dev/null +++ b/app/components/SearchBar.tsx @@ -0,0 +1,20 @@ +import { Form } from '@remix-run/react'; + +import styles from './SearchBar.module.scss'; + +export const SearchBar = () => { + return ( +
+ + Sonnets + + +
+ ); +}; diff --git a/app/features/profile/pages/show.tsx b/app/features/profile/pages/show.tsx index 11bdba9..20e2bcb 100644 --- a/app/features/profile/pages/show.tsx +++ b/app/features/profile/pages/show.tsx @@ -1,6 +1,7 @@ import type { Song } from '@prisma/client'; import { Link, useLoaderData, useRouteLoaderData, useSearchParams, Form } from '@remix-run/react'; +import { SearchBar } from 'app/components/SearchBar'; import { profileLayoutLoader, profileLoader } from 'app/features/profile/loader'; import styles from 'app/features/profile/pages/show.module.scss'; @@ -19,6 +20,7 @@ export default function Show() { return (
+
Date: Tue, 28 Oct 2025 00:53:02 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat(search):=20=EB=9D=BC=EC=9A=B0=ED=8A=B8?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD=20/profile/search=20->=20/search?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/features/profile/loader.ts | 16 -------- app/features/profile/pages/follows.tsx | 2 +- app/features/profile/services.ts | 35 ----------------- .../{profile => search}/components/Icons.tsx | 0 .../components/ProfileCard.module.scss | 0 .../components/ProfileCard.tsx | 2 +- app/features/search/loader.ts | 18 +++++++++ .../pages/search.module.scss | 0 .../{profile => search}/pages/search.tsx | 2 +- app/features/search/services.ts | 38 +++++++++++++++++++ app/routes/profile.search._index.tsx | 8 ---- ...e.$userId.search._index.tsx => search.tsx} | 4 +- 12 files changed, 61 insertions(+), 64 deletions(-) rename app/features/{profile => search}/components/Icons.tsx (100%) rename app/features/{profile => search}/components/ProfileCard.module.scss (100%) rename app/features/{profile => search}/components/ProfileCard.tsx (95%) create mode 100644 app/features/search/loader.ts rename app/features/{profile => search}/pages/search.module.scss (100%) rename app/features/{profile => search}/pages/search.tsx (98%) create mode 100644 app/features/search/services.ts delete mode 100644 app/routes/profile.search._index.tsx rename app/routes/{profile.$userId.search._index.tsx => search.tsx} (50%) diff --git a/app/features/profile/loader.ts b/app/features/profile/loader.ts index 434e092..b94fe38 100644 --- a/app/features/profile/loader.ts +++ b/app/features/profile/loader.ts @@ -1,4 +1,3 @@ -import { data } from '@remix-run/node'; import { redirect } from '@remix-run/react'; import { authenticator } from 'app/external/auth/auth.server'; @@ -13,8 +12,6 @@ import { fetchFollowing, fetchUserMusicMemo, fetchUserProfile, - findUserByHandleSim, - getRecommendedUsers, isUserFollowing, } from './services'; @@ -92,19 +89,6 @@ export const addTodaySongLoader = createLoader(async ({ db, params, request }) = return { initialSong: user.todayRecommendedSong, songs }; }); -export const searchLoader = createLoader(async ({ db, request }) => { - const url = new URL(request.url); - const handle = url.searchParams.get('handle'); - const recommendedUsersPromise = getRecommendedUsers(db)(); - const searchResultsPromise = handle ? findUserByHandleSim(db)({ handle }) : Promise.resolve(null); - const [recommendedUsers, searchResults] = await Promise.all([recommendedUsersPromise, searchResultsPromise]); - return data({ - recommendedUsers, - searchResults, - query: handle, - }); -}); - export const editHandleLoader = createLoader(async ({ request, params }) => { await requireUserOwnership(request, { userId: params.userId }); diff --git a/app/features/profile/pages/follows.tsx b/app/features/profile/pages/follows.tsx index fcb526c..415f9ed 100644 --- a/app/features/profile/pages/follows.tsx +++ b/app/features/profile/pages/follows.tsx @@ -3,7 +3,7 @@ import { Link, useLoaderData, useSearchParams, useFetcher } from '@remix-run/rea import type { followsLoader } from 'app/features/profile/loader'; import styles from './follows.module.scss'; -import { MusicNoteIcon } from '../components/Icons'; +import { MusicNoteIcon } from '../../search/components/Icons'; import { UserForFollowList } from '../services'; export default function FollowsPage() { diff --git a/app/features/profile/services.ts b/app/features/profile/services.ts index 8bc2efa..8a339c3 100644 --- a/app/features/profile/services.ts +++ b/app/features/profile/services.ts @@ -6,10 +6,6 @@ export const fetchUser = createService<{ id: number }, User | null>(async (db, { return await db.user.findFirst({ where: { id: id } }); }); -export type UserWithRecommendedSong = Prisma.UserGetPayload<{ - include: { todayRecommendedSong: true }; -}>; - export type UserForProfile = Prisma.UserGetPayload<{ include: { todayRecommendedSong: true; @@ -79,37 +75,6 @@ export const findUserByHandle = createService<{ handle: string }, User | null>(a return user; }); -export const findUserByHandleSim = createService<{ handle: string }, UserWithRecommendedSong[]>(async (db, args) => { - const users = await db.user.findMany({ - where: { - handle: { - contains: args.handle, - mode: 'insensitive', - }, - }, - include: { todayRecommendedSong: true }, - }); - return users; -}); - -export const getRecommendedUsers = createService, UserWithRecommendedSong[]>( - async (db, _args) => { - const users = await db.user.findMany({ - where: { - id: { - gte: 20, - lte: 25, - }, - }, - include: { todayRecommendedSong: true }, - }); - return users.map((user) => ({ - ...user, - avatarUrl: user.avatarUrl || '/images/features/profile/profile_default.png', - })); - } -); - export const updateUserHandle = createService<{ userId: string; handle: string }, User>( async (db, { userId, handle }) => { const existingUser = await db.user.findUnique({ diff --git a/app/features/profile/components/Icons.tsx b/app/features/search/components/Icons.tsx similarity index 100% rename from app/features/profile/components/Icons.tsx rename to app/features/search/components/Icons.tsx diff --git a/app/features/profile/components/ProfileCard.module.scss b/app/features/search/components/ProfileCard.module.scss similarity index 100% rename from app/features/profile/components/ProfileCard.module.scss rename to app/features/search/components/ProfileCard.module.scss diff --git a/app/features/profile/components/ProfileCard.tsx b/app/features/search/components/ProfileCard.tsx similarity index 95% rename from app/features/profile/components/ProfileCard.tsx rename to app/features/search/components/ProfileCard.tsx index a653a50..cf120c0 100644 --- a/app/features/profile/components/ProfileCard.tsx +++ b/app/features/search/components/ProfileCard.tsx @@ -1,7 +1,7 @@ import { Link } from '@remix-run/react'; -import { ChevronRightIcon, MusicNoteIcon } from './Icons'; import styles from './ProfileCard.module.scss'; +import { ChevronRightIcon, MusicNoteIcon } from '../../search/components/Icons'; import { UserWithRecommendedSong } from '../services'; export function ProfileCard({ user }: { user: UserWithRecommendedSong }) { diff --git a/app/features/search/loader.ts b/app/features/search/loader.ts new file mode 100644 index 0000000..86115cb --- /dev/null +++ b/app/features/search/loader.ts @@ -0,0 +1,18 @@ +import { data } from '@remix-run/node'; + +import createLoader from 'app/utils/createLoader'; + +import { findUserByHandleSim, getRecommendedUsers } from './services'; + +export const searchLoader = createLoader(async ({ db, request }) => { + const url = new URL(request.url); + const handle = url.searchParams.get('handle'); + const recommendedUsersPromise = getRecommendedUsers(db)(); + const searchResultsPromise = handle ? findUserByHandleSim(db)({ handle }) : Promise.resolve(null); + const [recommendedUsers, searchResults] = await Promise.all([recommendedUsersPromise, searchResultsPromise]); + return data({ + recommendedUsers, + searchResults, + query: handle, + }); +}); diff --git a/app/features/profile/pages/search.module.scss b/app/features/search/pages/search.module.scss similarity index 100% rename from app/features/profile/pages/search.module.scss rename to app/features/search/pages/search.module.scss diff --git a/app/features/profile/pages/search.tsx b/app/features/search/pages/search.tsx similarity index 98% rename from app/features/profile/pages/search.tsx rename to app/features/search/pages/search.tsx index 9127d81..91db424 100644 --- a/app/features/profile/pages/search.tsx +++ b/app/features/search/pages/search.tsx @@ -12,7 +12,7 @@ export default function SearchPage() { return (
-

User Search

+

Search

diff --git a/app/features/search/services.ts b/app/features/search/services.ts new file mode 100644 index 0000000..d7adb09 --- /dev/null +++ b/app/features/search/services.ts @@ -0,0 +1,38 @@ +import { Prisma } from '@prisma/client'; + +import createService from 'app/utils/createService'; + +export type UserWithRecommendedSong = Prisma.UserGetPayload<{ + include: { todayRecommendedSong: true }; +}>; + +export const findUserByHandleSim = createService<{ handle: string }, UserWithRecommendedSong[]>(async (db, args) => { + const users = await db.user.findMany({ + where: { + handle: { + contains: args.handle, + mode: 'insensitive', + }, + }, + include: { todayRecommendedSong: true }, + }); + return users; +}); + +export const getRecommendedUsers = createService, UserWithRecommendedSong[]>( + async (db, _args) => { + const users = await db.user.findMany({ + where: { + id: { + gte: 20, + lte: 25, + }, + }, + include: { todayRecommendedSong: true }, + }); + return users.map((user) => ({ + ...user, + avatarUrl: user.avatarUrl || '/images/features/profile/profile_default.png', + })); + } +); diff --git a/app/routes/profile.search._index.tsx b/app/routes/profile.search._index.tsx deleted file mode 100644 index a3cb262..0000000 --- a/app/routes/profile.search._index.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import { searchLoader } from 'app/features/profile/loader'; -import Search from 'app/features/profile/pages/search'; - -export const meta = () => [{ title: '유저 검색' }]; - -export const loader = searchLoader; - -export default Search; diff --git a/app/routes/profile.$userId.search._index.tsx b/app/routes/search.tsx similarity index 50% rename from app/routes/profile.$userId.search._index.tsx rename to app/routes/search.tsx index a3cb262..7ecbf25 100644 --- a/app/routes/profile.$userId.search._index.tsx +++ b/app/routes/search.tsx @@ -1,5 +1,5 @@ -import { searchLoader } from 'app/features/profile/loader'; -import Search from 'app/features/profile/pages/search'; +import { searchLoader } from 'app/features/search/loader'; +import Search from 'app/features/search/pages/search'; export const meta = () => [{ title: '유저 검색' }]; From f17d3a6b74c0e252627646cbd5227678ad99ab5b Mon Sep 17 00:00:00 2001 From: mokjakA Date: Thu, 30 Oct 2025 19:07:13 +0900 Subject: [PATCH 3/4] =?UTF-8?q?feat(search):=20=EB=85=B8=EB=9E=98=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/SearchBar.module.scss | 11 +- app/components/SearchBar.tsx | 25 +-- .../music/pages/music.user.module.scss | 1 - .../profile/components/SearchSongInput.tsx | 73 --------- .../components/SearchSongInputloader.tsx | 10 -- app/features/profile/loader.ts | 65 ++++---- .../profile/pages/addTodaySong.module.scss | 88 +++++----- app/features/profile/pages/addTodaySong.tsx | 120 +++++++------- app/features/profile/pages/editList.tsx | 2 +- .../profile/pages/searchSong.module.scss | 90 ----------- app/features/search/action.ts | 0 .../search/components/SearchSongInput.tsx | 73 +++++++++ .../components/SearchSongInputloader.tsx | 5 + .../components/SongItem.module.scss | 0 .../components/SongItem.tsx | 0 .../search/components/searchSong.module.scss | 90 +++++++++++ app/features/search/loader.ts | 17 +- app/features/search/pages/search.module.scss | 153 +++++++++++------- app/features/search/pages/search.tsx | 100 ++++++------ 19 files changed, 484 insertions(+), 439 deletions(-) delete mode 100644 app/features/profile/components/SearchSongInput.tsx delete mode 100644 app/features/profile/components/SearchSongInputloader.tsx delete mode 100644 app/features/profile/pages/searchSong.module.scss create mode 100644 app/features/search/action.ts create mode 100644 app/features/search/components/SearchSongInput.tsx create mode 100644 app/features/search/components/SearchSongInputloader.tsx rename app/features/{profile => search}/components/SongItem.module.scss (100%) rename app/features/{profile => search}/components/SongItem.tsx (100%) create mode 100644 app/features/search/components/searchSong.module.scss diff --git a/app/components/SearchBar.module.scss b/app/components/SearchBar.module.scss index 3db5b16..3893eae 100644 --- a/app/components/SearchBar.module.scss +++ b/app/components/SearchBar.module.scss @@ -4,11 +4,18 @@ position: sticky; top: 0; z-index: 20; - padding: 12px 8px; + padding: 12px; background: #fff; display: flex; - align-items: center; justify-content: center; + width: auto; + gap: 15px; +} + +.inner { + width: 100%; + display: flex; + align-items: center; gap: 15px; } diff --git a/app/components/SearchBar.tsx b/app/components/SearchBar.tsx index 65b2bda..d325338 100644 --- a/app/components/SearchBar.tsx +++ b/app/components/SearchBar.tsx @@ -2,19 +2,22 @@ import { Form } from '@remix-run/react'; import styles from './SearchBar.module.scss'; -export const SearchBar = () => { +export const SearchBar = ({ defaultQuery = '' }: { defaultQuery?: string }) => { return ( - - Sonnets - - + ); }; diff --git a/app/features/music/pages/music.user.module.scss b/app/features/music/pages/music.user.module.scss index b4a7e02..59680eb 100644 --- a/app/features/music/pages/music.user.module.scss +++ b/app/features/music/pages/music.user.module.scss @@ -26,7 +26,6 @@ } } -// 유저 정보(작성자) 스타일 .author-info { margin-bottom: 2rem; display: flex; diff --git a/app/features/profile/components/SearchSongInput.tsx b/app/features/profile/components/SearchSongInput.tsx deleted file mode 100644 index bacaa8a..0000000 --- a/app/features/profile/components/SearchSongInput.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { Await, Form } from '@remix-run/react'; -import { Suspense } from 'react'; - -import type { MusicInfo } from 'app/external/music/IMusicSearchAPI'; -import styles from 'app/features/profile/pages/searchSong.module.scss'; - -const PLACEHOLDER = '/images/features/profile/album_default2.png'; - -interface Props { - songs: Promise | undefined; - onSelect: (song: MusicInfo, index: number) => void; -} - -export default function SearchSongInput({ onSelect, songs }: Props) { - return ( -
-
- -
- - -
- ); -} - -const SearchedSongList = ({ - songs, - onSelect, -}: { - songs: Promise | undefined; - onSelect: (song: MusicInfo, index: number) => void; -}) => { - return ( -
    - 검색 중...

    }> - - {(resolvedSongs) => ( - <> - {resolvedSongs?.length === 0 &&

    검색 결과가 없습니다.

    } - {resolvedSongs?.map((song, i) => ( -
  • onSelect(song, i)} - > - }> - - {(albumCover) => ( - {song.album} { - e.currentTarget.src = PLACEHOLDER; - }} - /> - )} - - -
    -
    {song.title}
    -
    {song.artist}
    -
    {song.album}
    -
    -
  • - ))} - - )} -
    -
    -
- ); -}; diff --git a/app/features/profile/components/SearchSongInputloader.tsx b/app/features/profile/components/SearchSongInputloader.tsx deleted file mode 100644 index 312abcb..0000000 --- a/app/features/profile/components/SearchSongInputloader.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { searchMusic } from 'app/external/music/SearchMusic'; - -export const searchSongInputLoader = ({ request }: { request: Request }) => { - const searchQuery = new URL(request.url).searchParams.get('query'); - if (!searchQuery) { - return undefined; - } - - return searchMusic.searchSongWithQuery(searchQuery); -}; diff --git a/app/features/profile/loader.ts b/app/features/profile/loader.ts index b94fe38..6689313 100644 --- a/app/features/profile/loader.ts +++ b/app/features/profile/loader.ts @@ -5,7 +5,6 @@ import { getCurrentDBUser, getCurrentUser, requireUserOwnership } from 'app/exte import { buildSpotifyTrackUrl, getSpotifyEmbed } from 'app/external/music/SpotifyOEmbed'; import createLoader from 'app/utils/createLoader'; -import { searchSongInputLoader } from './components/SearchSongInputloader'; import { fetchAccountSettingsData, fetchFollowers, @@ -56,38 +55,38 @@ export const profileRedirectLoader = createLoader(async ({ request }) => { return redirect(`/profile/${user.id}/feed`); }); -export const addTodaySongLoader = createLoader(async ({ db, params, request }) => { - await requireUserOwnership(request, { userId: params.userId }); - - const userId = Number(params.userId); - if (isNaN(userId)) { - throw new Response('잘못된 사용자입니다.', { status: 400 }); - } - - const user = await db.user.findUnique({ - where: { id: userId }, - select: { - todayRecommendedSong: { - select: { - id: true, - title: true, - artist: true, - album: true, - thumbnailUrl: true, - spotifyId: true, - }, - }, - }, - }); - - if (!user) { - throw new Response('유저가 존재하지 않습니다.', { status: 404 }); - } - - const songs = searchSongInputLoader({ request }); - - return { initialSong: user.todayRecommendedSong, songs }; -}); +// export const addTodaySongLoader = createLoader(async ({ db, params, request }) => { +// await requireUserOwnership(request, { userId: params.userId }); + +// const userId = Number(params.userId); +// if (isNaN(userId)) { +// throw new Response('잘못된 사용자입니다.', { status: 400 }); +// } + +// const user = await db.user.findUnique({ +// where: { id: userId }, +// select: { +// todayRecommendedSong: { +// select: { +// id: true, +// title: true, +// artist: true, +// album: true, +// thumbnailUrl: true, +// spotifyId: true, +// }, +// }, +// }, +// }); + +// if (!user) { +// throw new Response('유저가 존재하지 않습니다.', { status: 404 }); +// } + +// const songs = searchSongInputLoader({ request }); + +// return { initialSong: user.todayRecommendedSong, songs }; +// }); export const editHandleLoader = createLoader(async ({ request, params }) => { await requireUserOwnership(request, { userId: params.userId }); diff --git a/app/features/profile/pages/addTodaySong.module.scss b/app/features/profile/pages/addTodaySong.module.scss index 5c9921d..14c8106 100644 --- a/app/features/profile/pages/addTodaySong.module.scss +++ b/app/features/profile/pages/addTodaySong.module.scss @@ -1,48 +1,48 @@ -@use 'styles/layout' as *; +// @use 'styles/layout' as *; -.add-today-song-container { - display: flex; - padding: 36px 16px; - flex-direction: column; - border-radius: 8px; -} +// .add-today-song-container { +// display: flex; +// padding: 36px 16px; +// flex-direction: column; +// border-radius: 8px; +// } -.title-box { - display: flex; - align-items: center; - .back-button { - color: $black-500; - font-size: 24px; - font-weight: 600; - margin-left: 16px; - margin: 0; - text-decoration: none; - } - .title { - flex: 1 0 auto; - font-size: 24px; - font-weight: 600; - margin-left: 16px; - margin: 0; - text-align: center; - } -} +// .title-box { +// display: flex; +// align-items: center; +// .back-button { +// color: $black-500; +// font-size: 24px; +// font-weight: 600; +// margin-left: 16px; +// margin: 0; +// text-decoration: none; +// } +// .title { +// flex: 1 0 auto; +// font-size: 24px; +// font-weight: 600; +// margin-left: 16px; +// margin: 0; +// text-align: center; +// } +// } -.search-section { - margin-top: 24px; - h2 { - font-size: 20px; - font-weight: 600; - margin-bottom: 16px; - } -} +// .search-section { +// margin-top: 24px; +// h2 { +// font-size: 20px; +// font-weight: 600; +// margin-bottom: 16px; +// } +// } -.initial-song-container { - margin-top: 24px; - h3 { - margin: 0; - font-size: 20px; - font-weight: 600; - margin-bottom: 16px; - } -} +// .initial-song-container { +// margin-top: 24px; +// h3 { +// margin: 0; +// font-size: 20px; +// font-weight: 600; +// margin-bottom: 16px; +// } +// } diff --git a/app/features/profile/pages/addTodaySong.tsx b/app/features/profile/pages/addTodaySong.tsx index c88f914..3eaf4b9 100644 --- a/app/features/profile/pages/addTodaySong.tsx +++ b/app/features/profile/pages/addTodaySong.tsx @@ -1,60 +1,60 @@ -import { Link, useLoaderData, useSubmit } from '@remix-run/react'; - -import type { MusicInfo } from 'app/external/music/IMusicSearchAPI'; -import SongItem, { type SimpleSong } from 'app/features/profile/components/SongItem'; -import styles from 'app/features/profile/pages/addTodaySong.module.scss'; - -import SearchSongInput from '../components/SearchSongInput'; -import { addTodaySongLoader } from '../loader'; - -export default function AddTodaySongPage() { - const { initialSong, songs } = useLoaderData(); - const savePickedSong = useSavePickedSong(); - - return ( -
-
- - {'<'} - -

오늘의 추천곡 수정

-
- -
- -
- - {initialSong && ( -
-

현재 추천곡

- -
- )} -
- ); -} - -const useSavePickedSong = () => { - const submit = useSubmit(); - - return async (song: MusicInfo) => { - const ok = window.confirm(`"${song.title}" – ${song.artist}\n오늘의 추천곡으로 등록하시겠습니까?`); - if (!ok) return; - - const simple: SimpleSong = { - id: 0, - title: song.title, - artist: song.artist, - album: song.album ?? null, - thumbnailUrl: song.albumCover ? await song.albumCover : null, - }; - - const fd = new FormData(); - fd.append('title', simple.title); - fd.append('artist', simple.artist); - fd.append('album', simple.album ?? ''); - fd.append('thumbnailUrl', simple.thumbnailUrl ?? ''); - fd.append('spotifyId', song.spotifyId ?? ''); - submit(fd, { method: 'post' }); - }; -}; +// import { Link, useLoaderData, useSubmit } from '@remix-run/react'; + +// import type { MusicInfo } from 'app/external/music/IMusicSearchAPI'; +// import styles from 'app/features/profile/pages/addTodaySong.module.scss'; +// import SongItem, { type SimpleSong } from 'app/features/search/components/SongItem'; + +// import SearchSongInput from '../../search/components/SearchSongInput'; +// import { addTodaySongLoader } from '../loader'; + +// export default function AddTodaySongPage() { +// const { initialSong, songs } = useLoaderData(); +// const savePickedSong = useSavePickedSong(); + +// return ( +//
+//
+// +// {'<'} +// +//

오늘의 추천곡 수정

+//
+ +//
+// +//
+ +// {initialSong && ( +//
+//

현재 추천곡

+// +//
+// )} +//
+// ); +// } + +// const useSavePickedSong = () => { +// const submit = useSubmit(); + +// return async (song: MusicInfo) => { +// const ok = window.confirm(`"${song.title}" – ${song.artist}\n오늘의 추천곡으로 등록하시겠습니까?`); +// if (!ok) return; + +// const simple: SimpleSong = { +// id: 0, +// title: song.title, +// artist: song.artist, +// album: song.album ?? null, +// thumbnailUrl: song.albumCover ? await song.albumCover : null, +// }; + +// const fd = new FormData(); +// fd.append('title', simple.title); +// fd.append('artist', simple.artist); +// fd.append('album', simple.album ?? ''); +// fd.append('thumbnailUrl', simple.thumbnailUrl ?? ''); +// fd.append('spotifyId', song.spotifyId ?? ''); +// submit(fd, { method: 'post' }); +// }; +// }; diff --git a/app/features/profile/pages/editList.tsx b/app/features/profile/pages/editList.tsx index 0f4be99..81f535b 100644 --- a/app/features/profile/pages/editList.tsx +++ b/app/features/profile/pages/editList.tsx @@ -17,7 +17,7 @@ import { Link } from '@remix-run/react'; import { useState } from 'react'; import styles from './editList.module.scss'; -import SongItem from '../components/SongItem'; +import SongItem from '../../search/components/SongItem'; import { SortableItem } from '../components/SortableItem'; // 새로 추가된 컴포넌트 export default function EditList() { diff --git a/app/features/profile/pages/searchSong.module.scss b/app/features/profile/pages/searchSong.module.scss deleted file mode 100644 index 8a14db4..0000000 --- a/app/features/profile/pages/searchSong.module.scss +++ /dev/null @@ -1,90 +0,0 @@ -.searchWrapper { - background-color: #f2f4f5; - padding: 24px; - border-radius: 8px; - - margin: 16px 24px; -} - -.formRow { - display: flex; - gap: 8px; -} - -.input { - flex: 1; - padding: 8px 12px; - border: 1px solid #d1d5db; - border-radius: 100px; - font-size: 14px; -} - -.button { - padding: 8px 16px; - border: none; - background-color: #2563eb; - color: #fff; - border-radius: 4px; - cursor: pointer; - - &:disabled { - opacity: 0.6; - cursor: default; - } -} - -.songList { - margin-top: 16px; - display: flex; - flex-direction: column; - gap: 12px; - padding: 0; -} - -.songItem { - display: flex; - gap: 16px; - align-items: center; - padding: 12px; - background-color: #fff; - border: 1px solid #e5e7eb; - border-radius: 6px; - cursor: pointer; - - &:hover { - background-color: #f9fafb; - } -} - -.cover { - width: 64px; - height: 64px; - object-fit: cover; - border-radius: 4px; - background-color: #f3f4f6; -} - -.texts { - overflow: hidden; -} - -.title { - font-weight: 600; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - margin-bottom: 2px; -} - -.artist, -.album { - font-size: 12px; - color: #6b7280; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.empty { - color: #9ca3af; -} diff --git a/app/features/search/action.ts b/app/features/search/action.ts new file mode 100644 index 0000000..e69de29 diff --git a/app/features/search/components/SearchSongInput.tsx b/app/features/search/components/SearchSongInput.tsx new file mode 100644 index 0000000..f0badd2 --- /dev/null +++ b/app/features/search/components/SearchSongInput.tsx @@ -0,0 +1,73 @@ +// import { Await, Form } from '@remix-run/react'; +// import { Suspense } from 'react'; + +// import type { MusicInfo } from 'app/external/music/IMusicSearchAPI'; +// import styles from 'app/features/profile/pages/searchSong.module.scss'; + +// const PLACEHOLDER = '/images/features/profile/album_default2.png'; + +// interface Props { +// songs: Promise | undefined; +// onSelect: (song: MusicInfo, index: number) => void; +// } + +// export default function SearchSongInput({ onSelect, songs }: Props) { +// return ( +//
+//
+// +//
+ +// +//
+// ); +// } + +// const SearchedSongList = ({ +// songs, +// onSelect, +// }: { +// songs: Promise | undefined; +// onSelect: (song: MusicInfo, index: number) => void; +// }) => { +// return ( +//
    +// 검색 중...

    }> +// +// {(resolvedSongs) => ( +// <> +// {resolvedSongs?.length === 0 &&

    검색 결과가 없습니다.

    } +// {resolvedSongs?.map((song, i) => ( +//
  • onSelect(song, i)} +// > +// }> +// +// {(albumCover) => ( +// {song.album} { +// e.currentTarget.src = PLACEHOLDER; +// }} +// /> +// )} +// +// +//
    +//
    {song.title}
    +//
    {song.artist}
    +//
    {song.album}
    +//
    +//
  • +// ))} +// +// )} +//
    +//
    +//
+// ); +// }; diff --git a/app/features/search/components/SearchSongInputloader.tsx b/app/features/search/components/SearchSongInputloader.tsx new file mode 100644 index 0000000..77dcdfe --- /dev/null +++ b/app/features/search/components/SearchSongInputloader.tsx @@ -0,0 +1,5 @@ +import { searchMusic } from 'app/external/music/SearchMusic'; + +export const searchSongInputLoader = ({ searchQuery }: { searchQuery: string }) => { + return searchMusic.searchSongWithQuery(searchQuery); +}; diff --git a/app/features/profile/components/SongItem.module.scss b/app/features/search/components/SongItem.module.scss similarity index 100% rename from app/features/profile/components/SongItem.module.scss rename to app/features/search/components/SongItem.module.scss diff --git a/app/features/profile/components/SongItem.tsx b/app/features/search/components/SongItem.tsx similarity index 100% rename from app/features/profile/components/SongItem.tsx rename to app/features/search/components/SongItem.tsx diff --git a/app/features/search/components/searchSong.module.scss b/app/features/search/components/searchSong.module.scss new file mode 100644 index 0000000..608bbf1 --- /dev/null +++ b/app/features/search/components/searchSong.module.scss @@ -0,0 +1,90 @@ +// .searchWrapper { +// background-color: #f2f4f5; +// padding: 24px; +// border-radius: 8px; + +// margin: 16px 24px; +// } + +// .formRow { +// display: flex; +// gap: 8px; +// } + +// .input { +// flex: 1; +// padding: 8px 12px; +// border: 1px solid #d1d5db; +// border-radius: 100px; +// font-size: 14px; +// } + +// .button { +// padding: 8px 16px; +// border: none; +// background-color: #2563eb; +// color: #fff; +// border-radius: 4px; +// cursor: pointer; + +// &:disabled { +// opacity: 0.6; +// cursor: default; +// } +// } + +// .songList { +// margin-top: 16px; +// display: flex; +// flex-direction: column; +// gap: 12px; +// padding: 0; +// } + +// .songItem { +// display: flex; +// gap: 16px; +// align-items: center; +// padding: 12px; +// background-color: #fff; +// border: 1px solid #e5e7eb; +// border-radius: 6px; +// cursor: pointer; + +// &:hover { +// background-color: #f9fafb; +// } +// } + +// .cover { +// width: 64px; +// height: 64px; +// object-fit: cover; +// border-radius: 4px; +// background-color: #f3f4f6; +// } + +// .texts { +// overflow: hidden; +// } + +// .title { +// font-weight: 600; +// white-space: nowrap; +// overflow: hidden; +// text-overflow: ellipsis; +// margin-bottom: 2px; +// } + +// .artist, +// .album { +// font-size: 12px; +// color: #6b7280; +// white-space: nowrap; +// overflow: hidden; +// text-overflow: ellipsis; +// } + +// .empty { +// color: #9ca3af; +// } diff --git a/app/features/search/loader.ts b/app/features/search/loader.ts index 86115cb..7717a2e 100644 --- a/app/features/search/loader.ts +++ b/app/features/search/loader.ts @@ -2,17 +2,16 @@ import { data } from '@remix-run/node'; import createLoader from 'app/utils/createLoader'; -import { findUserByHandleSim, getRecommendedUsers } from './services'; +import { searchSongInputLoader } from './components/SearchSongInputloader'; -export const searchLoader = createLoader(async ({ db, request }) => { +export const searchLoader = createLoader(async ({ request }) => { const url = new URL(request.url); - const handle = url.searchParams.get('handle'); - const recommendedUsersPromise = getRecommendedUsers(db)(); - const searchResultsPromise = handle ? findUserByHandleSim(db)({ handle }) : Promise.resolve(null); - const [recommendedUsers, searchResults] = await Promise.all([recommendedUsersPromise, searchResultsPromise]); + const query = url.searchParams.get('search') ?? ''; + + const songs = searchSongInputLoader({ searchQuery: query }); + return data({ - recommendedUsers, - searchResults, - query: handle, + query, + songs, }); }); diff --git a/app/features/search/pages/search.module.scss b/app/features/search/pages/search.module.scss index 7f3ffcb..5fecb89 100644 --- a/app/features/search/pages/search.module.scss +++ b/app/features/search/pages/search.module.scss @@ -1,73 +1,52 @@ @use 'styles/layout' as *; -.wrapper { - padding: 2rem 1rem; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; +.root { + padding: 0 0 10px; } -.searchContainer { +.pageRow { + width: 100%; display: flex; - gap: 0.5rem; - margin-bottom: 2.5rem; - margin-top: 2rem; - align-items: center; + justify-content: center; } -.searchInputWrapper { - position: relative; - flex-grow: 1; - - svg { - position: absolute; - left: 0.8rem; - top: 50%; - transform: translateY(-50%); - color: #adb5bd; - width: 18px; - height: 18px; - } -} +.side { + display: flex; + justify-content: center; + align-items: flex-start; + padding: 14px 10px; -.searchInput { - width: 93%; - height: 39px; - border-radius: 8px; - border: 1px solid #dee2e6; - padding: 0.5rem 1rem 0.5rem 2.5rem; - font-size: 0.95rem; - transition: - border-color 0.2s, - box-shadow 0.2s; - - &:focus { - outline: none; - border-color: #4dabf7; - box-shadow: 0 0 0 3px rgba(77, 171, 247, 0.2); + position: sticky; + align-self: flex-start; + + @include tablet { + display: none; + } + @include mobile { + display: none; } } -.searchButton { - background-color: #4dabf7; - color: #fff; - border: none; - width: 65px; - height: 55px; - flex-shrink: 0; - border-radius: 8px; - cursor: pointer; - transition: background-color 0.2s; +.content { display: flex; - align-items: center; - justify-content: center; - - &:hover { - background-color: #339af0; - } + flex-direction: column; + width: 100%; + max-width: 800px; +} - svg { - width: 20px; - height: 20px; - } +.backButton { + position: static; + display: inline-flex; + display: inline-flex; + align-items: center; + height: 36px; + border: 1px solid white; + background: white; + color: #333; + font-weight: 600; + cursor: pointer; + color: $primary-500; + font-size: 18px; } .title { @@ -94,3 +73,63 @@ grid-template-columns: 1fr; } } + +.formRow { + display: flex; + gap: 8px; +} + +.songList { + margin: 10px 16px; + display: flex; + flex-direction: column; + gap: 12px; + padding: 0; +} + +.empty { + color: #9ca3af; +} + +.songItem { + display: flex; + gap: 16px; + align-items: center; + background-color: #fff; + border: 1px solid #e5e7eb; + border-radius: 6px; + cursor: pointer; + + &:hover { + background-color: #f9fafb; + } +} + +.cover { + width: 64px; + height: 64px; + object-fit: cover; + border-radius: 4px; + background-color: #f3f4f6; +} + +.texts { + overflow: hidden; +} + +.title { + font-weight: 600; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + margin-bottom: 2px; +} + +.artist, +.album { + font-size: 12px; + color: #6b7280; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} diff --git a/app/features/search/pages/search.tsx b/app/features/search/pages/search.tsx index 91db424..725e86f 100644 --- a/app/features/search/pages/search.tsx +++ b/app/features/search/pages/search.tsx @@ -1,60 +1,64 @@ -import { useLoaderData, Form } from '@remix-run/react'; +import { Await, useNavigate, useLoaderData } from '@remix-run/react'; +import { Suspense } from 'react'; + +import { SearchBar } from 'app/components/SearchBar'; +import { MusicInfo } from 'app/external/music/IMusicSearchAPI'; +import SongItem, { type SimpleSong } from 'app/features/search/components/SongItem'; import styles from './search.module.scss'; -import { SearchIcon } from '../components/Icons'; -import { ProfileCard } from '../components/ProfileCard'; import { searchLoader } from '../loader'; -export default function SearchPage() { - const { recommendedUsers, searchResults, query } = useLoaderData(); +const PLACEHOLDER = '/images/features/profile/album_default2.png'; - const isSearchMode = !!query; +export default function SearchPage() { + const { query, songs } = useLoaderData(); + const navigate = useNavigate(); return ( -
-

Search

- -
-
- - -
- -
- -
+
+
+ + +
+
+ ); +} - {isSearchMode ? ( -
-

{`🔍 검색 결과"${query}"`}

- {searchResults && searchResults.length > 0 ? ( -
- {searchResults.map((user) => ( - +const SearchedSongList = ({ songs }: { songs: Promise | undefined }) => { + return ( +
    + + + {(resolvedSongs) => ( + <> + {resolvedSongs?.length === 0 &&

    검색 결과가 없습니다.

    } + {resolvedSongs?.map((song, idx) => ( +
  • + }> + + {(albumCover) => { + const simple: SimpleSong = { + id: idx, + title: song.title, + artist: song.artist, + thumbnailUrl: albumCover ?? null, + album: song.album ?? null, + }; + return ; + }} + + +
  • ))} -
- ) : ( -

{`"${query}"에 대한 검색 결과가 없습니다.`}

+ )} -
- ) : ( -
-

✨ 오늘의 추천 프로필

-
- {recommendedUsers.map((user) => ( - - ))} -
-
- )} -
+ + + ); -} +}; From 4052b2d6211c93975810d2ee1dc1d64447d975db Mon Sep 17 00:00:00 2001 From: mokjakA Date: Thu, 30 Oct 2025 19:36:53 +0900 Subject: [PATCH 4/4] chore: disable addTodaySong route (WIP) --- app/routes/profile.$userId.addTodaySong.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/routes/profile.$userId.addTodaySong.tsx b/app/routes/profile.$userId.addTodaySong.tsx index 71c1508..3307fa9 100644 --- a/app/routes/profile.$userId.addTodaySong.tsx +++ b/app/routes/profile.$userId.addTodaySong.tsx @@ -1,12 +1,12 @@ -import type { MetaFunction } from '@remix-run/node'; +// import type { MetaFunction } from '@remix-run/node'; -import { addTodaySongAction } from 'app/features/profile/action'; -import { addTodaySongLoader } from 'app/features/profile/loader'; -import AddTodaySongPage from 'app/features/profile/pages/addTodaySong'; +// import { addTodaySongAction } from 'app/features/profile/action'; +// import { addTodaySongLoader } from 'app/features/profile/loader'; +// import AddTodaySongPage from 'app/features/profile/pages/addTodaySong'; -export const meta: MetaFunction = () => [{ title: '오늘의 추천곡 수정' }]; +// export const meta: MetaFunction = () => [{ title: '오늘의 추천곡 수정' }]; -export const loader = addTodaySongLoader; -export const action = addTodaySongAction; +// export const loader = addTodaySongLoader; +// export const action = addTodaySongAction; -export default AddTodaySongPage; +// export default AddTodaySongPage;