diff --git a/app/features/profile/action.ts b/app/features/profile/action.ts
index 29166d9..adc843c 100644
--- a/app/features/profile/action.ts
+++ b/app/features/profile/action.ts
@@ -7,46 +7,6 @@ import createAction from 'app/utils/createAction';
import { HandleSchema, FollowsActionSchema } from './schema';
-export const addTodaySongAction = createAction(async ({ request, db, params }) => {
- await requireUserOwnership(request, { userId: params.userId });
- const formData = await request.formData();
-
- const userId = Number(params.userId);
- if (isNaN(userId)) {
- return new Response(JSON.stringify('잘못된 사용자입니다.'), { status: 400 });
- }
-
- const title = (formData.get('title') as string | null)?.trim() ?? '';
- const artist = (formData.get('artist') as string | null)?.trim() ?? '';
- const album = (formData.get('album') as string | null)?.trim() || null;
- const thumbnailUrl = (formData.get('thumbnailUrl') as string | null)?.trim() || null;
- const spotifyId = (formData.get('spotifyId') as string | null)?.trim() || null;
-
- if (!title || !artist) {
- return new Response('제목과 아티스트는 필수입니다.', { status: 400 });
- }
-
- const existingSong =
- (spotifyId ? await db.song.findFirst({ where: { spotifyId } }) : null) ??
- (await db.song.findFirst({ where: { title, artist } }));
-
- const song = existingSong
- ? await db.song.update({
- where: { id: existingSong.id },
- data: { album, thumbnailUrl, spotifyId },
- })
- : await db.song.create({
- data: { title, artist, album, thumbnailUrl, spotifyId },
- });
-
- await db.user.update({
- where: { id: userId },
- data: { todayRecommendedSongId: song.id },
- });
-
- return redirect('../show');
-});
-
export const editHandleAction = createAction(async ({ request, db, params }) => {
await requireUserOwnership(request, { userId: params.userId });
diff --git a/app/features/profile/loader.ts b/app/features/profile/loader.ts
index 800add3..05579e4 100644
--- a/app/features/profile/loader.ts
+++ b/app/features/profile/loader.ts
@@ -55,39 +55,6 @@ 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 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
deleted file mode 100644
index 14c8106..0000000
--- a/app/features/profile/pages/addTodaySong.module.scss
+++ /dev/null
@@ -1,48 +0,0 @@
-// @use 'styles/layout' as *;
-
-// .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;
-// }
-// }
-
-// .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;
-// }
-// }
diff --git a/app/features/profile/pages/addTodaySong.tsx b/app/features/profile/pages/addTodaySong.tsx
deleted file mode 100644
index 3eaf4b9..0000000
--- a/app/features/profile/pages/addTodaySong.tsx
+++ /dev/null
@@ -1,60 +0,0 @@
-// 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/search/action.ts b/app/features/search/action.ts
index e69de29..dd8197a 100644
--- a/app/features/search/action.ts
+++ b/app/features/search/action.ts
@@ -0,0 +1,74 @@
+import { redirect } from '@remix-run/node';
+
+import { getCurrentUser } from 'app/external/auth/jwt.server';
+import createAction from 'app/utils/createAction';
+
+export const searchAction = createAction(async ({ request, db }) => {
+ const formData = await request.formData();
+ const actionType = String(formData.get('_action') ?? '');
+
+ const currentUser = await getCurrentUser(request);
+ if (!currentUser) {
+ return redirect('/login/error');
+ }
+ const userId = currentUser.id;
+ if (Number.isNaN(userId)) {
+ return new Response('잘못된 사용자입니다.', { status: 400 });
+ }
+
+ const songId = formData.get('songId');
+ const title = (formData.get('title') as string | null)?.trim() ?? '';
+ const artist = (formData.get('artist') as string | null)?.trim() ?? '';
+ const album = (formData.get('album') as string | null)?.trim() || null;
+ const thumbnailUrl = (formData.get('thumbnailUrl') as string | null)?.trim() || null;
+ const spotifyId = (formData.get('spotifyId') as string | null)?.trim() || null;
+
+ const maybeSongOrResponse = await (async () => {
+ if (songId) {
+ const s = await db.song.findUnique({ where: { id: Number(songId) } });
+ if (!s) return new Response('노래를 찾을 수 없습니다.', { status: 404 });
+ return s;
+ }
+
+ if (!title || !artist) {
+ return new Response('제목과 아티스트는 필수입니다.', { status: 400 });
+ }
+
+ const existingSong =
+ (spotifyId ? await db.song.findFirst({ where: { spotifyId } }) : null) ??
+ (await db.song.findFirst({ where: { title, artist } }));
+
+ if (existingSong) {
+ const updated = await db.song.update({
+ where: { id: existingSong.id },
+ data: { album, thumbnailUrl, spotifyId },
+ });
+ return updated;
+ }
+
+ const created = await db.song.create({
+ data: { title, artist, album, thumbnailUrl, spotifyId },
+ });
+ return created;
+ })();
+
+ if (maybeSongOrResponse instanceof Response) {
+ return maybeSongOrResponse;
+ }
+
+ const song = maybeSongOrResponse;
+
+ if (actionType === 'addToday') {
+ await db.user.update({
+ where: { id: userId },
+ data: { todayRecommendedSongId: song.id },
+ });
+ return redirect(`/profile/${userId}/show`);
+ }
+
+ if (actionType === 'createMemo') {
+ return redirect(`/music/${song.id}/user/${userId}/create`);
+ }
+
+ return new Response('unknown action', { status: 400 });
+});
diff --git a/app/features/search/components/SearchSongInput.tsx b/app/features/search/components/SearchSongInput.tsx
deleted file mode 100644
index f0badd2..0000000
--- a/app/features/search/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) => (
-//
{
-// e.currentTarget.src = PLACEHOLDER;
-// }}
-// />
-// )}
-//
-//
-//
-//
{song.title}
-//
{song.artist}
-//
{song.album}
-//
-//
-// ))}
-// >
-// )}
-//
-//
-//
-// );
-// };
diff --git a/app/features/search/components/SongItem.module.scss b/app/features/search/components/SongItem.module.scss
index b27d498..7099578 100644
--- a/app/features/search/components/SongItem.module.scss
+++ b/app/features/search/components/SongItem.module.scss
@@ -1,26 +1,84 @@
@use 'styles/layout' as *;
-.song-item {
+.songItem {
display: flex;
align-items: center;
gap: 12px;
- .item-image {
- height: 60px;
- border-radius: 8px;
+ width: 100%;
+ padding-right: 8px;
+ position: relative;
+
+ @include mobile {
+ height: 100px;
+ }
+
+ .itemImage {
+ height: 85px;
+ width: 85px;
+ object-fit: cover;
+ border-radius: 5px;
+
+ @include mobile {
+ height: 100%;
+ }
}
- .item-text-box {
+
+ .itemTextBox {
display: flex;
flex-direction: column;
gap: 4px;
- .item-title {
+ flex: 1;
+ min-width: 0;
+
+ @include mobile {
+ padding-bottom: 35px;
+ }
+
+ .itemTitle {
font-size: 1rem;
font-weight: 600;
color: #7b7b7b;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
}
- .item-artist {
+
+ .itemArtist {
font-size: 0.875rem;
font-weight: 400;
- color: #a0a0a0;
+ color: inherit;
+ }
+ }
+ .actions {
+ display: flex;
+ flex-direction: row;
+ gap: 6px;
+ margin-left: auto;
+ margin-right: 0;
+
+ @include mobile {
+ position: absolute;
+ right: 8px;
+ bottom: 8px;
+ margin-left: 0;
+ gap: 6px;
}
}
+
+ .memoButton,
+ .addTodayButton {
+ padding: 6px 10px;
+ font-size: 13px;
+ border-radius: 8px;
+ border: 1px solid rgba(0, 0, 0, 0.08);
+ background: white;
+ cursor: pointer;
+ white-space: inherit;
+ }
+
+ .addTodayButton {
+ background: $primary-500;
+ color: white;
+ border-color: transparent;
+ }
}
diff --git a/app/features/search/components/SongItem.tsx b/app/features/search/components/SongItem.tsx
index 7023fe7..d092393 100644
--- a/app/features/search/components/SongItem.tsx
+++ b/app/features/search/components/SongItem.tsx
@@ -1,11 +1,14 @@
+import { Form } from '@remix-run/react';
+
import styles from './SongItem.module.scss';
export interface SimpleSong {
- id: number;
+ id?: number;
title: string;
artist: string;
- thumbnailUrl: string | null;
- album: string | null;
+ thumbnailUrl?: string | null;
+ album?: string | null;
+ spotifyId?: string | null;
}
interface Props {
@@ -13,6 +16,32 @@ interface Props {
}
export default function SongItem({ song }: Props) {
+ const renderHiddenFieldsForSong = () => {
+ if (song.id && song.id > 0) {
+ return ;
+ }
+
+ return (
+ <>
+
+
+
+
+
+ >
+ );
+ };
+
+ const confirmAddToday = (e: React.FormEvent) => {
+ const ok = window.confirm(`"${song.title}" – ${song.artist}\n오늘의 추천곡으로 등록하시겠습니까?`);
+ if (!ok) e.preventDefault();
+ };
+
+ const confirmCreateMemo = (e: React.FormEvent) => {
+ const ok = window.confirm(`"${song.title}" – ${song.artist}\n이 곡을 메모로 추가하시겠습니까?`);
+ if (!ok) e.preventDefault();
+ };
+
return (
![]()
{song.title}
{song.artist}
+
+
+
+
+
+
);
}
diff --git a/app/features/search/components/searchSong.module.scss b/app/features/search/components/searchSong.module.scss
deleted file mode 100644
index 608bbf1..0000000
--- a/app/features/search/components/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/pages/search.module.scss b/app/features/search/pages/search.module.scss
index 5fecb89..b1d1770 100644
--- a/app/features/search/pages/search.module.scss
+++ b/app/features/search/pages/search.module.scss
@@ -14,11 +14,13 @@
display: flex;
justify-content: center;
align-items: flex-start;
- padding: 14px 10px;
position: sticky;
align-self: flex-start;
+ top: 15px;
+ height: fit-content;
+
@include tablet {
display: none;
}
diff --git a/app/features/search/pages/search.tsx b/app/features/search/pages/search.tsx
index 725e86f..5810341 100644
--- a/app/features/search/pages/search.tsx
+++ b/app/features/search/pages/search.tsx
@@ -3,7 +3,7 @@ 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 SongItem, { SimpleSong } from 'app/features/search/components/SongItem';
import styles from './search.module.scss';
import { searchLoader } from '../loader';
@@ -43,11 +43,12 @@ const SearchedSongList = ({ songs }: { songs: Promise | undefined }
{(albumCover) => {
const simple: SimpleSong = {
- id: idx,
+ id: 0,
title: song.title,
artist: song.artist,
thumbnailUrl: albumCover ?? null,
album: song.album ?? null,
+ spotifyId: song.spotifyId ?? null,
};
return ;
}}
diff --git a/app/routes/_index.tsx b/app/routes/_index.tsx
index ed87fec..d25457c 100644
--- a/app/routes/_index.tsx
+++ b/app/routes/_index.tsx
@@ -1,6 +1,3 @@
import Home from 'app/features/home';
-import { addTodaySongAction } from 'app/features/profile/action';
-
-export { addTodaySongAction as action };
export default Home;
diff --git a/app/routes/profile.$userId.addTodaySong.tsx b/app/routes/profile.$userId.addTodaySong.tsx
deleted file mode 100644
index 3307fa9..0000000
--- a/app/routes/profile.$userId.addTodaySong.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-// 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';
-
-// export const meta: MetaFunction = () => [{ title: '오늘의 추천곡 수정' }];
-
-// export const loader = addTodaySongLoader;
-// export const action = addTodaySongAction;
-
-// export default AddTodaySongPage;
diff --git a/app/routes/search.tsx b/app/routes/search.tsx
index 7ecbf25..897c357 100644
--- a/app/routes/search.tsx
+++ b/app/routes/search.tsx
@@ -1,8 +1,10 @@
+import { searchAction } from 'app/features/search/action';
import { searchLoader } from 'app/features/search/loader';
import Search from 'app/features/search/pages/search';
export const meta = () => [{ title: '유저 검색' }];
export const loader = searchLoader;
+export const action = searchAction;
export default Search;